@@ -22,6 +22,8 @@ import {
2222 calculateInitialStrength ,
2323 calculateEffectiveHalfLife ,
2424 calculateRetentionStrength ,
25+ calculateDormantDays ,
26+ reinforceMemory ,
2527} from "../src/workspace-memory.ts" ;
2628import { redactCredentials } from "../src/redaction.ts" ;
2729import { assessMemoryQuality , isHardQualityReason , isProgressSnapshotViolation } from "../src/memory-quality.ts" ;
@@ -312,6 +314,71 @@ test("calculateRetentionStrength falls back to updatedAt when retentionClock is
312314 assert . equal ( calculateRetentionStrength ( memory , now , 0 ) , initialStrength / 2 ) ;
313315} ) ;
314316
317+ test ( "calculateDormantDays applies fourteen day workspace activity grace" , ( ) => {
318+ const now = Date . UTC ( 2026 , 3 , 29 ) ;
319+ const activeWithinGrace : WorkspaceMemoryStore = {
320+ version : 1 ,
321+ workspace : { root : "/repo" , key : "abc" } ,
322+ limits : { maxRenderedChars : LONG_TERM_LIMITS . maxRenderedChars , maxEntries : LONG_TERM_LIMITS . maxEntries } ,
323+ entries : [ ] ,
324+ updatedAt : new Date ( now ) . toISOString ( ) ,
325+ lastActivityAt : new Date ( now - 13 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ,
326+ } ;
327+ const dormantPastGrace : WorkspaceMemoryStore = {
328+ ...activeWithinGrace ,
329+ lastActivityAt : new Date ( now - 20 * 24 * 60 * 60 * 1000 ) . toISOString ( ) ,
330+ } ;
331+
332+ assert . equal ( calculateDormantDays ( activeWithinGrace , now ) , 0 ) ;
333+ assert . equal ( calculateDormantDays ( dormantPastGrace , now ) , 6 ) ;
334+ } ) ;
335+
336+ test ( "normalizeWorkspaceMemoryWithAccounting uses dormant workspace days for strength ordering" , async ( ) => {
337+ const now = Date . now ( ) ;
338+ const reinforcedOldReference : LongTermMemoryEntry = {
339+ ...entry ( "reinforced-old" , "Reinforced legacy docs live at https://example.com/legacy" , "reference" ) ,
340+ retentionClock : now - 100 * 24 * 60 * 60 * 1000 ,
341+ reinforcementCount : 6 ,
342+ } ;
343+ const freshReference : LongTermMemoryEntry = {
344+ ...entry ( "fresh" , "Fresh docs live at https://example.com/fresh" , "reference" ) ,
345+ retentionClock : now ,
346+ } ;
347+ const store : WorkspaceMemoryStore = {
348+ version : 1 ,
349+ workspace : { root : "/repo" , key : "abc" } ,
350+ limits : { maxRenderedChars : LONG_TERM_LIMITS . maxRenderedChars , maxEntries : LONG_TERM_LIMITS . maxEntries } ,
351+ entries : [ freshReference , reinforcedOldReference ] ,
352+ updatedAt : new Date ( now ) . toISOString ( ) ,
353+ lastActivityAt : new Date ( now - ( 14 + 1000 ) * 24 * 60 * 60 * 1000 ) . toISOString ( ) ,
354+ } ;
355+
356+ const result = await normalizeWorkspaceMemoryWithAccounting ( "/repo" , store ) ;
357+
358+ assert . deepEqual ( result . kept . map ( memory => memory . id ) , [ "reinforced-old" , "fresh" ] ) ;
359+ } ) ;
360+
361+ test ( "reinforceMemory enforces session interval and max guards" , ( ) => {
362+ const now = Date . UTC ( 2026 , 3 , 29 ) ;
363+ const base = entry ( "reinforce" , "Durable memory should reinforce only when gated" ) ;
364+ const reinforced = reinforceMemory ( base , "session-a" , now ) ;
365+
366+ assert . notEqual ( reinforced , base ) ;
367+ assert . equal ( reinforced . reinforcementCount , 1 ) ;
368+ assert . equal ( reinforced . lastReinforcedAt , now ) ;
369+ assert . equal ( reinforced . lastReinforcedSessionID , "session-a" ) ;
370+
371+ assert . equal ( reinforceMemory ( reinforced , "session-a" , now + 2 * 60 * 60 * 1000 ) , reinforced ) ;
372+ assert . equal ( reinforceMemory ( reinforced , "session-b" , now + 30 * 60 * 1000 ) , reinforced ) ;
373+
374+ const atMax : LongTermMemoryEntry = {
375+ ...base ,
376+ reinforcementCount : 6 ,
377+ lastReinforcedAt : now - 2 * 60 * 60 * 1000 ,
378+ } ;
379+ assert . equal ( reinforceMemory ( atMax , "session-c" , now ) , atMax ) ;
380+ } ) ;
381+
315382test ( "enforceLongTermLimits orders entries by retention strength" , ( ) => {
316383 const now = Date . now ( ) ;
317384 const freshFeedback : LongTermMemoryEntry = {
@@ -1457,7 +1524,7 @@ test("renderWorkspaceMemory excludes superseded entries", () => {
14571524 assert . doesNotMatch ( rendered , / W a v e s 1 - 5 已 完 成 / ) ;
14581525} ) ;
14591526
1460- test ( "loadWorkspaceMemory does not rewrite an already normalized store" , async ( ) => {
1527+ test ( "loadWorkspaceMemory records activity for an already normalized store" , async ( ) => {
14611528 const sandbox = await mkdtemp ( join ( tmpdir ( ) , "wm-normalized-" ) ) ;
14621529 const dataHome = join ( sandbox , "xdg-data-home" ) ;
14631530 const root = join ( sandbox , "workspace" ) ;
@@ -1491,11 +1558,12 @@ test("loadWorkspaceMemory does not rewrite an already normalized store", async (
14911558 const before = ( await stat ( storePath ) ) . mtimeMs ;
14921559 await sleep ( 20 ) ;
14931560
1494- await loadWorkspaceMemory ( root ) ;
1561+ const loaded = await loadWorkspaceMemory ( root ) ;
14951562 await loadWorkspaceMemory ( root ) ;
14961563
14971564 const after = ( await stat ( storePath ) ) . mtimeMs ;
1498- assert . equal ( after , before , "normalized loads should not touch the store file" ) ;
1565+ assert . ok ( after > before , "normalized loads should update workspace activity timestamp" ) ;
1566+ assert . ok ( loaded . lastActivityAt , "load should expose last activity timestamp" ) ;
14991567 } finally {
15001568 if ( previousXdgDataHome === undefined ) {
15011569 delete process . env . XDG_DATA_HOME ;
@@ -1506,7 +1574,7 @@ test("loadWorkspaceMemory does not rewrite an already normalized store", async (
15061574 }
15071575} ) ;
15081576
1509- test ( "loadWorkspaceMemory does not persist pure ordering normalization " , async ( ) => {
1577+ test ( "loadWorkspaceMemory preserves ordering while recording activity " , async ( ) => {
15101578 const sandbox = await mkdtemp ( join ( tmpdir ( ) , "wm-ordering-" ) ) ;
15111579 const dataHome = join ( sandbox , "xdg-data-home" ) ;
15121580 const root = join ( sandbox , "workspace" ) ;
@@ -1557,7 +1625,7 @@ test("loadWorkspaceMemory does not persist pure ordering normalization", async (
15571625 const after = ( await stat ( storePath ) ) . mtimeMs ;
15581626
15591627 assert . deepEqual ( loaded . entries . map ( memory => memory . id ) , [ "feedback-first" , "reference-second" ] ) ;
1560- assert . equal ( after , before , "order-only normalization should not write during load " ) ;
1628+ assert . ok ( after > before , "load should write updated workspace activity timestamp " ) ;
15611629 } finally {
15621630 if ( previousXdgDataHome === undefined ) {
15631631 delete process . env . XDG_DATA_HOME ;
@@ -1618,7 +1686,10 @@ test("loadWorkspaceMemory persists redaction changes and is stable afterward", a
16181686 await sleep ( 20 ) ;
16191687 await loadWorkspaceMemory ( root ) ;
16201688 const afterSecondLoad = ( await stat ( storePath ) ) . mtimeMs ;
1621- assert . equal ( afterSecondLoad , beforeSecondLoad , "second load should not rewrite redacted content" ) ;
1689+ assert . ok ( afterSecondLoad > beforeSecondLoad , "second load should update workspace activity timestamp" ) ;
1690+ const persistedAfterSecondLoad = await readFile ( storePath , "utf-8" ) ;
1691+ assert . equal ( persistedAfterSecondLoad . includes ( "sk-test-123" ) , false ) ;
1692+ assert . equal ( persistedAfterSecondLoad . includes ( "sushi" ) , false ) ;
16221693 } finally {
16231694 if ( previousXdgDataHome === undefined ) {
16241695 delete process . env . XDG_DATA_HOME ;
0 commit comments