@@ -12,7 +12,7 @@ import { dataHome, extractionRejectionLogPath, migrationLogPath, workspaceKey, w
1212import { assessMemoryQuality , HARD_QUALITY_REASONS } from "../src/memory-quality.ts" ;
1313import { redactCredentials } from "../src/redaction.ts" ;
1414import { scanWorkspaceResidues } from "../src/workspace-cleanup.ts" ;
15- import { renderWorkspaceMemory } from "../src/workspace-memory.ts" ;
15+ import { calculateRetentionStrength , renderWorkspaceMemory } from "../src/workspace-memory.ts" ;
1616import type { LongTermMemoryEntry , LongTermSource , LongTermType , PendingMemoryJournalStore , WorkspaceMemoryStore } from "../src/types.ts" ;
1717import { LONG_TERM_LIMITS , PROMOTION_RETRY_LIMITS } from "../src/types.ts" ;
1818
@@ -65,6 +65,14 @@ type MigrationLogRecord = {
6565} ;
6666
6767const TYPES : LongTermType [ ] = [ "feedback" , "decision" , "project" , "reference" ] ;
68+ const TYPE_MAX_FOR_DIAG : Record < LongTermType , number > = {
69+ feedback : 10 ,
70+ decision : 10 ,
71+ project : 8 ,
72+ reference : 6 ,
73+ } ;
74+ const WORKSPACE_DORMANT_AFTER_DAYS_FOR_DIAG = 14 ;
75+ const DORMANT_DECAY_MULTIPLIER_FOR_DIAG = 0.25 ;
6876const SUSPICIOUS_REASONS = [
6977 "progress_snapshot" ,
7078 "active_file_snapshot" ,
@@ -229,6 +237,68 @@ function ageDays(entry: LongTermMemoryEntry): number | null {
229237 return Math . floor ( ( Date . now ( ) - time ) / 86_400_000 ) ;
230238}
231239
240+ function formatStrength ( value : number ) : string {
241+ return Number . isFinite ( value ) ? value . toFixed ( 3 ) : "0.000" ;
242+ }
243+
244+ function daysSinceIso ( value : string | undefined , now = Date . now ( ) ) : number | null {
245+ if ( ! value ) return null ;
246+ const ms = new Date ( value ) . getTime ( ) ;
247+ if ( ! Number . isFinite ( ms ) ) return null ;
248+ return Math . max ( 0 , ( now - ms ) / 86_400_000 ) ;
249+ }
250+
251+ function formatPercent ( ratio : number ) : string {
252+ return `${ ( ratio * 100 ) . toFixed ( 1 ) } %` ;
253+ }
254+
255+ type RetentionDiagItem = {
256+ entry : LongTermMemoryEntry ;
257+ strength : number ;
258+ } ;
259+
260+ function isSafetyCriticalForDiag ( entry : LongTermMemoryEntry ) : boolean {
261+ return entry . safetyCritical === true ;
262+ }
263+
264+ function retentionCandidatesForDiag ( store : WorkspaceMemoryStore ) : {
265+ sorted : RetentionDiagItem [ ] ;
266+ rendered : RetentionDiagItem [ ] ;
267+ typeCapped : RetentionDiagItem [ ] ;
268+ globalCapped : RetentionDiagItem [ ] ;
269+ } {
270+ const now = Date . now ( ) ;
271+ const active = store . entries . filter ( entry => entry . status !== "superseded" ) ;
272+ const sorted = active
273+ . map ( entry => ( { entry, strength : calculateRetentionStrength ( entry , now , store . lastActivityAt ) } ) )
274+ . sort ( ( a , b ) => b . strength - a . strength || a . entry . id . localeCompare ( b . entry . id ) ) ;
275+
276+ const rendered : RetentionDiagItem [ ] = [ ] ;
277+ const typeCapped : RetentionDiagItem [ ] = [ ] ;
278+ const globalCapped : RetentionDiagItem [ ] = [ ] ;
279+ const typeCounts : Partial < Record < LongTermType , number > > = { } ;
280+
281+ for ( const item of sorted ) {
282+ if ( ! isSafetyCriticalForDiag ( item . entry ) ) {
283+ const count = typeCounts [ item . entry . type ] ?? 0 ;
284+ const max = TYPE_MAX_FOR_DIAG [ item . entry . type ] ?? Infinity ;
285+ if ( count >= max ) {
286+ typeCapped . push ( item ) ;
287+ continue ;
288+ }
289+ typeCounts [ item . entry . type ] = count + 1 ;
290+ }
291+
292+ if ( rendered . length < LONG_TERM_LIMITS . maxEntries ) {
293+ rendered . push ( item ) ;
294+ } else {
295+ globalCapped . push ( item ) ;
296+ }
297+ }
298+
299+ return { sorted, rendered, typeCapped, globalCapped } ;
300+ }
301+
232302function promotionLimit ( source : LongTermSource ) : number {
233303 if ( source === "manual" ) return PROMOTION_RETRY_LIMITS . maxManualAttempts ;
234304 return PROMOTION_RETRY_LIMITS . maxExplicitAttempts ;
@@ -333,10 +403,13 @@ async function printWorkspaceHealth(input: {
333403
334404 const active = store . entries . filter ( entry => entry . status !== "superseded" ) ;
335405 const superseded = store . entries . filter ( entry => entry . status === "superseded" ) ;
406+ const retention = retentionCandidatesForDiag ( store ) ;
407+ const renderedEntries = retention . rendered . map ( item => item . entry ) ;
336408 const renderedEstimate = renderWorkspaceMemory ( store ) . length ;
337409
338- console . log ( `Active memories: ${ active . length } ` ) ;
410+ console . log ( `Stored active memories: ${ active . length } ` ) ;
339411 console . log ( `Superseded memories: ${ superseded . length } ` ) ;
412+ console . log ( `Rendered candidates: ${ renderedEntries . length } ` ) ;
340413 console . log ( `Rendered estimate: ${ renderedEstimate . toLocaleString ( ) } chars` ) ;
341414 console . log ( "" ) ;
342415
@@ -356,12 +429,18 @@ async function printWorkspaceHealth(input: {
356429
357430 console . log ( "By type:" ) ;
358431 for ( const type of TYPES ) {
359- const activeCount = active . filter ( entry => entry . type === type ) . length ;
432+ const storedCount = active . filter ( entry => entry . type === type ) . length ;
433+ const renderedCount = renderedEntries . filter ( entry => entry . type === type ) . length ;
360434 const supersededCount = superseded . filter ( entry => entry . type === type ) . length ;
361- console . log ( ` ${ type . padEnd ( 9 ) } active =${ String ( activeCount ) . padEnd ( 3 ) } superseded=${ supersededCount } ` ) ;
435+ console . log ( ` ${ type . padEnd ( 9 ) } stored =${ String ( storedCount ) . padEnd ( 3 ) } rendered= ${ String ( renderedCount ) . padEnd ( 3 ) } typeCap= ${ TYPE_MAX_FOR_DIAG [ type ] } superseded=${ supersededCount } ` ) ;
362436 }
363437 console . log ( "" ) ;
364438
439+ console . log ( "Retention caps:" ) ;
440+ console . log ( ` type-capped entries: ${ retention . typeCapped . length } ` ) ;
441+ console . log ( ` global-cap overflow: ${ retention . globalCapped . length } ` ) ;
442+ console . log ( "" ) ;
443+
365444 const olderThan30 = active . filter ( entry => ( ageDays ( entry ) ?? 0 ) > 30 ) . length ;
366445 const olderThan90 = active . filter ( entry => ( ageDays ( entry ) ?? 0 ) > 90 ) . length ;
367446 const staleMarked = active . filter ( entry => {
@@ -374,6 +453,33 @@ async function printWorkspaceHealth(input: {
374453 console . log ( ` older than 90d: ${ olderThan90 } ` ) ;
375454 console . log ( "" ) ;
376455
456+ const wallDaysSinceActivity = daysSinceIso ( store . lastActivityAt ) ;
457+ const dormantDiscountActive = wallDaysSinceActivity !== null && wallDaysSinceActivity > WORKSPACE_DORMANT_AFTER_DAYS_FOR_DIAG ;
458+ const dormantDaysPastGrace = wallDaysSinceActivity === null
459+ ? 0
460+ : Math . max ( 0 , wallDaysSinceActivity - WORKSPACE_DORMANT_AFTER_DAYS_FOR_DIAG ) ;
461+ console . log ( "Dormancy:" ) ;
462+ console . log ( ` lastActivityAt: ${ store . lastActivityAt ?? "(missing)" } ` ) ;
463+ console . log ( ` wall days since activity: ${ wallDaysSinceActivity === null ? "unknown" : wallDaysSinceActivity . toFixed ( 1 ) } ` ) ;
464+ console . log ( ` dormant discount active: ${ dormantDiscountActive ? "yes" : "no" } ` ) ;
465+ console . log ( ` dormant days past grace: ${ dormantDaysPastGrace . toFixed ( 1 ) } ` ) ;
466+ console . log ( ` dormant multiplier: ${ DORMANT_DECAY_MULTIPLIER_FOR_DIAG } ` ) ;
467+ console . log ( "" ) ;
468+
469+ const highImportanceCount = active . filter ( entry => entry . userImportance === "high" ) . length ;
470+ const safetyCriticalCount = active . filter ( isSafetyCriticalForDiag ) . length ;
471+ const maxReinforcedCount = active . filter ( entry => ( entry . reinforcementCount ?? 0 ) >= 6 ) . length ;
472+ const highImportanceRatio = active . length === 0 ? 0 : highImportanceCount / active . length ;
473+ const maxReinforcedRatio = active . length === 0 ? 0 : maxReinforcedCount / active . length ;
474+ const highImportanceAlert = highImportanceRatio > 0.3 ;
475+ const safetyCriticalAlert = safetyCriticalCount > 5 ;
476+ const maxReinforcedAlert = maxReinforcedRatio > 0.1 ;
477+ console . log ( "Retention monitoring:" ) ;
478+ console . log ( ` high_importance_ratio: ${ formatPercent ( highImportanceRatio ) } (alert > 30%)${ highImportanceAlert ? " ALERT" : "" } ` ) ;
479+ console . log ( ` safety_critical_count: ${ safetyCriticalCount } (alert > 5)${ safetyCriticalAlert ? " ALERT" : "" } ` ) ;
480+ console . log ( ` max_reinforced_count: ${ maxReinforcedAlert ? `${ maxReinforcedCount } (${ formatPercent ( maxReinforcedRatio ) } , alert > 10%) ALERT` : `${ maxReinforcedCount } (alert > 10% active)` } ` ) ;
481+ console . log ( "" ) ;
482+
377483 const qualityByEntry = active . map ( entry => ( { entry, quality : assessMemoryQuality ( entry ) } ) ) ;
378484 const duplicateCounts = countBy ( active . map ( entry => `${ entry . type } :${ canonicalMemoryText ( entry . text ) } ` ) ) ;
379485 const duplicateExtras = [ ...duplicateCounts . values ( ) ] . reduce ( ( sum , count ) => sum + Math . max ( 0 , count - 1 ) , 0 ) ;
@@ -400,12 +506,23 @@ async function printWorkspaceHealth(input: {
400506
401507 console . log ( "" ) ;
402508 console . log ( "Top rendered candidates:" ) ;
403- const top = [ ... active ] . sort ( ( a , b ) => b . text . length - a . text . length ) . slice ( 0 , 5 ) ;
509+ const top = retention . rendered . slice ( 0 , 5 ) ;
404510 if ( top . length === 0 ) {
405511 console . log ( " (none)" ) ;
406512 } else {
407- for ( const entry of top ) {
408- console . log ( ` - [${ entry . type } ] ${ truncate ( cleanText ( entry . text , input . raw ) ) } ` ) ;
513+ for ( const item of top ) {
514+ console . log ( ` - strength=${ formatStrength ( item . strength ) } [${ item . entry . type } ] ${ truncate ( cleanText ( item . entry . text , input . raw ) ) } ` ) ;
515+ }
516+ }
517+
518+ console . log ( "" ) ;
519+ console . log ( "Weakest active memories:" ) ;
520+ const weakest = retention . sorted . slice ( - 5 ) . reverse ( ) ;
521+ if ( weakest . length === 0 ) {
522+ console . log ( " (none)" ) ;
523+ } else {
524+ for ( const item of weakest ) {
525+ console . log ( ` - strength=${ formatStrength ( item . strength ) } [${ item . entry . type } ] ${ truncate ( cleanText ( item . entry . text , input . raw ) ) } ` ) ;
409526 }
410527 }
411528}
0 commit comments