From b3d270803f95db2467e20bb742aa42faf6867f91 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Fri, 12 Jun 2026 14:58:33 +0900 Subject: fix(turns): emit user prompt on the turn event stream (CR-3) A pure watcher (subscribed but not the sender) couldn't see the user prompt until the turn sealed: the user message was only persisted at seal and never entered the live/replayable stream. Add an additive TurnInputEvent {type:"user-message", conversationId, turnId, text} to the AgentEvent union and emit it via the broadcast/buffer path as the first event of every turn, so it is replayed to all subscribers (live + late-join) and on the HTTP path. Persistence and metrics unchanged; the union widening breaks no exhaustive switch. - @dispatch/wire 0.5.0->0.6.0; @dispatch/transport-contract 0.7.0->0.8.0 (re-export) - session-orchestrator: emit user-message at runTurnDetached start; +3 tests, 3 Wave-1 tests updated (user-message precedes turn-start) - FE courier: frontend-cr3-user-message-handoff.md Live-verified vs flash: watcher receives user-message (correct text) as its first chat.delta before turn-sealed. 894 vitest + transport bun green; tsc -b EXIT 0. --- packages/wire/package.json | 2 +- packages/wire/src/index.ts | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) (limited to 'packages/wire') diff --git a/packages/wire/package.json b/packages/wire/package.json index 80671cf..f4ede19 100644 --- a/packages/wire/package.json +++ b/packages/wire/package.json @@ -1,6 +1,6 @@ { "name": "@dispatch/wire", - "version": "0.5.0", + "version": "0.6.0", "type": "module", "private": true, "main": "dist/index.js", diff --git a/packages/wire/src/index.ts b/packages/wire/src/index.ts index 52662ef..28aab87 100644 --- a/packages/wire/src/index.ts +++ b/packages/wire/src/index.ts @@ -213,6 +213,7 @@ export interface TurnMetrics { export type AgentEvent = | StatusEvent | TurnStartEvent + | TurnInputEvent | TurnTextDeltaEvent | TurnReasoningDeltaEvent | TurnToolCallEvent @@ -238,6 +239,25 @@ export interface TurnStartEvent { readonly turnId: string; } +/** + * The user prompt that opened this turn, surfaced INTO the turn's outward event + * stream. The user message is persisted only when the turn seals (atomically with + * the assistant reply), so without this event a client that is merely WATCHING a + * conversation (subscribed but not the sender) has no source for the prompt text + * mid-turn — it would see the streaming reply with no preceding user bubble until + * seal. Emitted once, as the FIRST event of the turn (before `turn-start`), so it + * is buffered and replayed to every subscriber — live and late-join — exactly like + * the rest of the turn. The sender already echoes its own prompt optimistically, so + * a consumer should de-dup against that (e.g. by text); a pure watcher renders it + * directly. Carries the raw prompt `text` (the same text passed to the provider). + */ +export interface TurnInputEvent { + readonly type: "user-message"; + readonly conversationId: string; + readonly turnId: string; + readonly text: string; +} + /** Incremental text content from the model during a turn. */ export interface TurnTextDeltaEvent { readonly type: "text-delta"; -- cgit v1.2.3