(workspaceId: string)
| 2524 | } |
| 2525 | |
| 2526 | private async handleStreamCompletion(workspaceId: string): Promise<void> { |
| 2527 | const generation = this.streamingGenerations.get(workspaceId) ?? 0; |
| 2528 | const isIdleCompaction = this.idleCompactingWorkspaces.has(workspaceId); |
| 2529 | |
| 2530 | // Note: idle-compaction success/failure is reported from onIdleCompactionOutcome |
| 2531 | // (after the summary is actually persisted), not here — a clean provider stream-end |
| 2532 | // does not guarantee the post-stream history compaction succeeded. |
| 2533 | |
| 2534 | // Idle compaction is maintenance work, so preserve the pre-existing recency. |
| 2535 | // That keeps the workspace from jumping to the top of the sidebar and also |
| 2536 | // prevents the background activity path from treating compaction as a fresh response. |
| 2537 | |
| 2538 | if (!isIdleCompaction) { |
| 2539 | // Always use Date.now() for stream-completion recency. |
| 2540 | // extractTimestamp() returns the message-creation timestamp from stream |
| 2541 | // metadata, which is effectively the same as the sendMessage recency and |
| 2542 | // can lose the race against the frontend's lastRead (set via Date.now() |
| 2543 | // after the IPC round-trip). Using a fresh timestamp here ensures the |
| 2544 | // completion recency is strictly after any earlier lastRead write. |
| 2545 | await this.updateRecencyTimestamp(workspaceId, Date.now()); |
| 2546 | } |
| 2547 | |
| 2548 | await this.stopStreamingStatus(workspaceId, generation); |
| 2549 | // Goal mutations are drained by AgentSession after stream accounting. Doing |
| 2550 | // it here races with per-session stream-end listeners because EventEmitter |
| 2551 | // does not await async handlers. |
| 2552 | } |
| 2553 | |
| 2554 | private createInitLogger(workspaceId: string) { |
| 2555 | const hasInitState = () => this.initStateManager.getInitState(workspaceId) !== undefined; |
no test coverage detected