Skip to content
Merged
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
34 changes: 30 additions & 4 deletions docs/architecture/deepchat-agent-harness-boundaries/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ fence that matches their operation.
existing order: `sessions.status.changed`, `sessions.updated`, internal session update, then UI
refresh.
- `PendingInputAdmissionCoordinator` owns send/steer normalization, attachment acceptance lanes,
capacity and interaction gates, queue mutation commands, steer promotion, and interrupt
requests.
capacity and interaction gates, queue mutation commands, steer promotion, explicit Queue resume,
and interrupt requests. Listing pending inputs is a pure read.
- `PendingInputPump` owns steer-before-queue selection, per-session single-flight drain, claim,
release, consume, recovery, and starting a claimed turn. Durable input records remain owned by
`SessionPendingInputs`.
release, consume, process-local restart holds, recovery, and starting a claimed turn. Durable
input records remain owned by `SessionPendingInputs`.
- `TurnCoordinator` continues to own the distinct initial and resume preparation algorithms, but
delegates lifecycle settlement and pending-input disposition instead of mutating those owners
through parent callbacks.
Expand All @@ -126,6 +126,32 @@ event bus is not used because claim settlement and the next drain must observe a
order. Public `MessageStartResult`, including `attachmentPreparation`, remains unchanged and is
mapped losslessly from the internal completion.

### Pending Input Restart Boundary

Harness construction reconciles active pending inputs before transcript recovery and before any
Session scope is hydrated. `SessionPendingInputs` returns the affected Session IDs and Queue input
IDs that must be held. It terminalizes unread Steer messages, their Tape replacements, and their
pending rows atomically instead of handing them to a later global recovery phase.

The pump installs Queue IDs in a process-local hold set. Automatic `enqueue` and `completed` wakes
stop at a held Queue FIFO head; pending-input listing and Session hydration never schedule a drain.
An explicit composer Send may start without releasing historical drafts. The typed
`sessions.resumePendingQueue` operation validates normal Session gates, releases the Session hold,
and drains with a `manual` wake reason. Before that manually resumed turn enters the provider Run,
the pump consumes its Queue claim; a returned provider error therefore cannot restore the same item
to Queue. Manual intent is tracked by Queue item ID, so attachment block and resolution cannot erase
that boundary. The pending-input list projects authoritative resume availability, and Resume returns
`false` without draining when no hold exists. Failures before the provider boundary still release
safely. Durable Queue rows remain the only ordering and content source, and a later restart derives
a new hold from whatever rows remain.

An unclaimed Steer belongs to the previous process's interrupted delivery attempt. Startup changes
its linked messages to retryable `error`, appends their Tape replacements, and consumes its pending
row in one transaction. The renderer hides its receipt and adds no recovery-specific action; the
standard message toolbar remains unchanged. A claimed Steer keeps its sent user message and exposes
the interruption through the assistant error. Neither path adds a persisted recovery marker, age
policy, schema migration, or hydration-triggered execution.

### Test Boundary

Before production extraction, tests stop reflecting private coordinator members. Existing cases
Expand Down
19 changes: 17 additions & 2 deletions docs/architecture/session-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,28 @@ Steer: user message (Unread) -> claim (Read) -> new assistant message
仍是 source of truth。
- receipt 只由持久化 `readAt` 派生为 `Unread` 或 `Read`。`Read` 的短时显示和淡出属于 renderer,
不产生延迟数据库写入。
- 冷启动是单独的终止边界:上个进程尚未 claim 的 Steer 会消费 pending row,并把 linked user
message 内部终止为 `error`。renderer 不展示恢复专用 receipt 或按钮,仍保留普通消息工具栏;已经
claim 的 Steer 保留 sent user fact,由中断的 assistant message 承载失败。

## 恢复与查询

- `sessions.restore` 返回最近一页,`sessions.listMessagesPage` 使用 keyset pagination 拉取旧历史。
- 普通 list/history/binding query 不 hydrate Agent instance。
- active session 的 pending-input restore 若发现 `pending` Steer,会唤醒对应 backend 的正常 drain;
这是重启前已发送 `Unread` 消息的执行恢复点,不改变普通 transcript/history query。
- DeepChat harness 构造时会先 reconcile active pending inputs。历史 Queue row 保持 durable,并进入
process-local restart hold;Session 打开、hydrate、消息查询和 pending-input list 都不会释放 hold
或触发执行。用户通过 Queue lane 的 `Resume queue` 显式释放当前 Session 的 hold,之后继续沿用
Steer-first 与 Queue FIFO drain 规则。手动恢复的 Queue head 在写入 user/assistant fact 并进入
provider Run 前消费;manual 标记绑定 Queue item ID,附件 block 后的 retry/degrade 不会丢失该语义。
此后的 provider error 只保留在 transcript,不会把同一条目放回 Queue。pending-input list 同时返回
后端计算的 resume availability;普通 live Queue 不显示该操作,无 hold 的 Resume 不触发 drain。
- 重启前尚未 claim 的 Steer 不再自动 drain:reconciliation 消费对应 pending row,并把 linked
pending user message、Tape replacement 与 row 消费在同一事务内终结为 `error`,UI 不显示
receipt。用户通过普通消息工具栏 Retry 时走普通 turn,且不会释放同 Session 的历史 Queue hold。
- claimed Queue 创建 user message 时会在同一事务内回写 pending row 的 message ID,使冷启动可以
明确区分尚未开始的 draft 与已物化的 transcript fact。
- restart hold 仅由现有 active Queue ID 派生,不持久化、不改变 Queue 排序和容量,也不新增 schema;
再次重启会从剩余 durable row 重建。pending-input list 是纯读。
- DeepChat harness 构造时在 pending-input 与 transcript recovery 之前分类 Execution Journal。存在
dispatch-without-outcome、corruption 或缺失 terminal 的 Run 只输出结构化 parked 诊断,不依据该报告
自动重放遗留 operation;分类先于 runtime graph 构建,Journal 读取失败会阻止 harness 构造。诊断最多
Expand Down
32 changes: 27 additions & 5 deletions docs/features/im-style-steer-messages/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ Queue = mutable draft, normal admission time, no receipt
Steer = immutable sent message, interrupt admission time, Unread -> Read receipt
```

The receipt has only two named states: `Unread` and `Read`. It disappears after `Read`; disappearance
is presentation, not a third state. User-facing Chinese copy is `未读` / `已读`.
The live receipt has only two named states: `Unread` and `Read`. It disappears after `Read`;
disappearance is presentation, not a third live state. User-facing Chinese copy is `未读` / `已读`.
Cold restart before claim terminalizes the message internally as `error` and suppresses the receipt;
it does not add a third delivery label or a recovery-specific action.

## Problem

Expand Down Expand Up @@ -135,6 +137,7 @@ Promoting a Queue item to Steer is a one-way admission transition:
| `pending` | `pending` | `readAt: null` | `Unread` | Copy |
| `claimed` | `pending` | `readAt: <timestamp>` | `Read`, then disappears | Copy |
| `consumed` | `sent` | Original `readAt` retained | No receipt after timeout | Normal message actions |
| Cold restart before claim | `error` | `readAt: null` retained | No receipt | Normal message actions |

`ChatMessageRecord.status = 'pending'` prevents an accepted-but-unsettled Steer from entering later
context as historical input. `isContextHistoryRecord` already excludes pending user messages.
Expand All @@ -159,6 +162,12 @@ The claim transition is irreversible for UI semantics. A failure after claim pro
assistant error or interruption below the Steer; it does not move the message back to `Unread` or
silently delete it.

A cold restart before claim is different from a live failure after claim. The accepting runtime no
longer exists, so startup consumes the active Steer row and recovers its linked user messages to
`error`. The renderer uses that status to hide the stale live receipt and otherwise keeps the normal
user-message rendering. The standard toolbar Retry action starts a normal turn because there is no
active turn left to steer.

### Rapid consecutive Steers

The existing payload merge is retained, but the UI no longer merges user messages:
Expand Down Expand Up @@ -455,12 +464,13 @@ It receives no `Unread` / `Read` receipt.
| Persistence fails before acceptance | No partial pending row or message; keep draft and show error |
| Renderer misses acceptance event | Route result inserts the persisted message; later restore is authoritative |
| Renderer misses claim event | Session restore reconstructs `readAt` and the new assistant row |
| App restarts during pre-stream handoff | Keep the materialized source user fact, consume its claimed Queue record, and resume the `Unread` Steer |
| App restarts during pre-stream handoff before Steer claim | Keep the materialized source user fact, consume its claimed Queue record, and terminalize the Steer internally as `error` without a receipt |
| Previous DeepChat turn errors | Open the safe boundary and drain the durable Steer unless an interaction blocks it |
| Previous ACP turn cancellation fails | Keep Steer `Unread`; do not claim until the old operation is terminal |
| Runtime fails after claim | Keep user messages `Read`; settle a new assistant error row; do not delete or retry silently |
| App restarts before claim | Restore the Steer as `Unread` and resume normal pending-input drain |
| App restarts before claim | Atomically fail linked user messages, append Tape replacements, consume the Steer row, hide the receipt, and leave the standard toolbar Retry action available |
| App restarts after claim | Restore the persisted `Read` receipt and settlement facts; never duplicate user rows |
| App restarts with Queue drafts | Keep rows in Queue and hold them from automatic drain until explicit `Resume queue`; once the manually resumed head enters the provider Run, consume it so a provider error cannot restore it to Queue |
| Session is switched | Keep lifecycle in main; active renderer derives the state when restored |
| Session is deleted | Delete transcript and pending-input facts through the existing session deletion transaction |

Expand All @@ -477,10 +487,15 @@ It receives no `Unread` / `Read` receipt.
7. The claimed batch payload is supplied exactly once as the new loop input.
8. Settlement marks all linked user messages `sent` and appends the corresponding Tape replacement
facts.
9. Search and transcript restore may show an accepted `Unread` Steer because it is a real sent fact.
9. Reload in the same process may show an accepted `Unread` Steer because it is a real sent fact;
cold-start recovery terminalizes an unclaimed Steer as `error` and shows no receipt instead.
10. Event delivery is a cache update, never the source of truth.
11. Pre-stream acceptance materializes and links the current claimed Queue user fact in the same
transaction as the Steer, before assigning the Steer's `orderSeq`.
12. Normal claimed Queue materialization also creates the user fact and links its pending row in one
transaction.
13. Restart terminalization changes every unread Steer message to `error`, appends its Tape
replacement, and consumes the Steer row in one transaction.

## Acceptance Criteria

Expand All @@ -502,13 +517,20 @@ It receives no `Unread` / `Read` receipt.
- Queue items remain editable and reorderable in the bottom lane.
- Queue promotion creates a visible `Unread` Steer only after successful preparation.
- Normal Queue drain creates no receipt.
- Queue drafts retained across cold restart do not drain from hydration or lifecycle wakes and expose
`Resume queue` only when the backend reports an actual restart hold while the Session is idle.
- A manually resumed Queue head is consumed before its provider Run and does not return to Queue
after a provider error, including after attachment Retry or Send without image content.

### Reliability

- Rapid Steers create separate bubbles, enter one batch before claim, and receive one reply below
the last bubble.
- A Steer accepted after claim belongs below the newly reserved assistant message.
- Reload and restart never duplicate, hide, or reorder accepted Steer messages.
- Cold restart never executes an unclaimed Steer implicitly; it renders without a receipt and the
standard toolbar Retry action starts one normal turn.
- Retrying a restart-failed Steer does not release retained Queue drafts.
- A post-claim failure never removes or reverts the user message.
- DeepChat and ACP satisfy the same visible ordering contract.

Expand Down
Loading