7.0 KiB
Streaming Event Logs And Replay Fixtures
Purpose
Streaming rendering bugs are hard to diagnose from screenshots or final messages. Provider integrations should be able to capture the exact event stream that produced a bad UI state, then turn that capture into a replayable test fixture.
This should be part of the opencode preparation work and should apply to existing providers as well as new ones.
Goals
- Capture raw provider events before normalization.
- Capture normalized
chat_eventrecords after provider mapping. - Capture legacy markers while the compatibility path still exists.
- Preserve event order with a monotonically increasing sequence number.
- Redact secrets and volatile machine-specific data by default.
- Make captured logs easy to promote into fixture tests.
- Support both live-stream reproduction and restored-history reproduction.
Non-Goals
- Do not log API keys, auth tokens, request headers, or raw environment variables.
- Do not always persist logs; event capture should be opt-in or scoped to debug mode.
- Do not make logs the primary storage format for user history.
- Do not require every provider to expose identical raw event payloads.
Capture Points
Each provider should support these capture stages where practical:
| Stage | Meaning | Examples |
|---|---|---|
native_in |
Raw provider event received by the bridge | opencode /event, Codex item.completed, Claude SDK event |
normalized_out |
Provider event after mapping to chat_event |
kind: tool, phase: completed |
legacy_out |
Compatibility marker emitted for current handlers | [CONTENT_DELTA], [MESSAGE] |
handler_in |
Java handler receives marker or event | callback line or parsed message |
render_in |
Webview accumulator receives event | optional frontend fixture capture |
For the first infrastructure pass, native_in, normalized_out, and legacy_out provide the highest value.
Log Format
Use newline-delimited JSON so logs can be streamed, sliced, redacted, and replayed without loading a large array.
Each line should be one envelope:
{
"schemaVersion": 1,
"captureId": "cap_2026_06_14_001",
"sequence": 42,
"timestamp": "2026-06-14T12:00:00.000Z",
"provider": "opencode",
"sessionId": "ses_123",
"turnId": "turn_1",
"stage": "normalized_out",
"eventType": "chat_event",
"payload": {
"type": "chat_event",
"kind": "tool",
"phase": "completed",
"toolCallId": "call_abc"
},
"redactions": []
}
Recommended envelope fields:
schemaVersion: log format version.captureId: stable ID shared by all lines from one captured run.sequence: monotonic integer assigned by the bridge capture utility.timestamp: ISO timestamp for diagnostics only; replay should usesequence.provider: provider ID such asclaude,codex, oropencode.sessionId: provider session/thread ID when known.turnId: normalized turn ID when known.stage: capture stage.eventType: native event type, marker name, orchat_event.payload: captured event payload after redaction.redactions: list of fields or patterns that were redacted.
Optional fields:
modelagentpermissionModecwdHashprojectHashsource: file/module/function that emitted the capture linenotes: short diagnostic hint added by the capture utility
Redaction Rules
Default redaction should remove or normalize:
- API keys, auth tokens, cookies, passwords, and request headers.
- Full environment maps.
- Absolute home-directory paths, replaced with stable placeholders such as
<HOME>. - Project root paths, replaced with
<PROJECT>. - Temporary directory paths, replaced with
<TMP>. - Large binary attachment payloads, replaced with metadata.
- Very large tool outputs, truncated with original length preserved.
Text content should not be blindly removed because rendering bugs often depend on exact text, whitespace, or ordering. Instead, fixture promotion should allow an explicit sanitization step when logs contain private content.
Replay Modes
Captured logs should support at least two replay modes:
native_inreplay: feed raw provider events into the provider normalizer and assert producedchat_eventrecords.normalized_outreplay: feedchat_eventrecords into the frontend accumulator and assert render groups.
Legacy marker replay can remain useful during migration:
legacy_outreplay: feed compatibility markers into current Java/webview handlers and assert no regression.
Fixture Promotion
A captured log should be convertible into a test fixture by removing volatile fields and keeping ordered payloads.
Suggested workflow:
- Enable streaming event capture for a provider run.
- Reproduce the rendering issue.
- Save the captured JSONL file.
- Run a fixture sanitizer that removes timestamps, machine paths, capture IDs, and private text when needed.
- Check in the minimized fixture under provider-specific tests.
- Add a regression test that replays the fixture into the normalizer or accumulator.
Example fixture shape:
{
"name": "opencode-tool-result-before-text-boundary",
"provider": "opencode",
"source": "streaming-log",
"events": [
{
"sequence": 1,
"stage": "native_in",
"eventType": "message.part.delta",
"payload": { "field": "text", "text": "Running" }
},
{
"sequence": 2,
"stage": "normalized_out",
"eventType": "chat_event",
"payload": { "kind": "text", "phase": "delta", "text": "Running" }
}
]
}
Provider Requirements
Existing and new providers should use the same capture utility rather than ad hoc console.log debugging.
Minimum requirement for each provider:
- Capture raw incoming stream events at
native_in. - Capture emitted
chat_eventrecords atnormalized_out. - Capture emitted compatibility markers at
legacy_outwhile those markers exist. - Share the same redaction utility.
- Include capture tests that prove logs can be replayed or promoted into fixtures.
Opencode-Specific Notes
For opencode, capture should start before session creation or prompt submission because /event is shared and the bridge subscribes before sending prompts.
Useful raw events to capture:
message.part.deltamessage.part.updatedmessage.updatedpermission.askedpermission.repliedquestion.askedsession.diffsession.error
Useful metadata to preserve:
- opencode
sessionID - message ID
- part ID
- tool call ID when available
- permission/question request ID
- child session ID for task/subagent runs
Acceptance Checks
- A shared capture envelope schema is documented and versioned.
- Capture can be enabled for Claude, Codex, and opencode without changing provider logic.
- Captured logs contain raw provider events, normalized
chat_eventrecords, and legacy markers during migration. - Captured logs are redacted by default.
- A captured log can be sanitized into a deterministic fixture.
- At least one regression test replays a captured fixture into the normalizer or frontend accumulator.