@@ -6,6 +6,7 @@ import type { Config } from "@/config";
66import type { AIService } from "@/services/aiService" ;
77import type { HistoryService } from "@/services/historyService" ;
88import type { PartialService } from "@/services/partialService" ;
9+ import type { InitStateManager } from "@/services/initStateManager" ;
910import type { WorkspaceMetadata } from "@/types/workspace" ;
1011import type { WorkspaceChatMessage , StreamErrorMessage , SendMessageOptions } from "@/types/ipc" ;
1112import type { SendMessageError } from "@/types/errors" ;
@@ -36,6 +37,7 @@ interface AgentSessionOptions {
3637 historyService : HistoryService ;
3738 partialService : PartialService ;
3839 aiService : AIService ;
40+ initStateManager : InitStateManager ;
3941}
4042
4143export class AgentSession {
@@ -44,14 +46,18 @@ export class AgentSession {
4446 private readonly historyService : HistoryService ;
4547 private readonly partialService : PartialService ;
4648 private readonly aiService : AIService ;
49+ private readonly initStateManager : InitStateManager ;
4750 private readonly emitter = new EventEmitter ( ) ;
4851 private readonly aiListeners : Array < { event : string ; handler : ( ...args : unknown [ ] ) => void } > =
4952 [ ] ;
53+ private readonly initListeners : Array < { event : string ; handler : ( ...args : unknown [ ] ) => void } > =
54+ [ ] ;
5055 private disposed = false ;
5156
5257 constructor ( options : AgentSessionOptions ) {
5358 assert ( options , "AgentSession requires options" ) ;
54- const { workspaceId, config, historyService, partialService, aiService } = options ;
59+ const { workspaceId, config, historyService, partialService, aiService, initStateManager } =
60+ options ;
5561
5662 assert ( typeof workspaceId === "string" , "workspaceId must be a string" ) ;
5763 const trimmedWorkspaceId = workspaceId . trim ( ) ;
@@ -62,8 +68,10 @@ export class AgentSession {
6268 this . historyService = historyService ;
6369 this . partialService = partialService ;
6470 this . aiService = aiService ;
71+ this . initStateManager = initStateManager ;
6572
6673 this . attachAiListeners ( ) ;
74+ this . attachInitListeners ( ) ;
6775 }
6876
6977 dispose ( ) : void {
@@ -75,6 +83,10 @@ export class AgentSession {
7583 this . aiService . off ( event , handler as never ) ;
7684 }
7785 this . aiListeners . length = 0 ;
86+ for ( const { event, handler } of this . initListeners ) {
87+ this . initStateManager . off ( event , handler as never ) ;
88+ }
89+ this . initListeners . length = 0 ;
7890 this . emitter . removeAllListeners ( ) ;
7991 }
8092
@@ -121,13 +133,15 @@ export class AgentSession {
121133 private async emitHistoricalEvents (
122134 listener : ( event : AgentSessionChatEvent ) => void
123135 ) : Promise < void > {
136+ // Load chat history (persisted messages from chat.jsonl)
124137 const historyResult = await this . historyService . getHistory ( this . workspaceId ) ;
125138 if ( historyResult . success ) {
126139 for ( const message of historyResult . data ) {
127140 listener ( { workspaceId : this . workspaceId , message } ) ;
128141 }
129142 }
130143
144+ // Check for interrupted streams (active streaming state)
131145 const streamInfo = this . aiService . getStreamInfo ( this . workspaceId ) ;
132146 const partial = await this . partialService . readPartial ( this . workspaceId ) ;
133147
@@ -137,6 +151,13 @@ export class AgentSession {
137151 listener ( { workspaceId : this . workspaceId , message : partial } ) ;
138152 }
139153
154+ // Replay init state BEFORE caught-up (treat as historical data)
155+ // This ensures init events are buffered correctly by the frontend,
156+ // preserving their natural timing characteristics from the hook execution.
157+ await this . initStateManager . replayInit ( this . workspaceId ) ;
158+
159+ // Send caught-up after ALL historical data (including init events)
160+ // This signals frontend that replay is complete and future events are real-time
140161 listener ( {
141162 workspaceId : this . workspaceId ,
142163 message : { type : "caught-up" } ,
@@ -405,7 +426,35 @@ export class AgentSession {
405426 this . aiService . on ( "error" , errorHandler as never ) ;
406427 }
407428
408- private emitChatEvent ( message : WorkspaceChatMessage ) : void {
429+ private attachInitListeners ( ) : void {
430+ const forward = ( event : string , handler : ( payload : WorkspaceChatMessage ) => void ) => {
431+ const wrapped = ( ...args : unknown [ ] ) => {
432+ const [ payload ] = args ;
433+ if (
434+ typeof payload === "object" &&
435+ payload !== null &&
436+ "workspaceId" in payload &&
437+ ( payload as { workspaceId : unknown } ) . workspaceId !== this . workspaceId
438+ ) {
439+ return ;
440+ }
441+ // Strip workspaceId from payload before forwarding (WorkspaceInitEvent doesn't include it)
442+ const { workspaceId : _ , ...message } = payload as WorkspaceChatMessage & {
443+ workspaceId : string ;
444+ } ;
445+ handler ( message as WorkspaceChatMessage ) ;
446+ } ;
447+ this . initListeners . push ( { event, handler : wrapped } ) ;
448+ this . initStateManager . on ( event , wrapped as never ) ;
449+ } ;
450+
451+ forward ( "init-start" , ( payload ) => this . emitChatEvent ( payload ) ) ;
452+ forward ( "init-output" , ( payload ) => this . emitChatEvent ( payload ) ) ;
453+ forward ( "init-end" , ( payload ) => this . emitChatEvent ( payload ) ) ;
454+ }
455+
456+ // Public method to emit chat events (used by init hooks and other workspace events)
457+ emitChatEvent ( message : WorkspaceChatMessage ) : void {
409458 this . assertNotDisposed ( "emitChatEvent" ) ;
410459 this . emitter . emit ( "chat-event" , {
411460 workspaceId : this . workspaceId ,
0 commit comments