qwen-code/docs/developers/daemon/09-event-schema.md
Heyang Wang 9f8f65dde0
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 1/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 2/3 (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none - shard 3/3 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 1/2 (push) Waiting to run
E2E Tests / E2E Test - macOS - shard 2/2 (push) Waiting to run
E2E Tests / channel-plugin E2E (nightly) (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
Security Checks / Dependency CVE audit (push) Waiting to run
Security Checks / Secret scan (TruffleHog) (push) Waiting to run
feat: support fork from any conversation (#8817)
* feat(web-shell): branch from completed assistant responses

Add durable response checkpoints so Web Shell sessions can branch from
eligible completed Assistant turns without mutating the source history.

- Record and validate checkpoints behind serialized topology fences
- Preserve historical anchors through replay, daemon, SDK, and UI layers
- Publish bounded forks with crash-safe ownership and referenced backups
- Serialize prompt, rewind, branch, automatic turn, and close mutations
- Cover stale anchors, replay pagination, cleanup, and pending UI states

Note: Responses recorded before this change remain non-branchable.

# Conflicts:
#	packages/acp-bridge/src/bridge.ts
#	packages/acp-bridge/src/bridgeTypes.ts
#	packages/cli/src/acp-integration/acpAgent.test.ts
#	packages/cli/src/acp-integration/acpAgent.ts
#	packages/cli/src/serve/routes/session.ts
#	packages/cli/src/serve/server.test.ts
#	packages/core/src/services/chatRecordingService.ts
#	packages/core/src/services/sessionService.test.ts
#	packages/core/src/services/sessionService.ts
#	packages/sdk-typescript/src/daemon/DaemonClient.ts
#	packages/web-shell/client/components/MessageItem.tsx
#	packages/web-shell/client/components/MessageList.tsx

* fix(session): preserve historical branch checkpoints

Keep Assistant-response branching intact across the daemon stack after
rebases, including history serialization and persisted-session ownership.

- Forward durable checkpoint IDs through Bridge, SDK, and UI layers
- Serialize live history mutations and retain valid nested branch anchors
- Preserve persisted branches during generation cleanup
- Add cross-layer regression tests for replay and stale checkpoints

* fix(web-shell): harden response session branching

* chore: remove PR comment evaluation artifact

Keep the PR review report as a local ignored backup instead of
shipping it with the feature branch.

- Remove the generated PR comment evaluation from tracked files
- Preserve the report under the ignored analyze directory

* fix(web-shell): guard historical branch mutations

Historical branch requests could outlive the client timeout during an
active turn, and interactive forks lacked the recorder's cross-process
writer-lease barrier.

- Hide Assistant Branch actions while a turn is active
- Run interactive fork creation inside the recorder write barrier
- Use the concrete checkpoint recorder contract in Session
- Document committed-session ownership and implemented design status

* perf(core): index historical branch points during transcript scan

Build branch catalogs during the frozen index scan so the first history
page no longer reopens and materializes the complete active chain.

- Retain a compact projection for shared branch-point resolution
- Correlate live branch anchors with the completed prompt and final reply
- Complete recorder mocks required by the concrete Session contract
- Update the reviewed design with performance and correlation invariants

* fix(core): address review findings — dead code, boundary remap, promptId guard, stale toast (#8274)

* fix(core): address review findings — dead code, boundary remap, promptId guard, stale toast (#8274)

* fix(core): address review findings — archived GC, subtype registration, UUID validation, dead code (#8274)

* test: strengthen branch-point and fork coverage from review (#8274)

Add focused tests requested in PR review:
- branch catalog resolves checkpoints that fall on a later page
- accept a parallel tool batch closed within a single turn
- exercise the linkSync->copyFileSync fork backup fallback success path
- prove a remapped checkpoint stays usable via a nested fork
- isolate each branch-point validation conjunct across bridge and SDK

* fix: address round-4 review feedback for session branching (#8274)

- Make the directory-fsync durability test platform-aware (skip on win32),
  since fsyncDirectoryBestEffort swallows the injected error on Windows and
  the rejection path is non-Windows by design.
- Reject atRecordId on the side-task fork path instead of silently discarding
  it, so the API surface no longer implies acceptance.
- Correct the design doc: name the real promptQueue FIFO (not the nonexistent
  historyMutationQueue) and describe filtered checkpoint boundaries as
  remapped to the nearest retained predecessor, not unconditionally null.
- Add focused tests: branch-point assistantRecordUuid mismatch rejection, and
  insight-block branchRecordId anchoring (insight-only block must not anchor
  onto the previous reply).

* fix: address round-5 review feedback for session branching (#8274)

* fix: address round-6 review feedback for session branching (#8274)

* fix: address round-7 review feedback for session branching (#8274)

* fix(core): harden branch-point resolution against malformed transcript shapes (#8274)

- Filter null/non-object part elements in the shared branch resolver so a
  transcript containing null parts no longer makes forkSession throw a
  TypeError for every checkpoint.
- Tag tool calls carried in from the pre-boundary prefix so a dangling call
  left by a crashed turn no longer permanently disables checkpoint
  recording; only calls issued inside the turn must close.
- Merge duplicate-uuid records first-wins for identity fields in the
  transcript reader, matching the byUuid index and fork aggregation, so the
  reader never advertises a branch marker the fork path must reject.

* fix: address round-8 review feedback for session branching (#8274)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address round-9 review feedback for session branching (#8274)

* test(core): pin branch GC isolation from throwing warning callbacks (#8274)

* fix(acp-bridge): reject rewind at admission while a prompt is active (#8274)

* fix(web-shell): harden session branch publication

Preserve direct ACP prompt preemption while fencing branch and rewind history mutations at the Session boundary. Convert branch publication, backup staging, cleanup, and stale-claim GC to asynchronous filesystem APIs, and surface unsupported hard-link commits as typed ACP and HTTP errors. Expand regression coverage and update the reviewed design contract.

* fix(web-shell): harden historical response branching

Reject branches during prompt admission and keep dispatched mutations
owned until their real outcome is known.

- remove detached timeouts across ACP, SDK, and WebUI
- bound branch cleanup and make title scans asynchronous
- avoid full branch-point scans during transcript pagination
- add regression coverage from Core through the real daemon and browser

* fix: address round-11 review feedback for session branching (#8274)

* fix: address round-12 review feedback for session branching (#8274)

* refactor(branching): remove branch-specific overdesign

Simplify historical session branching around the minimum persistence,
recording, and navigation invariants required by the Web Shell flow.

- Replace branch claims and garbage collection with staged publication
- Validate completed turns incrementally instead of reloading transcripts
- Separate persisted branch creation from live session restoration
- Bound SDK waits and prevent late results from replacing navigation
- Remove unused checkpoint prompt IDs while reading legacy records

Note: A pre-commit crash may leave hidden staging or orphan backups.

* chore(sdk): update browser bundle budget

Account for the combined historical branching and transcript projection APIs after merging main while keeping the browser bundle size guard narrowly bounded.

* fix(branching): address review lifecycle gaps

Harden historical session branching against cancellation, observer,
navigation, and shutdown races found during review.

- Normalize cancellation keys and bound close-time mutation waits
- Preserve anchors after observer completion and load persisted forks
- Report success only when the guarded session switch starts
- Cover recorder cursors, fork cleanup, admission, and rollback
- Align daemon events and branch errors with runtime behavior

* refactor(session): simplify branching safeguards

Reduce the session branching surface after review while preserving the
critical concurrency, durability, and ownership guarantees.

- Remove the unused full-chain resolver and test production entry points
- Copy backups from verified open handles instead of using hard links
- Reuse the bounded title scan instead of maintaining an async mirror
- Deduplicate UI branch requests and fail fast for busy automatic turns
- Consolidate repeated mutation tests and retain critical race coverage
- Document the retained invariants and rejected overdesign explicitly

* test(branching): simplify regression coverage

Reduce duplicated branching tests while retaining regression coverage for
the safety, concurrency, and lifecycle fixes introduced by this feature.

- Consolidate symmetric bridge and agent scenarios with table-driven cases
- Remove repeated cross-layer assertions and brittle implementation spies
- Drop redundant UI permutations and branch-only visual snapshots

* fix(serve): handle branch busy admission

* fix(sdk): preserve v1 branch session contract

Keep existing latest-state branch callers source- and wire-compatible while
retaining the persisted-only behavior for historical checkpoint branches.

- Restore no-anchor branches before returning their live client identity
- Add a separate typed result for persisted historical branch requests
- Clean up restored attachments on stale navigation and disconnect races
- Cover immediate continuation and historical persistence independently

* fix(daemon): guard branching history mutations

Prevent branch creation and automatic Goal turns from racing session
teardown or interactive history mutations.

- Reject branch admission while a conditional close is authorized
- Serialize Goal continuations behind the history mutation gate
- Limit branch checkpoints to interactive prompts
- Add regressions for close and Goal scheduling races

* fix(branching): preserve fork and checkpoint semantics

Keep branch checkpoints and file-history snapshots correct across resumed,
forked, and non-interactive session flows.

- Track the restored active-chain base before the first appended turn
- Preserve backup file modes during fork publication
- Exclude authenticated channel prompts from checkpoint recording
- Add regressions for all three review failures

* fix(branching): harden branch and rewind behavior

Handle the remaining branch and rewind review findings without widening
the feature contract.

- Ignore benign concurrent branch rejections in the Web Shell
- Validate rewind prompt IDs before using string operations
- Pin mutation ordering, cleanup, compaction, and checkpoint invariants
- Align sourced-fork fixtures with the canonical side_task value

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-08-15 10:01:49 +00:00

37 KiB

Typed Daemon Event Schema v1

Overview

Every SSE frame emitted by the daemon on GET /session/:id/events has the shape { id, v, type, data, originatorClientId?, _meta? }. v: 1 is the current EVENT_SCHEMA_VERSION. type comes from the closed, version-pinned DAEMON_KNOWN_EVENT_TYPE_VALUES set in packages/sdk-typescript/src/daemon/events.ts. The envelope _meta field is stamped at the SSE write boundary by formatSseFrame() in packages/cli/src/serve/routes/sse-events.ts; see Envelope-level metadata.

The SDK exposes asKnownDaemonEvent(evt). It returns a discriminated KnownDaemonEvent for known event types and undefined for other types. SDK consumers can therefore handle forward compatibility without requiring a lockstep SDK upgrade when a newer daemon adds an event type; the session reducer records those as unrecognizedKnownEventCount.

The wire format lives in ../qwen-serve-protocol.md. This page is the payload contract for each event.

Responsibilities

  • Provide the single source of truth for the event vocabulary (DAEMON_KNOWN_EVENT_TYPE_VALUES).
  • Provide a typed envelope for each event type (DaemonEventEnvelope<TType, TData>).
  • Provide pure reducers (reduceDaemonSessionEvent, reduceDaemonAuthEvent) that project an event stream into SDK view state.
  • Broadcast the typed_event_schema capability tag as an informational signal. If the tag is absent, asKnownDaemonEvent still falls back to unknown.

Event vocabulary

Grouped by domain.

Core session

Type Direction Trigger Key payload fields
session_update S->C Any ACP sessionUpdate notification: agent text, thought, tool call, or plan sessionUpdate: string, content?: ... (opaque ACP shape)
session_metadata_updated S->C PATCH /session/:id/metadata sessionId, displayName?
session_died S->C terminal channel.exited sessionId, reason, exitCode? | null, signalCode? | null
session_closed S->C terminal DELETE /session/:id or programmatic close sessionId, reason: 'client_close' | string, closedBy?
session_snapshot S->C synthetic Snapshot frame after SSE attach / replay sessionId, currentModelId: string | null, currentApprovalMode: string | null, recordingDegraded: boolean
session_recording_degraded S->C The session transcript writer permanently stopped after an asynchronous write failure sessionId, reason: 'write_failed'

Subscriber-level synthetic frames

Type Trigger Notes
client_evicted Per-subscriber EventBus queue overflow. No id reason: 'queue_overflow' | 'queue_bytes_overflow' | string, droppedAfter?: number, queueSize?: number, maxQueued?: number, queuedBytes?: number, maxQueuedBytes?: number, eventBytes?: number; terminal only for the current subscriber, while the session remains alive.
slow_client_warning Live frame backlog or live serialized-byte backlog >= 75%; force-pushed and has no id queueSize, maxQueued, lastEventId, queuedBytes?, maxQueuedBytes?, threshold?: 'frames' | 'bytes' | 'frames_and_bytes'; re-armed after both frame and byte measurements drop below 37.5%.
stream_error SubscriberLimitExceededError or another route stream error error: string; terminal for the subscription.
state_resync_required subscribe({lastEventId}) detects that the daemon ring no longer holds [lastEventId+1, earliestInRing-1], or the client cursor is from a previous bus epoch. Force-pushed before remaining replay frames and has no id. reason: 'ring_evicted' | 'epoch_reset' | string, lastDeliveredId: number, earliestAvailableId: number. This is a recovery signal, not terminal: the SSE stream stays open and replay + live frames continue. The SDK reducer sets awaitingResync = true and skips deltas until the caller resets with loadSession.
history_truncated POST /session/:id/load returns a bounded replay snapshot after older in-memory replay entries were dropped. Prepended to compactedReplay and has no id. reason: 'replay_window_exceeded', truncatedEvents: number, retainedEvents: number, maxBytes: number, truncatedTurns?: number, fullTranscriptAvailable: boolean. This is a status marker, not a resync request; clients render it and continue applying retained replay.
replay_complete Id-less sentinel emitted after the Last-Event-ID replay loop finishes, for both clean replay and ring-evicted paths, even when data.replayedCount === 0. No id replayedCount: number; lets consumers remove catch-up UI deterministically without a timeout.

fullTranscriptAvailable is a boolean capability flag, not a literal true type. Current daemons emit true when /session/:id/transcript can be used to page the persisted transcript; older or constrained daemons may emit false, and clients should keep rendering the bounded replay normally.

Permissions (F3 + base)

Type Direction Trigger Key payload fields
permission_request S->C Agent calls requestPermission requestId, sessionId, toolCall, options[]; the envelope stamps originatorClientId from the prompt originator.
permission_resolved S->C Mediator has decided requestId, outcome (ACP PermissionOutcome)
permission_already_resolved S->C Vote arrives after the request was already decided requestId, sessionId, outcome
permission_partial_vote S->C consensus policy records a non-final vote requestId, sessionId, votesReceived, votesNeeded (>= 1), quorum, optionTallies: Record<string, number>, originatorClientId?
permission_forbidden S->C Policy rejects a vote requestId, sessionId, clientId?, reason: 'designated_mismatch' | 'remote_not_allowed', originatorClientId?; anonymous voters omit clientId.

Models

Type Direction Payload
model_switched S->C sessionId, modelId
model_switch_failed S->C sessionId, requestedModelId, error: string

MCP guardrails (PR 14b + F2)

Type Direction Payload
mcp_budget_warning S->C liveCount, reservedCount, budget, thresholdRatio: 0.75, mode: 'warn' | 'enforce', scope?: 'workspace' | 'session'
mcp_child_refused_batch S->C refusedServers: [{ name, transport, reason: 'budget_exhausted' }], budget, liveCount, reservedCount, mode: 'enforce', scope?: 'workspace' | 'session'
mcp_server_restarted S->C serverName, durationMs, entryIndex? for F2 multi-entry pool restarts
mcp_server_restart_refused S->C serverName, reason: 'budget_would_exceed' | 'in_flight' | 'disabled' | 'restart_failed', entryIndex?, details?. The fourth value, restart_failed, carries an underlying hard failure for pool-mode multi-entry restart. MCP_RESTART_REFUSED_REASONS rejects unknown reasons; an older SDK reducer silently drops additive new reason values because parseDaemonEvent returns undefined. Ship a new reason with an SDK that knows it.

Mutation control (Wave 4 PR 16+17)

Type Direction Payload
memory_changed S->C File memory: scope: 'workspace' | 'global', filePath, mode, bytesWritten; managed memory: scope: 'managed', source, taskId, touchedScopes
agent_changed S->C change: 'created' | 'updated' | 'deleted', name, level: 'project' | 'user'
approval_mode_changed S->C sessionId, previous, next, persisted: boolean
tool_toggled S->C toolName, enabled; affects the next ACP child spawn and does not mutate already-running sessions.
settings_changed S->C Workspace settings write completed. Payload is open; consumers should refresh with read-after-write.
settings_reloaded S->C Daemon workspace service reread settings. Payload is open.
trust_change_requested S->C workspaceCwd, desiredState: 'trusted' | 'untrusted', reason?
workspace_initialized S->C path, action: 'created' | 'overwrote' | 'noop', originatorClientId?
github_setup_completed S->C releaseTag, readmeUrl, secretsUrl?, workflows: [{path, status, sizeBytes?, error?}], gitignore: {path, status, added?, error?}

memory_changed also covers sessionless managed-memory tasks. For those payloads, scope is "managed", source is one of "workspace_memory_remember", "workspace_memory_forget", or "workspace_memory_dream", taskId is the queued task id, and touchedScopes lists the managed memory scopes that changed ("user" and/or "project"). No event is emitted when a remember/forget/dream task completes without touching managed memory.

Auth device flow (PR 21)

These events are workspace-keyed, not session-keyed. The session reducer treats them as no-ops; reduceDaemonAuthEvent projects them into workspace-level state.

Type Direction Payload
auth_device_flow_started S->C deviceFlowId, providerId, expiresAt
auth_device_flow_throttled S->C deviceFlowId, intervalMs
auth_device_flow_authorized S->C deviceFlowId, providerId, expiresAt?, accountAlias?
auth_device_flow_failed S->C deviceFlowId, errorKind, hint?
auth_device_flow_cancelled S->C deviceFlowId

MCP runtime mutation

Type Direction Trigger Key payload fields
mcp_server_added S->C Server added at runtime through POST /workspace/mcp/servers name, transport, replaced, shadowedSettings, toolCount, originatorClientId
mcp_server_removed S->C Server removed at runtime name, wasShadowingSettings, originatorClientId

Extensions lifecycle

Type Direction Trigger Key payload fields
extensions_changed S->C Background extension install/refresh work completed or status change refreshed, failed, status?: 'installed' | 'enabled' | 'disabled' | 'updated' | 'uninstalled' | 'failed', source?, name?, version?, error?

Mid-turn message injection

Type Direction Trigger Key payload fields
mid_turn_message_injected S->C Web-shell or remote client injected messages into a running turn via POST /session/:id/inject sessionId, messages: string[], originatorClientId?; consumers MUST compare originatorClientId to their own id before deduping.

Turn lifecycle / assistant pushes

Type Direction Trigger Key payload fields
prompt_cancelled S->C Prompt was cancelled through explicit cancelSession route or originator SSE disconnect Envelope stamps originatorClientId for the canceling client. This means "cancellation requested", not "cancellation confirmed". Peer subscribers learn that the prompt has ended.
turn_complete S->C A turn completed successfully sessionId, stopReason, promptId?, branchPoint?. promptId links to non-blocking prompt responses (202). Eligible completed turns include branchPoint: { assistantRecordUuid, checkpointUuid }.
turn_error S->C A turn failed sessionId, message, code?, promptId?; same promptId correlation mechanism.
session_rewound S->C POST /session/:id/rewind succeeded sessionId, promptId, targetTurnIndex, filesChanged[], filesFailed[], originatorClientId?
session_branched S->C Legacy compatibility event; the current branch endpoint returns its result directly and does not publish this event sourceSessionId, newSessionId, displayName, originatorClientId?. Readers retain support for older producers.
followup_suggestion S->C ACP child generated ghost-text follow-up suggestions after end_turn, forwarded over per-session SSE sessionId, suggestion, promptId; wire only carries suggestions whose getFilterReason()===null. Clients render them as input-placeholder ghost text and invalidate them on next sendPrompt.
user_shell_command S->C User started a shell command through POST /session/:id/shell; fanned out to other subscribers in the same session sessionId, command, shellId, originatorClientId?. There is no typed DaemonXxxData interface yet; asKnownDaemonEvent returns undefined and the UI normalizer parses it ad hoc.
user_shell_result S->C Result of the shell command above sessionId, shellId, exitCode, output, aborted. Same ad hoc parsing note as user_shell_command.

Architecture

Concern Source Notes
EVENT_SCHEMA_VERSION = 1 packages/acp-bridge/src/eventBus.ts Sent on every frame.
DAEMON_KNOWN_EVENT_TYPE_VALUES packages/sdk-typescript/src/daemon/events.ts Closed list with 53 types.
DaemonEventEnvelope<TType, TData> events.ts Generic envelope.
DaemonKnownEventType events.ts typeof DAEMON_KNOWN_EVENT_TYPE_VALUES[number].
Per-event payload types events.ts Most event types have a DaemonXxxData interface; user_shell_* is currently parsed ad hoc by the UI normalizer.
asKnownDaemonEvent(evt) events.ts Returns KnownDaemonEvent | undefined.
reduceDaemonSessionEvent(state, evt) events.ts Projects into DaemonSessionViewState.
reduceDaemonAuthEvent(state, evt) events.ts Projects into DaemonAuthState.
isWorkspaceScopedBudgetEvent(evt) events.ts Detects F2 scope: 'workspace'.

DaemonSessionViewState

reduceDaemonSessionEvent fills this view state. CLI TUI adapter, DaemonChannelBridge, and VS Code IDE consume it. Key fields:

  • alive: boolean - becomes false after a terminal frame (session_died, session_closed, client_evicted, stream_error).
  • currentModelId?: string - from model_switched.
  • displayName?: string - from session_metadata_updated.
  • recordingDegraded: boolean - sticky session recording state from session_recording_degraded; an explicit session_snapshot.recordingDegraded value is authoritative.
  • pendingPermissions: Record<string, DaemonPermissionRequestData> - open requests keyed by requestId; cleared by permission_resolved / permission_already_resolved.
  • lastSessionUpdate?: DaemonSessionUpdateData - latest session_update.
  • lastModelSwitchFailure?: DaemonModelSwitchFailedData - from model_switch_failed.
  • terminalEvent? - raw terminal event.
  • streamError?: DaemonStreamErrorData - latest stream_error payload.
  • unrecognizedKnownEventCount, lastUnrecognizedKnownEvent? - event was recognized by asKnownDaemonEvent but the reducer has no dedicated state for it yet.
  • droppedPermissionRequestCount, lastDroppedPermissionRequestId? - malformed permission request could not enter the pending map.
  • unmatchedPermissionResolutionCount, lastUnmatchedPermissionResolutionId? - permission resolution had no matching pending request.
  • slowClientWarningCount, lastSlowClientWarning? - from slow_client_warning.
  • mcpBudgetWarningCount, lastMcpBudgetWarning? - from mcp_budget_warning.
  • mcpChildRefusedBatchCount, lastMcpChildRefusedBatch? - from mcp_child_refused_batch.
  • lastWorkspaceMutation?, lastWorkspaceMutationType? - from memory_changed / agent_changed.
  • approvalMode?, approvalModeChangedCount, lastApprovalModeChange? - from approval_mode_changed.
  • toolToggleCount, lastToolToggle? - from tool_toggled.
  • workspaceInitCount, lastWorkspaceInit? - from workspace_initialized.
  • mcpRestartCount, lastMcpRestart? - from mcp_server_restarted.
  • mcpRestartRefusedCount, lastMcpRestartRefused? - from mcp_server_restart_refused.
  • settings_changed / settings_reloaded - recognized by asKnownDaemonEvent; the session reducer does not maintain dedicated view-state fields, and UIs usually treat them as refresh signals.
  • permissionVoteProgress: Record<string, DaemonPermissionPartialVoteData> - consensus voting progress.
  • forbiddenVotes: DaemonPermissionForbiddenData[], forbiddenVoteCount - policy-rejected vote records, capped at 32.
  • awaitingResync: boolean - set by state_resync_required; cleared when consumer resets view state.
  • resyncRequiredCount, lastResyncRequired? - resync observability.
  • lastFollowupSuggestion?: DaemonFollowupSuggestionData - latest follow-up suggestion pushed by daemon.
  • lastTurnComplete?: DaemonTurnCompleteData - latest successful turn completion.
  • lastTurnError?: DaemonTurnErrorData - latest turn error.
  • rewindCount, lastRewind?, lastBranch? - latest rewind / branch events.

DaemonAuthState

One entry per providerId, driven by auth_device_flow_*. Each flow exposes { deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError? }.

Flow

Producer side

flowchart LR
    A["ACP child notification"] --> B["BridgeClient.sessionUpdate /<br/>BridgeClient.extNotification"]
    B --> C{"Mapped to event type?"}
    C -->|yes| D["EventBus.publish({type, data, originatorClientId?})"]
    C -->|no| E["No emit (drop or log)"]
    D --> F["Assign id + v=1, push to ring"]
    F --> G["Fan out to all subscribers"]

Consumer side (SDK)

flowchart LR
    A["SSE bytes"] --> B["parseSseStream -> DaemonEvent[]"]
    B --> C["asKnownDaemonEvent(evt)"]
    C -->|"KnownDaemonEvent"| D["reduceDaemonSessionEvent(state, evt)"]
    C -->|"auth_device_flow_*"| E["reduceDaemonAuthEvent(state, evt)"]
    C -->|"undefined"| F["unrecognizedKnownEventCount++<br/>(forward-compat)"]

Envelope-level metadata

Beyond each event's data payload, the daemon stamps two envelope-level fields.

_meta.serverTimestamp - daemon clock

EventBus.publish() in packages/acp-bridge/src/eventBus.ts stamps _meta.serverTimestamp when the event enters the bus. The BridgeEvent type includes _meta?: Record<string, unknown>, so internal daemon consumers do see _meta on every bus-published event. formatSseFrame() in packages/cli/src/serve/routes/sse-events.ts provides a fallback timestamp only for synthetic frames (e.g. stream_error) that bypass EventBus.publish.

{
  "id": 47,
  "v": 1,
  "type": "session_update",
  "data": { ... },
  "_meta": { "serverTimestamp": 1716287345123 }
}

The merge preserves any existing _meta keys from the input event ({...input._meta, serverTimestamp: Date.now()}). Producers may attach additional envelope-level _meta keys; EventBus.publish merges them with the timestamp rather than overwriting.

Why it matters: multi-client UIs that render relative time or sort transcript blocks should use server time instead of each browser/tab/phone local clock. Server stamping keeps ordering consistent across clients.

SDK access: prefer event._meta?.serverTimestamp. Compatibility paths may also probe event.serverTimestamp or event.data._meta.serverTimestamp. Do not mix ACP payload data._meta with daemon envelope _meta.

originatorClientId

Events triggered by a request that carried a registered X-Qwen-Client-Id may stamp this field. See 08-session-lifecycle.md.

Tool-call _meta (provenance / serverId)

This is separate from envelope _meta: ACP session/update payloads can carry their own _meta in event.data._meta. ToolCallEmitter (packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts) stamps two fields on emitStart, emitResult, and emitError:

Field Type Resolution rule
provenance 'builtin' | 'mcp' | 'subagent' ToolCallEmitter.resolveToolProvenance: subagentMeta wins with subagent; tool name matching mcp__<server>__<tool> maps to mcp; everything else maps to builtin.
serverId string only when provenance === 'mcp' Extracted heuristically from mcp__<serverId>__<tool>.

The existing _meta.toolName display name is preserved. UI uses these fields to render builtin / MCP server / subagent badges without reparsing the tool name.

SDK reducer behavior

reduceDaemonSessionEvent(state, evt) in packages/sdk-typescript/src/daemon/events.ts projects the stream into DaemonSessionViewState. The resync-related fields are:

  • awaitingResync: boolean - set by state_resync_required; caller clears it, typically after POST /session/:id/load resets view state.
  • resyncRequiredCount: number - observability counter.
  • lastResyncRequired?: DaemonStateResyncRequiredData - latest payload.

While awaitingResync = true, the reducer skips delta application and only allows the closed RESYNC_PASSTHROUGH_TYPES set:

Passthrough type Why it is still applied during resync
state_resync_required Rare second resync should update lastResyncRequired / resyncRequiredCount.
session_died Terminal stream signal must remain visible during resync.
session_closed Same as above.
client_evicted Same as above.
stream_error Same as above.
session_snapshot Full-state authoritative frame; safe to apply during resync.
session_recording_degraded Sticky safety signal independent of transcript delta state.

lastEventId still advances monotonically through advanceLastEventId(base) during resync. After the caller resets and clears awaitingResync, subsequent deltas align to the correct cursor.

reduceDaemonAuthEvent projects device-flow events into workspace-level auth state entries shaped like {deviceFlowId, status, providerId, expiresAt?, lastThrottleIntervalMs?, lastError?} conceptually. In code the reducer stores status, errorKind, hint, intervalMs, lastSeenEventId, authorizedExpiresAt, and accountAlias on DaemonDeviceFlowReducerState; the daemon event payloads themselves remain the per-event shapes listed above.

State and forward compatibility

  • Add a known event type by appending to DAEMON_KNOWN_EVENT_TYPE_VALUES. Old SDKs return undefined for unrecognized event types through the fallback path and increment unrecognizedKnownEventCount; new SDKs rely on the discriminated union.
  • Adding optional fields to an existing payload is safe because payloads are open ({ [key: string]: unknown }).
  • Changing an existing payload shape is breaking and must bump EVENT_SCHEMA_VERSION plus advertise a compatible capability tag such as caps.features.typed_event_schema_v2.
  • id is per-session monotonic. Subscriber-level synthetic frames (client_evicted, slow_client_warning, stream_error, state_resync_required, replay_complete, session_snapshot) intentionally have no id so other subscribers do not see gaps.
  • originatorClientId lives on the envelope rather than data. F3 partial-vote / forbidden payloads also merge it into data through mergeOriginator so view-state consumers do not need to retain the envelope.

Dependencies

Configuration

  • Always advertised: typed_event_schema, mcp_guardrail_events, and permission_mediation (with supported policy modes).
  • No env var or flag directly controls the schema itself. QWEN_SERVE_NO_MCP_POOL=1 changes MCP event scope from 'workspace' to absent or 'session'.

Caveats and known limits

  • Six synthetic frame types intentionally have no id; SDK code must not assume every event has an id.
  • permission_partial_vote only appears under consensus. permission_forbidden appears under designated, consensus, and local-only, but not under first-responder.
  • mcp_child_refused_batch only appears in mode: 'enforce'; warn mode never refuses.
  • auth_device_flow_* events are not session-keyed. When consuming through DaemonSessionClient, use reduceDaemonAuthEvent for them rather than the session reducer.

References

  • packages/sdk-typescript/src/daemon/events.ts
  • packages/acp-bridge/src/eventBus.ts (EVENT_SCHEMA_VERSION)
  • packages/cli/src/serve/capabilities.ts (typed_event_schema, mcp_guardrail_events, permission_mediation)
  • Wire reference: ../qwen-serve-protocol.md