(
workspaceId: string,
streaming: boolean,
update: ExtensionMetadataStreamingUpdate = {}
)
| 2447 | } |
| 2448 | |
| 2449 | private async updateStreamingStatus( |
| 2450 | workspaceId: string, |
| 2451 | streaming: boolean, |
| 2452 | update: ExtensionMetadataStreamingUpdate = {} |
| 2453 | ): Promise<void> { |
| 2454 | const streamGeneration = update.generation ?? this.streamingGenerations.get(workspaceId) ?? 0; |
| 2455 | try { |
| 2456 | let { hasTodos, todoStatus } = update; |
| 2457 | if (!streaming && (hasTodos === undefined || todoStatus === undefined)) { |
| 2458 | // Stop snapshots need an authoritative todo summary even for background workspaces, |
| 2459 | // and centralizing the read here preserves the fire-and-forget abort/error handlers. |
| 2460 | const sessionDir = this.config.getSessionDir(workspaceId); |
| 2461 | const todos = await readTodosForSessionDir(sessionDir); |
| 2462 | hasTodos ??= todos.length > 0; |
| 2463 | // When there are no todos to derive from, leave `todoStatus` undefined |
| 2464 | // so setStreaming doesn't touch the slot. AgentStatusService writes |
| 2465 | // its AI-generated summary into the same `todoStatus` field — passing |
| 2466 | // `null` here would clobber a freshly generated summary every time a |
| 2467 | // free-form (no-todo) turn ends. Explicit clears still happen via |
| 2468 | // setTodoStatus(null) when the agent calls `todo_write([])`. |
| 2469 | todoStatus ??= deriveTodoStatus(todos); |
| 2470 | } |
| 2471 | if ( |
| 2472 | !streaming && |
| 2473 | update.generation !== undefined && |
| 2474 | update.generation !== (this.streamingGenerations.get(workspaceId) ?? 0) |
| 2475 | ) { |
| 2476 | // A newer stream has started since this stop was initiated, so dropping the stale |
| 2477 | // streaming=false write preserves the active stream's metadata snapshot. |
| 2478 | return; |
| 2479 | } |
| 2480 | |
| 2481 | const snapshot = await this.extensionMetadata.setStreaming(workspaceId, streaming, { |
| 2482 | ...update, |
| 2483 | ...(todoStatus !== undefined ? { todoStatus } : {}), |
| 2484 | ...(hasTodos !== undefined ? { hasTodos } : {}), |
| 2485 | }); |
| 2486 | // Compaction tagging is stop-snapshot only. Never tag streaming=true updates, |
| 2487 | // otherwise fast follow-up turns can inherit stale compaction metadata before cleanup runs. |
| 2488 | const shouldTagCompaction = |
| 2489 | !streaming && this.compactionStreamGenerations.get(workspaceId) === streamGeneration; |
| 2490 | const shouldTagIdleCompaction = !streaming && this.idleCompactingWorkspaces.has(workspaceId); |
| 2491 | this.emitWorkspaceActivity( |
| 2492 | workspaceId, |
| 2493 | await this.mergeCurrentActiveWorkflowRunCount(workspaceId, { |
| 2494 | ...snapshot, |
| 2495 | ...(shouldTagCompaction ? { isCompaction: true } : {}), |
| 2496 | ...(shouldTagIdleCompaction ? { isIdleCompaction: true } : {}), |
| 2497 | }) |
| 2498 | ); |
| 2499 | } catch (error) { |
| 2500 | log.error("Failed to update workspace streaming status", { workspaceId, error }); |
| 2501 | } finally { |
| 2502 | // Compaction markers are turn-scoped. Always clear matching streaming=false |
| 2503 | // transitions, even when metadata writes fail, so stale state cannot leak into |
| 2504 | // future user streams. Match by generation so an old stop cannot clear a newer |
| 2505 | // compaction that started while the stop snapshot was doing async work. |
| 2506 | if (!streaming) { |
no test coverage detected