@@ -41,21 +41,29 @@ import {bytesToTokens} from '../../../services/memory-core/helpers/consumerFrict
4141const __filename = fileURLToPath ( import . meta. url ) ;
4242const __dirname = path . dirname ( __filename ) ;
4343
44- // Bounds the re-serve ONLY for the DETERMINISTICALLY-terminal failure: `skip-over-band` (payload over the
45- // model's safe band) is a reliable size check — that session genuinely cannot be processed at this cadence
46- // until a deep-digest lane exists, so deferring it after the `maxDigestAttempts` config leaf is a safe bleed-stop.
47- // `under-band-choke` (a bare-null return UNDER the band) and a transient ingestion-failure are NOT proven
48- // terminal — the extractor returns the same null for choke / schema-failure / timeout alike, so treating an
49- // under-band null as un-digestible is a guess. They stay `undigested` (attempt-tracked, re-served) until
50- // typed extractor failure semantics exist; deferring on a guessed-terminal null would risk silently
51- // dropping a digestible session.
52- const PERMANENT_DEFER_REASONS = new Set ( [ 'skip-over-band' ] ) ;
53-
5444function estimatePayloadTokens ( payload ) {
5545 const text = payload === undefined || payload === null ? '' : String ( payload ) ;
5646 return bytesToTokens ( Buffer . byteLength ( text , 'utf8' ) ) ;
5747}
5848
49+ function isTriVectorFailureDescriptor ( value ) {
50+ return value ?. ok === false ;
51+ }
52+
53+ function getTriVectorFailureAttempts ( failure ) {
54+ return Number . isFinite ( failure ?. evidence ?. attempts ) ? failure . evidence . attempts : 1 ;
55+ }
56+
57+ function getTriVectorFailureKind ( failure ) {
58+ return failure ?. deferReason || failure ?. frictionSymptom || 'typed-failure' ;
59+ }
60+
61+ function getTriVectorFailureMessage ( failure ) {
62+ return failure ?. evidence ?. note ||
63+ failure ?. evidence ?. errorMessage ||
64+ `tri-vector extraction failed (${ getTriVectorFailureKind ( failure ) } )` ;
65+ }
66+
5967function toErrorMessage ( error ) {
6068 return error && error . message !== undefined ? String ( error . message ) : String ( error ) ;
6169}
@@ -457,9 +465,9 @@ class DreamService extends Base {
457465 }
458466
459467 const startTime = Date . now ( ) ;
460- let success ;
468+ let extractionResult ;
461469 try {
462- success = await SemanticGraphExtractor . executeTriVectorExtraction ( session ) ;
470+ extractionResult = await SemanticGraphExtractor . executeTriVectorExtraction ( session ) ;
463471 } catch ( e ) {
464472 sessionState . triVector = {
465473 status : 'failed' ,
@@ -476,16 +484,24 @@ class DreamService extends Base {
476484 }
477485 const triVectorTime = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 1 ) ;
478486 logger . info ( `[DreamService] -> Tri-Vector Synthesis took: ${ triVectorTime } s` ) ;
487+ const extractionFailure = isTriVectorFailureDescriptor ( extractionResult ) ? extractionResult : null ;
488+ const success = extractionResult && ! extractionFailure ;
479489 sessionState . triVector = {
480490 status : success ? 'completed' : 'failed' ,
481- attempts : 1 ,
482- errorKind : success ? undefined : 'null-result'
491+ attempts : extractionFailure ? getTriVectorFailureAttempts ( extractionFailure ) : 1 ,
492+ errorKind : success ? undefined : ( extractionFailure ? getTriVectorFailureKind ( extractionFailure ) : 'null-result' )
483493 } ;
494+ if ( extractionFailure ) {
495+ sessionState . triVector . deferReason = extractionFailure . deferReason ;
496+ sessionState . triVector . frictionSymptom = extractionFailure . frictionSymptom ;
497+ sessionState . triVector . terminalForCadence = extractionFailure . terminalForCadence === true ;
498+ }
484499 if ( ! success ) {
485- sessionState . failureReasons . push ( 'tri-vector extraction returned null' ) ;
500+ sessionState . failureReasons . push ( extractionFailure ? getTriVectorFailureMessage ( extractionFailure ) : 'tri-vector extraction returned null' ) ;
486501 }
487502 perPhaseStates . push ( finishPhase ( 'triVector' , startTime , success ? 'completed' : 'failed' , {
488- sessionId : session . meta . sessionId
503+ sessionId : session . meta . sessionId ,
504+ deferReason : extractionFailure ?. deferReason
489505 } ) ) ;
490506
491507 const topoStart = Date . now ( ) ;
@@ -530,7 +546,7 @@ class DreamService extends Base {
530546
531547 const capStart = Date . now ( ) ;
532548 try {
533- await this . inferTestGapsFromSession ( success ) ;
549+ await this . inferTestGapsFromSession ( success ? extractionResult : null ) ;
534550 } catch ( e ) {
535551 sessionState . gapSession = {
536552 status : 'failed' ,
@@ -561,33 +577,28 @@ class DreamService extends Base {
561577 sessionState . graphDigestedFlag = true ;
562578 logger . info ( `[DreamService] Session ${ session . meta . sessionId } marked as graphDigested in Memory Core.` ) ;
563579 } else {
564- // Digest failed (extractor returned null OR memory-ingestion errors). Bound the
565- // re-serve: count the attempt, and once it reaches `maxDigestAttempts` mark the
566- // session `deferred` so findUndigestedSessions stops re-serving it every cycle —
567- // the chronic local-model load bleed. `deferReason` records WHY for the honest
568- // consolidation-gap surface + a future deep-digest lane. Back-compat: graphDigested
569- // stays unset, so a consumer that does not yet read digestState still sees an
570- // un-digested (never a falsely-digested) session.
571- const digestAttempts = ( Number ( session . meta . digestAttempts ) || 0 ) + 1 ;
572- const safeBandTokens = aiConfig . localModels ?. chat ?. safeProcessingLimitTokens ;
573- const deferReason = ingestErrors > 0
580+ // Digest failed (typed extractor failure OR memory-ingestion errors). Bound the
581+ // re-serve only when the extractor supplies cadence-terminal evidence; ingestion
582+ // errors and legacy bare-null returns stay retryable so a storage/transient failure
583+ // never removes a digestible session from the steady cadence.
584+ const digestAttempts = ( Number ( session . meta . digestAttempts ) || 0 ) + 1 ;
585+ const maxDigestAttempts = readRequiredNumberLeaf ( 'maxDigestAttempts' ) ;
586+ const terminalForCadence = ingestErrors === 0 && extractionFailure ?. terminalForCadence === true ;
587+ const deferReason = ingestErrors > 0
574588 ? 'ingestion-failure'
575- : ( safeBandTokens > 0 && sessionState . payloadSizeTokens > safeBandTokens ? 'skip-over-band' : 'under-band-choke' ) ;
576- // Bound the re-serve ONLY for the deterministically-terminal reason (skip-over-band);
577- // under-band-choke + transient ingestion-failure keep retrying (attempt-tracked) so a
578- // digestible session is never silently dropped on a guessed-terminal null.
579- const maxDigestAttempts = readRequiredNumberLeaf ( 'maxDigestAttempts' ) ;
580- const digestState = PERMANENT_DEFER_REASONS . has ( deferReason ) && digestAttempts >= maxDigestAttempts
589+ : ( extractionFailure ?. deferReason || 'schema-failure' ) ;
590+ const digestState = terminalForCadence && digestAttempts >= maxDigestAttempts
581591 ? 'deferred'
582592 : 'undigested' ;
583593
584594 await this . sessionsCollection . update ( {
585595 ids : [ session . id ] ,
586596 metadatas : [ { ...session . meta , digestState, digestAttempts, deferReason } ]
587597 } ) ;
588- sessionState . digestState = digestState ;
589- sessionState . deferReason = deferReason ;
590- sessionState . digestAttempts = digestAttempts ;
598+ sessionState . digestState = digestState ;
599+ sessionState . deferReason = deferReason ;
600+ sessionState . digestAttempts = digestAttempts ;
601+ sessionState . terminalForCadence = terminalForCadence ;
591602
592603 if ( digestState === 'deferred' ) {
593604 logger . warn ( `[DreamService] Session ${ session . meta . sessionId } marked 'deferred' after ${ digestAttempts } failed digest attempt(s) (reason: ${ deferReason } ); excluded from the steady REM cadence to stop the re-serve bleed.` ) ;
0 commit comments