Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/design/2026-08-28-web-shell-assistant-turn-settlement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Web Shell assistant turn settlement

## Goal

Expose a host callback for the semantic end of an assistant turn without requiring consumers to infer completion from prompt-status transitions or scan a changing transcript.

The callback is a generic Web Shell lifecycle contract. It is not tied to any embedding product, artifact projection, or workspace side effect.

## Contract

`onAssistantTurnSettled` receives:

- `sessionId` and daemon-assigned `promptId`; their tuple is the stable idempotency key;
- `outcome`: `completed`, `cancelled`, or `failed`;
- the daemon `stopReason` for completed and cancelled turns;
- `error` (`{ message, code? }`) for failed turns; when no assistant content exists, this is the settlement's only failure diagnostic;
- `transcriptComplete`, which is false when replay integrity is degraded or bounded live-journal repair could not restore the complete turn before failing or being discarded;
- the final visible assistant message when it remains available in the committed current-session transcript; turns without assistant content and events delivered across a session switch omit it, while cancelled and failed turns may carry partial content.

Transport cursors such as the daemon SSE `eventId` remain internal to the session layer. Web Shell explicitly projects the stable host contract instead of forwarding the internal event object.

The callback is optional. Existing `onSessionChange({ type: 'turn_complete' })` behavior remains unchanged.

## Lifecycle and ordering

The daemon prompt terminal (`turn_complete` or `turn_error`) is authoritative. Prompt-status `idle`, render completion, and history replay are not terminal signals.

If a process or transport fails without delivering either terminal event, no settlement is published. This fail-closed behavior avoids reporting an unproven completion; connection health remains a separate lifecycle.

The daemon session provider publishes a settlement only after it has:

1. flushed buffered transcript deltas;
2. applied `assistant.done` and the terminal event's own transcript projection;
3. completed live-journal repair when a truncated active turn can be repaired, or classified the retained transcript as incomplete before discarding an unsuccessful repair.

Ordinary session load, branch/split transcript replay, and older-history pagination never publish settlements. A terminal received while reconnecting an already active prompt may publish because it is a previously unseen live lifecycle transition, not history playback.

The callback covers live turns from both the primary chat and interactive Split View panes. Every pane observes its own session provider, and a bounded Web Shell-level dispatcher suppresses duplicates when the primary session is also mounted in a pane. Merely opening a pane and replaying its transcript remains silent.

Recent duplicate terminal delivery is suppressed by a bounded in-memory window in the mounted session provider using `(sessionId, promptId)`. Hosts must use the same key for durable idempotency across remounts and long-lived sessions.

`prompt_cancelled` is a cancellation request, not a confirmed prompt terminal, and does not publish a settlement by itself. Waiting for permission or `ask_user_question` also does not publish.

Listener failures are isolated from daemon stream processing. Artifact, tool-result persistence, and workspace projection keep their independent lifecycle.

## Implementation boundary

The WebUI daemon session layer owns authoritative terminal observation, transcript ordering, replay suppression, repair, and duplicate suppression. It exposes a subscription hook with prompt-level settlement metadata.

Web Shell owns projection of the final visible assistant message and the public callback type. It does not add daemon routes or change daemon event payloads.

## Tests

- `TC-01`: normal terminal publishes once with the complete final message.
- `TC-02`: assistant/tool/assistant turn returns the final assistant message.
- `TC-03`: permission or `ask_user_question` waiting does not publish.
- `TC-04`: confirmed cancellation publishes `cancelled`; cancellation request alone does not.
- `TC-05`: `turn_error` publishes `failed` and preserves any partial assistant message.
- `TC-06`: reconnect catch-up for an active local prompt publishes once.
- `TC-07`: ordinary history replay and older-history pagination do not publish.
- `TC-08`: duplicate terminal events publish once for a mounted provider.
- `TC-09`: session switch cannot attribute an old terminal to the new session.
- `TC-10`: successful live-journal repair delays publication until the repaired transcript is committed; failed repair marks `transcriptComplete: false`.
- `TC-11`: listener exceptions do not interrupt subsequent daemon events or listeners.
- `TC-12`: clearing or replacing a repair episode releases its held settlement once with `transcriptComplete: false`.
- `TC-13`: degraded catch-up replay and unrecoverable live-journal markers publish with `transcriptComplete: false`.
- `TC-14`: a terminal after reconnect consumes the restored-active snapshot and allows live or catch-up repair to finish.
- `TC-15`: a live turn in a Split View pane publishes with that pane's session and final message, while the shared dispatcher suppresses duplicate observation of the primary session.

No visual UI changes are introduced, so browser screenshot validation is not applicable. Package unit tests, build, typecheck, and repository preflight are the delivery gates.
31 changes: 31 additions & 0 deletions packages/web-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,37 @@ export function App() {
> **注意**:不要在已有 `DaemonSessionProvider` 下使用
> `WebShellWithProviders`,否则会创建嵌套的重复 Provider。

### Assistant Turn 终态回调

宿主需要在一轮 Assistant 回答真正结束后处理结果时,应使用
`onAssistantTurnSettled`,不要从 `onStreamingStateChange('idle')` 推断:

```tsx
<WebShellWithProviders
onAssistantTurnSettled={(event) => {
const key = `${event.sessionId}:${event.promptId}`;
persistResultOnce(key, event.message?.content, event.outcome);
}}
/>
```

该回调来自 daemon 的 prompt 终态,并在终态 transcript 已提交后触发。普通历史加载、
分支回放和向前分页不会触发;断线期间遗漏、重连后补收的活动 prompt 终态可以触发。
主聊天和 Split View 中的实时交互会话使用同一契约;仅打开 split pane 所产生的历史
回放不会触发。如果主会话也显示在 pane 中,Web Shell 的共享分发器会避免同一终态在
一个 Web Shell 实例内重复上报。
`(sessionId, promptId)` 是跨挂载幂等键。单个 Provider 挂载期间重复终态会被抑制,
宿主仍应使用该键做持久化幂等。

`outcome` 为 `completed`、`cancelled` 或 `failed`。取消和失败可能没有 Assistant
消息,也可能携带终止前的部分内容;失败时 `error` 提供 daemon 终止原因,即使没有
Assistant 消息也会保留该诊断。`transcriptComplete: false` 表示 Web Shell 已收到
终态,但 replay 已被标记为不完整,或受限 live journal 的修复在失败、会话切换或
repair episode 被替换前未能恢复完整 transcript。等待 permission 或
`ask_user_question` 不属于终态。Artifact 和 workspace projection 有各自的生命周期,
不由该回调表示。进程硬崩溃且没有送达 prompt 终态时不会触发回调;连接失败应由连接
状态单独处理,不能被当作已完成的 turn。

### 3. 只读 ChatRecord JSONL

`WebShellTranscript` 只接收已经投影完成的 blocks,不连接 daemon,也不提供 composer、
Expand Down
Loading
Loading