phase A: map the serve event stream, routed nowhere
The mapping half of the serve migration, written and pinned before anything depends on it,
so the switch-over is not also the moment the parsing turns out to be wrong. Nothing routes
through this — turns are still opencode run subprocesses, and the claude path is untouched.
The finding that matters: the serve publishes each turn TWICE, and reading the wrong one
makes it look like it cannot stream at all.
/api/session/{id}/event?after= durable, per session, replayable, durable.seq on every
event, whole values only, NO deltas
/api/event live, GLOBAL, ephemeral, carries text.delta and
tool.input.delta, no cursor
Same turn: 13 events durable, 21 live, the difference being 3 text.delta and 5
tool.input.delta. I probed the per-session one first and nearly recorded "no streaming" as
a fact — it would have removed the main reason to migrate. The split maps exactly onto what
officer already does for claude: durable to chat_session_events, live to UI deltas. The cost
is that the live stream is global, so a consumer must filter on sessionID.
tool:start is emitted on tool.called, not tool.input.started, because only tool.called has
the resolved input object — the input arrives as JSON fragments ({"comman) and a tool row
rendered with half-parsed arguments is worse than one that appears a moment later.
step.ended with finish tool-calls is a step boundary MID-turn, not the end of the turn, so
nothing terminal is emitted for it. Treating it as the end would cut every tool-using
conversation in half.
Fixtures are verbatim captures from 1.18.16. Replaying both real streams through the mapper
reconstructs the turn identically from each, with the reassembled deltas exactly equal to
the committed text and identical cost, and zero unrecognised events.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,15 +15,31 @@ works, it is verified end to end, and its limits are all consequences of that on
|
||||
|
||||
The serve's `/api/session/*` surface offers, and I have run each of these against 1.18.16:
|
||||
|
||||
| Capability | How | Verified |
|
||||
| ------------------------- | -------------------------------------------------------- | -------- |
|
||||
| Mid-turn injection | `POST /prompt` `{delivery: "steer"}` | yes — steered a running turn |
|
||||
| Queue behind a turn | `POST /prompt` `{delivery: "queue"}` | yes — "ONE" then "TWO", no errors |
|
||||
| Token streaming | `GET /event` → `text/event-stream` | yes — `text.started`/`text.ended` observed |
|
||||
| Reconnect + replay | same, `?after=<seq>` | yes — replayed a finished session |
|
||||
| Interrupt, session lives | `POST /interrupt` → 204 | endpoint only, not exercised |
|
||||
| Model selection | `POST /model` → 204 | yes — runs on the chosen model |
|
||||
| Images | `prompt.files` | not exercised (we have images via `--file` already) |
|
||||
| Capability | How | Verified |
|
||||
| ------------------------ | --------------------------------------------------- | --------------------------------------------------- |
|
||||
| Mid-turn injection | `POST /prompt` `{delivery: "steer"}` | yes — steered a running turn |
|
||||
| Queue behind a turn | `POST /prompt` `{delivery: "queue"}` | yes — "ONE" then "TWO", no errors |
|
||||
| Token streaming | `GET /api/event` (GLOBAL, live) — `text.delta` | yes — deltas reassemble to the committed text |
|
||||
| Reconnect + replay | `GET /api/session/{id}/event?after=<seq>` (durable) | yes — replayed a finished session |
|
||||
| Interrupt, session lives | `POST /interrupt` → 204 | endpoint only, not exercised |
|
||||
| Model selection | `POST /model` → 204 | yes — runs on the chosen model |
|
||||
| Images | `prompt.files` | not exercised (we have images via `--file` already) |
|
||||
|
||||
## There are TWO streams, and this is the thing to get right
|
||||
|
||||
Corrected after Phase A; the table above originally implied one. The serve publishes each turn twice:
|
||||
|
||||
- **`GET /api/session/{id}/event?after=<seq>`** — durable, per session, replayable, every event carrying
|
||||
`durable.seq`. Whole values only (`text.ended` with the full text). **No deltas.**
|
||||
- **`GET /api/event`** — live, **global**, ephemeral. Carries `text.delta` and `tool.input.delta`. No cursor.
|
||||
|
||||
Measured on one real turn: 13 events durable, 21 live, the difference being 3 `text.delta` and 5
|
||||
`tool.input.delta`. **Reading only the per-session stream — which is what I did first — makes it look
|
||||
like the serve cannot stream at all**, and would have quietly removed the main reason to migrate.
|
||||
|
||||
The split maps exactly onto what officer already does for Claude: durable → `chat_session_events`, live →
|
||||
UI deltas. The cost is that the live stream is GLOBAL, so a consumer must filter on `sessionID` and
|
||||
cannot assume it owns the socket.
|
||||
|
||||
## Facts that will bite whoever implements this
|
||||
|
||||
|
||||
Reference in New Issue
Block a user