@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
33import os from "node:os" ;
44import path from "node:path" ;
55import type { DatabaseSync } from "node:sqlite" ;
6+ import { emitSessionTranscriptUpdate } from "openclaw/plugin-sdk/agent-harness-runtime" ;
67import {
78 resolveSessionTranscriptsDirForAgent ,
89 type OpenClawConfig ,
@@ -33,6 +34,25 @@ type SyncParams = {
3334 progress ?: ( update : MemorySyncProgressUpdate ) => void ;
3435} ;
3536
37+ type MemorySessionTranscriptUpdate = {
38+ agentId ?: string ;
39+ sessionFile ?: string ;
40+ sessionKey ?: string ;
41+ target ?: {
42+ agentId : string ;
43+ sessionId : string ;
44+ sessionKey : string ;
45+ } ;
46+ } ;
47+
48+ type MemoryTranscriptUpdateSubscriber = (
49+ listener : ( update : MemorySessionTranscriptUpdate ) => void ,
50+ ) => ( ) => void ;
51+
52+ const MEMORY_CORE_TRANSCRIPT_UPDATE_SUBSCRIBER_KEY = Symbol . for (
53+ "openclaw.memoryCore.sessionTranscriptUpdateSubscriber" ,
54+ ) ;
55+
3656type SourceStateRow = { path : string ; hash : string ; mtime : number ; size : number } ;
3757
3858class SessionStartupCatchupHarness extends MemoryManagerSyncOps {
@@ -111,10 +131,27 @@ class SessionStartupCatchupHarness extends MemoryManagerSyncOps {
111131 return Array . from ( this . sessionsDirtyFiles ) ;
112132 }
113133
134+ getPendingSessionTargets ( ) : MemorySyncParams [ "sessions" ] {
135+ return Array . from ( this . sessionPendingTargets . values ( ) ) ;
136+ }
137+
138+ getPendingSessionFiles ( ) : string [ ] {
139+ return Array . from ( this . sessionPendingFiles ) ;
140+ }
141+
114142 isSessionsDirty ( ) : boolean {
115143 return this . sessionsDirty ;
116144 }
117145
146+ startTranscriptListener ( ) : void {
147+ this . ensureSessionListener ( ) ;
148+ }
149+
150+ stopTranscriptListener ( ) : void {
151+ this . sessionUnsubscribe ?.( ) ;
152+ this . sessionUnsubscribe = null ;
153+ }
154+
118155 protected computeProviderKey ( ) : string {
119156 return "test" ;
120157 }
@@ -162,6 +199,8 @@ describe("session startup catch-up", () => {
162199 } ) ;
163200
164201 afterEach ( async ( ) => {
202+ vi . clearAllTimers ( ) ;
203+ vi . useRealTimers ( ) ;
165204 vi . unstubAllEnvs ( ) ;
166205 await fs . rm ( stateDir , { recursive : true , force : true } ) ;
167206 } ) ;
@@ -356,4 +395,84 @@ describe("session startup catch-up", () => {
356395
357396 expect ( harness . indexedPaths ) . toEqual ( [ ] ) ;
358397 } ) ;
398+
399+ it ( "queues transcript update identity without requiring a session file" , async ( ) => {
400+ vi . useFakeTimers ( ) ;
401+ const harness = new SessionStartupCatchupHarness ( [ ] ) ;
402+ const originalSubscriber = ( globalThis as Record < symbol , unknown > ) [
403+ MEMORY_CORE_TRANSCRIPT_UPDATE_SUBSCRIBER_KEY
404+ ] ;
405+ let transcriptListener : ( ( update : MemorySessionTranscriptUpdate ) => void ) | undefined ;
406+ ( globalThis as Record < symbol , unknown > ) [ MEMORY_CORE_TRANSCRIPT_UPDATE_SUBSCRIBER_KEY ] = ( (
407+ listener ,
408+ ) => {
409+ transcriptListener = listener ;
410+ return ( ) => {
411+ if ( transcriptListener === listener ) {
412+ transcriptListener = undefined ;
413+ }
414+ } ;
415+ } ) satisfies MemoryTranscriptUpdateSubscriber ;
416+ harness . startTranscriptListener ( ) ;
417+
418+ try {
419+ transcriptListener ?.( {
420+ target : {
421+ agentId : "main" ,
422+ sessionId : "thread" ,
423+ sessionKey : "agent:main:thread" ,
424+ } ,
425+ } ) ;
426+
427+ expect ( harness . getPendingSessionTargets ( ) ) . toEqual ( [
428+ { agentId : "main" , sessionId : "thread" , sessionKey : "agent:main:thread" } ,
429+ ] ) ;
430+ } finally {
431+ harness . stopTranscriptListener ( ) ;
432+ if ( originalSubscriber === undefined ) {
433+ delete ( globalThis as Record < symbol , unknown > ) [
434+ MEMORY_CORE_TRANSCRIPT_UPDATE_SUBSCRIBER_KEY
435+ ] ;
436+ } else {
437+ ( globalThis as Record < symbol , unknown > ) [ MEMORY_CORE_TRANSCRIPT_UPDATE_SUBSCRIBER_KEY ] =
438+ originalSubscriber ;
439+ }
440+ }
441+ } ) ;
442+
443+ it ( "keeps canonical path transcript update compatibility" , async ( ) => {
444+ vi . useFakeTimers ( ) ;
445+ const session = await writeSessionFile ( "thread.jsonl" ) ;
446+ const harness = new SessionStartupCatchupHarness ( [ ] ) ;
447+ harness . startTranscriptListener ( ) ;
448+
449+ emitSessionTranscriptUpdate ( {
450+ sessionFile : session . filePath ,
451+ sessionKey : "agent:main:thread" ,
452+ } ) ;
453+
454+ expect ( harness . getPendingSessionFiles ( ) ) . toEqual ( [ session . filePath ] ) ;
455+ expect ( harness . getPendingSessionTargets ( ) ) . toEqual ( [ ] ) ;
456+ harness . stopTranscriptListener ( ) ;
457+ } ) ;
458+
459+ it ( "prefers transcript update path compatibility before identity" , async ( ) => {
460+ vi . useFakeTimers ( ) ;
461+ const session = await writeSessionFile ( "thread.jsonl" ) ;
462+ const harness = new SessionStartupCatchupHarness ( [ ] ) ;
463+ harness . startTranscriptListener ( ) ;
464+
465+ emitSessionTranscriptUpdate ( {
466+ sessionFile : session . filePath ,
467+ target : {
468+ agentId : "main" ,
469+ sessionId : "identity-target" ,
470+ sessionKey : "agent:main:identity-target" ,
471+ } ,
472+ } ) ;
473+
474+ expect ( harness . getPendingSessionFiles ( ) ) . toEqual ( [ session . filePath ] ) ;
475+ expect ( harness . getPendingSessionTargets ( ) ) . toEqual ( [ ] ) ;
476+ harness . stopTranscriptListener ( ) ;
477+ } ) ;
359478} ) ;
0 commit comments