refactor: DB-driven agent output events with single emission point

DB log chunk insertion is now the sole trigger for agent:output events.
Eliminates triple emission (FileTailer, handleStreamEvent, output buffer)
in favor of: FileTailer.onRawContent → DB insert → EventBus emit.

- createLogChunkCallback emits agent:output after successful DB insert
- spawnInternal now wires onRawContent callback (fixes session 1 gap)
- Remove eventBus from FileTailer (no longer touches EventBus)
- Remove eventBus from ProcessManager constructor (dead parameter)
- Remove agent:output emission from handleStreamEvent text_delta
- Remove outputBuffers map and all buffer helpers from manager/handler
- Remove getOutputBuffer from AgentManager interface and implementations
- getAgentOutput tRPC: DB-only, no file fallback
- onAgentOutput subscription: no initial buffer yield, events only
- AgentOutputViewer: accumulates raw JSONL chunks, parses uniformly
This commit is contained in:
Lukas May
2026-02-10 11:47:36 +01:00
parent 771cd71c1e
commit 06f443ebc8
15 changed files with 50 additions and 165 deletions

View File

@@ -10,7 +10,7 @@
| `manager.ts` | `MultiProviderAgentManager` — main orchestrator class |
| `process-manager.ts` | `AgentProcessManager` — worktree creation, command building, detached spawn |
| `output-handler.ts` | `OutputHandler` — JSONL stream parsing, completion detection, proposal creation, task dedup |
| `file-tailer.ts` | `FileTailer` — watches output files, emits line events |
| `file-tailer.ts` | `FileTailer` — watches output files, fires parser + raw content callbacks |
| `file-io.ts` | Input/output file I/O: frontmatter writing, signal.json reading, tiptap conversion |
| `markdown-to-tiptap.ts` | Markdown to Tiptap JSON conversion using MarkdownManager |
| `index.ts` | Public exports, `ClaudeAgentManager` deprecated alias |
@@ -36,14 +36,15 @@
4. `file-io.writeInputFiles()` — writes `.cw/input/` with assignment files (initiative, pages, phase, task) and read-only context dirs (`context/phases/`, `context/tasks/`)
5. Provider config builds spawn command via `buildSpawnCommand()`
6. `spawnDetached()` — launches detached child process with file output redirection
7. `FileTailer` watches output file, fires `onEvent` and `onRawContent` callbacks
8. `OutputHandler.handleStreamEvent()` processes each JSONL line
9. DB record updated with PID, output file path, session ID
10. `agent:spawned` event emitted
7. `FileTailer` watches output file, fires `onEvent` (parsed stream events) and `onRawContent` (raw JSONL chunks) callbacks
8. `onRawContent` → DB insert via `createLogChunkCallback()``agent:output` event emitted (single emission point)
9. `OutputHandler.handleStreamEvent()` processes parsed events (session tracking, result capture — no event emission)
10. DB record updated with PID, output file path, session ID
11. `agent:spawned` event emitted
### Completion Detection
1. `FileTailer` detects process exit
1. Polling detects process exit, `FileTailer.stop()` flushes remaining output
2. `OutputHandler.handleCompletion()` triggered
3. **Primary path**: Reads `.cw/output/signal.json` from agent worktree
4. Signal contains `{ status: "done"|"questions"|"error", result?, questions?, error? }`
@@ -87,11 +88,13 @@ Each provider config specifies: `command`, `args`, `resumeStyle`, `promptMode`,
The `OutputHandler` processes JSONL streams from Claude CLI:
- `text_delta` eventsaccumulated as text output, emitted via `agent:output`
- `init` event → session ID extracted
- `result` event → final result with structured data
- `init` event → session ID extracted and persisted
- `text_delta` eventsno-op in handler (output streaming handled by DB log chunks)
- `result` event → final result with structured data captured on `ActiveAgent`
- Signal file (`signal.json`) → authoritative completion status
**Output event flow**: `FileTailer.onRawContent()` → DB `insertChunk()``EventBus.emit('agent:output')`. This is the single emission point — no events from `handleStreamEvent()` or `processLine()`.
For providers without structured output, the generic line parser accumulates raw text.
## Credential Management
@@ -113,8 +116,11 @@ Stored as `credentials: {"claudeAiOauth":{"accessToken":"<token>"}}` and `config
## Log Chunks
Agent output is persisted to `agent_log_chunks` table:
- `onRawContent` callback fires for every output chunk
- Fire-and-forget DB insert (no FK to agents — survives deletion)
Agent output is persisted to `agent_log_chunks` table and drives all live streaming:
- `onRawContent` callback fires for every raw JSONL chunk from `FileTailer`
- DB insert → `agent:output` event emission (single source of truth for UI)
- No FK to agents — survives agent deletion
- Session tracking: spawn=1, resume=previousMax+1
- Read path concatenates all sessions for full output history
- Read path (`getAgentOutput` tRPC): concatenates all DB chunks (no file fallback)
- Live path (`onAgentOutput` subscription): listens for `agent:output` events
- Frontend: initial query loads from DB, subscription accumulates raw JSONL, both parsed via `parseAgentOutput()`

View File

@@ -62,10 +62,10 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
| getAgent | query | Single agent by name or ID |
| getAgentResult | query | Execution result |
| getAgentQuestions | query | Pending questions |
| getAgentOutput | query | Full output (DB chunks or file fallback) |
| getAgentOutput | query | Full output from DB log chunks |
| getActiveRefineAgent | query | Active refine agent for initiative |
| listWaitingAgents | query | Agents waiting for input |
| onAgentOutput | subscription | Live output stream + buffered history |
| onAgentOutput | subscription | Live raw JSONL output stream via EventBus |
### Tasks
| Procedure | Type | Description |