Merge remote-tracking branch 'origin/worktree/context-source-cards' into worktree/context-forms-remaining
# Conflicts: # apps/web/tests/snapshots/queue-actions/layout.expected.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.i18n.yaml # docs/core-data-structures/goal.i18n.yaml # docs/core-data-structures/goal.md # docs/core-data-structures/goal.zh.md # docs/event-producer-consumer.md # examples/acp-agent/tests/goal-snapshots/goal-session/session.expected.jsonl # examples/acp-agent/tests/goal-snapshots/goal-wrapup/session.expected.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/bash-spill/session.jsonl # examples/acp-agent/tests/snapshots/bash-tool-turn/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/cancel-tool-calls/session.jsonl # examples/acp-agent/tests/snapshots/cancel/session.jsonl # examples/acp-agent/tests/snapshots/code-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/code-mode-workspace-context/session.jsonl # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # examples/acp-agent/tests/snapshots/empty-response-retry/session.jsonl # examples/acp-agent/tests/snapshots/error-finish/session.jsonl # examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl # examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl # examples/acp-agent/tests/snapshots/fs-edit/session.jsonl # examples/acp-agent/tests/snapshots/fs-escalation-approved/session.jsonl # examples/acp-agent/tests/snapshots/fs-policy-reject/session.jsonl # examples/acp-agent/tests/snapshots/fs-read-window/session.jsonl # examples/acp-agent/tests/snapshots/fs-read/session.jsonl # examples/acp-agent/tests/snapshots/fs-write-overwrite/session.jsonl # examples/acp-agent/tests/snapshots/fs-write/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-invalid-matcher/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-posttool-block/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-posttool-context/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-pretool-deny/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-promptsubmit-context/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-stop-continue/session.jsonl # examples/acp-agent/tests/snapshots/hook-codex-invalid-matcher/session.jsonl # examples/acp-agent/tests/snapshots/hook-codex-posttool-block/session.jsonl # examples/acp-agent/tests/snapshots/hook-codex-posttool-context/session.jsonl # examples/acp-agent/tests/snapshots/hook-codex-pretool-block/session.jsonl # examples/acp-agent/tests/snapshots/hook-codex-promptsubmit-context/session.jsonl # examples/acp-agent/tests/snapshots/hook-codex-stop-continue/session.jsonl # examples/acp-agent/tests/snapshots/lsp-definition/session.jsonl # examples/acp-agent/tests/snapshots/missing-sandbox-runner/session.jsonl # examples/acp-agent/tests/snapshots/multi-turn/session.jsonl # examples/acp-agent/tests/snapshots/packed-chunks/session.jsonl # examples/acp-agent/tests/snapshots/parallel-tool-calls/session.jsonl # examples/acp-agent/tests/snapshots/partial-landlock-child-failure/session.jsonl # examples/acp-agent/tests/snapshots/pty-tools/session.jsonl # examples/acp-agent/tests/snapshots/repeat-tool-guard/session.jsonl # examples/acp-agent/tests/snapshots/session-query-spill/session.jsonl # examples/acp-agent/tests/snapshots/session-sandbox-root/session.jsonl # examples/acp-agent/tests/snapshots/session-title-after-turn/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/subagent-continuable/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-continuable/session.jsonl # examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.2.jsonl # examples/acp-agent/tests/snapshots/subagent-depth-two-rejection/session.jsonl # examples/acp-agent/tests/snapshots/subagent-fork/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-fork/session.jsonl # examples/acp-agent/tests/snapshots/subagent-list-agents/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-list-agents/session.jsonl # examples/acp-agent/tests/snapshots/subagent-mixed/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-mixed/session.2.jsonl # examples/acp-agent/tests/snapshots/subagent-mixed/session.jsonl # examples/acp-agent/tests/snapshots/subagent-multi/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-multi/session.2.jsonl # examples/acp-agent/tests/snapshots/subagent-multi/session.jsonl # examples/acp-agent/tests/snapshots/subagent-published-run-failure/session.jsonl # examples/acp-agent/tests/snapshots/subagent-report/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-report/session.jsonl # examples/acp-agent/tests/snapshots/subagent-spawn/session.1.jsonl # examples/acp-agent/tests/snapshots/subagent-spawn/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/acp-agent/tests/snapshots/todo-write/session.jsonl # examples/acp-agent/tests/snapshots/tool-call-turn/session.jsonl # examples/acp-agent/tests/snapshots/web-fetch/session.jsonl # examples/acp-agent/tests/snapshots/workflow-run/session.1.jsonl # examples/acp-agent/tests/snapshots/workflow-run/session.jsonl # examples/acp-agent/tests/snapshots/workspace-context/session.jsonl # examples/acp-agent/tests/snapshots/workspace-edit/session.jsonl # examples/headless-agent/tests/snapshots/goal-tools/stream-json.expected.jsonl # examples/headless-agent/tests/snapshots/pty-tools/session.jsonl # examples/headless-agent/tests/snapshots/pty-tools/stream-json.expected.jsonl # examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/child.expected.jsonl # examples/headless-agent/tests/subagent-inheritance-snapshots/parent-override/parent.expected.jsonl # examples/jsonrpc-agent/tests/snapshots/persistent-tools/notifications.expected.jsonl # examples/jsonrpc-agent/tests/snapshots/persistent-tools/session.jsonl # packages/bash/tool-bash/tests/integration.spec.ts # packages/context/time-context/src/index.ts # packages/context/tmux-context/src/index.ts # packages/core/agent-loop/src/agent.ts # packages/core/system-prompt/src/index.ts # packages/goal/goal/src/domain.ts # packages/goal/goal/src/index.ts # packages/goal/goal/src/render.ts # packages/plan/plan-mode/src/index.ts
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md
|
||||
2026-06-18-compaction-capability-seam.md: ef37313bc6fb984689793fa5a3e7ac4d9238ea88
|
||||
2026-06-18-compaction-capability-seam.zh.md: 6d094062ad88bdb587128351fc7217de353ebbd8
|
||||
2026-06-18-compaction-capability-seam.md: 27dbde9f2349681cf47c4d25b16399b26ed9e1ca
|
||||
2026-06-18-compaction-capability-seam.zh.md: 1fe9ece2861bd6d75633a866a4a11eaadbf7ef26
|
||||
|
||||
@@ -10,7 +10,7 @@ A long-running agent conversation grows without bound. As the event log accumula
|
||||
|
||||
The [session surface](../architecture/2026-06-18-session-surface.md) was built as the foundation for exactly this — an ordered projection over the event log with a `surfaceOp: { op: 'replace', start, end }` operation purpose-built to shadow a range of entries and insert a replacement, with `sourceEventSeqs` recording provenance so the decision replays deterministically. What remained was the plugin that *decides what to compact and produces the summary*.
|
||||
|
||||
Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to five event types (`user/message`, `assistant/message`, `tool/result`, `context/message`, `steering/message`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler and Session's always-on append/seed boundary reject `surfaceOp` on it.
|
||||
Two forces shape the design. First, compaction policy and reusable token measurement vary independently: measurement belongs to the LLM-family [`ctx.tokenMeter` service](../architecture/2026-07-15-replay-token-meter-service.md), while summarization can be a model call, a template, or a remote service. Second, `SurfaceEventType` is closed to the message-producing event types (`user/message`, `assistant/message`, `tool/result`); only those may carry `surfaceOp`. A bespoke `compaction/*` event therefore **cannot** itself appear on the surface — the compiler and Session's always-on append/seed boundary reject `surfaceOp` on it.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -19,7 +19,7 @@ Two forces shape the design. First, compaction policy and reusable token measure
|
||||
Per the [capability-seams Agent Note](../architecture/2026-06-13-capability-seams.md), compaction ships as separate packages so the contract, the algorithm, and (later) the consumer surface evolve independently:
|
||||
|
||||
1. **Interface** — `@deepseek-ai/dsh-compact`: an abstract `CompactService` owning the `ctx.compact` key, the `CompactionResult` vocabulary, the `compact/*` session events, the manual failure taxonomy, and the canonical checkpoint message source. It declares `compactIfNeeded()`, `compactNow()`, and `compactRegion()` as **abstract** — the contract states *what* compaction does, not *how*.
|
||||
2. **Implementation** — `@deepseek-ai/dsh-compact-basic`: a concrete `BasicCompactService` that consumes `ctx.tokenMeter` and owns the tail→head retention walk, summarization via `ctx.llm.stream()`, the surface replacement, the lock, post-step pressure, and canonical context-overflow recovery. `summarize()` is its sole subclass hook; pricing and replay stay with the meter.
|
||||
2. **Implementation** — `@deepseek-ai/dsh-compact-basic`: a concrete `BasicCompactService` that consumes `ctx.tokenMeter` and owns the tail→head retention walk, summarization via `ctx.llm.stream()`, the surface replacement, the lock, pre-step pressure, and canonical context-overflow recovery. `summarize()` is its sole subclass hook; pricing and replay stay with the meter.
|
||||
3. **Model-free companion** — `@deepseek-ai/dsh-compact-tool-result-prune`: a concrete optional service that rewrites oversized current `tool/result` nodes before the backend selects a summary range. It is not a second compaction implementation and does not implement `CompactService`.
|
||||
4. **Human consumer** — `@deepseek-ai/dsh-command-compact` registers argument-free `/compact` through `ctx.commands` and calls the backend-independent `compactNow()` operation. It is direct human control, not a model-facing tool.
|
||||
|
||||
@@ -33,18 +33,18 @@ This is not a coupling smell — it is the contract's domain. The "only cordis"
|
||||
|
||||
An earlier draft put the full algorithm (the retention walk, token-summing, text extraction) as concrete methods on the interface. That recouples the contract to one strategy: a backend that wants a different retention policy or event sequence would have to fight inherited concrete code. Making all three operations abstract puts every *how* decision in the backend and keeps the interface a statement of *what*. Token measurement is not a compaction hook at all; the singleton service lets multiple consumers share one per-session replay fold.
|
||||
|
||||
`compactIfNeeded(agent, trigger, signal)` takes an explicit `'pressure' | 'context-overflow'` trigger and cancellation. It reads only the latest durable routed request; no header means no work, while any routed provider/model target uses the singleton estimator. `compactNow(agent, signal)` reserves idle turn admission and performs one useful balanced reduction even below pressure, returning `null` without writes when none exists. `compactRegion(start, end, agent, signal?)` uses `agent.session` as its single session identity and keeps an optional signal for explicit callers. The default summarizer resolves its target from explicit config, the latest logged routed target, then agent options, and records the provider/model pair after any `llm/stream` routing. It replays the routed request's prefix and appends the compaction directive as a trailing user message so the provider's warm KV cache is reused — see the [summary prefix-cache Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md). The call sets the provider-neutral `GenerateOptions.purpose` to `compaction`; adapters may map that purpose to model-hidden transport metadata, and the DeepSeek adapter sends `x-deepseek-harness-compact: 1`.
|
||||
`compactIfNeeded(agent, trigger, signal)` takes an explicit `'pressure' | 'context-overflow'` trigger and cancellation. It reads only the latest durable routed request; no header means no work, while any routed provider/model target uses the singleton estimator. `compactNow(agent, signal)` requires an idle agent and performs one useful balanced reduction even below pressure, returning `null` without writes when none exists. `compactRegion(start, end, agent, signal?)` uses `agent.session` as its single session identity and keeps an optional signal for explicit callers. The default summarizer resolves its target from explicit config, the latest logged routed target, then agent options, and records the provider/model pair after any `llm/stream` routing. It replays the routed request's prefix and appends the compaction directive as a trailing user message so the provider's warm KV cache is reused — see the [summary prefix-cache Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md). The call sets the provider-neutral `GenerateOptions.purpose` to `compaction`; adapters may map that purpose to model-hidden transport metadata, and the DeepSeek adapter sends `x-deepseek-harness-compact: 1`.
|
||||
|
||||
### Automatic pressure runs after successful durable step work
|
||||
|
||||
Successful-call pressure cannot run at pre-step because final `agent/request` routing, provider output, tool results, buffered context, and steering do not exist there. Serial `agent/post-step(agent, turn, step, signal)` fires after those facts are durable and before `step/end`. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, optional `ctx.toolResultPrune` rewriting runs before summary selection; compact-basic remeasures the durable surface and skips summarization if pruning restores safe pressure.
|
||||
Successful-call pressure runs at the next `agent/pre-step`, after the preceding response, tool results, buffered context, and steering are durable and before the next request is derived. `dsh-compact-basic` measures the canonical logged request through `ctx.tokenMeter`, so the next request sees any replacement without a speculative envelope override. Once pressure qualifies, optional `ctx.toolResultPrune` rewriting runs before summary selection; compact-basic remeasures the durable surface and skips summarization if pruning restores safe pressure.
|
||||
|
||||
Canonical provider context overflow takes a separate path. The failed step closes and `agent/request-error` receives the original request error. Compact-basic owns its per-agent overflow count, prunes before forcing one useful balanced reduction, and returns `{ kind: 'retry' }` only if `session.surface.replaceGeneration` increases, including pruning-only progress when no summary range exists. The loop then closes the failed turn, opens a new numbered retry turn, and reconstructs its request from the durable log. No replacement, a recovery failure before any replacement, cancellation, an exhausted cap, or an unrelated error preserves the original provider failure. If pruning already advanced the generation before later summary work fails, recovery retries from that durable pruned surface unless cancellation or disposal wins. The complete lifecycle decision is in the [after-call recovery Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md).
|
||||
|
||||
```
|
||||
assistant/message → tool/result/context/steering
|
||||
await serial agent/post-step ⟵ pressure compaction inside the successful step
|
||||
step/end
|
||||
assistant/message → tool/result/context/steering → step/end
|
||||
claim the next batch → await waterfall agent/pre-step ⟵ pressure compaction before the next request
|
||||
enter → next step/start
|
||||
|
||||
provider overflow → step/end
|
||||
await waterfall agent/request-error ⟵ forced compaction between attempts
|
||||
@@ -53,7 +53,7 @@ retry → next numbered step/start ⟵ derives from the replacement surface
|
||||
|
||||
### Retention is turn-agnostic; tool-pairing balance is the only structural guard
|
||||
|
||||
Auto-compaction checks after **every successful** step, not once per turn. This is load-bearing for runaway-turn survival: a tool-heavy ReAct turn appends an `assistant/message` + a `tool/result` per step, so the surface grows within a turn. The post-step check can compact early closed tool pairs before continuation opens the next step, and provider-confirmed overflow remains the backstop when a request crosses the limit first.
|
||||
Auto-compaction checks after **every successful** step, not once per turn. This is load-bearing for runaway-turn survival: a tool-heavy ReAct turn appends an `assistant/message` + a `tool/result` per step, so the surface grows within a turn. The next pre-step check can compact early closed tool pairs before continuation opens another step, and provider-confirmed overflow remains the backstop when a request crosses the limit first.
|
||||
|
||||
`compactIfNeeded` retains the smallest tail of whole surface units whose estimated size reaches the resolved retained-token budget and compacts older nodes. A unit is a complete closed step or one no-step message. If the token cutoff lands inside a step, retention expands until the cut is tool-pairing balanced. Balance is checked on surface order, not log sequence, because replacement summaries have new sequence numbers at old surface positions. `dsh-compact` exports the before/after edge helpers; their per-session cache folds only appended surface-tail nodes while `replaceGeneration` is unchanged, does no event reads for log-only growth, and rebuilds current membership and balances after replacement. `compactRegion` rejects boundaries that split a tool call from its result. The in-flight turn receives no special retention.
|
||||
|
||||
@@ -96,7 +96,7 @@ The `compact/start … compact/end` bracket is justified by two roles:
|
||||
1. **Crash-detectable orphan + provenance** (primary). Summarization is a slow model call persisted *after* `compact/start`. A crash mid-summarization leaves a `compact/start` with no matching `compact/end` — a detectable orphan. Releasing the lock last (rather than first) converts the crash window from *silent corruption* into that detectable orphan.
|
||||
2. **Prevents concurrent compaction.** Every automatic, manual, and explicit-range entry point refuses a live unmatched `compact/start`. The bracket is the single lock; no process-local mutex duplicates it.
|
||||
|
||||
The lock excludes another compaction, not unrelated facts. Its markers are time points rather than an exclusive container, so idle injected context may appear between a standalone manual start and end. Automatic work requires whole-surface stability inside its turn. Manual work revalidates only the selected positional span, letting append-only context outside it remain visible after replacement.
|
||||
The lock excludes another compaction, not unrelated facts. Its markers are time points rather than an exclusive container, so durable inbox splices may appear between a standalone manual start and end. Automatic work requires whole-surface stability inside its turn. Manual work revalidates only the selected positional span, letting append-only context outside it remain visible after replacement.
|
||||
|
||||
The lifecycle boundary makes crash state unambiguous:
|
||||
|
||||
@@ -111,7 +111,7 @@ The lifecycle boundary makes crash state unambiguous:
|
||||
## Alternatives considered
|
||||
|
||||
- **The full algorithm as concrete interface methods** — rejected because it recouples the contract to one retention strategy. All three operations are abstract; reusable measurement is a separate LLM-family service and `summarize()` is basic's sole hook.
|
||||
- **Compaction on `agent/request` or provisional `agent/pre-step` inputs** — rejected because neither proves the final durable request and both couple generic lifecycle to compaction-specific envelope data. Post-step replay plus canonical overflow recovery covers both successful and rejected calls.
|
||||
- **Compaction on `agent/request` or a compaction-specific loop callback** — rejected because the former observes a provisional request and the latter couples generic lifecycle to compaction policy. Pre-step replay of the prior durable request plus canonical overflow recovery covers successful and rejected calls.
|
||||
- **A `compact` boolean or untyped request metadata map** — rejected because multiple auxiliary call kinds would become mutually exclusive flags, while an open bag would discard compiler-checked vocabulary. One typed `purpose` discriminant extends with additional call kinds without adding another `GenerateOptions` field.
|
||||
- **A separate `compact/error` event** — rejected: `compact/end` keeps an `error?` field, mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling.
|
||||
- **Teaching core turn-repair about `compact/*`** — rejected: the general end-seed boundary already distinguishes prior-lifecycle history, and patching core for every future `xxx/start … xxx/end` pair is exactly the coupling the capability-seam architecture exists to avoid.
|
||||
@@ -119,7 +119,7 @@ The lifecycle boundary makes crash state unambiguous:
|
||||
## Consequences
|
||||
|
||||
- **Packages**: `packages/compact/compact` supplies the interface, `compact-basic` supplies the backend, `compact-tool-result-prune` supplies optional deterministic rewriting, and `command-compact` supplies human `/compact`. `packages/llm/token-meter` owns replay-aware measurement independently.
|
||||
- **Automatic seams**: `agent/post-step` (`@mode serial`) handles successful-call pressure and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. Generic `agent/pre-step` remains a four-argument checkpoint with no compaction-only prompt/prefix payload.
|
||||
- **Automatic seams**: `agent/pre-step` (`@mode waterfall`) handles pressure before request derivation and `agent/request-error` (`@mode waterfall`) handles final request failures after the failed step closes. Pre-step receives the claimed batch and `PreStepContext`, with no compaction-only prompt/prefix payload.
|
||||
- **`SessionEventMap`** gains `compact/start` / `compact/summary` / `compact/end` by declaration merging (merge-extensible); `SurfaceEventType` is **not** touched. These are session events, not cordis `Events`, so the event-taxonomy gate needs no entry.
|
||||
- **`dsh-compact`** owns `COMPACT_CHECKPOINT_SOURCE`, `isCompactCheckpointSource(source)`, `toolPairingBalancedBefore(session, seq)`, and `toolPairingBalancedAfter(session, seq)`. The marker identifies replacement summaries across backend implementations. The cached surface-edge checks prevent `compactRegion` and `compactIfNeeded` from splitting a tool-call/result pair, validate current membership by seq, answer both edges from one per-cut balance sequence, and reject stale or missing seqs and orphan results.
|
||||
- **`dsh-session`** validates positional replacement, complete provenance, and content-only single-node `tool/result` rewrites through its one surface manager. Its invariant companion treats fresh appended tool results as executions that require an open step and pending call, while the compaction companion owns numeric-turn versus standalone-null bracket relations.
|
||||
@@ -128,7 +128,7 @@ The lifecycle boundary makes crash state unambiguous:
|
||||
## Testing
|
||||
|
||||
- **Unit:** Real Loader and invariant plugins cover whole-unit retention, pruning configuration and replay, rich-block ordering, metadata preservation, convergence, both `compact/end` outcomes, open-tail refusal, pruning-only and summarized overflow recovery, generation proof, caps, and original-error preservation.
|
||||
- **Loop:** Tests pin post-step after durable tool results and before `step/end`, actual `agent/request` routing, closed failed steps, fresh retry numbering, and complete thrown/in-band overflow → compaction → reconstructed retry composition.
|
||||
- **Manual:** Admission, marker ordering, injection retention, live/stale orphan classification, cancellation, close/flush failures, command mapping, and the queued TUI journey are pinned without a model key.
|
||||
- **Loop:** Tests pin pre-step after the preceding `step/end` and before the next `step/start`, actual `agent/request` routing, closed failed steps, fresh retry numbering, and complete thrown/in-band overflow → compaction → reconstructed retry composition.
|
||||
- **Manual:** Maintenance serialization, marker ordering, injection retention, live/stale orphan classification, cancellation, close/flush failures, command mapping, and the queued TUI journey are pinned without a model key.
|
||||
- **With-key e2e:** A real model and bash session with lowered limits triggers compaction, records a complete `compact/start…end` pair, shrinks the surface, and finishes the task.
|
||||
- **Snapshot gap:** Runaway-turn compaction cannot yet replay because the summarization call records no `assistant/chunk` events or `sessionId`; interleaved summarization-call replay remains follow-up work.
|
||||
|
||||
@@ -10,7 +10,7 @@ Status: implemented
|
||||
|
||||
[会话接口面](../architecture/2026-06-18-session-surface.md)正是为此而构建的基础设施:一份建立在事件日志之上的有序投影,带有专门设计的 `surfaceOp: { op: 'replace', start, end }` 操作,用于遮蔽一段条目并插入替换内容,`sourceEventSeqs` 记录溯源信息以便决策可确定性地回放。剩下的是那个*决定压缩什么、并产出摘要*的插件。
|
||||
|
||||
两股力量塑造了设计。第一,压缩策略与可复用的 token 测量独立变化:测量归 LLM 系列的 [`ctx.tokenMeter` 服务](../architecture/2026-07-15-replay-token-meter-service.md)所有,摘要生成则可以使用模型调用、模板或远程服务。第二,`SurfaceEventType` 封闭为五种事件类型(`user/message`、`assistant/message`、`tool/result`、`context/message`、`steering/message`);只有这些类型可以携带 `surfaceOp`。因此一个专用的 `compaction/*` 事件**不能**出现在 surface 上,编译器与 Session 始终启用的 append/seed 边界都会拒绝在其上附加 `surfaceOp`。
|
||||
两股力量塑造了设计。第一,压缩策略与可复用的 token 测量独立变化:测量归 LLM 系列的 [`ctx.tokenMeter` 服务](../architecture/2026-07-15-replay-token-meter-service.md)所有,摘要生成则可以使用模型调用、模板或远程服务。第二,`SurfaceEventType` 封闭为产生消息的事件类型(`user/message`、`assistant/message`、`tool/result`);只有这些类型可以携带 `surfaceOp`。因此一个专用的 `compaction/*` 事件**不能**出现在 surface 上,编译器与 Session 始终启用的 append/seed 边界都会拒绝在其上附加 `surfaceOp`。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -19,7 +19,7 @@ Status: implemented
|
||||
遵循[能力 seam Agent Note](../architecture/2026-06-13-capability-seams.md),压缩以独立包发布,使契约、算法和(后续的)消费方 surface 各自独立演进:
|
||||
|
||||
1. **接口** — `@deepseek-ai/dsh-compact`:抽象 `CompactService`,拥有 `ctx.compact` 键、`CompactionResult` 词汇、`compact/*` 会话事件、手动失败分类体系以及规范的检查点消息来源。它将 `compactIfNeeded()`、`compactNow()` 和 `compactRegion()` 声明为**抽象方法**——契约说明压缩*做什么*,而非*怎么做*。
|
||||
2. **实现** — `@deepseek-ai/dsh-compact-basic`:具体的 `BasicCompactService`,消费 `ctx.tokenMeter`,并拥有尾→头保留遍历、通过 `ctx.llm.stream()` 生成摘要、surface 替换、锁、步骤后压力处理和规范的上下文溢出恢复。`summarize()` 是其唯一的子类钩子;计价与回放仍归 meter 所有。
|
||||
2. **实现** — `@deepseek-ai/dsh-compact-basic`:具体的 `BasicCompactService`,消费 `ctx.tokenMeter`,并拥有尾→头保留遍历、通过 `ctx.llm.stream()` 生成摘要、surface 替换、锁、步骤前压力处理和规范的上下文溢出恢复。`summarize()` 是其唯一的子类钩子;计价与回放仍归 meter 所有。
|
||||
3. **无模型配套服务** — `@deepseek-ai/dsh-compact-tool-result-prune`:一个具体的可选服务,在后端选择摘要范围之前,重写当前过大的 `tool/result` 节点。它不是第二种压缩实现,也不实现 `CompactService`。
|
||||
4. **面向用户的消费方** — `@deepseek-ai/dsh-command-compact` 通过 `ctx.commands` 注册无参数 `/compact`,并调用后端无关的 `compactNow()` 操作。它是供用户直接控制的命令,不是面向模型的工具。
|
||||
|
||||
@@ -33,18 +33,18 @@ Status: implemented
|
||||
|
||||
早期草案将完整算法(保留遍历、token 求和、文本提取)作为接口上的具体方法。这会将契约重新耦合到一种策略:想要不同保留策略或事件排序的后端必须与继承来的具体代码对抗。将三个操作都设为抽象,把所有*怎么做*的决策放在后端,并让接口保持为*做什么*的声明。token 测量根本不是压缩钩子;单例服务使多个消费方能够共享逐会话的回放折叠。
|
||||
|
||||
`compactIfNeeded(agent, trigger, signal)` 接受显式的 `'pressure' | 'context-overflow'` 触发原因与取消信号。它只读取最新的持久化已路由请求;没有 header 就不执行工作,任何已路由的提供方/模型目标都使用单例估算器。`compactNow(agent, signal)` 会预留空闲轮次接纳,即使未达到压力也进行一次有效的平衡缩减;不存在这种范围时返回 `null`,且不写入任何内容。`compactRegion(start, end, agent, signal?)` 将 `agent.session` 作为唯一会话身份,并为显式调用方保留可选 signal。默认摘要器依次从显式配置、最新记录的已路由目标和 agent 选项解析目标,并在任何 `llm/stream` 路由后记录提供方/模型对。它回放已路由请求的前缀,并将压缩指令追加为尾部 user 消息,从而复用提供方的热 KV Cache;见[摘要前缀缓存 Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md)。该调用将提供方无关的 `GenerateOptions.purpose` 设为 `compaction`;适配器可以将此用途映射为对模型隐藏的传输元数据,DeepSeek 适配器会发送 `x-deepseek-harness-compact: 1`。
|
||||
`compactIfNeeded(agent, trigger, signal)` 接受显式的 `'pressure' | 'context-overflow'` 触发原因与取消信号。它只读取最新的持久化已路由请求;没有 header 就不执行工作,任何已路由的提供方/模型目标都使用单例估算器。`compactNow(agent, signal)` 要求 agent 处于 idle,即使未达到压力也进行一次有效的平衡缩减;不存在这种范围时返回 `null`,且不写入任何内容。`compactRegion(start, end, agent, signal?)` 将 `agent.session` 作为唯一会话身份,并为显式调用方保留可选 signal。默认摘要器依次从显式配置、最新记录的已路由目标和 agent 选项解析目标,并在任何 `llm/stream` 路由后记录提供方/模型对。它回放已路由请求的前缀,并将压缩指令追加为尾部 user 消息,从而复用提供方的热 KV cache;见[摘要前缀缓存 Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md)。该调用将提供方无关的 `GenerateOptions.purpose` 设为 `compaction`;适配器可以将此用途映射为对模型隐藏的传输元数据,DeepSeek 适配器会发送 `x-deepseek-harness-compact: 1`。
|
||||
|
||||
### 成功的持久步骤工作完成后运行自动压力检查
|
||||
|
||||
成功调用的压力检查不能在步骤前运行,因为最终的 `agent/request` 路由、提供方输出、工具结果、缓冲上下文与 steering 当时尚不存在。串行的 `agent/post-step(agent, turn, step, signal)` 会在这些事实持久化后、`step/end` 之前触发。`dsh-compact-basic` 通过 `ctx.tokenMeter` 测量规范的已记录请求,因此下一个请求无需推测性覆盖信封即可看到任何替换。压力达到条件后,可选的 `ctx.toolResultPrune` 重写在摘要范围选择前运行;compact-basic 重新测量持久 surface,如果修剪恢复到安全压力便跳过摘要生成。
|
||||
成功调用的压力检查在下一个 `agent/pre-step` 运行;此时前一响应、工具结果、缓冲上下文与 steering 已经持久化,而下一个请求尚未派生。`dsh-compact-basic` 通过 `ctx.tokenMeter` 测量规范的已记录请求,因此下一个请求无需推测性覆盖信封即可看到任何替换。压力达到条件后,可选的 `ctx.toolResultPrune` 重写在摘要范围选择前运行;compact-basic 重新测量持久 surface,如果修剪恢复到安全压力便跳过摘要生成。
|
||||
|
||||
规范的提供方上下文溢出走另一条路径。失败步骤先关闭,`agent/request-error` 接收原始请求错误。compact-basic 自行持有按 agent 计的溢出次数,在强制执行一次有效且平衡的缩减前先修剪,且仅当 `session.surface.replaceGeneration` 增加时才返回 `{ kind: 'retry' }`;这包括没有摘要范围时仅修剪取得的进展。随后循环关闭失败轮次,开启新的编号重试轮次,并从持久日志重建请求。没有替换、任何替换前的恢复失败、取消、耗尽的上限或无关错误都会保留原始提供方失败。如果修剪已经推进 generation,而后续摘要工作失败,恢复会从该持久的已修剪 surface 重试,除非取消或资源释放胜出。完整生命周期决策见[调用后恢复 Agent Note](../architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md)。
|
||||
|
||||
```
|
||||
assistant/message → tool/result/context/steering
|
||||
await serial agent/post-step ⟵ pressure compaction inside the successful step
|
||||
step/end
|
||||
assistant/message → tool/result/context/steering → step/end
|
||||
claim the next batch → await waterfall agent/pre-step ⟵ pressure compaction before the next request
|
||||
enter → next step/start
|
||||
|
||||
provider overflow → step/end
|
||||
await waterfall agent/request-error ⟵ forced compaction between attempts
|
||||
@@ -53,7 +53,7 @@ retry → next numbered step/start ⟵ derives from the replacement surface
|
||||
|
||||
### 保留是轮次无关的;工具配对平衡是唯一的结构守卫
|
||||
|
||||
自动压缩在**每个成功的**步骤之后检查,而非每轮一次。这对失控轮次存活至关重要:工具密集型的 ReAct 轮次每步追加一个 `assistant/message` + 一个 `tool/result`,因此 surface 会在一轮之内增长。步骤后检查可以在后续步骤开启前压缩早期已关闭的工具对;如果请求率先越过限制,由提供方确认的溢出仍是兜底机制。
|
||||
自动压缩在**每个成功的**步骤之后检查,而非每轮一次。这对失控轮次存活至关重要:工具密集型的 ReAct 轮次每步追加一个 `assistant/message` + 一个 `tool/result`,因此 surface 会在一轮之内增长。下一个 pre-step 检查可以在继续执行打开另一步骤之前压缩早期已关闭的工具对;如果请求率先越过限制,由提供方确认的溢出仍是兜底机制。
|
||||
|
||||
`compactIfNeeded` 保留估算大小达到解析后保留 token 预算的最小完整 surface 单元尾部,压缩更早的节点。一个单元是一个完整的已关闭步骤或一条无步骤消息。如果 token 截断点落在步骤内部,保留范围会扩展直到切割点满足工具配对平衡。平衡按 surface 顺序检查,而非日志序号,因为替换摘要在旧的 surface 位置拥有新的序号。`dsh-compact` 导出前后边缘辅助函数;只要 `replaceGeneration` 不变,其逐会话缓存就只折叠新增的 surface 尾部节点,面对仅日志增长时不读取事件,并在替换后重建当前成员关系与平衡。`compactRegion` 拒绝将工具调用与其结果拆分的边界。进行中的轮次不享受特殊保留。
|
||||
|
||||
@@ -96,7 +96,7 @@ compact/end → log-only. Releases the lock (carries `error` on a recoverab
|
||||
1. **可检测的崩溃孤儿 + 来源追溯**(首要)。摘要生成是一次慢速模型调用,持久化在 `compact/start` *之后*。摘要生成中途崩溃会留下一个没有匹配 `compact/end` 的 `compact/start`——一个可检测的孤儿。最后释放锁(而非最先)将崩溃窗口从*静默损坏*转变为可检测的孤儿。
|
||||
2. **防止并发压缩。** 每个自动、手动和显式范围入口点都会拒绝活动的未匹配 `compact/start`。该标记对就是唯一的锁;没有进程本地 mutex 重复承担同一职责。
|
||||
|
||||
该锁只排除另一项压缩,不排除无关事实。其标记是时间点,而不是排他的容器,因此空闲注入的上下文可以出现在独立手动 start 与 end 之间。自动工作要求其轮次内的整个 surface 保持稳定。手动工作只重新验证所选位置 span,使其外部的仅追加上下文在替换后保持可见。
|
||||
该锁只排除另一项压缩,不排除无关事实。其标记是时间点,而不是排他的容器,因此持久 inbox splice 可以出现在独立手动 start 与 end 之间。自动工作要求其轮次内的整个 surface 保持稳定。手动工作只重新验证所选位置 span,使其外部的仅追加上下文在替换后保持可见。
|
||||
|
||||
生命周期边界使崩溃状态含义明确:
|
||||
|
||||
@@ -111,7 +111,7 @@ compact/end → log-only. Releases the lock (carries `error` on a recoverab
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **完整算法作为接口的具体方法**——否决,因为它将契约重新耦合到一种保留策略。三个操作都是抽象的;可复用测量属于单独的 LLM 系列服务,`summarize()` 是 basic 唯一的钩子。
|
||||
- **在 `agent/request` 或临时 `agent/pre-step` 输入上执行压缩**——否决,因为两者都无法证明最终的持久请求,而且都会将通用生命周期耦合到压缩专属的信封数据。步骤后回放与规范溢出恢复同时覆盖成功和被拒绝的调用。
|
||||
- **在 `agent/request` 或压缩专属的 loop 回调上执行压缩**——否决,因为前者观察的是临时请求,后者会将通用生命周期耦合到压缩策略。对先前持久请求进行 pre-step 回放,再加上规范溢出恢复,即可覆盖成功和被拒绝的调用。
|
||||
- **`compact` 布尔值或无类型的请求元数据 map**——否决,因为多个辅助调用种类会变成互斥标志,而开放 map 会丢弃由编译器检查的词汇。一个类型化的 `purpose` 判别字段可以扩展其他调用种类,而无需再为 `GenerateOptions` 添加字段。
|
||||
- **单独的 `compact/error` 事件**——否决:`compact/end` 保留 `error?` 字段,与 `tool/result` 的自包含错误一致——一个事件即可区分成功与失败,无需关联兄弟事件。
|
||||
- **教导核心轮次修复识别 `compact/*`**——否决:通用 end-seed 边界已经能够区分先前生命周期的历史;为每个未来的 `xxx/start … xxx/end` 插件对修补核心模块,恰好是能力 seam 架构存在的意义所要避免的耦合。
|
||||
@@ -119,7 +119,7 @@ compact/end → log-only. Releases the lock (carries `error` on a recoverab
|
||||
## 后果
|
||||
|
||||
- **包**:`packages/compact/compact` 提供接口,`compact-basic` 提供后端,`compact-tool-result-prune` 提供可选的确定性重写,`command-compact` 提供面向用户的 `/compact`。`packages/llm/token-meter` 独立拥有回放感知的测量。
|
||||
- **自动 seam**:`agent/post-step`(`@mode serial`)处理成功调用的压力,`agent/request-error`(`@mode waterfall`)处理失败步骤关闭后的最终请求失败。通用 `agent/pre-step` 保持为四参数检查点,不携带压缩专属的提示词/前缀 payload。
|
||||
- **自动 seam**:`agent/pre-step`(`@mode waterfall`)在请求派生前处理压力,`agent/request-error`(`@mode waterfall`)处理失败步骤关闭后的最终请求失败。pre-step 接收已领取批次与 `PreStepContext`,不携带压缩专属的提示词/前缀 payload。
|
||||
- **`SessionEventMap`** 通过可合并扩展的声明合并获得 `compact/start` / `compact/summary` / `compact/end`;`SurfaceEventType` **未被**触及。这些是会话事件,不是 cordis `Events`,因此事件分类门禁无需新增条目。
|
||||
- **`dsh-compact`** 拥有 `COMPACT_CHECKPOINT_SOURCE`、`isCompactCheckpointSource(source)`、`toolPairingBalancedBefore(session, seq)` 与 `toolPairingBalancedAfter(session, seq)`。该标记用于跨后端实现识别替换摘要。带缓存的 surface 边缘检查会防止 `compactRegion` 和 `compactIfNeeded` 拆分工具调用/结果对,按 seq 校验当前成员关系,从每个切割点的一条平衡序列回答两侧边缘,并拒绝陈旧或缺失的 seq 与孤立结果。
|
||||
- **`dsh-session`** 通过唯一的 surface 管理器校验位置替换、完整溯源信息和仅内容的单节点 `tool/result` 重写。其不变式配套插件将新追加的工具结果视为执行,要求存在已打开的步骤与待处理调用,而压缩配套组件拥有数字轮次归属与独立 `null` 归属标记对之间的关系。
|
||||
@@ -128,7 +128,7 @@ compact/end → log-only. Releases the lock (carries `error` on a recoverab
|
||||
## 测试
|
||||
|
||||
- **单元测试:** 使用真实 Loader 和 invariant 插件覆盖完整单元保留、修剪配置与回放、富块顺序、元数据保留、收敛、`compact/end` 的两种结果、开放尾部拒绝、仅修剪与带摘要的溢出恢复、generation 证明、上限和原始错误保留。
|
||||
- **循环测试:** 测试固定步骤后处理发生在持久工具结果之后、`step/end` 之前,使用实际 `agent/request` 路由,关闭失败步骤,分配新的重试编号,并覆盖完整的抛出/带内溢出 → 压缩 → 重建重试组合。
|
||||
- **手动测试:** 无需模型密钥即可固定接纳、标记顺序、注入保留、活动/陈旧未匹配标记分类、取消、闭合/flush 失败、命令映射以及排队 TUI 流程。
|
||||
- **循环测试:** 测试固定 pre-step 发生在前一个 `step/end` 之后、下一个 `step/start` 之前,使用实际 `agent/request` 路由,关闭失败步骤,分配新的重试编号,并覆盖完整的抛出/带内溢出 → 压缩 → 重建重试组合。
|
||||
- **手动测试:** 无需模型密钥即可固定 maintenance 串行化、标记顺序、注入保留、活动/陈旧未匹配标记分类、取消、闭合/flush 失败、命令映射以及排队 TUI 流程。
|
||||
- **带密钥 e2e:** 真实模型和 bash 会话在降低的限制下触发压缩,记录完整的 `compact/start…end` 对,缩小 surface,并完成任务。
|
||||
- **快照缺口:** 失控轮次压缩尚无法回放,因为摘要调用未记录 `assistant/chunk` 事件或 `sessionId`;交错摘要调用的回放仍是后续工作。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-24-workspace-context.md
|
||||
2026-06-24-workspace-context.md: 8baced0143abb38ff34d16a072761ec016a53d6e
|
||||
2026-06-24-workspace-context.zh.md: 3697d5bb00b2b166f8cd6c19e515aed1d0a183c7
|
||||
2026-06-24-workspace-context.md: df7ae58f8a42a8c1aac8113a429ef0f50b2fb795
|
||||
2026-06-24-workspace-context.zh.md: aecc57a5c63b4c6282260863f4183944c14b5cf8
|
||||
|
||||
@@ -14,7 +14,7 @@ The lifecycle has two distinct classes of content. The initial applicable chain
|
||||
|
||||
## Decision
|
||||
|
||||
The implementation lives in `packages/context/workspace-context` as `@deepseek-ai/dsh-workspace-context`. It is a request-context extension, not a core service or a filesystem backend. The shared demo spine and Host Runtime mount it from an explicit `{ maxBytes } | false` deployment choice; `dsh web` enables a 65,536-byte budget while the Host Runtime's headless consumer disables it. The plugin consumes `agent/step`, `tools/post-execute`, and the optional `ctx.fs` capability.
|
||||
The implementation lives in `packages/context/workspace-context` as `@deepseek-ai/dsh-workspace-context`. It is a request-context extension, not a core service or a filesystem backend. The shared demo spine and Host Runtime mount it from an explicit `{ maxBytes } | false` deployment choice; `dsh web` enables a 65,536-byte budget while the Host Runtime's headless consumer disables it. The plugin consumes `agent/pre-step`, `tools/post-execute`, and the optional `ctx.fs` capability.
|
||||
|
||||
The plugin does not statically inject `fs`. Providerless product trees therefore boot normally and the plugin no-ops until a filesystem provider exists. All production reads go through that provider. Candidate probes resolve each path and stat the result, so a final-component symlink is followed to its target: a link to a regular file loads, while a missing path or a non-file target is a confirmed absence. Following repository-owned links across the trust boundary is a deliberate reversal of the original no-follow probe; the [instruction-symlink follow note](2026-07-21-follow-instruction-symlinks.md) owns that decision and its residual risk. The step signal and dynamic tool execution signal propagate through resolution, metadata probes, and streaming reads, so cancellation does not wait for an unrelated filesystem scan. A resolve or stat exception is classified as unavailable: it skips only that candidate and is never interpreted as the deletion of an already-loaded scope.
|
||||
|
||||
@@ -28,9 +28,9 @@ The user-global file is fixed at `$DSH_HOME/AGENTS.md`, is not affected by eithe
|
||||
|
||||
### Baseline Injection
|
||||
|
||||
At the first `agent/step` of an agent-loop instance, the plugin injects one sourced user-role message before the request is derived. It loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads one candidate from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root.
|
||||
At the first `agent/pre-step` of an agent-loop instance, the plugin composes one sourced user-role baseline. When the downstream decision enters a nonempty first-step batch, the plugin folds the baseline into that final batch right after the claimed prompt, so it becomes durable with the direct prompt and reaches the first request. Rejection or an empty first-step decision leaves the baseline in the next-step inbox for a later wakeup. The plugin loads the user-global file first, then finds the project root by walking upward from `agent.session.header.cwd` to a configured root marker (default `.git`), then loads the configured candidates from each directory from the root to the cwd. A `.git` file and a `.git` directory are both valid markers, covering linked worktrees and submodules. Without a marker, the cwd itself is the root.
|
||||
|
||||
The injection becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes the complete startup or resume baseline from later deltas, and its change list persists the included scopes and content digests. In the product spine workspace instructions are registered before the skills catalog, so their `agent/step` listener injects first. The loop drains both messages before deriving the first request.
|
||||
The baseline becomes a durable `user/message` with a typed `workspace-instructions` source. Its `baseline: true` marker distinguishes the complete startup or resume baseline from later deltas, and its change list persists the included scopes and content digests. If a previously queued workspace baseline is still pending, the plugin removes that exact message and prepends its replacement instead of accumulating duplicates.
|
||||
|
||||
A resumed agent creates a new loop instance and injects a baseline composed from current files before its first request. This permits current baseline content on resume without mutating an earlier history event. A resume and a hot plugin remount both face a log that may already hold a baseline; they are told apart by `agent/session-start`, which a startup or resume emits before the first step while a remount attaches to an already-live session and never sees it. A remount retains the existing baseline only when its typed event remains in the current visible surface, and still rebuilds scope and provider-version tracking from current files. If compaction has shadowed that event, the remount injects a current baseline. A resume always re-composes.
|
||||
|
||||
@@ -68,7 +68,7 @@ There is intentionally no watcher. Detection occurs at the next successful struc
|
||||
|
||||
**Use a global `ctx.systemPrompt.section()`.** Rejected because one Cordis context can host sessions with different cwd values, while repository-owned text is lower-authority context rather than top-authority provider system content.
|
||||
|
||||
**Inject the baseline on every `agent/step`.** Rejected because repeated history injection wastes tokens and complicates duplicate state. A per-mount session guard gives one visible baseline event while it remains on the surface; dynamic append-only messages handle changes and compaction re-arming.
|
||||
**Always leave prepared workspace context in the inbox.** Rejected because context prepared during pre-step would then survive the current request and start a second model step by itself. The inbox remains the staging and rejection fallback, while an entering pre-step owns atomic delivery with its final batch.
|
||||
|
||||
**Load both `AGENTS.md` and `CLAUDE.md` in one directory.** Rejected because repositories in transition commonly duplicate guidance across both files. Ordered candidates make precedence explicit and configurable.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
该实现在 `packages/context/workspace-context` 中,包名为 `@deepseek-ai/dsh-workspace-context`。它是请求上下文扩展,不是核心服务或文件系统后端。共享 demo 主干与 Host Runtime 根据显式的 `{ maxBytes } | false` 部署选择挂载它;`dsh web` 启用 65,536 字节预算,Host Runtime 的 headless 消费方则禁用它。该插件使用 `agent/step`、`tools/post-execute` 和可选的 `ctx.fs` 功能。
|
||||
该实现在 `packages/context/workspace-context` 中,包(package)名为 `@deepseek-ai/dsh-workspace-context`。它是请求上下文扩展,不是核心服务或文件系统后端。共享 demo 主干与 Host Runtime 根据显式的 `{ maxBytes } | false` 部署选择挂载它;`dsh web` 启用 65,536 字节预算,Host Runtime 的 headless 消费方则禁用它。该插件使用 `agent/pre-step`、`tools/post-execute` 和可选的 `ctx.fs` 功能。
|
||||
|
||||
插件不会静态注入 `fs`。因此,不带提供方的产品树仍能正常启动;在文件系统提供方出现之前,插件保持无操作。所有生产读取都通过该提供方完成。候选项探测会解析每个路径并对结果执行 stat,因此会跟随最终路径组件的符号链接至其目标:指向普通文件的链接会被加载,缺失路径或非文件目标则确认为不存在。允许仓库拥有的链接跨越信任边界,是对最初不跟随探测方式的刻意反转;[跟随指令符号链接记录](2026-07-21-follow-instruction-symlinks.md)负责说明该决策及其残余风险。步骤信号与动态工具执行信号会贯穿解析、元数据探测和流式读取,因此取消不会等待无关的文件系统扫描。解析或 stat 异常归类为不可用:它只跳过该候选项,绝不被解释为已经加载的作用域被删除。
|
||||
|
||||
@@ -28,9 +28,9 @@ Status: implemented
|
||||
|
||||
### 基线注入
|
||||
|
||||
在 agent loop(智能体循环)实例的第一个 `agent/step`,插件会在派生请求前注入一条带来源的 user 角色消息。它先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录各加载一个候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。
|
||||
在 agent loop(智能体循环)实例的第一个 `agent/pre-step`,插件会组合一条带来源的 user 角色基线。当下游决策让非空的第一步批次进入时,插件会将基线折入最终批次、紧随已领取的直接提示词之后,使其与直接提示词一同成为持久记录并抵达第一次请求。reject 或空的第一步决策会将基线留在 next-step inbox,等待后续唤醒。插件先加载用户全局文件,再从 `agent.session.header.cwd` 向上遍历至配置的根标记(默认为 `.git`)以确定项目根目录,随后从根目录至 cwd 的每级目录加载已配置候选项。`.git` 文件与 `.git` 目录都是有效标记,因而能覆盖链接 worktree 和 submodule。找不到标记时,cwd 本身就是根目录。
|
||||
|
||||
该注入成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。在产品主干中,工作区指令的注册先于 skill(技能)目录,所以其 `agent/step` 监听器先注入。循环会在派生第一次请求前 drain 这两条消息。
|
||||
该基线会成为一条持久 `user/message`,并携带带类型的 `workspace-instructions` 来源。其 `baseline: true` 标记将完整的启动或恢复基线与后续增量区分开来,变更列表则持久保存已纳入的作用域和内容 digest。若先前排队的 workspace 基线仍在等待,插件会删除该确切消息并 prepend 替代值,而不会累积副本。
|
||||
|
||||
恢复 agent 会创建新的循环实例,并在其第一次请求前注入由当前文件组合的基线。这样,恢复时可以使用当前基线内容,而无需修改先前的历史事件。恢复与插件热重挂都会面对日志中可能已存在基线的情况;二者通过 `agent/session-start` 区分:启动或恢复会在第一步前发出该事件,而热重挂附着到一个已存活的会话、永远不会看到它。只有当基线的类型化事件仍在当前可见表层中时,热重挂才保留既有基线,同时仍会根据当前文件重建 scope 与提供方版本跟踪。如果压缩(compaction)已遮蔽该事件,热重挂会注入当前基线。恢复则始终重新组合。
|
||||
|
||||
@@ -68,7 +68,7 @@ shell 命令不会触发发现。本地 bash 调用会启动全新的 shell,
|
||||
|
||||
**使用全局 `ctx.systemPrompt.section()`。** 不予采纳,因为同一个 Cordis 上下文可以承载 cwd 不同的多个会话,而仓库自身拥有的文本属于低权威用户上下文,不是最高权威的提供方系统内容。
|
||||
|
||||
**在每次 `agent/step` 时注入基线。** 不予采纳,因为重复注入历史会浪费 token,并使重复状态复杂化。逐挂载会话防护会在基线事件仍留在表面期间提供一条可见基线事件;动态仅追加消息负责处理变更和压缩后的重新启用。
|
||||
**始终把准备好的 workspace 上下文留在 inbox 中。** 不予采纳,因为在 pre-step 中准备的上下文会因此在当前请求结束后继续留存,并自行启动第二个模型步骤。inbox 仍作为暂存区以及 reject 时的后备,而进入步骤的 pre-step 负责随最终批次原子投递。
|
||||
|
||||
**在一个目录中同时加载 `AGENTS.md` 和 `CLAUDE.md`。** 不予采纳,因为正在迁移的仓库通常会在两个文件中重复指引。按顺序排列的候选项让优先级显式且可配置。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-hook-bridges.md
|
||||
2026-06-30-hook-bridges.md: 99c6b1941a10e198ec3028f5fe505dabfff9abbe
|
||||
2026-06-30-hook-bridges.zh.md: 72c275128dbf2bf673beeb6b17daf657d728e418
|
||||
2026-06-30-hook-bridges.md: a31116818c57e18d04e656f371efd1101a63b6af
|
||||
2026-06-30-hook-bridges.zh.md: 8827408b67e2dd19f867a8658309f00231ba06c5
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-06-30-hook-bridges.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/prompt-submit`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)).
|
||||
The harness's extension surface is its typed interception seams ([the interception-seams Agent Note](2026-06-30-interception-seams.md)): a "native hook" is just an ordinary cordis plugin subscribing to `agent/session-start`, `agent/pre-step`, `tools/pre-execute`, `tools/post-execute`, `agent/turn-stopping`, `subagent/start`, or `subagent/end`. But users arrive with **existing** Claude Code (CC) and Codex hook configs — a `hooks.json` (or a settings file's `hooks` key) full of shell-command hooks — and want those to run unmodified. This Agent Note introduces the two **bridge plugins** that translate that external shell-hook protocol onto the typed seams, built on the shared wire-protocol library ([the hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md)).
|
||||
|
||||
The framing that shapes the whole design: **a bridge is a compatibility adapter, not a power tool.** Anything a bridge does (block a tool, inject context, force continuation, observe a subagent) a native cordis plugin does more powerfully — typed returns, full `ctx`, no serialization boundary. The bridge's reason to exist is to run the explicitly supported subset of external CC/Codex command hooks. That keeps each bridge thin: parse the config, pick a matcher mode, build the per-event payload, call `runHook` + `mergeHookOutputs` from the shared lib, and map the neutral outcome onto a seam Decision. The package READMEs own the exact current unsupported-event and partial-field inventory against the official protocols.
|
||||
|
||||
@@ -24,7 +24,7 @@ Each bridge maps the neutral `MergedHookOutcome` from the shared lib onto the se
|
||||
| Seam | CC | Codex |
|
||||
|---|---|---|
|
||||
| `agent/session-start` (emit) | additionalContext → `agent.inject()` | plain-stdout output → additionalContext → `agent.inject()` |
|
||||
| `agent/prompt-submit` | `deny`→`block`; context-only→delegate+fold | `block`→`block`; context-only→delegate+fold |
|
||||
| `agent/pre-step` | `deny`→`reject`; context-only→delegate+fold into `enter` | `block`→`reject`; context-only→delegate+fold into `enter` |
|
||||
| `tools/pre-execute` | `deny`→`deny`; `ask`→`ask` | `block`→`deny` (no allow/ask) |
|
||||
| `tools/post-execute` | `deny`→`block`+feedback; context-only→delegate+fold | same |
|
||||
| `agent/turn-stopping` | blocking Stop → next-step steering | same |
|
||||
@@ -37,11 +37,11 @@ The CC bridge's `ask` result is a real permission path, not a terminal bridge de
|
||||
|
||||
Every bridge `inject()` and additional-context input explicitly passes `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`. Unit coverage pins the resulting `user/message.source` as the plugin rather than the user.
|
||||
|
||||
`UserPromptSubmit` runs during admission, before any turn opens. It therefore writes no turn-scoped `hook/invoked` / `hook/result` pair: a block leaves no transcript, while allowed additional context is durably represented by its sourced `user/message`. The Codex payload still receives the candidate next `turn_id`; rejection does not consume that number.
|
||||
`UserPromptSubmit` runs at pre-step after `turn/start`, so every invocation writes its turn-scoped `hook/invoked` / `hook/result` pair. Rejection leaves the claimed input removed, closes the turn as blocked with no step, and retains the hook pair as its durable decision evidence. The Codex payload receives that open turn's `turn_id`.
|
||||
|
||||
### Adding context is not a veto — delegate, then prepend
|
||||
|
||||
A hook that only attaches `additionalContext` (no block/deny) is NOT a decision the bridge should return on its own: returning `allow`/`accept` from a waterfall listener WITHOUT calling `next()` short-circuits every later `agent/prompt-submit` / `tools/post-execute` listener, so a policy/sandbox plugin registered after the bridge would never see the prompt. Each bridge therefore delegates via `next()` before adding its context to the downstream decision. Both seams carry ordered `additionalContexts` arrays, so the bridge prepends its separately sourced entry while preserving every downstream source, envelope, and metadata field; a downstream prompt block still drops all context because the prompt never reaches the model, while post-tool block semantics may explicitly retain contexts. Code Mode ferries the same array through the outer `run_code` result. Only a real `deny`/`block` from the hook itself short-circuits. Tests assert a later listener can still block a prompt a context-only hook allowed and that retained prompt and post-tool contexts remain separate.
|
||||
A hook that only attaches `additionalContext` (no block/deny) is NOT a decision the bridge should return on its own: returning `enter` from a waterfall listener WITHOUT calling `next()` short-circuits every later `agent/pre-step` / `tools/post-execute` listener, so a policy/sandbox plugin registered after the bridge would never see the prompt. Each bridge therefore delegates via `next()` before adding its context to a downstream enter decision. The bridge preserves every downstream message, while a downstream pre-step rejection drops the whole claimed batch because no step opens. Post-tool decisions retain their independent ordered `additionalContexts` semantics, including Code Mode deferral through the outer `run_code` result. Only a real `deny`/`block` from the hook itself short-circuits. Tests assert a later listener can still reject a prompt after a context-only hook and that retained prompt and post-tool contexts remain separate.
|
||||
|
||||
### CLAUDE_PROJECT_DIR defaults to the session workspace
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/prompt-submit`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。
|
||||
harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](2026-06-30-interception-seams.md)):所谓「原生钩子」不过是一个普通的 Cordis 插件,订阅 `agent/session-start`、`agent/pre-step`、`tools/pre-execute`、`tools/post-execute`、`agent/turn-stopping`、`subagent/start` 或 `subagent/end`。但用户带着**既有的** Claude Code(CC)和 Codex 钩子配置到来,一个 `hooks.json`(或 settings 文件中的 `hooks` 键)里满是 shell 命令钩子,并希望它们原样运行。本 Agent Note 引入两个**桥接插件**,将外部 shell 钩子协议翻译到类型化 seam 上,构建于共享的协议格式(wire format)库之上(见 [hook-protocol-lib Agent Note](2026-06-30-hook-protocol-lib.md))。
|
||||
|
||||
贯穿整个设计的定位:**桥接是兼容性适配器,不是高级工具。** 桥接能做的事(阻止工具、注入上下文、强制继续、观察 subagent),原生 Cordis 插件都能做得更强——类型化返回值、完整 `ctx`、无序列化边界。桥接存在的理由是运行外部 CC/Codex 命令钩子中被明确支持的子集。这使每个桥接保持精简:解析配置、选择匹配模式、构建每事件的 payload、调用共享库的 `runHook` + `mergeHookOutputs`,再将中性结果映射为 seam Decision。各包的 README 维护着当前不支持的事件和部分字段的完整清单,以官方协议为参照。
|
||||
|
||||
@@ -24,7 +24,7 @@ harness 的扩展面是其类型化的拦截 seam(见[拦截 seam Agent Note](
|
||||
| Seam | CC | Codex |
|
||||
|---|---|---|
|
||||
| `agent/session-start`(emit) | additionalContext → `agent.inject()` | 纯 stdout 输出 → additionalContext → `agent.inject()` |
|
||||
| `agent/prompt-submit` | `deny`→`block`;仅上下文→delegate+fold | `block`→`block`;仅上下文→delegate+fold |
|
||||
| `agent/pre-step` | `deny`→`reject`;仅上下文→委托并折叠到 `enter` | `block`→`reject`;仅上下文→委托并折叠到 `enter` |
|
||||
| `tools/pre-execute` | `deny`→`deny`;`ask`→`ask` | `block`→`deny`(无 allow/ask) |
|
||||
| `tools/post-execute` | `deny`→`block`+feedback;仅上下文→delegate+fold | 同上 |
|
||||
| `agent/turn-stopping` | 阻塞的 Stop → 下一步 steering(中途引导) | 同上 |
|
||||
@@ -37,11 +37,11 @@ CC 桥接的 `ask` 结果是一条真正的权限路径,而非终态桥接决
|
||||
|
||||
每个桥接的 `inject()` 和 additional-context 输入都显式传入 `{ kind: 'plugin', plugin: 'hooks-claude' | 'hooks-codex' }`。单元测试固定验证结果中的 `user/message.source` 为插件而非用户。
|
||||
|
||||
`UserPromptSubmit` 在准入阶段运行,早于任何轮次开启。因此它不写入任何轮次范围的 `hook/invoked` / `hook/result` 对:阻止不会留下 transcript(文本记录),而被允许的额外上下文由其带来源的 `user/message` 持久呈现。Codex payload 仍会收到候选的下一个 `turn_id`;拒绝不会消耗该编号。
|
||||
`UserPromptSubmit` 在 `turn/start` 之后的 pre-step 运行,因此每次调用都会写入轮次范围的 `hook/invoked` / `hook/result` 对。reject 会让已领取输入保持删除,将轮次关闭为 blocked 且不包含步骤,并保留该 hook 对作为持久决策证据。Codex payload 会收到这个已打开轮次的 `turn_id`。
|
||||
|
||||
### 添加上下文不是否决——先 delegate,再 prepend
|
||||
|
||||
仅附加 `additionalContext`(没有 block/deny)的钩子并不是桥接可以独自返回的决策:在 waterfall 监听器中不调用 `next()` 就返回 `allow`/`accept`,会短路其后的每个 `agent/prompt-submit` / `tools/post-execute` 监听器,使注册在桥接之后的策略/沙箱插件看不到该提示词。因此,每个桥接都会先通过 `next()` 委托,再将自身上下文加入下游决策。两个 seam 都携带有序的 `additionalContexts` 数组,因此桥接会在保留所有下游来源、信封和元数据字段的同时,前置加入其独立来源的条目;下游提示词阻止仍会丢弃所有上下文,因为提示词从未到达模型,而工具后阻止语义可以显式保留上下文。Code Mode 会通过外层 `run_code` 结果转送同一数组。只有钩子本身真正返回 `deny`/`block` 才会短路。测试断言:上下文钩子允许后,较晚的监听器仍能阻止提示词,且保留的提示词和工具后上下文仍彼此分离。
|
||||
仅附加 `additionalContext`(没有 block/deny)的钩子并不是桥接可以独自返回的决策:在 waterfall 监听器中不调用 `next()` 就返回 `enter`,会短路其后的每个 `agent/pre-step` / `tools/post-execute` 监听器,使注册在桥接之后的策略/沙箱插件看不到该提示词。因此,每个桥接都会先通过 `next()` 委托,再将自身上下文加入下游 enter 决策。桥接会保留所有下游消息;下游 pre-step reject 会丢弃整个已领取批次,因为步骤从未打开。工具后决策仍保留独立的有序 `additionalContexts` 语义,包括 Code Mode 通过外层 `run_code` 结果延迟上下文。只有钩子本身真正返回 `deny`/`block` 才会短路。测试断言:仅上下文钩子之后,较晚的监听器仍能 reject 提示词,且保留的提示词和工具后上下文仍彼此分离。
|
||||
|
||||
### CLAUDE_PROJECT_DIR 默认为会话工作区
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md
|
||||
2026-06-30-hook-protocol-lib.md: ce25f40e96ffd5c319d9845e36eab5130cec5857
|
||||
2026-06-30-hook-protocol-lib.zh.md: 42843c1ef17f1586bab12ca2ab071aa9998f2631
|
||||
2026-06-30-hook-protocol-lib.md: 2edff0d501cd7695873c68054eeb3a1e9942eced
|
||||
2026-06-30-hook-protocol-lib.zh.md: 42e526fdf8073465d5f6faf99a6c92fbe09505f5
|
||||
|
||||
@@ -21,7 +21,7 @@ A new `packages/hooks/` group with `hook-protocol` as a pure library. It owns fo
|
||||
- **Merge** — `mergeHookOutputs(outputs)`, folding multiple matched hooks into one most-restrictive `MergedHookOutcome`: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined `\n\n`, context/system-messages accumulated in order.
|
||||
- **`hook/*` session events** — `hook/invoked` / `hook/result`, declaration-merged into `SessionEventMap` (log-only, like `compact/*` — NOT `SurfaceEventType`s), with `appendHookInvoked`/`appendHookResult` helpers so the invoked/result pairing and owner-defined execution relation stay consistent across bridges. `appendHookResult` also owns the durable record's semantics — the decision string (the hook's parsed decision, else `'stop'` on `continue:false`, else `'pass'`) and the 500-character `stderrSummary` truncation derive from the `HookOutput` here, not per-bridge.
|
||||
|
||||
**Per-dialect (the bridge plugins):** building each event's stdin payload (CC's base+per-event field sets vs Codex's snake_case with `turn_id`/`model` extras), the dialect's env + `${CLAUDE_PLUGIN_ROOT}` substitution (CC) vs none (Codex), and mapping the neutral `HookOutput`/`MergedHookOutcome` onto the harness's seam-specific typed Decisions (`PreToolDecision`, `PromptDecision`, `ContinuationDecision`, `PostToolDecision`).
|
||||
**Per-dialect (the bridge plugins):** building each event's stdin payload (CC's base+per-event field sets vs Codex's snake_case with `turn_id`/`model` extras), the dialect's env + `${CLAUDE_PLUGIN_ROOT}` substitution (CC) vs none (Codex), and mapping the neutral `HookOutput`/`MergedHookOutcome` onto the harness's seam-specific typed Decisions (`PreToolDecision`, `PreStepDecision`, `ContinuationDecision`, `PostToolDecision`).
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Status: implemented
|
||||
- **合并** — `mergeHookOutputs(outputs)`,将多个匹配钩子的输出折叠为一个最严格的 `MergedHookOutcome`:权限优先级 **deny > ask > allow**,halt 在首个 `continue:false` 时粘滞,阻止原因以 `\n\n` 拼接,context/system-messages 按序累积。
|
||||
- **`hook/*` 会话事件** — `hook/invoked` / `hook/result`,通过声明合并进入 `SessionEventMap`(仅日志,如 `compact/*`——不是 `SurfaceEventType`),配有 `appendHookInvoked`/`appendHookResult` 辅助函数,确保 invoked/result 配对与由所有者定义的执行关系在各桥接插件间保持一致。`appendHookResult` 还负责定义持久化记录的语义:decision 字符串(钩子解析出的 decision,否则 `continue:false` 时为 `'stop'`,否则为 `'pass'`)和 500 字符的 `stderrSummary` 截断均从本库的 `HookOutput` 派生,而非各桥接插件各自实现。
|
||||
|
||||
**方言专属(桥接插件):** 构建每个事件的 stdin payload(CC 的 base+per-event 字段集 vs Codex 的 snake_case 加 `turn_id`/`model` 额外字段)、方言的 env 与 `${CLAUDE_PLUGIN_ROOT}` 替换(CC)vs 两者皆无(Codex),以及将方言无关的 `HookOutput`/`MergedHookOutcome` 映射为 harness seam 专属的类型化 Decision(`PreToolDecision`、`PromptDecision`、`ContinuationDecision`、`PostToolDecision`)。
|
||||
**方言专属(桥接插件):** 构建每个事件的 stdin payload(CC 的 base+per-event 字段集 vs Codex 的 snake_case 加 `turn_id`/`model` 额外字段)、方言的 env 与 `${CLAUDE_PLUGIN_ROOT}` 替换(CC)vs 无替换(Codex),以及将方言无关的 `HookOutput`/`MergedHookOutcome` 映射为 harness seam 专属的类型化 Decision(`PreToolDecision`、`PreStepDecision`、`ContinuationDecision`、`PostToolDecision`)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-06-30-interception-seams.md
|
||||
2026-06-30-interception-seams.md: 4658983e1f098ecd199eecec4408e7c2f134cbf7
|
||||
2026-06-30-interception-seams.zh.md: 9c7a8509915d583d8f58d9d6d50509fdfcfca42f
|
||||
2026-06-30-interception-seams.md: 629a1aed509bd9bce9a2da89ce84b17a1db8e6b6
|
||||
2026-06-30-interception-seams.zh.md: d6958c9d1e7a8af8fa06d859d1905719a19cd43d
|
||||
|
||||
@@ -16,7 +16,7 @@ The canonical surface separates transformable policy, around-dispatch control, a
|
||||
|
||||
**Agent events** (`dsh-agent`):
|
||||
- `agent/session-start(agent, source)` — emit, once before turn 1, carrying a `SessionStartSource` (`startup` for a fresh/forked create, `resume` for a reloaded persisted session; `clear`/`compact` reserved). A pure notification — it CANNOT block startup (a deliberate gap: a bridge logs/injects, it does not gate startup). A listener seeds context via `agent.inject()`.
|
||||
- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` — waterfall, fired for one claimed queued message before the loop opens a turn or appends `user/message`. The explicit admission signal is placed before the final `next`; `allow` optionally rewrites the prompt `content` or attaches separately sourced `additionalContexts[]`, while `block` discards the candidate without creating session history.
|
||||
- `agent/pre-step(agent, messages, context, next) → PreStepDecision` — waterfall, fired before every proposed step after the loop has atomically removed its exclusive inbox batch. `PreStepContext` carries that request's `turn`, `step`, and cancellation `signal`; `messages` is empty for a tool continuation with no intervening input. `enter` returns the complete message batch, including any current-request context a listener contributes; `reject` opens no step and leaves the claimed messages removed.
|
||||
|
||||
**`agent/turn-stopping`** is an awaited notification at the natural stop boundary. A listener that needs another step calls `agent.steer()` with explicitly sourced model-facing content; the loop then re-reads the outbox and either continues or closes the turn.
|
||||
|
||||
@@ -35,7 +35,7 @@ Core dispatch and the tool body sit inside normalization boundaries, so tool, li
|
||||
|
||||
### Three load-bearing loop decisions
|
||||
|
||||
1. **Run prompt policy before opening the turn.** A blocked prompt creates no turn or durable event. On allow, the loop stages the rewritten prompt followed by every returned `additionalContexts` entry, opens the turn, and drains that outbox before the first step. Each claimed ordinary-send item is the sole direct prompt in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md).
|
||||
1. **Run pre-step policy at every proposed step.** The loop opens the turn before the initial claim and decision, so rejection closes a durable blocked turn with no step or model-visible message. A tool continuation with no newly claimed input still submits an empty batch, allowing per-request context producers to add logged messages to that exact request. On enter, the loop opens the step and appends the returned batch as `user/message` events before request derivation. Each claimed follow-up remains the sole direct prompt in its turn under the [one-send-one-turn simplification](../simplification/2026-07-17-one-send-one-turn.md).
|
||||
|
||||
2. **Post-tool `additionalContexts` and asynchronous injections enter the active-batch FIFO and append when that batch settles.** `content`/`feedback` shape the result `execute()` returns, but each context is a separate sourced `user/message`, and a single step or composite tool can produce many. Appending context immediately would interleave `result(c1) → context → result(c2)` or place nested context before its outer result, breaking tool-call/result adjacency. `ToolRunContext.deferContext()` therefore collects nested-dispatch context through failures, `execute()` surfaces the ordered array on `ToolExecutionResult`, and the loop accepts it into the same FIFO as `agent.inject()` calls made during execution. The FIFO appends after every recorded result when the batch settles, including before an interrupted turn closes. An accepted outer call preserves deferred contexts before decision contexts; an outer block discards deferred contexts and exposes only contexts explicitly supplied by the blocking decision.
|
||||
|
||||
@@ -56,4 +56,4 @@ The seam package does **not** declare `hook/*` session events (the durable hook-
|
||||
|
||||
## Consequences
|
||||
|
||||
The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-turn prompt admission, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an admission rejection as `cancelled` after the agent becomes idle with no owned turn, while hook-driven snapshots verify the observable bridge behavior end to end.
|
||||
The canonical interception surface is uniformly typed without giving every extension the same power: hooks return decisions, execution wrappers wrap, terminal guards only deny, and final observers only observe. The loop owns session-start, pre-step claim settlement, post-tool context buffering, and stopping; `dsh-tools` owns identity sealing and the five-phase execution pipeline. Their contracts are documented in [architecture.md](../../../../docs/architecture.md), package READMEs, [core interception decisions](../../../../docs/core-data-structures/core.md#interception-decisions), and [tool structures](../../../../docs/core-data-structures/tools.md). The ACP bridge settles an initial pre-step rejection from its blocked no-step turn as `end_turn`, while hook-driven snapshots verify the observable bridge behavior end to end.
|
||||
|
||||
@@ -15,8 +15,8 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
|
||||
规范表面将可变换策略、环绕调度控制与仅观测通知分离。策略 waterfall(瀑布式事件)返回小型的、seam 专属的**类型化 Decision 联合类型**;包装层返回规范化结果;通知接收不可变快照,无法影响结果。覆盖的钩子点包括 `session-start`、`prompt-submit`、`pre-tool`、`post-tool`、通过 continuation 实现的 `stop`,同时将非钩子的执行策略留作独立可组合。
|
||||
|
||||
**Agent 事件**(`dsh-agent`):
|
||||
- `agent/session-start(agent, source)` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。
|
||||
- `agent/prompt-submit(agent, content, source, signal, next) → PromptDecision` ——waterfall,针对一条取得所有权的排队消息触发,早于循环开启轮次或追加 `user/message`。显式准入 signal 位于最后的 `next` 之前;`allow` 可以重写提示词 `content` 或附加来源各自独立的 `additionalContexts[]`,而 `block` 会丢弃该候选消息,不产生会话历史。
|
||||
- `agent/session-start(agent, source)` ——emit,在第 1 轮次之前触发一次,携带 `SessionStartSource`(`startup` 表示全新/fork 创建,`resume` 表示重新加载的持久化会话;`clear`/`compact` 保留)。纯通知,不能阻塞启动(这是有意的空白:桥接可以记录/注入,但不管控启动)。监听器通过 `agent.inject()` 注入上下文。
|
||||
- `agent/pre-step(agent, messages, context, next) → PreStepDecision` ——waterfall,在每个拟议步骤之前、循环原子移除其独占 inbox 批次后触发。`PreStepContext` 携带该请求的 `turn`、`step` 与取消 `signal`;没有中途输入的工具续步会收到空批次。`enter` 返回完整消息批次,其中包括监听器为当前请求贡献的上下文;`reject` 不打开步骤,并让已领取消息保持已删除。
|
||||
|
||||
**`agent/turn-stopping`** 是自然停止边界上的一次 awaited 通知。需要再执行一步的监听器调用 `agent.steer()`,传入来源显式的 steering(中途引导)内容供模型使用;循环随后重新读取 outbox,继续执行或关闭轮次。
|
||||
|
||||
@@ -35,7 +35,7 @@ harness 需要一套钩子子系统:用户像 Claude Code(CC)和 Codex 那
|
||||
|
||||
### 三个承重的循环决策
|
||||
|
||||
1. **在开启轮次之前运行提示词策略。** 被阻止的提示词不会创建轮次,也不产生持久事件。允许时,循环先暂存重写后的提示词,再暂存每个返回的 `additionalContexts` 条目,然后开启轮次并在第一个步骤之前排空该 outbox。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个取得所有权的 ordinary-send 条目都是其轮次中唯一的直接提示词。
|
||||
1. **在每个拟议步骤运行 pre-step 策略。** 循环会在首次领取和决策之前打开轮次,因此 reject 会关闭一个持久、blocked 且不含步骤或模型可见消息的轮次。即使工具续步没有新取得所有权的输入,也会提交空批次,使逐请求上下文生产方可以把带日志的消息加入这一次请求。enter 时,循环先开启步骤,再把返回批次作为 `user/message` 追加,然后派生请求。依照[一次 send 对应一个轮次的简化](../simplification/2026-07-17-one-send-one-turn.md),每个已领取 follow-up 仍是其轮次中唯一的直接提示词。
|
||||
|
||||
2. **工具执行后的 `additionalContexts` 与异步注入进入活跃批次 FIFO,并在该批次结算时追加。** `content`/`feedback` 塑造 `execute()` 返回的结果,但每项上下文都是一条独立的带来源 `user/message`,而单个步骤或组合工具可以产生许多上下文。立即追加上下文会产生 `result(c1) → context → result(c2)` 的交错,或把嵌套上下文放在外层结果之前,破坏工具调用/工具结果邻接性。因此 `ToolRunContext.deferContext()` 会在失败路径上也收集嵌套调度上下文,`execute()` 在 `ToolExecutionResult` 上暴露有序数组,循环再把它接纳到与执行期间 `agent.inject()` 调用相同的 FIFO 中。FIFO 在批次结算时,在所有已记录结果之后追加,其中也包括被中断轮次关闭之前。被接受的外层调用将 deferred contexts 保留在 decision contexts 之前;被外层阻止时则丢弃 deferred contexts,只暴露阻止 decision 显式提供的上下文。
|
||||
|
||||
@@ -56,4 +56,4 @@ seam 包**不**声明 `hook/*` 会话事件(持久的钩子调用日志);
|
||||
|
||||
## 后果
|
||||
|
||||
规范拦截表面采用统一的类型体系,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层封装执行过程,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、轮次前的提示词准入、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP(Agent Client Protocol)桥接在 agent 空闲且不再拥有轮次后,将准入拒绝结算为 `cancelled`,而钩子驱动的快照端到端验证可观测的桥接行为。
|
||||
规范拦截表面具有统一的类型化,同时不给每个扩展相同的权力:钩子返回 decision,执行包装层做包装,终结 guard 只能拒绝,最终观测者只能观测。循环负责 session-start、pre-step 领取结算、工具执行后上下文缓冲和 stopping;`dsh-tools` 负责身份封存与五阶段执行流水线。它们的契约记录在 [architecture.md](../../../../docs/architecture.md)、各包 README、[核心拦截 decision](../../../../docs/core-data-structures/core.md#interception-decisions) 与[工具结构](../../../../docs/core-data-structures/tools.md)中。ACP 桥接会把 blocked 无步骤轮次中的首次 pre-step reject 结算为 `end_turn`,而钩子驱动的快照端到端验证可观测的桥接行为。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-05-skill-system.md
|
||||
2026-07-05-skill-system.md: 36a2ae220db24c3183253c18957769ab5d5c068c
|
||||
2026-07-05-skill-system.zh.md: 3a673d44d74ab38732dd5f5bda4d607b174db251
|
||||
2026-07-05-skill-system.md: dd2fb1d22949f55ea7cb2c9f280e7cfbfcbbb226
|
||||
2026-07-05-skill-system.zh.md: 96656a8e1dfc2ae1ce7301ba29e7739349b6aab6
|
||||
|
||||
@@ -22,7 +22,7 @@ Each skill is either `<name>/SKILL.md` or `<name>.md` with YAML frontmatter. `na
|
||||
|
||||
Local skill filesystem I/O goes through `ctx.fs` when a filesystem service is loaded: project-root lookup probes `.git` with `resolve` and `stat`, root discovery uses `listDir`, and skill reads use `readText`. The Node filesystem remains a fallback for minimal contexts that mount `dsh-skill-local` without the fs seam. Missing roots, unreadable or malformed skill files, and transient provider `list()` failures degrade to warn-and-skip so one bad source does not make every agent request fail; malformed candidates still fail fast because they are provider contract violations.
|
||||
|
||||
`dsh-tool-skill` injects one durable user-role `<system-reminder>` catalog as a sourced `user/message` at the session's first `agent/step`, and only when that agent's tool view resolves this plugin's exact `skill` registration. The catalog contains sorted skill name and description only; it excludes bodies, paths, sources, providers, and routing hints. Descriptions are whitespace-normalized, XML-escaped, and capped by `catalogDescriptionMaxLength`, whose default is `500` and minimum is `3`. Full skill bodies are never included in the catalog. (The catalog originally rode the request-only [session-prefix seam](../../archived/feature/2026-07-07-session-prefix.md), archived; the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md) moved it into durable history.)
|
||||
`dsh-tool-skill` injects one durable user-role `<system-reminder>` catalog as a sourced `user/message` at the session's first `agent/pre-step`, and only when that agent's tool view resolves this plugin's exact `skill` registration. The catalog contains sorted skill name and description only; it excludes bodies, paths, sources, providers, and routing hints. Descriptions are whitespace-normalized, XML-escaped, and capped by `catalogDescriptionMaxLength`, whose default is `500` and minimum is `3`. Full skill bodies are never included in the catalog. (The catalog originally rode the request-only [session-prefix seam](../../archived/feature/2026-07-07-session-prefix.md), archived; the [unified sourced-message decision](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md) moved it into durable history.)
|
||||
|
||||
The registry's `list()` returns every winning summary, while model and user consumers apply the invocation predicates owned by the [independent invocation-policy decision](2026-07-28-skill-invocation-policy.md). The `skill({ name })` tool loads one model-invocable skill for the current agent cwd and returns a tool result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`. `resourceBase` supplies a directory, URL, or opaque provider-managed base for explicitly referenced scripts, references, and assets; resources load only as needed, without directory enumeration. An unresolved name reports that the skill is unknown or no longer available; invalid names and skills with `invocation.modelInvocable: false` retain distinct tool errors. The tool result is the model-visible disclosure path.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ DeepSeek Harness 使用同一原语,使项目特定的评审、插件编写和
|
||||
|
||||
本地 skill 的文件系统 I/O 在加载了文件系统服务时通过 `ctx.fs` 进行:项目根目录查找使用 `resolve` 和 `stat` 探测 `.git`,根目录发现使用 `listDir`,skill 读取使用 `readText`。Node 文件系统作为后备,供在不挂载 fs seam 的最小上下文中加载 `dsh-skill-local` 时使用。缺失的根目录、不可读或格式错误的 skill 文件、以及提供方 `list()` 的瞬态失败均降级为警告并跳过,使一个坏源不会导致所有 agent 请求失败;格式错误的候选项仍然快速失败,因为它们违反了提供方契约。
|
||||
|
||||
`dsh-tool-skill` 在会话的第一个 `agent/step` 注入一个持久化的 user-role `<system-reminder>` 目录,作为带来源的 `user/message`,且仅当该 agent 的工具视图解析到本插件精确的 `skill` 注册时才注入。该目录仅包含排序后的 skill 名称与描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 上限约束,其默认值为 `500`,最小值为 `3`。完整的 skill 正文从不包含在目录中。(目录最初通过仅请求的[会话前缀 seam](../../archived/feature/2026-07-07-session-prefix.md)(已归档)传递;[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)将其移入持久化历史。)
|
||||
`dsh-tool-skill` 在会话的第一个 `agent/pre-step` 注入一个持久化的 user-role `<system-reminder>` 目录,作为带来源的 `user/message`,且仅当该 agent 的工具视图解析到本插件精确的 `skill` 注册时才注入。该目录仅包含排序后的 skill 名称与描述;不包含正文、路径、来源、提供方和路由提示。描述经过空白规范化、XML 转义,并受 `catalogDescriptionMaxLength` 上限约束,其默认值为 `500`,最小值为 `3`。完整的 skill 正文从不包含在目录中。(目录最初通过仅请求的[会话前缀 seam](../../archived/feature/2026-07-07-session-prefix.md)(已归档)传递;[统一带来源消息的决策](../architecture/2026-07-22-unified-send-and-coalesced-user-messages.md)将其移入持久化历史。)
|
||||
|
||||
注册表的 `list()` 返回全部胜出摘要,而模型与用户消费方应用[独立调用策略决策](2026-07-28-skill-invocation-policy.md)定义的调用判定。`skill({ name })` 工具为当前 agent cwd 加载一个模型可调用的 skill,返回包含 `<skill_content name="...">`、`<skill_resources>` 和 `<skill_instructions>` 的工具结果。`resourceBase` 提供一个目录、URL 或不透明的提供方管理的基路径,用于显式引用的脚本、参考资料和资产;资源仅按需加载,不进行目录枚举。无法解析的名称报告该 skill 未知或不再可用;无效名称和 `invocation.modelInvocable` 为 `false` 的 skill 保留不同的工具错误。工具结果是面向模型的可见披露路径。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-06-sandbox.md
|
||||
2026-07-06-sandbox.md: 6934691e9b3f37e50bb02dfec0240a03ec9b5f30
|
||||
2026-07-06-sandbox.zh.md: cc5b7e5be0f697d77732efbf55667c258b69a95f
|
||||
2026-07-06-sandbox.md: aed5ac1ceb02130ce97a8c83c0f77869fdc32146
|
||||
2026-07-06-sandbox.zh.md: db95b1a5b7a7cae1e0fcdd8deba9dcb6ad020a67
|
||||
|
||||
@@ -40,7 +40,7 @@ The swap is invisible to every consumer of `ctx.bash`: the bash tools, hook comm
|
||||
|
||||
Misconfiguration fails loud: `mode` outside the closed vocabulary is rejected at plugin load, and a host with no usable backend throws the structured `SANDBOX_UNAVAILABLE` at `confine()` rather than degrading to unconfined execution. If the selected runner rejects with attributable `ENOENT` or `EACCES`, the consumer reports the same infrastructure error from the spawn channel before any command starts; other spawn errors retain local command-start semantics while still running nothing. `runnerCommand` on `dsh-sandbox-local` is the operator's explicit assertion of a bwrap-compatible runner (chain and probes skipped); it doubles as the deterministic fake-runner seam for keyless tests.
|
||||
|
||||
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The owner-derived runtime context states the current file policy without replacing those enforcement boundaries. When `dsh-permission` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
|
||||
Denied file effects return a `[sandbox: file access denied under <mode> mode]` marker and instructions not to work around the denial. A confining executor adds paired `sandbox_permissions` and `justification` fields for one approved retry that must be strictly wider than the session's effective mode. A grant widens only that retry; rejection executes nothing, returns `the user rejected escalating this command to "<mode>"`, and permits no re-ask. The owner-derived pending policy context states the current file policy without replacing those enforcement boundaries. When `dsh-permission` is composed with a UI adapter, one preset selects both knob values; unmatched values fold to `custom`. The [ACP automation composition](../../../../examples/acp-agent/README.md) does not mount that UI service and selects its deployment mode explicitly.
|
||||
|
||||
### Design detail
|
||||
|
||||
@@ -72,7 +72,7 @@ Backend profiles share the mode contract but differ in necessary host grants. La
|
||||
|
||||
`dsh-bash-sandbox` extends `LocalBashExecutor`, hands `ctx.sandbox` the exact `['bash', '-c', command]` argv, and directly spawns the provider result. This leaves shell semantics and `BASH_ENV` on the inner Bash after the shipped native runner establishes confinement. A provider error propagates unchanged. A pre-process rejection is runner-owned only when the caller-owned workdir is independently usable and Node reports `ENOENT` or `EACCES` with positive provenance for provider argv[0]; a bare `syscall: 'spawn'` without an exact error path, other codes, invalid workdirs, resource failures, unrelated syscalls, and unstructured rejections retain local command-start semantics. Foreground execution converts runner-owned rejections to `SANDBOX_UNAVAILABLE` with the original detail; an asynchronous background rejection stamps `runnerFailed: true`, `denied: false`. A `SubprocessService` that synchronously throws the same provenanced shape makes background start throw `SANDBOX_UNAVAILABLE`, while other synchronous errors propagate unchanged. After a process starts, foreground and background use one runner-failure classifier that requires the rule's exit-code gate and a remaining fatal line after informational exclusions. A match outranks denial: foreground execution throws `SANDBOX_UNAVAILABLE` with that fatal line as detail; a settled `BashProcess` stamps `sandbox.runnerFailed`, and the bash producer renders it through generic `task_output`.
|
||||
|
||||
The model sees the current effective file policy in the owner-derived `sandbox:policy` runtime context, while the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). [The current-policy decision](2026-07-30-current-sandbox-policy-context.md) owns the context's rationale and boundaries.
|
||||
The model sees the current effective file policy in the owner-derived `sandbox:policy` context, while the static tool description explains the denial marker (`[sandbox: file access denied under <mode> mode]`), encourages attempting commands that may be denied, and forbids retrying around a denial; when the escalation fields are advertised, a denied result additionally carries the escalation hint itself, so the sanctioned same-turn retry is prompted at the decision point rather than depending on the model recalling the description (§ Escalation). [The current-policy decision](2026-07-30-current-sandbox-policy-context.md) owns the context's rationale and boundaries.
|
||||
|
||||
#### Escalation: one approved wider retry after a denial
|
||||
|
||||
@@ -105,7 +105,7 @@ interface SessionEventMap {
|
||||
|
||||
Each owner exports the same three-piece kit: the event declaration, a pure fold (`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)` — a `findLast`, typed to the domain's closed union), and THE write path (`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)` — a switch IS its event; nothing mutates state out of band). No shared owner service, no generic facts map, no registry: a third knob copies the ~40-line pattern into its own package. Execution follows the fold on both sides — the bash tool's per-call stamp reads it as the middle rung of the § Escalation precedence chain, and the approval seam's `'never'` gate is [the approval Agent Note](2026-07-06-approval-seam.md)'s side of the same pattern.
|
||||
|
||||
Sandbox and approval policy are rendered as ordered contributions to one runtime-context snapshot before every request. The loop records the complete snapshot as a sourced `user/message`; both `'ask'` and `'never'` are explicit, so neither owner needs switch narration or last-told state.
|
||||
Before each proposed step, sandbox and approval policy are rendered as ordered contributions to one desired policy-context message. The listener reconciles that message against session history, the claimed batch, and its pending `next-step` inbox entry. Once claimed and entered, the loop records the complete sourced `user/message`; both `'ask'` and `'never'` are explicit, so neither owner needs switch narration or last-told state.
|
||||
|
||||
**The optional UI surface** is `PermissionService`: a deployment-defined preset table whose entries bundle one sandbox mode with one approval policy. The shipped `workspace-write` and `danger-full-access` presets write through to both domain setters; a knob combination outside the table is reported as `custom`. UI adapters may expose that table as a selector. The automation-only ACP transport advertises no configuration selector and mounts no permission-preset service.
|
||||
|
||||
@@ -149,9 +149,9 @@ Each phase gets its full design when picked up, validated against the code at th
|
||||
- **Per-session dynamic tool schemas** — rejected: schemas are registry-global by design (one assembly vocabulary, the pinned-header snapshot contract), and re-registering per session would buy only what the execution-time strict-wider check already guarantees, at the cost of a per-session schema surface and header churn on every switch.
|
||||
- **Hard-match the retry to a prior denial** — rejected: command-string identity is fragile (quoting, `workdir`, env prefixes, a pipeline retried as its failing stage) — false-rejects honest retries or is trivially satisfied; the real boundary is the human seeing command + justification. Revisit only if `allow_always` grant storage ever needs machine-checkable scopes.
|
||||
- **A generic `env/state` facts map with an owner service** — rejected: approval and sandbox compose independently, so neither's state may drag in a third package; single-key folds are one `findLast` each, dissolving the owner service; no invariant spans the knobs, so atomic multi-key patches bought nothing.
|
||||
- **Narrate each switch through `agent.inject()` plus a bus event** — rejected: independent notices expose owner ordering and intermediate combinations, while one assembly pass can materialize the complete current state atomically at the request boundary.
|
||||
- **Narrate each switch through `agent.inject()` plus a bus event** — rejected: independent notices expose owner ordering and intermediate combinations, while one pre-step composition can enqueue the complete current state atomically.
|
||||
- **State sandbox mode in the stable system prompt** — shipped first, then removed on live evidence: with `Bash commands run under the "read-only" file sandbox.` in every request, the model refused to ATTEMPT denied-then-escalatable work (five of twelve turns in the first manual session ended with zero tool calls), turning the sandbox into a soft lockout. The denial marker names the mode at the moment it matters and the escalation fields carry the recovery. The absence decision is superseded by [the current-policy decision](2026-07-30-current-sandbox-policy-context.md); this measurement and causal observation remain the evidence that any replacement must counter-test.
|
||||
- **Track "last told" with its own bookkeeping events** — rejected: the latest sourced runtime-context `user/message` records the exact full snapshot the model saw. Materializing that snapshot from current owner contributions replaces a second bookkeeping stream — events are needed only where they ARE the store.
|
||||
- **Track "last told" with its own bookkeeping events** — rejected: session history records the exact policy context the model saw, while the claimed batch and pending inbox entry show what is entering or queued. Recomputing the desired message replaces a second bookkeeping stream — events are needed only where they ARE the store.
|
||||
- **Independent sandbox and approval selectors** — rejected: one deployment-defined permission preset keeps the two policy knobs coherent for UI clients that expose runtime switching.
|
||||
|
||||
## Consequences
|
||||
@@ -160,12 +160,12 @@ What shipped pins — the tiers in Testing hold each:
|
||||
|
||||
- A denied command retried with `sandbox_permissions` + `justification` prompts the user through the composed answerer chain; a grant runs THAT call under the wider mode (result facts say so) while every other call keeps its own effective mode; every non-grant outcome produces its distinct error text and executes nothing.
|
||||
- The escalation fields exist exactly when the mounted executor confines; a request that is not strictly wider than the call's effective mode fails closed with its own text and prompts no one; a deployment with no ApprovalService fails escalating calls closed and leaves plain calls untouched.
|
||||
- One sourced runtime-context message states the complete current sandbox and approval policies atomically; the whole exchange — context snapshots, headers, knob events, approval notices, approvals, and results — reconstructs from the session log alone, with no policy bookkeeping events beyond the two knob events.
|
||||
- One preset selection records only changed knob values, while a no-op selection records nothing; the next request snapshots both current values atomically, and a committed sandbox switch is honored by the next call's stamp.
|
||||
- A resumed session's overrides enter its first new runtime-context snapshot with no catch-up state; a composition default changed while the process was down likewise appears in that snapshot.
|
||||
- One sourced policy-context message states the complete current sandbox and approval policies atomically; the whole exchange — context messages, headers, knob events, approval notices, approvals, and results — reconstructs from the session log alone, with no policy bookkeeping events beyond the two knob events.
|
||||
- One preset selection records only changed knob values, while a no-op selection records nothing; the next pre-step upserts both current values atomically, and a committed sandbox switch is honored by the next call's stamp.
|
||||
- A resumed session's overrides enter its first new policy-context message with no catch-up state; a composition default changed while the process was down likewise appears in that message.
|
||||
- Two concurrent sessions never see each other's state or notices.
|
||||
- Two concurrent project sessions in one Cordis context resolve independent workspace roots; bash and fs writes succeed inside the calling session's cwd and fail against its neighbor's cwd.
|
||||
- Policy ownership stays in plugins through `systemPrompt.context`, `SessionEventMap` merging, and capability-owned resolution; the generic loop change materializes every owner's ordered context as one sourced message.
|
||||
- Policy ownership stays in plugins through `SessionEventMap` merging, inbox mutation from `agent/pre-step`, and capability-owned resolution; the generic loop only claims and records the final entered batch.
|
||||
|
||||
Costs and accepted limits:
|
||||
|
||||
@@ -191,8 +191,8 @@ Costs and accepted limits:
|
||||
- **Does the sandbox restrict network or process visibility?** No — `SandboxMode` claims FILE effects only; the bwrap profile deliberately does not unshare pid, and no backend claims network. Whether network restriction becomes its own knob is left open in § The seam.
|
||||
- **Which tools actually run confined?** OS subprocesses through `ctx.bash` — the bash tools, and hook commands transitively — plus the filesystem tools (`read`/`write`/`edit`) through the sandboxed `ctx.fs` provider (the [cross-family fs sandbox RFC](2026-07-14-cross-family-fs-sandbox.md)): bash confines via the OS runner, fs via an in-process path fence, both keying off the same `ctx.sandboxPolicy` mode. web/todo stay in-process and unfenced (web's only effect is network, outside the file-effect mode vocabulary).
|
||||
- **Does a granted escalation persist?** No. The grant is consumed by the exact foreground or background call that asked; every neighboring call keeps its own effective mode. A later background denial surfaces through `task_output` and may ground a new exact-command retry.
|
||||
- **When does a runtime mode switch take effect?** Once its session event commits, the next runtime-context snapshot and the next capability resolution fold the new mode. The sourced context message records what the model was told, and any later denial names the same policy at the point of use.
|
||||
- **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline enters the next full runtime-context snapshot.
|
||||
- **When does a runtime mode switch take effect?** Once its session event commits, the next pre-step policy-context reconciliation and the next capability resolution fold the new mode. The sourced context message records what the model was told, and any later denial names the same policy at the point of use.
|
||||
- **What survives a restart — and what if the operator changed the config default while the process was down?** Overrides replay from the session log (`effective = fold ?? config`), so a resumed session keeps its modes with zero catch-up machinery; a default that drifted offline enters the next full policy-context message.
|
||||
- **What does `enforcement: 'partial'` on a result mean?** The selected backend enforces the subset its kernel ABI governs — e.g. Landlock before ABI v3 does not govern path truncate — and says so structurally instead of refusing the host; the probe's report line distinguishes the cases. The bwrap and Seatbelt profiles govern every promised file effect by construction, so they always report `full`.
|
||||
|
||||
## Prior art
|
||||
|
||||
@@ -40,7 +40,7 @@ harness 是一个 SDK,因此约束必须是开发者可组合的能力:是
|
||||
|
||||
配置错误会显式导致失败:`mode` 不在封闭词汇中时在插件加载时被拒绝;主机上没有可用后端时在 `confine()` 阶段抛出结构化的 `SANDBOX_UNAVAILABLE`,而非降级为无约束执行。如果所选 runner 以可归因的 `ENOENT` 或 `EACCES` 拒绝,消费方会在任何命令开始前通过 spawn 通道报告同一基础设施错误;其他 spawn 错误仍保留本地命令启动语义,同时也不会运行任何内容。`dsh-sandbox-local` 上的 `runnerCommand` 是运维人员对一个 bwrap 兼容 runner 的显式断言(跳过链和探测);它同时充当 keyless 测试的确定性 fake-runner seam。
|
||||
|
||||
被拒绝的文件操作返回 `[sandbox: file access denied under <mode> mode]` 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 `sandbox_permissions` 和 `justification` 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 `the user rejected escalating this command to "<mode>"`,且不允许再次请求。由归属方派生的运行时上下文会说明当前文件策略,但不会取代这些强制执行边界。当 `dsh-permission` 与某个 UI 适配器一起组合时,一个 preset 会同时选定两个配置项的值;不匹配的组合折叠为 `custom`。[ACP 自动化组合](../../../../examples/acp-agent/README.md)不挂载该 UI 服务,而是显式选定其部署模式。
|
||||
被拒绝的文件操作返回 `[sandbox: file access denied under <mode> mode]` 标记,并附带不要绕过拒绝的指令。约束执行器添加配对的 `sandbox_permissions` 和 `justification` 字段,用于一次经批准的重试,该重试必须严格宽于会话的有效模式。授权仅放宽该次重试;拒绝则不执行任何内容,返回 `the user rejected escalating this command to "<mode>"`,且不允许再次请求。由归属方派生的待处理策略上下文会说明当前文件策略,但不会取代这些强制执行边界。当 `dsh-permission` 与某个 UI 适配器一起组合时,一个 preset 同时选定两个旋钮值;不匹配的组合折叠为 `custom`。[ACP 自动化组合](../../../../examples/acp-agent/README.md)不挂载该 UI 服务,而是显式选定其部署模式。
|
||||
|
||||
### 设计细节
|
||||
|
||||
@@ -72,7 +72,7 @@ Landlock launcher 源码和包工作区位于 `native/landlock-run`,与 harnes
|
||||
|
||||
`dsh-bash-sandbox` 扩展 `LocalBashExecutor`,把精确的 `['bash', '-c', command]` argv 交给 `ctx.sandbox`,并直接 spawn 提供方返回的 argv。这样,随附的原生 runner 建立约束后,shell 语义与 `BASH_ENV` 仍由内层 Bash 处理。提供方错误原样传播。进程启动前,只有当调用方拥有的 workdir 经独立验证可用,并且 Node 报告 `ENOENT` 或 `EACCES`,且带有明确指向提供方 argv[0] 的来源信息时,拒绝才会归因于 runner;没有精确错误路径的裸 `syscall: 'spawn'`、其他错误码、无效 workdir、资源失败、无关 syscall 与无结构拒绝保留本地命令启动语义。前台执行会将可归因于 runner 的拒绝转为 `SANDBOX_UNAVAILABLE` 并附上原始详细信息;异步后台拒绝则盖章 `runnerFailed: true`、`denied: false`。如果 `SubprocessService` 同步抛出同样带有来源信息的 `ENOENT`/`EACCES` 形态,后台启动会抛出 `SANDBOX_UNAVAILABLE`;其他同步错误原样传播。进程启动后,前台与后台共用一个 runner 失败分类器:先排除信息性行,再要求规则的退出码门控与余下的一行致命诊断同时匹配。匹配结果优先于拒绝:前台执行抛出 `SANDBOX_UNAVAILABLE`,并以该致命行作为详细信息;结算后的 `BashProcess` 会盖章 `sandbox.runnerFailed`,bash 生产者再通过通用 `task_output` 渲染它。
|
||||
|
||||
模型会在归属方派生的 `sandbox:policy` 运行时上下文中看到当前有效的文件策略;静态工具描述则解释拒绝标记(`[sandbox: file access denied under <mode> mode]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试。当升级字段被公布时,被拒绝的结果还会携带升级提示本身,使按规定进行的同轮次重试在决策点得到提示,而非依赖模型回忆描述(§ 升级机制)。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)负责该上下文的理由与边界。
|
||||
模型会在归属方派生的 `sandbox:policy` 上下文中看到当前有效的文件策略;静态工具描述则解释拒绝标记(`[sandbox: file access denied under <mode> mode]`),鼓励尝试可能被拒绝的命令,并禁止绕过拒绝重试。当升级字段被公布时,被拒绝的结果还会携带升级提示本身,使被认可的同轮次重试在决策点获得提示,而非依赖模型回忆描述(§ 升级机制)。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)负责该上下文的理由与边界。
|
||||
|
||||
#### 升级机制:拒绝后一次经批准的更宽重试
|
||||
|
||||
@@ -105,7 +105,7 @@ interface SessionEventMap {
|
||||
|
||||
每个拥有者导出相同的三件套:事件声明、纯 fold(`effectiveSandboxMode(events)` / `effectiveApprovalPolicy(events)`——一个 `findLast`,类型化到领域的封闭联合),以及唯一的写入路径(`setSandboxMode(session, mode)` / `setApprovalPolicy(session, policy)`——切换即其事件;没有任何东西在带外修改状态)。无需共享的归属服务、通用 facts map 或注册表:第三个配置项只需将约 40 行模式复制到自己的包中。执行在两侧都遵循 fold——bash 工具的按调用盖章将其作为 § 升级机制优先级链的中间层读取,approval seam 的 `'never'` 门控是[批准 Agent Note](2026-07-06-approval-seam.md) 同一模式的另一侧。
|
||||
|
||||
沙箱策略与批准策略会在每次请求前渲染为同一份运行时上下文快照中的有序贡献。循环会将完整快照记录为一条带来源的 `user/message`;`'ask'` 与 `'never'` 都会明确写入,因此两个归属方都无需切换叙述或「上次告知」状态。
|
||||
每次拟议步骤之前,沙箱策略与批准策略都会渲染为一条目标策略上下文消息中的有序贡献。监听器将该消息与会话历史、已领取批次及其待处理的 `next-step` inbox 条目协调。消息一旦被领取并进入步骤,循环就会记录完整且带来源的 `user/message`;`'ask'` 与 `'never'` 都会明确写入,因此两个归属方都无需切换叙述或「上次告知」状态。
|
||||
|
||||
**可选的 UI 界面**是 `PermissionService`:一张部署定义的 preset 表,每个条目捆绑一个沙箱模式与一个批准策略。随附的 `workspace-write` 和 `danger-full-access` preset 写入两个领域 setter;preset 表之外的旋钮组合报告为 `custom`。UI 适配器可以把该表暴露为选择器。仅面向自动化的 ACP 传输层不公布任何配置选择器,也不挂载权限 preset 服务。
|
||||
|
||||
@@ -149,9 +149,9 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
|
||||
- **按会话动态工具 schema**:否决。schema 设计上是注册表全局的(一套 assembly 词汇、固定 header 快照契约),按会话重新注册只能买到执行时严格放宽检查已保证的东西,代价是按会话的 schema 表面和每次切换的 header 变动。
|
||||
- **将重试硬匹配到先前的拒绝**:否决。命令字符串同一性脆弱(引号、`workdir`、env 前缀、作为失败阶段重试的管道)——要么误拒诚实的重试,要么被轻易满足;真正的边界是人看到命令 + 理由。仅在 `allow_always` 授权存储需要机器可检查的范围时才重新考虑。
|
||||
- **通用 `env/state` facts map 加拥有者服务**:否决。approval 和沙箱独立组合,因此任何一方的状态都不应拖入第三个包;单键 fold 各自是一个 `findLast`,拥有者服务自然消解;没有跨旋钮的不变式,因此原子多键补丁无收益。
|
||||
- **通过 `agent.inject()` 加总线事件逐次叙述切换**:否决。独立通知会暴露归属方顺序和中间组合,而一次组装过程可以在请求边界以原子方式具体化完整的当前状态。
|
||||
- **通过 `agent.inject()` 加总线事件逐次叙述切换**:否决。独立通知会暴露归属方顺序和中间组合,而一次 pre-step 组合可以原子排队完整的当前状态。
|
||||
- **在稳定系统提示词中声明沙箱模式**:先行交付,随后根据线上证据移除:每次请求都带有 `Bash commands run under the "read-only" file sandbox.` 时,模型会拒绝尝试本可在被拒后升级的工作(首次人工会话的十二个轮次中有五个以零工具调用结束),使沙箱变成软锁死。拒绝标记会在相关时刻指出模式,升级字段则承载恢复路径。[当前策略决策](2026-07-30-current-sandbox-policy-context.md)取代了省略策略的决策;这项测量和因果观察仍是任何替代方案必须进行反证测试的依据。
|
||||
- **用专门的簿记事件追踪「上次告知」**:否决。最新一条带来源的运行时上下文 `user/message` 会记录模型看到的确切完整快照。根据当前归属方贡献具体化该快照,取代了第二条簿记流——事件仅在它们本身即为存储时才需要。
|
||||
- **用专门的簿记事件追踪「上次告知」**:否决。会话历史记录模型看到的确切策略上下文,已领取批次与待处理 inbox 条目则表明正在进入或已经排队的内容。重新计算目标消息取代了第二条簿记流——事件仅在它们本身即为存储时才需要。
|
||||
- **相互独立的沙箱与批准选择器**:否决。一个部署定义的权限 preset 让两个策略旋钮对暴露运行时切换的 UI 客户端保持一致。
|
||||
|
||||
## 后果
|
||||
@@ -160,12 +160,12 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
|
||||
|
||||
- 被拒绝的命令以 `sandbox_permissions` + `justification` 重试时,通过组合的应答器链提示用户;授权使该次调用在更宽模式下运行(结果事实如此报告),而其他所有调用保持各自的有效模式;每种非授权结果产生各自不同的错误文本且不执行任何内容。
|
||||
- 升级字段恰好在已挂载的执行器约束时存在;不严格宽于调用有效模式的请求以自身文本失败关闭且不提示任何人;没有 ApprovalService 的部署对升级调用失败关闭,对普通调用不影响。
|
||||
- 一条带来源的运行时上下文消息会以原子方式声明完整的当前沙箱策略与批准策略;整个交互——上下文快照、header、旋钮事件、批准通知、批准与结果——仅从会话日志即可重建,除两个旋钮事件外没有策略簿记事件。
|
||||
- 一次 preset 选择只记录发生变化的旋钮值,而无操作的选择不记录任何内容;下一个请求会把两个当前值共同纳入一份原子快照,已提交的沙箱切换由下一次调用的盖章兑现。
|
||||
- 恢复会话的覆盖项会进入其首个新运行时上下文快照,无需追赶状态;进程停止期间变更的组合默认值也会出现在该快照中。
|
||||
- 一条带来源的策略上下文消息会以原子方式声明完整的当前沙箱策略与批准策略;整个交互——上下文消息、header、旋钮事件、批准通知、批准与结果——仅从会话日志即可重建,除两个旋钮事件外没有策略簿记事件。
|
||||
- 一次 preset 选择只记录发生变化的旋钮值,而无操作的选择不记录任何内容;下一次 pre-step 会原子 upsert 两个当前值,已提交的沙箱切换由下一次调用的盖章兑现。
|
||||
- 恢复会话的覆盖项会进入其首条新策略上下文消息,无需追赶状态;进程停止期间变更的组合默认值也会出现在该消息中。
|
||||
- 两个并发会话永远看不到彼此的状态或通知。
|
||||
- 同一个 Cordis 上下文中的两个并发项目会话解析各自独立的工作区根目录;bash 和 fs 写入在调用方会话的 cwd 内成功,对其相邻会话的 cwd 则失败。
|
||||
- 策略归属仍通过 `systemPrompt.context`、`SessionEventMap` 合并和由能力归属方拥有的解析留在插件中;通用循环变更会将每个归属方的有序上下文具体化为一条带来源的消息。
|
||||
- 策略归属仍通过 `SessionEventMap` 合并、从 `agent/pre-step` 变更 inbox,以及由能力归属方拥有的解析留在插件中;通用循环只领取并记录最终进入步骤的批次。
|
||||
|
||||
代价与已接受的限制:
|
||||
|
||||
@@ -191,8 +191,8 @@ fs/web/todo 在进程内执行,因此它们的沙箱语义是各自 seam 层
|
||||
- **沙箱限制网络或进程可见性吗?** 不——`SandboxMode` 仅声称文件操作;bwrap profile 刻意不 unshare pid,没有后端声称网络。网络限制是否成为自己的旋钮留在 § seam 中开放。
|
||||
- **哪些工具实际在约束下运行?** 通过 `ctx.bash` 的 OS 子进程——bash 工具及传递性的钩子命令——再加上通过沙箱化 `ctx.fs` 提供方运行的文件系统工具(`read`/`write`/`edit`,见[跨工具族 fs 沙箱 Agent Note](2026-07-14-cross-family-fs-sandbox.md)):bash 通过 OS runner 约束,fs 通过进程内路径围栏约束,二者都以同一个 `ctx.sandboxPolicy` 模式为键。web/todo 仍在进程内且不受限制(web 的唯一效果是网络,不在文件效果模式词汇内)。
|
||||
- **授权的升级会持久化吗?** 不会。授权由发起请求的确切前台或后台调用消费;每个相邻调用保留自己的有效模式。后续的后台拒绝通过 `task_output` 呈现,并且可以作为一次新的精确命令重试的依据。
|
||||
- **运行时模式切换何时生效?** 一旦其会话事件提交,下一个运行时上下文快照与下一次能力解析都会折叠新模式。带来源的上下文消息会记录模型收到的内容,之后的任何拒绝都会在使用点命名同一策略。
|
||||
- **重启后什么存活——如果运维人员在进程停止期间改了配置默认值呢?** 覆盖从会话日志回放(`effective = fold ?? config`),因此恢复的会话以零追赶机制保持其模式;离线漂移的默认值会进入下一份完整运行时上下文快照。
|
||||
- **运行时模式切换何时生效?** 一旦其会话事件提交,下一次 pre-step 策略上下文协调与下一次能力解析都会折叠新模式。带来源的上下文消息会记录模型收到的内容,之后的任何拒绝都会在使用点命名同一策略。
|
||||
- **重启后什么存活——如果运维人员在进程停止期间改了配置默认值呢?** 覆盖从会话日志回放(`effective = fold ?? config`),因此恢复的会话以零追赶机制保持其模式;离线漂移的默认值会进入下一条完整策略上下文消息。
|
||||
- **结果上的 `enforcement: 'partial'` 是什么意思?** 所选后端强制其内核 ABI 管控的子集——例如 ABI v3 之前的 Landlock 不管控路径 truncate——并以结构化方式如此声明而非拒绝主机;探测的报告行区分各种情况。bwrap 和 Seatbelt profile 构造上管控所有承诺的文件操作,因此始终报告 `full`。
|
||||
|
||||
## 先例
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-durable-per-step-time-context.md
|
||||
2026-07-16-durable-per-step-time-context.md: 4bc17b3c08707fcaa4f0f431e71ddbe567a03c9e
|
||||
2026-07-16-durable-per-step-time-context.zh.md: 7d4a66ed711b396fd46957a24f5f702e1e4829ea
|
||||
2026-07-16-durable-per-step-time-context.md: e1a5c65894913ad93f46db8ae45e5ef5ead215f3
|
||||
2026-07-16-durable-per-step-time-context.zh.md: 9cbf653f5d43060e99502b640eed7907ffbd3840
|
||||
|
||||
@@ -12,9 +12,9 @@ A process-local refresh cache makes displayed time depend on state that cannot s
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-time-context` is an opt-in function plugin in `packages/context/time-context/`. The `context/` group holds bounded request-context enrichments that define neither a tool nor a service, and shipped examples do not mount this plugin because its time-zone disclosure and token cost are deployment policy. It registers a prepended `agent/pre-step` listener and, when an injection is due, calls `agent.inject()` for a pre-step attempt whose signal is not already aborted. The injected `user/message` carries source `{ kind: 'plugin', plugin: 'time-context' }` and append surface metadata; a suppressed attempt appends nothing.
|
||||
`@deepseek-ai/dsh-time-context` is an opt-in function plugin in `packages/context/time-context/`. The `context/` group holds bounded request-context enrichments that define neither a tool nor a service, and shipped examples do not mount this plugin because its time-zone disclosure and token cost are deployment policy. It registers a prepended `agent/pre-step` listener and, when a reading is due and the downstream decision enters, returns one additional `UserMessage`. The message carries source `{ kind: 'plugin', plugin: 'time-context' }`; a suppressed, rejected, or failed attempt appends nothing.
|
||||
|
||||
The listener records preparation context before a possible `step/start`. Its prepended registration runs before ordinary automatic compaction listeners, so pressure estimation and any resulting surface rewrite observe a newly appended reading. A later pre-step listener can cancel or fail the attempt before the step opens; the reading remains because the durable log is append-only and this plugin performs no rollback.
|
||||
The listener samples before `step/start`, then settles its reading only in the final enter decision. AgentLoop records it after `step/start` and before request derivation. A downstream rejection or failure therefore prevents the reading from entering durable history.
|
||||
|
||||
The optional `timeZone` config resolves the Node process's IANA zone once at plugin load when omitted; an explicit value is validated by `Intl.DateTimeFormat`. The timestamp includes the numeric UTC offset and resolved IANA zone.
|
||||
|
||||
@@ -44,7 +44,7 @@ Their baseline is the durable event timestamp of the preceding time-context mess
|
||||
|
||||
Each reading remains a normal surface node until compaction shadows it; positive interval scheduling never removes existing readings. A later request therefore sees the cumulative unshadowed readings that affected earlier preparation and steps, rather than a system-prompt value rewritten in place.
|
||||
|
||||
The plugin contributes nothing to system-prompt assembly. `request/header` contains no time-context text; request reconstruction obtains the complete durable surface prefix at each `step/start`. Readings and requests need not map one-to-one because a failed preparation can leave a reading while interval suppression can prepare a request without appending one. The plugin depends on the agent registry for its lifecycle listener and does not require the system-prompt service at runtime.
|
||||
The plugin contributes nothing to system-prompt assembly. `request/header` contains no time-context text; request reconstruction obtains the complete durable surface prefix at each `step/start`. Readings and requests need not map one-to-one because interval suppression can enter a request without appending a reading, while rejection or failure appends neither. The plugin depends on the agent registry for its lifecycle listener and does not require the system-prompt service at runtime.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。`context/` 分组容纳有界的请求上下文增强,这些增强既不定义工具也不定义服务;已交付示例不挂载此插件,因为时区披露与 token 成本属于部署策略。它注册一个前置的 `agent/pre-step` 监听器,并在需要注入时,为信号尚未中止的预步骤尝试调用 `agent.inject()`。注入的 `user/message` 携带来源 `{ kind: 'plugin', plugin: 'time-context' }` 和追加表层元数据;受间隔抑制的尝试不会追加任何内容。
|
||||
`@deepseek-ai/dsh-time-context` 是位于 `packages/context/time-context/`、需要显式启用的函数插件。`context/` 分组容纳有界的请求上下文增强,这些增强既不定义工具也不定义服务;已交付示例不挂载此插件,因为时区披露与 token 成本属于部署策略。它注册一个前置的 `agent/pre-step` 监听器;需要读数且下游决策 enter 时,返回一条额外的 `UserMessage`。该消息携带来源 `{ kind: 'plugin', plugin: 'time-context' }`;受间隔抑制、reject 或失败的尝试不会追加任何内容。
|
||||
|
||||
监听器在可能出现的 `step/start` 之前记录准备上下文。它采用前置注册,因此先于普通自动压缩监听器运行,使压力估算和由此产生的表层重写都能观察到新追加的读数。后续预步骤监听器可能在步骤开启前取消尝试或使其失败;持久日志仅追加,且本插件不执行回滚,因此该读数会保留下来。
|
||||
监听器在 `step/start` 之前采样,并只在最终 enter 决策中结算读数。AgentLoop 会在 `step/start` 之后、请求派生之前记录它。因此,下游 reject 或失败会阻止读数进入持久历史。
|
||||
|
||||
省略可选配置 `timeZone` 时,插件在加载时解析一次 Node 进程的 IANA 时区;显式值由 `Intl.DateTimeFormat` 校验。时间戳包含数字 UTC 偏移和解析后的 IANA 时区。
|
||||
|
||||
@@ -44,7 +44,7 @@ Elapsed since the preceding step context: <duration-or-unavailable>.
|
||||
|
||||
每个读数都作为普通表层节点保留,直至压缩将其隐藏;正数间隔调度绝不会移除已有读数。因此,后续请求会看到影响先前准备过程和步骤且尚未被隐藏的累计读数,而不是一个被原地改写的系统提示词值。
|
||||
|
||||
插件不向系统提示词组装贡献任何内容。`request/header` 不包含时间上下文文本;请求重建从每个 `step/start` 取得完整的持久表层前缀。读数与请求无需一一对应,因为失败的准备过程可能留下读数,而间隔抑制也可能使请求准备过程不追加读数。插件通过 agent 注册表使用生命周期监听器,运行时不需要系统提示词服务。
|
||||
插件不向系统提示词组装贡献任何内容。`request/header` 不包含时间上下文文本;请求重建从每个 `step/start` 取得完整的持久表层前缀。读数与请求无需一一对应,因为间隔抑制可以让请求进入步骤而不追加读数,reject 或失败则两者都不追加。插件通过 agent 注册表使用生命周期监听器,运行时不需要系统提示词服务。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-16-harness-level-loop.md
|
||||
2026-07-16-harness-level-loop.md: 15b5ce7e20b7afc429f6ff7b8a4d2d69150c22a0
|
||||
2026-07-16-harness-level-loop.zh.md: fd2e411e3270162edd2c268a369203fcf4838b09
|
||||
2026-07-16-harness-level-loop.md: 36a567204ee1082d48126369ee0b7277c9f24ca8
|
||||
2026-07-16-harness-level-loop.zh.md: cb8d29b4c77848613a6718ea3a343b0ee3294ea4
|
||||
|
||||
@@ -42,11 +42,11 @@ Time-based `/loop` or scheduled execution is a third policy and is not implement
|
||||
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`, human-command producer | Registers `/goal` status, creation, edit, pause, resume, and clear over the goal domain for TUI. |
|
||||
| `@deepseek-ai/dsh-tool-ralph` | `packages/workflow/tool-ralph/`, fixed workflow consumer | Registers `ralph({ objective, maxRounds? })`, validates the fresh structured provider and bounded `RalphRoundReport`, and returns `complete`, `blocked`, or `budget-limited`. |
|
||||
|
||||
The detailed contracts live in the [goal-domain](2026-07-19-persisted-same-session-goal-domain.md), [model goal-tools](2026-07-19-model-facing-goal-tools.md), [goal-round driver](2026-07-19-same-session-goal-round-driver.md), [command registry](2026-07-19-plugin-command-registration.md), [human goal-command](2026-07-19-human-goal-command.md), and [Ralph workflow-tool](2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Notes.
|
||||
The detailed contracts live in the [goal-domain](2026-07-19-persisted-same-session-goal-domain.md), [goal-owned event](../architecture/2026-07-31-goal-owned-durable-events.md), [model goal-tools](2026-07-19-model-facing-goal-tools.md), [goal-round driver](2026-07-19-same-session-goal-round-driver.md), [command registry](2026-07-19-plugin-command-registration.md), [human goal-command](2026-07-19-human-goal-command.md), and [Ralph workflow-tool](2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Notes.
|
||||
|
||||
### Durable goal state and live authority
|
||||
|
||||
One session has at most one current goal. Every non-clear mutation appends a full, versioned, model-visible goal snapshot through `Agent.inject()`; clear appends a revisioned tombstone. The session log is the only durable source of truth, so normal persistence, resume, compaction semantics, and `SessionStore.fork()` carry the goal without a second database or an artificial cancellation record.
|
||||
One session has at most one current goal. Every mutation commits through a durable `goal/change` event carrying a full versioned snapshot or revisioned clear tombstone; inbox state does not participate. The session log is the only durable source of truth, so normal persistence, resume, and `SessionStore.fork()` carry the goal without a second database or an artificial cancellation record.
|
||||
|
||||
Durable phases are only `active`, `paused`, `blocked`, and `complete`. A blocked goal carries a required `GoalBlockReason` with a stable lower-kebab-case `code` and a non-empty human-readable `message`; usage limits, round exhaustion, model failures, and policy rejection are reason codes rather than extra lifecycle phases. Separate activation is `armed` or `disarmed` and is never persisted. Creation and explicit resume arm a goal; stop transitions, session start, fork replay, driver replacement, and driver teardown leave it disarmed.
|
||||
|
||||
@@ -58,9 +58,9 @@ Forked sessions inherit the durable goal prefix because that is the natural repl
|
||||
|
||||
### Same-session continuation
|
||||
|
||||
The goal-round driver owns at most one pending reservation per exact live agent. It admits a reservation only when the goal is active and armed, the agent is idle, no competing human work exists, pending mutations are durable, the exact goal id/revision/round still matches, and downstream prompt policy accepts it. The prompt-submit fence checks those facts both before and after asynchronous listeners, preventing an edit, pause, human message, or unload race from admitting obsolete work.
|
||||
The goal-round driver owns at most one pending reservation per exact live agent. It admits a reservation only when the goal is active and armed, the agent is idle, no competing human work exists, the latest mutation has passed its durability checkpoint, the exact goal id/revision/round still matches, and downstream pre-step policy accepts it. Its `agent/pre-step` fence checks those facts both before and after downstream listeners, preventing an edit, pause, human message, or unload race from admitting obsolete work.
|
||||
|
||||
Only the durable goal-sourced `user/message` charges a round. Stale reservations become rejected zero-step turns without consuming the cap. A concurrent goal revision wins over settlement from an older round.
|
||||
Only an admitted positive-round goal-sourced `user/message` charges a round. A stale reservation closes a blocked no-step turn without consuming the cap. A concurrent goal revision wins over settlement from an older round.
|
||||
|
||||
Normal turn completion schedules another round only while the goal remains active, armed, and below its cap. Cancellation pauses. Rate limiting or quota exhaustion blocks with code `usage-limited`; cap exhaustion blocks with `round-limit`; queue failure uses `queue-failed`; turn errors, max-token stops, policy rejection, and unknown terminal results use their corresponding blocker codes. An independently composed request-recovery plugin may retry transient provider failures within that same turn; the goal driver never invents another round after an abnormal terminal outcome. A human can later authorize resume through ordinary language or `/goal resume`.
|
||||
|
||||
@@ -122,7 +122,7 @@ The six owning Agent Notes record unit, integration, process, snapshot, cancella
|
||||
- **Aggregate budgets** — `maxGoalRounds` and Ralph `maxRounds` are the only aggregate effort limits. Token, currency, elapsed-time, provider-usage, and per-round price admission policies are absent.
|
||||
- **No persistent autonomous runner** — same-session goal facts persist, but activation and scheduling are process-local and deliberately wait for human input after restore. Ralph runs are foreground and cannot resume after process loss. Background collection, restart recovery, and unattended resident execution are deferred.
|
||||
- **No time scheduler** — interval `/loop`, cron, proactive maintenance, and cloud or desktop scheduling are outside this decision.
|
||||
- **No generic loop journal or execution-world rewind** — session replay reconstructs model-visible goal history, not prior files, processes, environment, credentials, or external side effects. Ralph treats the current workspace as authority and carries no cross-run journal.
|
||||
- **No generic loop journal or execution-world rewind** — session replay reconstructs goal history, not prior files, processes, environment, credentials, or external side effects. Ralph treats the current workspace as authority and carries no cross-run journal.
|
||||
- **No goal reflector** — concern events, automatic no-progress heuristics, goal revision by an independent reflector, stuck-pattern detection, and `loop_split` are not implemented. Humans can edit, pause, clear, or resume the goal directly.
|
||||
- **Ralph policy remains narrow** — one round creates one fresh child; within-round fan-out, evaluator/worker role separation, dynamic provider/model selection, and structural recursive-Ralph tool denial need separate policy surfaces. Prompt guidance is not enforcement.
|
||||
- **Ralph does not retry a failed child** — an ordinary failure preserves the failed round and last good handoff, while fatal workflow infrastructure failures can end before that state is available. Retry count, backoff, and richer failure transport need separate policy and seam design.
|
||||
|
||||
@@ -42,11 +42,11 @@ Status: implemented
|
||||
| `@deepseek-ai/dsh-command-goal` | `packages/goal/command-goal/`,人类命令生产方 | 为 TUI 注册构建在目标领域之上的 `/goal` 状态、创建、编辑、暂停、恢复与清除。 |
|
||||
| `@deepseek-ai/dsh-tool-ralph` | `packages/workflow/tool-ralph/`,固定工作流消费方 | 注册 `ralph({ objective, maxRounds? })`,验证全新结构化 provider 与有界 `RalphRoundReport`,并返回 `complete`、`blocked` 或 `budget-limited`。 |
|
||||
|
||||
详细契约分别由[目标领域](2026-07-19-persisted-same-session-goal-domain.md)、[模型目标工具](2026-07-19-model-facing-goal-tools.md)、[目标回合驱动器](2026-07-19-same-session-goal-round-driver.md)、[命令注册表](2026-07-19-plugin-command-registration.md)、[人类目标命令](2026-07-19-human-goal-command.md)与 [Ralph 工作流工具](2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Note 拥有。
|
||||
详细契约分别由[目标领域](2026-07-19-persisted-same-session-goal-domain.md)、[Goal 自有事件](../architecture/2026-07-31-goal-owned-durable-events.md)、[模型目标工具](2026-07-19-model-facing-goal-tools.md)、[目标回合驱动器](2026-07-19-same-session-goal-round-driver.md)、[命令注册表](2026-07-19-plugin-command-registration.md)、[人类目标命令](2026-07-19-human-goal-command.md)与 [Ralph 工作流工具](2026-07-19-fresh-agent-ralph-workflow-tool.md) Agent Note 拥有。
|
||||
|
||||
### 持久目标状态与实时权限
|
||||
|
||||
一个会话至多有一个当前目标。每次非清除变更都通过 `Agent.inject()` 追加一份完整、带版本且模型可见的目标快照;清除会追加带修订号的墓碑。会话日志是唯一持久真源,因此普通持久化、恢复、压缩(compaction)语义与 `SessionStore.fork()` 会携带目标,无需第二个数据库或人为取消记录。
|
||||
一个会话至多有一个当前 goal。每次变更都通过持久 `goal/change` 事件提交,并携带带版本的完整快照或带修订号的 clear 墓碑;inbox 状态不参与其中。会话日志是唯一持久真源,因此普通持久化、恢复与 `SessionStore.fork()` 会携带 goal,无需第二个数据库或人为取消记录。
|
||||
|
||||
持久阶段只有 `active`、`paused`、`blocked` 与 `complete`。阻塞目标必须携带 `GoalBlockReason`,其中包含稳定的小写 kebab-case `code` 与非空的人类可读 `message`;用量限制、Round 耗尽、模型失败与策略拒绝都是原因代码,而不是额外生命周期阶段。独立激活态是 `armed` 或 `disarmed`,且永不持久化。创建与显式恢复会激活目标;停止转换、会话启动、fork 回放、驱动器替换和驱动器拆卸都会让目标保持未激活。
|
||||
|
||||
@@ -58,9 +58,9 @@ fork 会话会继承持久目标前缀,因为这是自然的重放结果。for
|
||||
|
||||
### 同会话续行
|
||||
|
||||
Goal Round 驱动器为每个特定的实时 agent 至多拥有一个待定预留。只有目标处于活跃且已激活状态、agent 空闲、不存在竞争性人类工作、待定变更已经持久、确切的目标 id/修订号/Round 仍匹配,并且下游提示词策略接受时,它才会接纳预留。prompt-submit 围栏在异步监听器前后都检查这些事实,防止编辑、暂停、人类消息或卸载竞争接纳过时工作。
|
||||
Goal Round 驱动器为每个准确实时 agent 至多拥有一个待定预留。只有 goal 处于活跃且已激活状态、agent 空闲、不存在竞争性人类工作、最新变更已经通过持久性检查点、准确 goal id/revision/Round 仍匹配,并且下游 pre-step 策略接受时,它才会接纳预留。其 `agent/pre-step` 围栏会在下游监听器前后检查这些事实,防止编辑、暂停、人类消息或卸载竞争接纳陈旧工作。
|
||||
|
||||
只有持久的目标来源 `user/message` 会计入一个 Round。陈旧预留会成为不消耗上限的零步骤拒绝轮次。并发目标修订会胜过旧 Round 的结算。
|
||||
只有已准入、Round 为正数且来源为 goal 的 `user/message` 会计入一个 Round。陈旧预留会关闭一个 blocked 的无步骤轮次,不会消耗上限。并发 goal revision 会胜过旧 Round 的结算。
|
||||
|
||||
普通 Turn 完成后,只有目标仍活跃、已激活且低于上限时才会安排另一个 Round。取消会暂停。速率限制或配额耗尽以代码 `usage-limited` 阻塞;上限耗尽使用 `round-limit`;队列失败使用 `queue-failed`;Turn 错误、max-token 停止、策略拒绝与未知终止结果使用各自对应的阻塞代码。独立组合的请求恢复插件可以在同一个 Turn 内重试暂时性 provider 失败;目标驱动器绝不会在异常终止结果后凭空发起另一个 Round。人类随后可以通过普通语言或 `/goal resume` 授权恢复。
|
||||
|
||||
@@ -122,7 +122,7 @@ Codex 提供了这里采用的最小可观察目标 UX:一个附着于聊天
|
||||
- **聚合预算**——`maxGoalRounds` 与 Ralph `maxRounds` 是唯一聚合工作量限制。token、货币、耗时、provider 用量与逐 Round 价格准入策略均不存在。
|
||||
- **没有持久自治运行器**——同会话目标事实会持久化,但激活与调度只存在于进程内,并且有意在恢复后等待人类输入。Ralph 位于前台,进程丢失后无法恢复。后台收集、重启恢复与无人值守常驻执行均予以延期。
|
||||
- **没有时间调度器**——间隔 `/loop`、cron、主动维护以及云端或桌面调度不在本决策范围内。
|
||||
- **没有通用 loop 日志或执行世界回退**——会话重放会重建模型可见目标历史,而不会恢复此前文件、进程、环境、凭据或外部副作用。Ralph 把当前工作区作为权威,并且没有跨运行日志。
|
||||
- **没有通用 loop 日志或执行世界回退**——会话重放会重建目标历史,而不会恢复此前文件、进程、环境、凭据或外部副作用。Ralph 把当前工作区作为权威,并且没有跨运行日志。
|
||||
- **没有目标反思器**——concern 事件、自动无进展启发式、由独立反思器执行的目标修订、卡住模式检测与 `loop_split` 均未实现。人类可以直接编辑、暂停、清除或恢复目标。
|
||||
- **Ralph 策略仍然狭窄**——一个 Round 创建一个全新子 agent;Round 内扇出、评估器/工作者角色分离、动态 provider/模型选择与从结构上禁止递归调用 Ralph 工具都需要独立策略表面。提示词指导不是强制执行。
|
||||
- **Ralph 不会重试失败的子 agent**——普通失败会保留失败 Round 与上一份有效交接,而致命工作流基础设施错误可能在该状态可用前结束。重试次数、退避与更丰富的失败传输需要独立的策略与 seam 设计。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-human-goal-command.md
|
||||
2026-07-19-human-goal-command.md: ce5c37fd28f9432d8c9a8797cac32c632617e317
|
||||
2026-07-19-human-goal-command.zh.md: 85b47f610d12d5a70a4978e2041c8b38cd2895b0
|
||||
2026-07-19-human-goal-command.md: 5fdd80f7423b80e84e58f7379130ee59a2e8a723
|
||||
2026-07-19-human-goal-command.zh.md: 74dd0a2bec923687c91ddf4ce7efe4cc5b25bfc9
|
||||
|
||||
@@ -34,7 +34,7 @@ Status output omits branded ids and compare-and-set revisions because those are
|
||||
|
||||
Expected `GoalError` failures become one stable, branded-id-free `CommandResult.error`, so domain diagnostics do not leak compare-and-set internals into the human surface and invalid operations never enter model history. The current status supplies the actionable state-specific recovery. Other exceptions remain adapter-visible command failures; treating programmer faults as ordinary domain errors would hide defects. The command handler performs only synchronous domain mutations, so request cancellation is decided by the command registry before the mutation begins and there is no escaped asynchronous side effect to unwind.
|
||||
|
||||
Generic slash input, status text, and errors are not persisted. Successful goal mutations use the existing `Agent.inject()` path, producing the raw model-visible goal snapshot or clear tombstone that persistence already owns. The command therefore changes no session format and introduces no second audit record that could disagree with the domain event.
|
||||
Generic slash input, status text, and errors are not persisted. Successful goal mutations append the domain-owned `goal/change` event and do not queue model context. The command introduces no second audit record that could disagree with the domain event.
|
||||
|
||||
### App composition
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Status: implemented
|
||||
|
||||
预期的 `GoalError` 失败会变为一个稳定且不含品牌化 id 的 `CommandResult.error`,使领域诊断不会向面向人类的界面泄露比较并交换内部细节,非法操作也绝不会进入模型历史。当前状态负责提供针对具体状态且可执行的恢复路径。其他异常仍是适配器可见的命令失败;若把程序缺陷当成普通领域错误,就会隐藏问题。命令处理器只执行同步领域变更,因此请求取消会在变更开始前由命令注册表决定,不存在需要回滚的外逸异步副作用。
|
||||
|
||||
通用斜杠输入、状态文本与错误不会持久化。成功的目标变更使用现有 `Agent.inject()` 路径,产出持久化本就拥有的原始模型可见目标快照或清除墓碑。因此该命令不会改变会话格式,也不会引入可能与领域事件不一致的第二份审计记录。
|
||||
通用斜杠输入、状态文本与错误不会持久化。成功的 goal 变更会追加领域自有的 `goal/change` 事件,而且不会把模型上下文排队。该命令不会引入可能与领域事件不一致的第二份审计记录。
|
||||
|
||||
### 应用组合
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-model-facing-goal-tools.md
|
||||
2026-07-19-model-facing-goal-tools.md: 18235c484194f5daf10556ebfc13bdc2d672be2e
|
||||
2026-07-19-model-facing-goal-tools.zh.md: c8bc49ef907e4055960074c6a7d1e852ba77c9ad
|
||||
2026-07-19-model-facing-goal-tools.md: 0271194a38503711de77290c915010c26a9de74b
|
||||
2026-07-19-model-facing-goal-tools.zh.md: 235c3c5369871a8cdf731eee822abc4a3e6b58d1
|
||||
|
||||
@@ -28,7 +28,7 @@ An autonomous goal round that successfully reports completion or blocking defers
|
||||
|
||||
Every call requires an `exec.agent` that is the exact running object in `AgentRegistry`, is the current inherited driver initiator, and has an open turn. These are execution-time checks and cannot be bypassed by prompt injection or hand-authored tool arguments.
|
||||
|
||||
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: every `Agent.send()` or `steer()` input requires an explicit source, so the host labels direct human content `{ kind: 'user' }` and non-human producers label their own provenance. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
|
||||
Create, edit, pause, and resume additionally require an accepted user message or user steering event in the current turn of a runtime-root agent. Root ownership is derived from the live agent graph rather than durable fork ancestry: a resumed fork can receive direct human authority, while a live child remains a subagent and cannot mutate these states. User source is a host attestation: every `Agent.followup()` or `steer()` input requires an explicit source, so the host labels direct human content `{ kind: 'user' }` and non-human producers label their own provenance. The runtime proves provenance, not whether the human's wording semantically warrants creation or resumption; that interpretation remains with the model.
|
||||
|
||||
Complete and blocked accept either direct-human authority or the exact current goal round. Goal-round authority requires a goal-sourced `user/message` whose goal id, revision, and round all equal the folded current goal. It grants only the two terminal reports. Direct human authority may stop a goal immediately.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ Status: implemented
|
||||
|
||||
每次调用都要求存在 `exec.agent`,且它必须是 `AgentRegistry` 中完全相同的运行中对象、当前继承的驱动发起者,并处于开放轮次内。这些检查在执行时进行,不能通过提示词注入或手写工具参数绕过。
|
||||
|
||||
创建、编辑、暂停与恢复还要求运行时根 agent 的当前轮次已经接纳一条用户消息或用户 steering 事件。根所有权来自实时 agent 图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子级则仍是 subagent,不能改变这些状态。用户来源是宿主的证明:每个 `Agent.send()` 或 `steer()` 输入都必须显式提供来源,因此宿主把直接人类内容标为 `{ kind: 'user' }`,非人类生产者则标注自己的来源信息。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
|
||||
创建、编辑、暂停与恢复还要求运行时根 agent 的当前轮次已经接纳一条用户消息或用户 steering 事件。根所有权来自实时 agent 图,而非持久的 fork 祖先关系:恢复后的派生会话可以接收新的直接人类权限,实时子级则仍是 subagent,不能改变这些状态。用户来源是宿主的证明:每个 `Agent.followup()` 或 `steer()` 输入都必须显式提供来源,因此宿主把直接人类内容标为 `{ kind: 'user' }`,非人类生产者则标注自己的来源信息。运行时证明来源,而不判断人类措辞在语义上是否足以创建或恢复目标;该解释仍由模型完成。
|
||||
|
||||
完成与阻塞既接受直接人类权限,也接受准确的当前 Goal Round。Goal Round 权限要求存在一条来源为目标的 `user/message`,其中目标 id、修订号和 Round 都与折叠后的当前目标相等。它只授予这两种终止报告权限。直接人类权限可以立即停止目标。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.md
|
||||
2026-07-19-persisted-same-session-goal-domain.md: 00600b2c49646ebd3b692154ef945eb79a33b032
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: 00a75eebbad40b4551711c47fbf654684092a5e6
|
||||
2026-07-19-persisted-same-session-goal-domain.md: ce93a652f3910912fbf6afc28061de4d13eb2de2
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: 7fb8dc3a6c76b91e0abf495782d51b30d3cd5b53
|
||||
|
||||
@@ -18,11 +18,11 @@ The durable phases are `active`, `paused`, `blocked`, and `complete`. A blocked
|
||||
|
||||
### Durable record and replay
|
||||
|
||||
Every non-clear mutation uses `Agent.inject()` to append a model-visible `context/message` containing a versioned full snapshot; the session projects that content verbatim. Clear appends a revisioned tombstone. The context source is `{ kind: 'goal', goalId, revision, round: 0 }`; metadata and rendered `<goal_state>...</goal_state>` content must agree exactly. This descriptive delimiter follows the repository's existing `<workspace_context>` convention and [Anthropic's published guidance to structure mixed prompt content with consistent descriptive XML tags](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags). That is public model-experience prior art, not evidence about any provider's proprietary training corpus. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field.
|
||||
Every mutation appends a versioned `goal/change` session event containing a full snapshot or, for clear, a revisioned tombstone. The session log is the only durable source of truth, so persistence and fork inherit goal records without another database or header field. The [goal-owned durable event decision](../architecture/2026-07-31-goal-owned-durable-events.md) owns the separation from inbox state and model context.
|
||||
|
||||
The replay fold validates JSON shape, source attribution, rendered content, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds are positive sequential `user/message` source numbers for the current active revision and cannot exceed `maxGoalRounds`; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired.
|
||||
The replay fold derives lifecycle mutations only from `goal/change` and validates JSON shape, fresh ids, revision continuity, lifecycle transitions, counters, and monotonic per-goal timestamps. Goal rounds advance only from positive sequential admitted `user/message` source numbers for the current active revision and cannot exceed `maxGoalRounds`; ordinary session turns do not affect the counter. A malformed current-format record fails replay rather than being ignored or repaired.
|
||||
|
||||
When `Agent.inject()` defers a mutation inside an active tool batch, the service overlays the accepted payload in process memory so a later mutation can use its new revision. Reconciliation removes only an exact matching payload when the FIFO append becomes visible; reentrant append observers project each mutation exactly once. Incremental replay advances its cursor after each valid event and remains positioned at the first corrupt event, so later reads report the same durable fault. The durable log remains authoritative after restart.
|
||||
Incremental replay advances its cursor after each valid event and remains positioned at the first corrupt event, so later reads report the same durable fault. The durable log remains authoritative after restart.
|
||||
|
||||
### Lifecycle and live activation
|
||||
|
||||
@@ -32,16 +32,16 @@ A cache built from any seed starts disarmed, and every `agent/session-start` edg
|
||||
|
||||
### Service boundary
|
||||
|
||||
The service accepts only the exact live `Agent` object registered under its id. Successful mutation injection emits the scoped `goal/changed` event with contained listener failures. Policy consumers use this service plus the public `Agent` interface and `agent/*` events; the goal domain does not import or modify `dsh-agent-loop`.
|
||||
The service accepts only the exact live `Agent` object registered under its id. A committed mutation emits the scoped `goal/changed` event with contained listener failures. Policy consumers use this service plus the public `Agent` interface and `agent/*` events; the goal domain does not import or modify `dsh-agent-loop`.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, blocker reason validation and retention, cap enforcement on resume, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start and lifecycle-owner disarming, active-goal rearming, FIFO deferred mutation reconciliation, reentrant append observation, rejected-injection rollback, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, source/content agreement, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the model-visible snapshot and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
|
||||
Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set rejection, every lifecycle transition, blocker reason validation and retention, cap enforcement on resume, clear/replacement, seeded replay and `SessionStore.fork()` inheritance, session-start and lifecycle-owner disarming, active-goal rearming, durable event folding, inbox independence, stable corrupt-event replay, service/listener disposal, listener containment, backward-clock clamping, strict record decoding, lifecycle continuity, and sequential round attribution. A keyless Loader/stdio process test mounts the service and a lifecycle consumer through test-only `cordis.yml`, then reads the persisted JSONL externally to verify the goal record and absence of an unrequested goal round. The package source is held to the repository's per-file 100% coverage gate.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Store goals in a separate database or session header** — rejected because the session log already supplies ordering, persistence, fork prefixes, and reconstructability; a second store introduces atomicity and lineage questions.
|
||||
- **Use hidden log-only events** — rejected because durable state that changes future model behavior must be model-visible and reconstructable under the repository's logging invariant.
|
||||
- **Couple each durable mutation to queued model context** — rejected by the later [goal-owned durable event decision](../architecture/2026-07-31-goal-owned-durable-events.md): goal tools and scheduled continuation prompts expose state when needed, while domain persistence remains independent from queue outcomes.
|
||||
- **Persist activation and restart automatically** — rejected because opening or resuming a session must wait for human input; durable phase records status, not fresh authority to spend resources.
|
||||
- **Count all session turns as goal rounds** — rejected because one session can contain human clarification, inspection, and unrelated work; only goal-attributed continuation turns consume this budget.
|
||||
- **Add goal state or a generic loop abstraction to `dsh-agent-loop`** — rejected because state and continuation policy can compose through existing plugins, `Agent` verbs, and events without privileging the shipped loop implementation.
|
||||
@@ -50,7 +50,7 @@ Unit coverage pins creation defaults, exact-live-agent checks, compare-and-set r
|
||||
|
||||
- Goal history survives persistence, resume, compaction of unrelated nodes, and session fork as ordinary session data.
|
||||
- Resume and fork expose the same durable phase while remaining operationally inert until an explicit resume mutation arms activation.
|
||||
- Full snapshots simplify inspection and strict replay but repeat the objective and state fields in model history until compaction shadows them.
|
||||
- Full snapshots simplify inspection, strict replay, and last-wins projection without adding mutation-only messages to model history.
|
||||
- Revision and lifecycle validation reject tampered, partially written, or producer-inconsistent goal records early.
|
||||
- Round caps bound continuation count only; policy consumers map round, token, currency, time, and provider limits to blocked reasons when they stop work.
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ Status: implemented
|
||||
|
||||
### 持久记录与回放
|
||||
|
||||
每次非清除变更都通过 `Agent.inject()` 追加一条模型可见的 `context/message`,其中包含带版本的完整快照;会话会将其内容原样投射给模型。清除操作追加带修订号的墓碑。上下文来源为 `{ kind: 'goal', goalId, revision, round: 0 }`;元数据必须与渲染后的 `<goal_state>...</goal_state>` 内容完全一致。这个描述性分隔符沿用了仓库已有的 `<workspace_context>` 约定,也符合 [Anthropic 关于用一致且描述明确的 XML 标签组织混合提示词内容的公开指南](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#structure-prompts-with-xml-tags)。这是公开的模型体验先例,并非对任何提供方专有训练语料的推断。会话日志是唯一的持久真源,因此持久化和 fork 会继承目标记录,而无需另设数据库或头字段。
|
||||
每次变更都会追加带版本的 `goal/change` 会话事件,其中包含完整快照;clear 则包含带修订号的墓碑。会话日志是唯一的持久真源,因此持久化和 fork 会继承 goal 记录,而无需另设数据库或头字段。[Goal 自有持久事件决策](../architecture/2026-07-31-goal-owned-durable-events.md)负责 goal 状态与 inbox 状态、模型上下文之间的职责分离。
|
||||
|
||||
回放折叠会校验 JSON 形状、来源归属、渲染内容、从未出现过的 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。Goal Round 是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
|
||||
回放折叠只从 `goal/change` 派生生命周期变更,并校验 JSON 形状、新 id、修订连续性、生命周期转换、计数器以及单个 goal 内单调递增的时间戳。只有当前活跃修订上带正数且连续编号、已准入的 `user/message` 来源才会推进 Goal Round,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
|
||||
|
||||
当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内存中以覆盖层记录已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重入的追加观察器对每次变更只投影一次。增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
|
||||
增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
|
||||
|
||||
### 生命周期与实时激活态
|
||||
|
||||
@@ -32,16 +32,16 @@ Status: implemented
|
||||
|
||||
### 服务边界
|
||||
|
||||
服务只接受在对应 id 下注册的同一个实时 `Agent` 对象。成功注入变更后,它会发出带作用域的 `goal/changed` 事件,并隔离监听器失败。策略消费者通过本服务、公共 `Agent` 接口和 `agent/*` 事件工作;目标领域既不导入也不修改 `dsh-agent-loop`。
|
||||
服务只接受在对应 id 下注册的同一个实时 `Agent` 对象。变更提交后,它会发出带作用域的 `goal/changed` 事件,并隔离监听器失败。策略消费者通过本服务、公共 `Agent` 接口和 `agent/*` 事件工作;目标领域既不导入也不修改 `dsh-agent-loop`。
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟回拨校正、严格记录解码、生命周期连续性、来源与内容一致性,以及连续 Goal Round 归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的 Goal Round。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动与生命周期所有者解除激活、活跃目标重新激活、持久事件折叠、inbox 独立性、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性,以及连续 Goal Round 归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费方,再从外部读取持久 JSONL,以验证 goal 记录以及不存在未经请求的 Goal Round。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **把目标存入独立数据库或会话头**——不予采纳,因为会话日志已经提供顺序、持久化、fork 前缀与可重建性;第二份存储会引入原子性和谱系问题。
|
||||
- **使用模型不可见的纯日志事件**——不予采纳,因为会改变后续模型行为的持久状态必须满足仓库日志不变量,保持模型可见且可重建。
|
||||
- **把每次持久变更与排队的模型上下文绑定。** 后续的 [Goal 自有持久事件决策](../architecture/2026-07-31-goal-owned-durable-events.md)不采用这一方案:goal 工具与已调度的继续执行提示词会在需要时暴露状态,而领域持久化不依赖队列结果。
|
||||
- **持久化激活态并自动重启**——不予采纳,因为打开或恢复会话时必须等待人类输入;持久阶段记录状态,而不是再次消耗资源的授权。
|
||||
- **把所有会话轮次都计为 Goal Round**——不予采纳,因为同一会话可以包含人类澄清、检查和无关工作;只有归属于目标的继续执行轮次才消耗该预算。
|
||||
- **向 `dsh-agent-loop` 添加目标状态或通用循环抽象**——不予采纳,因为状态与继续执行策略可以通过现有插件、`Agent` 动词和事件组合,而无需赋予默认循环实现特权。
|
||||
@@ -50,7 +50,7 @@ Status: implemented
|
||||
|
||||
- 目标历史作为普通会话数据,在持久化、恢复、无关节点压缩和会话 fork 后继续保留。
|
||||
- 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
|
||||
- 完整快照便于检查和严格回放,但在压缩隐藏它们之前,会在模型历史中重复目标描述与状态字段。
|
||||
- 完整快照便于检查、严格回放与 last-wins 投影,且不会向模型历史添加只用于变更的消息。
|
||||
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
|
||||
- 回合上限只约束继续执行次数;当回合、token、费用、时间或提供方限制停止工作时,策略消费者会把它们映射为不同的阻塞原因。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-plugin-command-registration.md
|
||||
2026-07-19-plugin-command-registration.md: 343cb5d946dba9fb881adf12c197961dfd6a359b
|
||||
2026-07-19-plugin-command-registration.zh.md: f91cc399d4ee3eb95263974d345c01df41baad7d
|
||||
2026-07-19-plugin-command-registration.md: 5233ce511dc9798733513ccbf6824f3e1b68d2e6
|
||||
2026-07-19-plugin-command-registration.zh.md: 7f41b9d2d00373901b51e3338fa51e1cadf886a9
|
||||
|
||||
@@ -36,7 +36,7 @@ Expected handler failures return `CommandResult.error`. Thrown or malformed resu
|
||||
|
||||
### TUI mapping
|
||||
|
||||
The TUI registers its built-in slash commands as agent-scoped command definitions instead of switching on strings. Its autocomplete and help view read the live catalog, so plugin commands appear and disappear with their effects. Any submitted line beginning with `/` stays in the command plane; unknown input produces a terminal warning rather than falling through to `Agent.send()` or `Agent.steer()`.
|
||||
The TUI registers its built-in slash commands as agent-scoped command definitions instead of switching on strings. Its autocomplete and help view read the live catalog, so plugin commands appear and disappear with their effects. Any submitted line beginning with `/` stays in the command plane; unknown input produces a terminal warning rather than falling through to `Agent.steer()`.
|
||||
|
||||
Each submitted command owns an `AbortController`. TUI disposal aborts outstanding dispatches, removes the local definitions, and waits for the command-producing fiber before completing teardown.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ TUI 拥有斜杠命令。如果命令名、帮助文本、自动补全、分派
|
||||
|
||||
### TUI 映射
|
||||
|
||||
TUI 把内置斜杠命令注册为智能体作用域命令定义,不再对字符串执行 switch。自动补全与帮助视图读取实时目录,因此插件命令会随其副作用出现和消失。任何以 `/` 开头的提交行都留在命令平面;未知输入产生终端警告,不会落入 `Agent.send()` 或 `Agent.steer()`。
|
||||
TUI 把内置斜杠命令注册为智能体作用域命令定义,不再对字符串执行 switch。自动补全与帮助视图读取实时目录,因此插件命令会随其副作用出现和消失。任何以 `/` 开头的提交行都留在命令平面;未知输入产生终端警告,不会落入 `Agent.steer()`。
|
||||
|
||||
每次提交命令都会创建一个专属 `AbortController`。TUI 释放会中止未完成的分派、移除本地定义,并等待命令生产者的 fiber(纤程)完全停稳后再完成清理。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-19-same-session-goal-round-driver.md
|
||||
2026-07-19-same-session-goal-round-driver.md: 0e6be9fe3109336d47867ab52c585dc267309fb4
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: 9f535dd3b0bf49fff5ff962b04b89330d0b27a55
|
||||
2026-07-19-same-session-goal-round-driver.md: a989b9d3487ebbf8ff11a4e34cb08029f1e4ace4
|
||||
2026-07-19-same-session-goal-round-driver.zh.md: b49a759c8f3577db30b8ab45449bb7faed85a7da
|
||||
|
||||
@@ -8,7 +8,7 @@ English | [中文](2026-07-19-same-session-goal-round-driver.zh.md)
|
||||
|
||||
The goal domain can retain an objective and the model-facing tools can mutate its lifecycle, but neither should decide when another model turn begins. A continuation driver must bridge active goal state to the ordinary agent loop without adding goal-specific branches to `dsh-agent-loop`, inventing a second conversation, or treating every human turn as an autonomous iteration.
|
||||
|
||||
That bridge has concurrency and durability obligations. Human input, cancellation, a goal edit, persistence failure, session restart, plugin unload, and a downstream prompt policy can all race a pending continuation. A naive `goal/changed -> agent.send()` listener can admit obsolete work, run alongside a human prompt, spend beyond the cap, or restart from replay without new authority.
|
||||
That bridge has concurrency and durability obligations. Human input, cancellation, a goal edit, persistence failure, session restart, plugin unload, and a downstream prompt policy can all race a pending continuation. A naive `goal/changed -> agent.followup()` listener can admit obsolete work, run alongside a human prompt, spend beyond the cap, or restart from replay without new authority.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -20,15 +20,15 @@ The plugin has no configuration. `maxGoalRounds` is resolved and persisted by `d
|
||||
|
||||
### Reservation and admission
|
||||
|
||||
When an agent is idle, has no competing queued work, and its current goal is `active` plus `armed`, the driver checkpoints pending goal mutations and rechecks every predicate after the await. If `roundsStarted` already equals `maxGoalRounds`, it records `blocked` with code `round-limit`. Otherwise it reserves the exact identity `{ goalId, revision, round: roundsStarted + 1 }` and the complete rendered prompt before calling `Agent.send()` with `GoalMessageSource`. The prompt JSON-quotes the objective so multiline or tag-like text remains an unambiguous data value inside the familiar frame.
|
||||
When an agent is idle, has no competing queued work, and its current goal is `active` plus `armed`, the driver checkpoints pending goal mutations and rechecks every predicate after the await. If `roundsStarted` already equals `maxGoalRounds`, it records `blocked` with code `round-limit`. Otherwise it reserves the exact identity `{ goalId, revision, round: roundsStarted + 1 }` and the complete rendered prompt before calling `Agent.followup()` with `GoalMessageSource`. The prompt JSON-quotes the objective so multiline or tag-like text remains an unambiguous data value inside the familiar frame.
|
||||
|
||||
The `agent/prompt-submit` waterfall is the admission fence. A positive goal source is allowed only when it exactly matches the driver's pending identity and content, the live goal still has that id and revision, activation remains armed, and the round is still the next number. The plugin checks once before delegating and again after downstream hooks return. This second check prevents an async hook from editing or pausing the goal while still admitting the old prompt.
|
||||
The `agent/pre-step` waterfall is the entry fence. A positive goal source enters only when it exactly matches the driver's pending identity and content, the live goal still has that id and revision, activation remains armed, and the round is still the next number. The plugin checks once before delegating and again after downstream listeners return. This second check prevents an async listener from editing or pausing the goal while still entering the old prompt.
|
||||
|
||||
Only the resulting `user/message` is an admitted round and advances the goal fold. A stale reservation is discarded before a turn opens; the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy.
|
||||
Only the resulting `user/message` is an entered round and advances the goal fold. A stale reservation closes a blocked no-step turn; the driver marks it stale and does not charge the round. A downstream policy rejection that is not caused by staleness blocks the goal rather than retrying around policy.
|
||||
|
||||
### Human work and revision races
|
||||
|
||||
`agent/queued` distinguishes the driver's complete accepted record from every other prompt. Ordinary work already queued before a reservation prevents scheduling. Ordinary work queued while an automatic prompt is pending makes that reservation stale, so a mixed batch admits the human prompt but rejects the automatic one. Ordinary work arriving after the goal round was admitted remains queued for its own next turn; continuation is reconsidered only when the agent later becomes idle.
|
||||
The reserved `MessageId` distinguishes the driver's complete record from every other prompt. Ordinary work already queued before a reservation prevents scheduling. Ordinary work queued while an automatic prompt is pending makes that reservation stale, so a mixed claimed batch rejects the automatic proposal. Ordinary work arriving after the goal round entered remains queued for its own next turn; continuation is reconsidered only when the agent later becomes idle.
|
||||
|
||||
A goal mutation during a round advances its durable revision. Settlement of the older revision cannot overwrite that mutation. The driver discards the old attempt outcome, reads the new projection, and continues only if the new revision is still active and armed. This makes model-recorded completion, pause, block, and edit authoritative over the physical turn's later close reason.
|
||||
|
||||
@@ -53,9 +53,9 @@ No abnormal outcome requests an automatic retry. A later human prompt can ask to
|
||||
|
||||
Every `goal/changed` notification creates a checkpoint obligation. The driver awaits `ctx.sessions.flush(session)` before reserving work, then checks for a newer mutation, agent lifecycle change, or competing prompt. Turn-end flush failure is reported by the existing `agent/error` notification after `turn/end`; the driver finds that exact closed turn even when a concurrent one-shot injection appended a later turn, associates the failure with the exact attempt, and disarms before the next idle decision.
|
||||
|
||||
Broad cancellation previously exposed only its effects after queues were cleared or the request aborted. The public agent vocabulary now includes observe-only `agent/cancel-requested(agent, reason)`. The concrete loop emits it for effective cancellation before either action; fused notification containment means a broken listener cannot veto cancellation. The goal driver uses this edge to clear its reservation before the loop destroys the queued-work evidence. When that reservation is a queued or admitted goal attempt, cancellation durably pauses the goal; when cancellation belongs to unrelated human work with no goal attempt, it only removes process-local activation. If the pause mutation throws, the driver falls back to disarming rather than allowing cancelled automatic work to restart.
|
||||
Broad cancellation clears pending inbox work and aborts the active loop phase. The goal driver follows the reserved message through inbox claim/discard events and the durable aborted turn ending. Because a turn now opens before its initial claim, cancellation can close a claimed no-step attempt; the driver marks that attempt cancelled and lets the following idle edge pause the goal, just as it does for an admitted attempt. Cancellation with no matching goal attempt only removes process-local activation. If the pause mutation throws, the driver falls back to disarming rather than allowing cancelled automatic work to restart.
|
||||
|
||||
This is a coordination notification, not a second stop API. `Agent.cancel()` remains the only public broad cancellation verb, idle calls remain no-ops, and custom `Agent` implementations that claim the interface must honor the event ordering if consumers depend on it.
|
||||
`Agent.cancel()` remains the only public broad cancellation verb. Custom `Agent` implementations that claim the interface must honor the inbox, turn-ending, status, and quiescence ordering if consumers depend on it.
|
||||
|
||||
### Process lifecycle
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
目标领域可以保留目标,模型可见工具也可以变更其生命周期,但两者都不应决定下一个模型轮次何时开始。继续执行驱动器必须把活跃目标状态桥接到普通 agent(智能体)循环,同时不能向 `dsh-agent-loop` 添加目标专用分支、创建第二段对话,也不能把每个人类轮次都视为自主迭代。
|
||||
|
||||
这层连接还承担并发与持久性义务。人类输入、取消、目标编辑、持久化失败、会话重启、插件卸载以及下游提示词策略都可能与待处理的继续执行发生竞争。简单的 `goal/changed -> agent.send()` 监听器可能接纳过期工作、与人类提示词同时运行、超出上限消耗资源,或在回放后未经新授权自行重启。
|
||||
这层连接还承担并发与持久性义务。人类输入、取消、目标编辑、持久化失败、会话重启、插件卸载以及下游提示词策略都可能与待处理的继续执行发生竞争。简单的 `goal/changed -> agent.followup()` 监听器可能接纳过期工作、与人类提示词同时运行、超出上限消耗资源,或在回放后未经新授权自行重启。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -20,15 +20,15 @@ Status: implemented
|
||||
|
||||
### 预留与接纳
|
||||
|
||||
当 agent 空闲、没有竞争中的排队工作,且当前目标为 `active` 加 `armed` 时,驱动器会先将待处理的目标变更持久化到检查点,并在等待完成后重新校验所有条件。若 `roundsStarted` 已等于 `maxGoalRounds`,它会记录代码为 `round-limit` 的 `blocked`;否则,它会先预留精确身份 `{ goalId, revision, round: roundsStarted + 1 }` 和完整渲染提示词,再以 `GoalMessageSource` 调用 `Agent.send()`。提示词用 JSON 引号编码目标描述,使多行或类似标签的文本在熟悉框架中仍是无歧义的数据值。
|
||||
当 agent 空闲、没有竞争中的排队工作,且当前目标为 `active` 加 `armed` 时,驱动器会先检查点持久化待处理的目标变更,并在等待之后重新校验所有条件。若 `roundsStarted` 已等于 `maxGoalRounds`,它会记录代码为 `round-limit` 的 `blocked`;否则,它会先预留精确身份 `{ goalId, revision, round: roundsStarted + 1 }` 和完整渲染提示词,再以 `GoalMessageSource` 调用 `Agent.followup()`。提示词用 JSON 引号编码目标描述,使多行或类似标签的文本在熟悉框架中仍是无歧义的数据值。
|
||||
|
||||
`agent/prompt-submit` waterfall(瀑布式事件)是接纳栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed,并且该回合仍是下一个编号时才会获准。插件在委托下游监听器前检查一次,在下游返回后再检查一次。第二次检查防止异步钩子编辑或暂停目标后,旧提示词仍被接纳。
|
||||
`agent/pre-step` 瀑布是进入栅栏。正数目标来源只有在完全匹配驱动器待处理的身份和内容、实时目标仍具有相同 id 与修订号、激活态仍为 armed,并且该回合仍是下一个编号时才会进入步骤。插件在委托下游监听器前检查一次,在下游返回后再检查一次。第二次检查防止异步监听器编辑或暂停目标后,旧提示词仍进入步骤。
|
||||
|
||||
只有最终产生的 `user/message` 才是已接纳 Goal Round,并推进目标折叠。陈旧预留会在轮次打开前被丢弃;驱动器会把它标记为陈旧,不消耗 Round 数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。
|
||||
只有最终产生的 `user/message` 才是进入步骤的目标回合,并推进目标折叠。陈旧预留会关闭一个 blocked 的无步骤轮次;驱动器会把它标记为陈旧,不消耗回合数。若下游策略拒绝并非由陈旧状态导致,目标会进入 blocked,而不会绕过该策略自动重试。
|
||||
|
||||
### 人类工作与修订竞争
|
||||
|
||||
`agent/queued` 会区分驱动器自己的完整已接受记录与其他所有提示词。预留之前已经排队的普通工作会阻止调度;自动提示词待处理时进入的普通工作会使该预留过期,因此混合批次只接纳人类提示词而拒绝自动提示词。Goal Round 已经接纳后到达的普通工作会保留在队列中,成为下一个独立轮次;只有 agent 再次空闲后才重新考虑继续执行。
|
||||
预留的 `MessageId` 会区分驱动器自己的完整记录与其他所有提示词。预留之前已经排队的普通工作会阻止调度;自动提示词待处理时进入的普通工作会使该预留过期,因此混合的已领取批次会 reject 自动提案。目标回合已经进入步骤后到达的普通工作会保留在队列中,成为下一个独立轮次;只有 agent 再次空闲后才重新考虑继续执行。
|
||||
|
||||
目标在回合内发生变更时会推进持久修订号。旧修订的结算不得覆盖该变更。驱动器会丢弃旧尝试的结果、读取新投影,并且只在新修订仍为 active 与 armed 时继续。因此,模型记录的完成、暂停、阻塞和编辑相对于物理轮次稍后的关闭原因具有最终权威。
|
||||
|
||||
@@ -53,9 +53,9 @@ Status: implemented
|
||||
|
||||
每次 `goal/changed` 通知都会产生一个检查点义务。驱动器在预留工作前等待 `ctx.sessions.flush(session)`,随后检查是否出现了更新的变更、agent 生命周期变化或竞争提示词。轮次结束时的 flush 失败会在 `turn/end` 之后通过现有 `agent/error` 通知报告;即使并发的一次性注入已追加后续轮次,驱动器仍会找到对应的同一个已关闭轮次,把失败关联到对应的同一次尝试,并在下一次空闲决策前解除激活。
|
||||
|
||||
广义取消此前只在队列已清除或请求已中止后暴露结果。公共 agent 词汇现在新增只观察的 `agent/cancel-requested(agent, reason)`。具体循环仅在取消有效时发出该事件,并且发生在清除队列和中止步骤之前;融合后的通知机制会隔离各监听器的失败,因此发生故障的监听器不能否决取消。目标驱动器利用该边沿在循环销毁排队工作证据前清除预留。若该预留是排队中或已接纳的目标尝试,取消会持久暂停目标;若取消属于没有目标尝试的无关人类工作,则只移除进程内激活态。若暂停变更抛错,驱动器会回退到解除激活,避免已取消的自动工作重新启动。
|
||||
广义取消会清除待处理 inbox 工作,并中止活跃 loop 阶段。目标驱动器通过 inbox 的领取/丢弃事件和持久的 aborted 轮次结束来跟踪预留消息。由于轮次现在会在首次领取前打开,取消可以关闭已领取的无步骤尝试;驱动器会把该尝试标记为已取消,并让随后的 idle 边沿暂停目标,与已准入尝试的处理方式相同。没有匹配目标尝试的取消只会移除进程内激活态。若暂停变更抛错,驱动器会回退到解除激活,避免已取消的自动工作重新启动。
|
||||
|
||||
该通知是协调事件,不是第二个停止 API。`Agent.cancel()` 仍是唯一的公共广义取消动词,空闲调用仍是无操作;若消费方依赖此 seam,自定义 `Agent` 实现就必须满足该事件顺序。
|
||||
`Agent.cancel()` 仍是唯一的公共广义取消动词。若消费方依赖其顺序,实现该接口的自定义 `Agent` 必须遵守 inbox、轮次结束、status 与完全停稳的顺序。
|
||||
|
||||
### 进程生命周期
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-21-cross-session-references.md
|
||||
2026-07-21-cross-session-references.md: 61ea30cabb2abf4d4a9b4391891b5987affb91d0
|
||||
2026-07-21-cross-session-references.zh.md: 47ee062dbe5d5d5fe94f2f6036952db070902173
|
||||
2026-07-21-cross-session-references.md: b2d428e0937fdd881720720401948a1c3e7ef1f6
|
||||
2026-07-21-cross-session-references.zh.md: a59657bfcd46393a5cd1c5a5076a6eddd5d065ff
|
||||
|
||||
@@ -18,17 +18,17 @@ The service uses `ctx.sessionQuery.readSurface(sessionId)`, which loads one live
|
||||
|
||||
## Snapshot and projection
|
||||
|
||||
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partially prepared context: any read, cancellation, validation, or budget error rejects the operation before `send()` or `steer()`. Cancellation races in-flight discovery and exact reads, so a host settles promptly even when a persistence backend cannot interrupt its pending operation; any late backend settlement is observed but cannot enqueue the message. A source is read before enqueue, so later source messages, compaction, deletion, or persistence replacement cannot change the target session.
|
||||
Preparation deduplicates in first-appearance order, rejects the target id, enforces a configurable limit with a hard maximum of three references, and performs all reads in parallel. It returns no partially prepared context: any read, cancellation, validation, or budget error rejects the operation before `followup()` or `steer()`. Cancellation races in-flight discovery and exact reads, so a host settles promptly even when a persistence backend cannot interrupt its pending operation; any late backend settlement is observed but cannot enqueue the message. A source is read before enqueue, so later source messages, compaction, deletion, or persistence replacement cannot change the target session.
|
||||
|
||||
Projection retains direct-user messages and steering, completed assistant text, and checkpoint user messages carrying the canonical source exported by `dsh-compact`. That marker is part of the compaction capability contract rather than a backend package name. When a source prompt already contains baked prefix context, projection reads only its model-hidden display content, so referencing that target later does not recursively propagate an earlier snapshot. Projection excludes shadowed pre-compaction nodes, tools and results, reasoning, injected context, other plugin user messages, log-only records, and incomplete assistant chunks. Repeated compaction therefore exposes only the latest folded checkpoint lineage still on the current surface plus its retained tail; there is no raw/current switch and no shadow recovery.
|
||||
|
||||
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags or escape the data region. The same serializer drives each source's independent byte accounting. AgentLoop persists the snapshot as a sourced `user/message` immediately before the direct `user/message` or `steering/message`; target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type, placement mode, or prompt envelope.
|
||||
One aggregated context is serialized as JSON beneath a fixed untrusted-background warning. The warning tells the model not to follow instructions, permission claims, or tool requests from referenced sessions unless the current user repeats them. Tag-safe serialization emits every data `<` as the lossless JSON escape `\u003c`; source strings therefore cannot spell the surrounding XML-like tags or escape the data region. The same serializer drives each source's independent byte accounting. AgentLoop persists the snapshot as a sourced `user/message` immediately before the direct `user/message`; target replay therefore satisfies the model-visible/log-reconstructable invariant without a new event type, placement mode, or prompt envelope.
|
||||
|
||||
## Message ownership
|
||||
|
||||
TUI owns the snapshot/direct-message transaction without extending the generic inbox record. Outside the next-step acceptance window, it installs a one-shot outer `agent/prompt-submit` listener before `followup()`; an allowed decision receives the snapshot as `additionalContexts`, while a blocked or discarded prompt releases the listener and writes neither message. During prompt admission or an open turn, TUI calls `inject(snapshot)` then `steer(prompt)`, and AgentLoop stages both for the same safe boundary. If admission fails before that boundary, both remain staged for retry or a later admitted prompt; cancellation or disposal may discard them. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this generic delivery boundary.
|
||||
TUI owns the snapshot/direct-message transaction without extending the generic inbox record. While the agent is idle, it installs a one-shot outer `agent/pre-step` listener before `followup()`; an enter decision receives the snapshot as another message, while rejection or an earlier ordinary discard releases the listener and writes neither message. While the agent is running, TUI calls `inject(snapshot)` then `steer(prompt)`, placing both in the next-step inbox for the same later claim. A rejecting or failed pre-step leaves that claimed pair removed; messages inserted after the claim remain pending. The [separate-context decision](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) owns this generic delivery boundary.
|
||||
|
||||
Reference preparation is not a new steering protocol and does not create a turn by itself. A `followup()` outside the next-step acceptance window dispatches prompt admission; steering inside the window bypasses it while retaining snapshot order through the shared outbox.
|
||||
Reference preparation is not a new steering protocol and does not create a turn by itself. Idle delivery uses `followup()` and pre-step entry; running delivery uses the shared next-step inbox while retaining snapshot order.
|
||||
|
||||
## Host adapters
|
||||
|
||||
@@ -43,10 +43,10 @@ Each of at most three references is independently capped at 65,536 UTF-8 bytes b
|
||||
## Alternatives considered
|
||||
|
||||
- **Wait for SQLite FTS5** — rejected because snapshot correctness requires exact id reads and canonical surface folding, not content search. FTS improves discovery only.
|
||||
- **Put mention syntax in `Agent.send()`** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
|
||||
- **Put mention syntax in agent delivery methods** — rejected because it would make the core protocol parse one host's presentation syntax and prevent typed non-text hosts from sharing the semantic layer.
|
||||
- **Implement references separately in each host** — rejected because projection, security warning, retention, and persistence would drift across hosts.
|
||||
- **Attach context to `SendOptions` and the inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. A domain-specific admission wrapper and the existing next-step outbox preserve the required pairing without enlarging every message.
|
||||
- **Bake the prefix host-side before `send()`** — rejected because `agent/prompt-submit` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets TUI hide background bytes from the direct user bubble.
|
||||
- **Attach context to `SendOptions` and the direct prompt's inbox record** — rejected because generic delivery would own a domain transaction through admission, steering, cancellation, and observation. A domain-specific admission wrapper and the existing next-step inbox preserve the required pairing without enlarging every direct prompt.
|
||||
- **Bake the prefix host-side before `followup()`** — rejected because `agent/pre-step` must inspect and rewrite only the direct prompt. Keeping the snapshot as a separate sourced message preserves that boundary and lets TUI hide background bytes from the direct user bubble.
|
||||
- **Replay the raw source log or restore shadowed events** — rejected because compact defines the current model surface and may intentionally retire sensitive or expensive history.
|
||||
- **Resume or fork the source** — rejected because the feature supplies read-only background for one target message, not identity or lifecycle continuity.
|
||||
- **Inject at request time by rereading the source** — rejected because the reference would become nondeterministic, cancellation races could alter its bytes, and target replay would depend on external mutable state.
|
||||
|
||||
@@ -18,17 +18,17 @@ TUI 用户需要把另一场对话中的相关工作带入一条新消息,但
|
||||
|
||||
## 快照与投影
|
||||
|
||||
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分完成的上下文:任何读取、取消、校验或预算错误都会在调用 `send()` 或 `steer()` 前拒绝本次操作。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,宿主也能及时结束等待;后端迟到的完成结果仍会得到处理,但不能让消息入队。源会话在入队前完成读取,因此源会话后续新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中的快照。
|
||||
准备过程按首次出现的顺序去重、拒绝目标会话自身的 id,并且执行可配置的数量限制,但引用硬上限为三个,所有读取均并行执行。该过程不会返回部分完成的上下文:任何读取、取消、校验或预算错误都会在调用 `followup()` 或 `steer()` 前拒绝本次操作。取消会与进行中的候选发现和精确读取竞速,因此即使持久化后端无法中断待处理操作,宿主也能及时结束等待;后端迟到的完成结果仍会被观察,但不能让消息入队。源会话在入队前完成读取,因此源会话后续新增消息、执行压缩、被删除或替换持久化内容,都无法改变目标会话中的快照。
|
||||
|
||||
投影会保留直接用户消息与 steering(中途引导)、已完成的 assistant 文本,以及携带由 `dsh-compact` 导出的规范来源标记的检查点用户消息。该标记属于压缩能力契约的一部分,而非某个后端包名称。当源提示词已包含合并写入的前缀上下文时,投影只读取其模型不可见的显示内容,因此后续引用该目标不会递归传播先前的快照。投影会排除压缩前已被遮蔽的节点、工具及其结果、推理(reasoning)、注入的上下文、其他插件用户消息、仅用于日志的记录,以及尚未完成的 assistant 分片。因此,重复压缩只会暴露当前表层仍保留的最新折叠检查点谱系及其尾部消息;系统不提供 raw/current 开关,也不恢复被遮蔽的内容。
|
||||
|
||||
系统把一个聚合上下文序列化为 JSON,并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求,除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。AgentLoop 会把快照持久化为一条带来源信息的 `user/message`,紧接在直接 `user/message` 或 `steering/message` 之前。因此,目标回放无需新增事件类型、放置模式或提示词封套,也能满足「模型可见/日志可重建」不变量。
|
||||
系统把一个聚合上下文序列化为 JSON,并置于固定的不可信背景警告之后。该警告要求模型不要遵循被引用会话中的指令、权限声明或工具请求,除非当前用户再次提出这些内容。标签安全序列化会把数据中的每个 `<` 无损转义为 JSON `\u003c`;因此源字符串无法拼出外围类似 XML 的标签,也无法逃逸数据区域。同一个序列化器会独立核算每个源的字节数。AgentLoop 会把快照持久化为一条带来源信息的 `user/message`,紧接在直接 `user/message` 之前。因此,目标回放无需新增事件类型、放置模式或提示词封套,也能满足「模型可见/日志可重建」不变量。
|
||||
|
||||
## 消息所有权
|
||||
|
||||
TUI 负责快照/直接消息事务,不扩展通用收件箱记录。在 next-step 接受窗口之外,它会在调用 `followup()` 前安装一次性的外层 `agent/prompt-submit` 监听器;获准决策会把快照作为 `additionalContexts` 接收,而被阻止或丢弃的提示词会释放监听器,并且不写入任何消息。提示词准入期间或轮次打开时,TUI 会依次调用 `inject(snapshot)` 和 `steer(prompt)`,AgentLoop 则将两者暂存到同一个安全边界。如果准入在抵达该边界前失败,两者都会保留暂存状态,供重试或后续获准提示词使用;取消或资源释放可能丢弃它们。这一通用交付边界由[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定。
|
||||
TUI 负责快照/直接消息事务,不扩展通用收件箱记录。agent 空闲时,它会在调用 `followup()` 前安装一次性的外层 `agent/pre-step` 监听器;enter 决策会把快照作为另一条消息接收,而 reject 或更早的普通丢弃会释放监听器,并且不写入任何消息。agent 运行时,TUI 会依次调用 `inject(snapshot)` 和 `steer(prompt)`,把两者放入 next-step inbox,等待后续同一次领取。pre-step reject 或失败会让这对已领取消息保持删除;领取后插入的消息继续等待。这一通用交付边界由[上下文分离决策](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md)规定。
|
||||
|
||||
引用准备过程不是新的 steering 协议,本身也不会创建轮次。在 next-step 接受窗口之外调用 `followup()` 会分派提示词准入;窗口内的 steering 会绕过它,同时通过共享 outbox 保持快照顺序。
|
||||
引用准备过程不是新的 steering 协议,本身也不会创建轮次。空闲交付使用 `followup()` 和 pre-step 进入决策;运行期间的交付使用共享 next-step inbox,并保持快照顺序。
|
||||
|
||||
## 宿主适配器
|
||||
|
||||
@@ -43,10 +43,10 @@ TUI 把会话候选与现有 `@` 文件提供方组合在一起。候选查询
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **等待 SQLite FTS5**:不予采纳,因为快照正确性依赖按准确 id 读取和规范表层折叠,而不是内容搜索。FTS 只改进候选发现。
|
||||
- **把提及标记语法放入 `Agent.send()`**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
|
||||
- **把提及标记语法放入 agent 投递方法**:不予采纳,因为这会迫使核心协议解析某个宿主的展示语法,并阻止带类型的非文本宿主复用同一语义层。
|
||||
- **在每个宿主中分别实现引用**:不予采纳,因为投影、安全警告、保留策略和持久化会在不同宿主之间逐渐偏离。
|
||||
- **把上下文附加到 `SendOptions` 和收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域专用的准入包装层和现有 next-step outbox 可以保持所需配对,而无需扩大每条消息。
|
||||
- **在调用 `send()` 前由宿主合并前缀**:不予采纳,因为 `agent/prompt-submit` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 TUI 从直接用户气泡中隐藏背景字节。
|
||||
- **把上下文附加到 `SendOptions` 和直接提示词的收件箱记录**:不予采纳,因为通用投递将不得不负责贯穿准入、steering、取消和观察的领域事务。领域专用的准入包装层和现有 next-step inbox 可以保持所需配对,而无需扩大每条直接提示词。
|
||||
- **在调用 `followup()` 前由宿主合并前缀**:不予采纳,因为 `agent/pre-step` 必须只检查和改写直接提示词。将快照保留为独立的带来源消息,可以维持该边界,并让 TUI 从直接用户气泡中隐藏背景字节。
|
||||
- **回放原始源日志或恢复被遮蔽的事件**:不予采纳,因为压缩定义了当前模型表层,并且可能有意淘汰敏感或开销高昂的历史内容。
|
||||
- **恢复或 fork 源会话**:不予采纳,因为本功能只为一条目标消息提供只读背景,不提供身份或生命周期连续性。
|
||||
- **在请求时重新读取源会话并注入**:不予采纳,因为这会让引用变得不确定,取消竞态可能改变其字节内容,目标回放也会依赖可变的外部状态。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-docked-web-goal-bar.md
|
||||
2026-07-22-docked-web-goal-bar.md: 21f200f165acdf370a5896acb8f54afe33a4ee99
|
||||
2026-07-22-docked-web-goal-bar.zh.md: eb8e3edda54207b06e25812a38f969e74f342c34
|
||||
2026-07-22-docked-web-goal-bar.md: ffddef6cec8eb632cd44bb5352de246db7413c02
|
||||
2026-07-22-docked-web-goal-bar.zh.md: b732f71cfc3d3f813641c2ad9c594134beb2e440
|
||||
|
||||
@@ -16,13 +16,13 @@ Visibility drives the label and actions: active shows "Ongoing Goal" with pause/
|
||||
|
||||
`GoalBarActions` lives in ui-goal's slot contract (`packages/client/ui-goal/src/client/slots.ts`) and carries exactly the rendered verbs: `onEdit`/`onPause`/`onResume`/`onClear`. Each callback asynchronously returns an explicit success/failure result so `GoalBar` owns its transitions and error display. `apply.ts` wires them to the runtime session methods; the runtime session resolves the current goal's compare-and-set ref internally, so the UI passes no ref.
|
||||
|
||||
The runtime session gains the goal surface the strip (and future UI) needs: `fetchGoal` populates the snapshot on open, and a live `context/message` carrying `goal/change` meta triggers a coalesced refetch — concurrent triggers share the in-flight `goal.get`, while a trigger received during that read schedules one coalesced trailing read so independently ordered notifications and GET responses cannot leave stale state. Window replays never refetch, and matching the meta kind (rather than a goal key) also catches clear tombstones written by other clients. The six mutation verbs fold transport failures into `{ ok: false }` results like every sibling session method, and a get result older than a mutation response that landed mid-flight is dropped.
|
||||
The runtime session gains the goal surface the strip (and future UI) needs through the host-computed `goal` projection. The history tail seeds its whole current value, and `session/projection` frames update it when durable `agent/inbox/spliced` insertions commit goal snapshots or clear tombstones; later context admission is irrelevant to UI freshness. The four rendered mutation verbs fold transport failures into `{ ok: false }` results like every sibling session method.
|
||||
|
||||
The strip's background is `--dsw-alias-interactive-bg-hover` rather than the mock's literal `#F5F6F7`: the translucent hover gray resolves to that value over the white light-theme base and lifts the strip off the composer card in dark mode, where a static light token would sink. All colors are `--dsw-*` tokens.
|
||||
|
||||
## Testing
|
||||
|
||||
`packages/client/ui-goal/tests/goalbar.spec.tsx` pins the behavior through props alone: loading/absent/complete render nothing, the active strip renders label/objective and fires clear, rapid same-frame clear clicks dispatch once and a successful clear hides before projection convergence, the edit form prefills, rejects empty, saves on Enter, cancels on Esc, and resets when the goal's identity changes, the active strip fires pause, the paused strip fires resume, and the blocked strip exposes the reason tooltip. Component failure-path cases prove that a failed edit preserves its draft and that edit/resume/clear errors remain visible and retryable in the bar. The skeleton specs mount `ConversationRoot` with and without `goalActions`; the undefined case is seeded with an active goal, so the missing gate — not the missing goal — is what hides the strip. Runtime session specs pin the folded-error results, the live-only in-flight-plus-trailing refetch, and the stale-read guard. A keyless real-browser smoke boots the assembled application through `boot → RPC → runtime → GoalBar` and records an inline snapshot of the rendered label, objective, and actions.
|
||||
`packages/client/ui-goal/tests/goalbar.spec.tsx` pins the behavior through props alone: loading/absent/complete render nothing, the active strip renders label/objective and fires clear, rapid same-frame clear clicks dispatch once and a successful clear hides before projection convergence, the edit form prefills, rejects empty, saves on Enter, cancels on Esc, and resets when the goal's identity changes, the active strip fires pause, the paused strip fires resume, and the blocked strip exposes the reason tooltip. Component failure-path cases prove that a failed edit preserves its draft and that edit/resume/clear errors remain visible and retryable in the bar. The skeleton specs mount `ConversationRoot` with and without `goalActions`; the undefined case is seeded with an active goal, so the missing gate — not the missing goal — is what hides the strip. Runtime session specs pin folded-error results and projection updates. A keyless real-browser smoke boots the assembled application through `boot → RPC → runtime → GoalBar` and records an inline snapshot of the rendered label, objective, and actions.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -35,6 +35,6 @@ The strip's background is `--dsw-alias-interactive-bg-hover` rather than the moc
|
||||
|
||||
- Goal presence in the web UI is a standalone composer-context strip: sparkle, phase label, truncated objective, and pause/edit/clear (resume replacing pause when paused) — the browser client's first goal surface.
|
||||
- Goal mutations are single-flight within the component; a successful clear hides its exact goal immediately while projection delivery converges, preventing duplicate CAS errors without making transient UI state authoritative.
|
||||
- The runtime session exposes the goal verbs over RPC with folded transport errors, and refreshes the snapshot's goal on open and on live goal-change meta (coalesced, guarded against stale reads).
|
||||
- The runtime session exposes the goal verbs over RPC with folded transport errors and consumes the host's durable whole-goal projection on open and live updates.
|
||||
- Objective editing is reachable from the UI for the first time, through `goal.edit` with the runtime-owned ref; complete remains available to other surfaces (`/goal`, model tools).
|
||||
- `goal === null` renders nothing; the composer carries no persistent create affordance — creation is the `/goal` command's job.
|
||||
|
||||
@@ -16,13 +16,13 @@ Web UI 此前没有任何目标相关的界面:目标栈已随模型工具、T
|
||||
|
||||
`GoalBarActions` 位于 ui-goal 的槽位契约(`packages/client/ui-goal/src/client/slots.ts`),只携带实际渲染的动词:`onEdit`/`onPause`/`onResume`/`onClear`。每个回调都会异步返回显式成功/失败结果,因此 `GoalBar` 自行负责界面转换和错误显示。`apply.ts` 把它们接到运行时会话方法上;运行时会话在内部解析当前目标的 compare-and-set ref,因此 UI 不传 ref。
|
||||
|
||||
运行时会话获得了横条(以及未来 UI)所需的目标表面:`fetchGoal` 在打开时填充快照;携带 `goal/change` 元数据的 live `context/message` 触发合并重新拉取——并发触发器共享正在执行的 `goal.get`,读取期间收到的触发器会安排一次合并后的尾随读取,避免彼此独立排序的通知和 GET 响应留下陈旧状态。窗口重放绝不触发重新拉取,且匹配元数据 kind(而不是 goal 键)还能捕获其他客户端写入的清除墓碑。六个变更动词与所有同类会话方法一样,把传输层失败折叠为 `{ ok: false }` 结果;比在拉取途中落地的变更响应更旧的 get 结果会被丢弃。
|
||||
运行时会话通过由 host 计算的 `goal` 投影获得横条(以及未来 UI)所需的 goal 表面。历史尾页会提供完整当前值作为初始状态;持久 `agent/inbox/spliced` 插入项提交 goal 快照或 clear 墓碑时,`session/projection` 帧会更新该值,后续上下文准入与 UI 新鲜度无关。4 个实际渲染的变更动词与所有同类会话方法一样,把传输层失败折叠为 `{ ok: false }` 结果。
|
||||
|
||||
横条的背景色用 `--dsw-alias-interactive-bg-hover`,而不是设计稿里的字面值 `#F5F6F7`:这个半透明的悬浮灰在浅色主题的白色底上正好解析为该值,而在深色模式下能把横条从输入框卡片上衬托出来,静态的浅色 token 在深色模式下会沉进去。所有颜色都是 `--dsw-*` token。
|
||||
|
||||
## 测试
|
||||
|
||||
`packages/client/ui-goal/tests/goalbar.spec.tsx` 仅通过 props 固定这些行为:加载中/无目标/已完成时不渲染;active 横条渲染标签和目标内容并触发清除;同一帧内快速连续点击清除只会分发一次,清除成功后横条会在投影收敛前隐藏;编辑表单预填内容、拒绝空值、按 Enter 保存、按 Esc 取消,并在目标身份变化时重置;active 横条触发暂停;paused 横条触发恢复;blocked 横条暴露原因悬浮提示。组件失败路径用例证明编辑失败时保留草稿,并且编辑/恢复/清除错误持续显示在横条中且可重试。skeleton 规格测试分别挂载带与不带 `goalActions` 的 `ConversationRoot`;未定义的情形预置了一个 active 目标,因此隐藏横条的是缺失的挂载门,而不是缺失的目标。运行时会话规格测试固定了折叠错误结果、仅 live 的执行中读取加尾随读取,以及陈旧读取守卫。一个无密钥真实浏览器冒烟测试通过 `boot → RPC → runtime → GoalBar` 启动组装后的应用,并以内联快照记录渲染出的标签、目标内容和操作。
|
||||
`packages/client/ui-goal/tests/goalbar.spec.tsx` 仅通过 props 固定这些行为:加载中/无目标/已完成时不渲染;active 横条渲染标签和目标内容并触发清除;同一帧内快速连续点击清除只会分发一次,清除成功后横条会在投影收敛前隐藏;编辑表单预填内容、拒绝空值、按 Enter 保存、按 Esc 取消,并在目标身份变化时重置;active 横条触发暂停;paused 横条触发恢复;blocked 横条暴露原因悬浮提示。组件失败路径用例证明编辑失败时保留草稿,并且编辑/恢复/清除错误持续显示在横条中且可重试。skeleton 规格测试分别挂载带与不带 `goalActions` 的 `ConversationRoot`;未定义的情形预置了一个 active 目标,因此隐藏横条的是缺失的挂载门,而不是缺失的目标。运行时会话规格测试固定折叠错误结果和投影更新。一个无密钥真实浏览器冒烟测试通过 `boot → RPC → runtime → GoalBar` 启动组装后的应用,并以内联快照记录渲染出的标签、目标内容和操作。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
@@ -35,6 +35,6 @@ Web UI 此前没有任何目标相关的界面:目标栈已随模型工具、T
|
||||
|
||||
- Web UI 中目标的存在形式是独立的 composer 上下文横条:闪光图标、阶段标签、截断的目标内容,以及暂停/编辑/清除(暂停时恢复取代暂停)——这是浏览器客户端的第一个目标界面。
|
||||
- 目标变更在组件内走 single-flight;清除成功后会在投影投递收敛期间立即隐藏与其 id 完全匹配的目标,既防止重复 CAS 错误,又不会把瞬态 UI 状态视为权威。
|
||||
- 运行时会话通过 RPC 暴露目标动词并折叠传输层错误,且在打开时和 live 目标变更元数据到达时刷新快照中的目标(合并拉取,带陈旧读取守卫)。
|
||||
- 运行时会话通过 RPC 暴露 goal 动词并折叠传输层错误,在打开时和 live 更新时消费 host 的持久完整 goal 投影。
|
||||
- 目标内容首次可以从 UI 编辑,经由 `goal.edit`,ref 由运行时持有;完成对其他界面(`/goal`、模型工具)照常可用。
|
||||
- `goal === null` 时不渲染任何内容;输入框不提供常驻的创建入口,创建是 `/goal` 命令的职责。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-23-web-assistant-markdown.md
|
||||
2026-07-23-web-assistant-markdown.md: d5074e6090699229f5c43dd93eef0fdfbfedab76
|
||||
2026-07-23-web-assistant-markdown.zh.md: 31f0fd6835c9921f544f4b6217a0c834dff79859
|
||||
2026-07-23-web-assistant-markdown.md: 8a8778351911bcb3448366c718aa124c4a89de58
|
||||
2026-07-23-web-assistant-markdown.zh.md: 2ac024e24ff95b4eb296112562c93f343b832187
|
||||
|
||||
@@ -14,7 +14,7 @@ The Web conversation preserves assistant Markdown source through session events,
|
||||
|
||||
`MarkdownText` uses `react-markdown` with `remark-gfm` to build React elements from an AST. It covers CommonMark blocks plus GFM tables, task lists, strikethrough, and autolinks without raw-HTML parsing. Fenced code routes through the shared `CodeBlock`, which highlights registered grammars with the client's shiki singleton (`--shiki-*` tokens) and falls back to plain monospace otherwise. While a turn streams, fences stay on the plain arm so growing fences are not retokenized every chunk.
|
||||
|
||||
Visual spacing, tables, links, blockquotes, inline code, and code-block chrome follow deepsuite `@deepseek/md` (`markdown.css` / `code-block.css`) and the same `--dsw-alias-markdown-*`, `--dsw-font-markdown-*`, `--dsw-alias-border-l*`, and `--dsw-alias-label-*` tokens. Links use `--dsw-alias-state-business-primary` (deepsuite's sheet uses `--dsw-alias-brand-text`, which is blue only under newDesign; design-platform keeps brand-text near-black and is not retuned here). `CodeBlock` ships a language banner and a copy control (`复制` / `复制成功`). Citation pills, KaTeX, heading anchors, the thinking-small markdown variant, and custom □/☑ task markers are out of scope until matching product DOM exists; GFM task lists keep native checkboxes.
|
||||
Visual spacing, tables, links, blockquotes, inline code, and code-block chrome follow deepsuite `@deepseek/md` (`markdown.css` / `code-block.css`) and the same `--dsw-alias-markdown-*`, `--dsw-font-markdown-*`, `--dsw-alias-border-l*`, and `--dsw-alias-label-*` tokens. Links use `--dsw-alias-state-business-primary` (deepsuite's sheet uses `--dsw-alias-brand-text`, which is blue only under newDesign; design-platform keeps brand-text near-black and is not retuned here). `CodeBlock` ships a language banner and a copy control (`复制` / `复制成功`). Finalized text renders KaTeX through `remark-math` and `rehype-katex`; `remarkMathCompatibility` maps `\(...\)`, `\[...\]`, and block-level same-line `$$...$$` to the same standard math AST nodes. This is a narrow parser compatibility layer, not a regex rewrite or malformed-model-output repair. Streaming stays literal until finalization so incomplete formulae do not flash errors. Citation pills, heading anchors, the thinking-small markdown variant, and custom □/☑ task markers remain out of scope; GFM task lists keep native checkboxes.
|
||||
|
||||
The dependency is explicit in `ui-primitives`; because that pure library is seeded by the Web shell, the parser and highlighter are part of the initial browser bundle.
|
||||
|
||||
@@ -38,4 +38,4 @@ Fenced code and GFM tables own horizontal overflow so long content cannot widen
|
||||
|
||||
## Consequences
|
||||
|
||||
Assistant replies render semantic Markdown consistently during streaming and replay, while tool cards, reasoning rows, interactions, user bubbles, and the host protocol remain unchanged. Streaming reparses the current text after each accumulated update; incomplete Markdown can temporarily change structure, but the isolated tail bounds React invalidation and the final event does not switch renderers. Code fences share one chrome and copy path with tool and details surfaces. The initial Web shell includes the Markdown parser, GFM runtime, and shiki allowlist; cite/math/anchor/thinking-small surfaces remain deferred.
|
||||
Assistant replies render semantic Markdown consistently during streaming and replay, while tool cards, reasoning rows, interactions, user bubbles, and the host protocol remain unchanged. Streaming reparses the current text after each accumulated update; incomplete Markdown can temporarily change structure, but the isolated tail bounds React invalidation and the final event does not switch renderers. Code fences share one chrome and copy path with tool and details surfaces. The initial Web shell includes the Markdown parser, GFM runtime, KaTeX, and shiki allowlist; citation, anchor, and thinking-small surfaces remain deferred.
|
||||
|
||||
@@ -14,7 +14,7 @@ Web 对话通过会话事件、历史回放与流式累积保留 assistant Markd
|
||||
|
||||
`MarkdownText` 使用 `react-markdown` 与 `remark-gfm`,从 AST 构建 React 元素。它覆盖 CommonMark 块,以及 GFM 表格、任务列表、删除线与自动链接,且不解析原始 HTML。围栏代码经共享的 `CodeBlock` 路由;该组件用客户端的 shiki 单例(`--shiki-*` token)高亮已注册语法,否则回退为纯等宽文本。轮次流式输出期间,围栏停留在纯文本分支,以免每收到一个分片就对增长中的围栏重新分词。
|
||||
|
||||
视觉间距、表格、链接、引用块、行内代码与代码块外框遵循 deepsuite `@deepseek/md`(`markdown.css` / `code-block.css`),并使用同一套 `--dsw-alias-markdown-*`、`--dsw-font-markdown-*`、`--dsw-alias-border-l*` 与 `--dsw-alias-label-*` token。链接使用 `--dsw-alias-state-business-primary`(deepsuite 的样式表使用 `--dsw-alias-brand-text`,仅在 newDesign 下为蓝色;design-platform 将 brand-text 保持为近黑色,此处不做重新调色)。`CodeBlock` 提供语言横幅与复制控件(`复制` / `复制成功`)。引用胶囊、KaTeX、标题锚点、thinking-small markdown 变体,以及自定义 □/☑ 任务标记均不在范围内,直至存在匹配的产品 DOM;GFM 任务列表继续使用原生复选框。
|
||||
视觉间距、表格、链接、引用块、行内代码与代码块外框遵循 deepsuite `@deepseek/md`(`markdown.css` / `code-block.css`),并使用同一套 `--dsw-alias-markdown-*`、`--dsw-font-markdown-*`、`--dsw-alias-border-l*` 与 `--dsw-alias-label-*` token。链接使用 `--dsw-alias-state-business-primary`(deepsuite 的样式表使用 `--dsw-alias-brand-text`,仅在 newDesign 下为蓝色;design-platform 将 brand-text 保持为近黑色,此处不做重新调色)。`CodeBlock` 提供语言横幅与复制控件(`复制` / `复制成功`)。已完成的文本通过 `remark-math` 和 `rehype-katex` 渲染 KaTeX;`remarkMathCompatibility` 将 `\(...\)`、`\[...\]` 和块级同一行 `$$...$$` 映射为同一套标准数学 AST 节点。这是一层小范围的解析器兼容层,不是正则重写,也不修复格式错误的模型输出。流式输出在完成前保持按字面渲染,避免不完整公式闪现错误。引用胶囊、标题锚点、thinking-small markdown 变体,以及自定义 □/☑ 任务标记仍不在范围内;GFM 任务列表继续使用原生复选框。
|
||||
|
||||
该依赖在 `ui-primitives` 中显式声明;由于这一纯库由 Web shell 预置,解析器与高亮器会成为初始浏览器 bundle 的一部分。
|
||||
|
||||
@@ -38,4 +38,4 @@ assistant 生成的链接目标地址仅限绝对 HTTP、HTTPS 与 mailto URL。
|
||||
|
||||
## 后果
|
||||
|
||||
assistant 回复在流式输出与回放期间都会一致地渲染为语义化 Markdown,而工具卡片、推理行、交互、用户气泡和宿主协议保持不变。每次累积更新后,流式输出都会重新解析当前文本;未完成的 Markdown 可能暂时改变结构,但独立的尾部会限定 React 失效范围,最终事件也不会切换渲染器。代码围栏与工具及详情表层共用同一外框与复制路径。初始 Web shell 包含 Markdown 解析器、GFM 运行时与 shiki 允许列表;cite/math/anchor/thinking-small 表层仍暂缓。
|
||||
assistant 回复在流式输出与回放期间都会一致地渲染为语义化 Markdown,而工具卡片、推理行、交互、用户气泡和宿主协议保持不变。每次累积更新后,流式输出都会重新解析当前文本;未完成的 Markdown 可能暂时改变结构,但独立的尾部会限定 React 失效范围,最终事件也不会切换渲染器。代码围栏与工具及详情表层共用同一外框与复制路径。初始 Web shell 包含 Markdown 解析器、GFM 运行时、KaTeX 与 shiki 允许列表;citation、anchor 和 thinking-small 表层仍暂缓。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-23-web-permission-and-approval.md
|
||||
2026-07-23-web-permission-and-approval.md: a3d25f83aa6ecee5a874cef1706fd4f0b212d638
|
||||
2026-07-23-web-permission-and-approval.zh.md: fdd9d51a7771b571f436ed1a23a76c5f64f4c786
|
||||
2026-07-23-web-permission-and-approval.md: 06e184fe32ac684e3d8b0820d915ca7c372a5760
|
||||
2026-07-23-web-permission-and-approval.zh.md: b74e93006cb95c398eb464a9bd9a59e8c9cc57be
|
||||
|
||||
@@ -14,7 +14,7 @@ The web host composes the same sandboxed product path as the acp-agent compositi
|
||||
|
||||
`createApiProxy` owns the approval pending registry. Its `approval/request` waterfall answerer reads the approval id from the session's just-appended `approval/asked` audit event (an ask with no audit event is a foreign channel and delegates), mints one stable rpcId per question, broadcasts the answerable `approval/requested` frame to every open mux stream, and replays still-pending frames verbatim on each mux open — the refresh-recovery baseline the contract already promised. `respond` routes by the echoed rpcId, validates `ApprovalResponsePayload` with the existing zod schema, cross-checks the payload's audit correlation against the routed entry, resolves the answerer, and broadcasts `approval/resolved`; the ask's abort signal withdraws the question as `cancelled`.
|
||||
|
||||
The permission select rides two new unary RPCs, `session.permissions` and `session.setPermission`, projecting `ctx.permission` into a protocol-owned `PermissionOption` DTO (the ACP bridge precedent: each protocol owns its presentation shape). A permission-less composition serves an empty select and clients hide the control. Idle switches are held last-write-wins in a proxy-side pending map and flushed on `agent/prompt-submit`, because knob events must stay turn-enclosed for durable replay; the shared `hasOpenTurn` fold moved to `dsh-session` and replaced the private copies in `dsh-user-approval`, the ACP bridge, and the proxy.
|
||||
The permission select rides two new unary RPCs, `session.permissions` and `session.setPermission`, projecting `ctx.permission` into a protocol-owned `PermissionOption` DTO (the ACP bridge precedent: each protocol owns its presentation shape). A permission-less composition serves an empty select and clients hide the control. Idle switches are held last-write-wins in a proxy-side pending map and flushed on `agent/pre-step`, because knob events must stay turn-enclosed for durable replay; the shared `hasOpenTurn` fold moved to `dsh-session` and replaced the private copies in `dsh-user-approval`, the ACP bridge, and the proxy.
|
||||
|
||||
Client-side, `Session` gained `permissions` and `setPermission`, and approval answering rides the runtime's `PendingWait` carrier. Per the designer draft, a pending approval takes over the composer: `ApprovalPanel` registers as a selector-routed entry of the conversation-declared `conversation.composer` chain (the ui-question pattern), replacing the InputBar with the justification headline, the paired command, and one-shot refuse/allow buttons; the `PendingApproval` domain face in ui-conversation's contract owns the `ApprovalResponsePayload` wire encoding over the carrier, and the broadcast resolved frame settles the wait and restores the composer. Pending questions take over through ui-question, including the `plan-review` decision shape. The sidebar mirrors every blocked interaction with an amber warning dot that outranks the running ring, including during search: the manager tracks per-session approval and question request identities rather than reading Session instances, classifies only requests satisfying the plan-review composer's binary rendering constraints as plan reviews, and presents the first pending question ahead of concurrent approvals to match composer routing. Pre-instantiation buffering retains each live request identity, replaces replay duplicates, and removes resolved requests so sidebar status never outlives the answerable `PendingWait`; tracking clears per connection generation so reopen replay is authoritative. Sessions never instantiated still light their dot. The composer's bottom-row chip hosts the `PermissionSelect` control fed through the conversation inject face. The connection fixture mirrors the host: its resident approval is answerable once, and its permission select persists per session.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Web 承载层组合与 acp-agent 相同的沙箱化产品路径:`dsh-sandbox-l
|
||||
|
||||
`createApiProxy` 拥有审批 pending 注册表。它的 `approval/request` waterfall(瀑布式事件)应答者从会话刚追加的 `approval/asked` 审计事件中读取审批 id(没有审计事件的 ask 属于外部通道,予以委托),为每个问题 mint 一个稳定的 rpcId,向每个打开的 mux 流广播可应答的 `approval/requested` 帧,并在每次 mux 打开时原样重放仍处于 pending 的帧——这正是契约早已承诺的刷新恢复基线。`respond` 按回显的 rpcId 路由,用既有的 zod schema 校验 `ApprovalResponsePayload`,将载荷的审计关联与所路由的条目交叉核对,解析应答者,并广播 `approval/resolved`;ask 的中断信号会以 `cancelled` 撤回该问题。
|
||||
|
||||
权限选择依托两个新的一元 RPC,`session.permissions` 与 `session.setPermission`,把 `ctx.permission` 投影为一个由协议拥有的 `PermissionOption` DTO(沿用 ACP bridge 的先例:每个协议拥有自己的呈现形状)。无权限的组合提供空的选择项,client 隐藏该控件。空闲期的切换以后写胜出(last-write-wins)的方式保存在 proxy 侧的 pending map 中,并在 `agent/prompt-submit` 时冲刷,因为旋钮事件必须保持轮次内闭合以支持持久回放;共享的 `hasOpenTurn` 折叠迁入 `dsh-session`,取代了 `dsh-user-approval`、ACP bridge 与 proxy 中各自的私有副本。
|
||||
权限选择依托两个新的一元 RPC,`session.permissions` 与 `session.setPermission`,把 `ctx.permission` 投影为一个由协议拥有的 `PermissionOption` DTO(沿用 ACP bridge 的先例:每个协议拥有自己的呈现形状)。无权限的组合提供空的选择项,client 隐藏该控件。空闲期的切换以后写胜出(last-write-wins)的方式保存在 proxy 侧的 pending map 中,并在 `agent/pre-step` 时冲刷,因为旋钮事件必须保持轮次内闭合以支持持久回放;共享的 `hasOpenTurn` 折叠迁入 `dsh-session`,取代了 `dsh-user-approval`、ACP bridge 与 proxy 中各自的私有副本。
|
||||
|
||||
在 client 侧,`Session` 新增了 `permissions` 与 `setPermission`,审批应答则依托运行时的 `PendingWait` 载体。按照设计师草稿,处于 pending 的审批会接管 composer:`ApprovalPanel` 注册为由会话声明的 `conversation.composer` 链中一个按选择器路由的条目(即 ui-question 模式),以理由标题、配对的命令与一次性的拒绝/允许按钮取代 InputBar;ui-conversation 契约中的 `PendingApproval` 领域面拥有 `ApprovalResponsePayload` 在该载体上的协议编码(wire encoding),广播的 resolved 帧使该等待落定并恢复 composer。pending 问题通过 ui-question 接管 composer,包括 `plan-review` 决策形状。侧边栏用一枚优先级高于运行中圆环的琥珀色警示圆点,同步呈现每个被阻塞的交互,搜索期间也不例外:manager 跟踪每个会话的审批与问题请求标识,而非读取 Session 实例;它只把满足 plan-review composer 二元呈现约束的请求分类为计划审查,并在问题与审批并发时优先呈现第一个 pending 问题,以匹配 composer 路由。实例化前的缓冲会保留每个仍有效的请求标识,替换回放产生的重复项,并移除已解决的请求,因此侧边栏状态绝不会比可应答的 `PendingWait` 存续得更久;跟踪以连接代次为单位清除,以保证重开后的回放才是权威依据。从未实例化过的会话仍会点亮该圆点。composer 底行的 chip 经会话注入面挂载 `PermissionSelect` 控件。连接 fixture(测试前置数据)与 host 保持一致:它的常驻审批可应答一次,其权限选择项按会话持久保存。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-24-web-session-model-selector.md
|
||||
2026-07-24-web-session-model-selector.md: 003c0b5ac1c6963701e1d93e4c3ff8615fc45dd5
|
||||
2026-07-24-web-session-model-selector.zh.md: a8e74fb1539348535f89f49d7c6d56b2bab79278
|
||||
2026-07-24-web-session-model-selector.md: 05e923fb3b5df72485ffb8d6ff5aa0104b19a8a0
|
||||
2026-07-24-web-session-model-selector.zh.md: bdfa2f856a799cc86600650829a2df62a7dfdc18
|
||||
|
||||
@@ -12,11 +12,11 @@ The Web conversation displayed and sent through the Host's fixed provider/model
|
||||
|
||||
The Web Host reuses `installAgentLlmTarget` for every created or resumed agent. The provider/model/reasoning target starts from the latest `request/header` when the session has used a model, otherwise from the Host default route. `session.selectModel` changes the session-local mutable target, and prompt assembly captures it with request routing; a switch during a running step therefore applies to the next assembled step. The next consumed target persists through the existing full `request/header` snapshot, while a choice that has not reached a request remains process-local.
|
||||
|
||||
The session RPC domain exposes a `session.models` directory and `session.selectModel`. The directory is built dynamically from the LLM registry and grouped by provider; each listed model's exact metadata adds adapter-owned reasoning effort ids, names, descriptions, and optional default. Provider catalogs and exact metadata load concurrently by provider and fail independently, so successful groups remain usable alongside retryable failure records. Catalog membership stays advisory: the current model is inserted as an unlisted row when its registered provider omits it, while exact resolution decides whether a route and explicit effort are available. Selection uses `resolveCallConfig` to reject unsupported effort ids and materialize an adapter-configured default before updating the target.
|
||||
The session RPC domain exposes a `session.models` directory and `session.selectModel`. The directory is built dynamically from the LLM registry and grouped by provider; each listed model's exact metadata adds adapter-owned reasoning effort ids, names, descriptions, and optional default. Provider catalogs and exact metadata load concurrently by provider and fail independently, so successful groups remain usable alongside retryable failure records. Catalog membership stays advisory: `session.models.current` is returned independently and can remain routable when absent from every group, but the Host does not synthesize an unlisted row after its provider stops advertising it. The two surfaces answer that state differently on purpose: the TUI still renders the unlisted current model as its own row and marks it current, while Web shows the unset trigger label and asks for a replacement. Web is the surface where a catalog is edited, so a target the user just deleted should read as a decision to make rather than a selection to keep; the TUI, which only picks from what exists, has no such edit to reconcile. The cost is real and accepted — a Web composer showing the unset label can still send to the routed target — and the divergence is deliberate, not a missed migration. Exact resolution decides whether a route and explicit effort are available. Selection uses `resolveCallConfig` to reject unsupported effort ids and materialize an adapter-configured default before updating the target.
|
||||
|
||||
The browser `ModelService` owns one `ModelDirectory` per live session. Its snapshot contains the current complete target, grouped catalog, provider failures, operation error, and `idle`/`loading`/`ready`/`selecting`/`error` state. Mounting primes the trigger label and each menu open refreshes the directory. Directory and selection calls share an operation generation so older responses cannot replace a newer result; connection reset discards the process-local projection before restoring the Host target. Failures retain the previous current target and usable groups.
|
||||
|
||||
`@deepseek-ai/dsh-client-ui-conversation` declares the session-scoped single slot `conversation.input.model` as a child of its composer-bar entry. InputBar renders the seat in its trailing controls immediately before the pending indicator and primary button; the seat receives the bar's `locked` owner prop and session scope. `@deepseek-ai/dsh-client-ui-model` occupies that seat and also contributes `/model` over the same directory. Its compact trigger displays the catalog model name and effective reasoning label, falling back to ids when metadata is absent. The upward menu first offers Model and, when the current exact model supports it, Effort; Model drills into provider groups, while Effort drills into the adapter-ordered levels. The provider-default row appears only when the adapter does not configure a model default.
|
||||
`@deepseek-ai/dsh-client-ui-conversation` declares the session-scoped single slot `conversation.input.model` as a child of its composer-bar entry. InputBar renders the seat in its trailing controls immediately before the pending indicator and primary button; the seat receives the bar's `locked` owner prop and session scope. `@deepseek-ai/dsh-client-ui-model` occupies that seat and also contributes `/model` over the same directory. Its compact trigger displays the exact catalog model name and effective reasoning label. When the current target is absent from the groups, the trigger instead displays `Select model`, the model list marks no row active, and the Effort row stays absent; choosing a listed model replaces the complete target through the existing selection path. The upward menu otherwise first offers Model and Effort; Model drills into provider groups, while Effort drills into the adapter-ordered levels. The provider-default row appears only when the adapter does not configure a model default.
|
||||
|
||||
The production browser roster is assembled from `apps/cli/config/base.cordis.yml` plus `apps/cli/config/web.cordis.yml`; the model feature is one `dshClient` row rather than a package hardcoded in Web boot code. Its package manifest orders it after the runtime and command feature, while Cordis service injection waits for the conversation slot before registering the composer occupant.
|
||||
|
||||
@@ -40,4 +40,4 @@ Any Host-backed Web conversation, including a blank session, can switch among dy
|
||||
|
||||
## Testing
|
||||
|
||||
Host tests pin grouped discovery, catalog and exact-metadata failure isolation, logged effort restoration, unlisted current targets, unsupported effort rejection, default materialization, and next-assembly switching. Client tests pin the shared directory, reconnect restoration, and complete-target submission. Component tests pin dynamic effort labels, descriptions, provider-default exposure, and effort submission. The keyless built-app fixture loads the production model plugin, selects OpenAI's GPT-5 and its Max effort, sends a turn, and verifies that the next generated response reports both ids.
|
||||
Host tests pin grouped discovery, catalog and exact-metadata failure isolation, logged effort restoration without stale-row injection, advisory unlisted selection, unsupported effort rejection, default materialization, and next-assembly switching. Client tests pin the shared directory, reconnect restoration, and complete-target submission. Component tests pin dynamic effort labels, descriptions, provider-default exposure, effort submission, and the `Select model` fallback for a removed row. The keyless built-app fixture loads the production model plugin, selects OpenAI's GPT-5 and its Max effort, sends a turn, and verifies that the next generated response reports both ids; the DeepSeek configuration fixture removes the active catalog row and pins the fallback before choosing a replacement.
|
||||
|
||||
@@ -12,11 +12,11 @@ Web 对话原本通过 Host 固定的提供方与模型路由显示并发送消
|
||||
|
||||
Web Host 为每个新建或恢复的 agent(智能体)复用 `installAgentLlmTarget`。如果会话已经使用过模型,提供方/模型/推理(reasoning)目标从最新的 `request/header` 开始;否则采用 Host 默认路由。`session.selectModel` 会更改会话级可变目标,提示词组装则将该目标与请求路由一并捕获,因此运行中步骤发生的切换会应用于下一个组装步骤。下一个实际采用的目标通过现有的完整 `request/header` 快照持久化;尚未进入请求的选择则仅保存在当前进程中。
|
||||
|
||||
会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM(大语言模型)注册表动态构建,并按提供方分组;每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载,且彼此独立失败,因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考:如果当前模型的已注册提供方没有列出该模型,系统会将其作为未列出行插入;精确解析则决定路由与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID,并在更新目标前具体化适配器配置的默认值。
|
||||
会话 RPC 领域公开 `session.models` 模型目录与 `session.selectModel`。该目录从 LLM(大语言模型)注册表动态构建,并按提供方分组;每个已列出模型的精确元数据还会加入由适配器持有的推理强度 ID、名称、说明和可选默认值。各提供方的目录与精确元数据会按提供方并发加载,且彼此独立失败,因此成功加载的分组仍可与可重试的失败记录一同使用。模型是否位于目录仅供参考:`session.models.current` 独立返回,即使不在任何分组中也仍然可以路由,但提供方停止公布该模型后,Host 不会合成未列出行。两个前门有意对这一状态给出不同回答:TUI 仍把未列出的当前模型渲染为独立一行并标记为当前,Web 则显示未设置状态的触发器标签并要求选择替代模型。Web 正是编辑目录所在的前门,因此用户刚刚删除的目标应当读作一个有待作出的决定,而不是一项可以保留的选择;TUI 只在已存在的模型中挑选,没有这类编辑需要调和。这一代价真实存在且已被接受——显示未设置标签的 Web composer 仍会发送到实际路由的目标——这一分歧是有意为之,而不是一处遗漏的迁移。精确解析决定路由与显式推理强度是否可用。选择操作通过 `resolveCallConfig` 拒绝不支持的推理强度 ID,并在更新目标前具体化适配器配置的默认值。
|
||||
|
||||
浏览器中的 `ModelService` 为每个实时会话持有一个 `ModelDirectory`。其快照包含当前完整目标、分组目录、提供方失败记录、操作错误,以及 `idle`、`loading`、`ready`、`selecting`、`error` 状态。挂载时会预先填充触发器标签,此后每次打开菜单都会刷新目录。目录与选择调用共用操作代次,防止较早响应覆盖较新结果;连接重置会先丢弃当前进程中的投影,再恢复 Host 目标。失败时保留先前的当前目标和可用分组。
|
||||
|
||||
`@deepseek-ai/dsh-client-ui-conversation` 将会话作用域的单实例 slot `conversation.input.model` 声明为其输入栏 entry 的子 slot。InputBar 在尾部控件区将该 seat 渲染于 pending 指示器与主按钮之前;该 seat 接收输入栏的 `locked` owner prop 与会话作用域。`@deepseek-ai/dsh-client-ui-model` 占用该 seat,并在同一目录上提供 `/model`。其紧凑型触发器显示目录中的模型名称与生效的推理强度标签;元数据缺失时则回退到相应 ID。向上展开的菜单首先提供 Model,并在当前精确模型支持时提供 Effort;Model 可深入提供方分组,Effort 可深入适配器排序的级别。仅当适配器没有配置模型默认值时,才显示提供方默认值行。
|
||||
`@deepseek-ai/dsh-client-ui-conversation` 将会话作用域的单实例 slot `conversation.input.model` 声明为其输入栏 entry 的子 slot。InputBar 在尾部控件区将该 seat 渲染于 pending 指示器与主按钮之前;该 seat 接收输入栏的 `locked` owner prop 与会话作用域。`@deepseek-ai/dsh-client-ui-model` 占用该 seat,并在同一目录上提供 `/model`。其紧凑型触发器显示目录中精确模型的名称与生效的推理强度标签。当前目标不在分组中时,触发器改为显示 `Select model`,模型列表不标记任何活动行,Effort 行也保持隐藏;选择一个已列出的模型,会通过现有选择路径替换完整目标。除此情形外,向上展开的菜单会首先提供 Model 与 Effort;Model 可深入提供方分组,Effort 可深入适配器排序的级别。仅当适配器没有配置模型默认值时,才显示提供方默认值行。
|
||||
|
||||
生产环境的浏览器名册由 `apps/cli/config/base.cordis.yml` 与 `apps/cli/config/web.cordis.yml` 共同组装;模型功能对应其中一行 `dshClient` 配置项,而不是 Web boot 代码中硬编码的包。其包 manifest(元数据清单)将加载顺序置于运行时与命令功能之后;Cordis 服务注入则等待 conversation slot 可用,再注册 composer 占用方。
|
||||
|
||||
@@ -40,4 +40,4 @@ Web Host 为每个新建或恢复的 agent(智能体)复用 `installAgentLlm
|
||||
|
||||
## 测试
|
||||
|
||||
Host 测试固定分组发现、目录与精确元数据失败隔离、已记录推理强度恢复、当前未列出目标、不支持的推理强度拒绝、默认值具体化,以及切换仅影响下一次组装。客户端测试固定共享目录、重连恢复与完整目标提交。组件测试固定动态推理强度标签、说明、提供方默认值展示与推理强度提交。无密钥 built-app fixture(测试前置数据)加载生产模型插件,选择 OpenAI 的 GPT-5 及其 Max 推理强度,发起一个轮次,并验证下一条生成的响应会报告两个 ID。
|
||||
Host 测试固定分组发现、目录与精确元数据失败隔离、已记录推理强度恢复且不注入陈旧行、建议性的未列出模型选择、不支持的推理强度拒绝、默认值具体化,以及切换仅影响下一次组装。客户端测试固定共享目录、重连恢复与完整目标提交。组件测试固定动态推理强度标签、说明、提供方默认值展示、推理强度提交,以及已删除模型行的 `Select model` 回退。无密钥 built-app fixture(测试前置数据)加载生产模型插件,选择 OpenAI 的 GPT-5 及其 Max 推理强度,发起一个轮次,并验证下一条生成的响应会报告两个 ID;DeepSeek 配置 fixture 会删除活动目录行,在选择替代模型之前固定该回退。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-skill-catalog-hot-refresh.md
|
||||
2026-07-27-skill-catalog-hot-refresh.md: 8e70fb10e7da4292325b72f3a0392bef2271738c
|
||||
2026-07-27-skill-catalog-hot-refresh.zh.md: 9a6b6d944baa4a9cc4dddb5158fdcbac2b05f5db
|
||||
2026-07-27-skill-catalog-hot-refresh.md: 43c2da75aeb0031dffb5f47d7e76805baa2920e5
|
||||
2026-07-27-skill-catalog-hot-refresh.zh.md: 04d39dd767e5438c57d3eb29523a8ff26592465c
|
||||
|
||||
@@ -18,7 +18,7 @@ The skill capability separates catalog membership from instruction-body loading.
|
||||
|
||||
A missing root is followed from its nearest existing ancestor one absent segment at a time with `fs.watchFile`, then handed to Chokidar once the real root exists. Before scanning, each discovery re-probes the retained root/ancestor mode. That independent probe re-establishes ancestor observation after deletion even when child removals invalidate and publish an authoritative empty catalog before, or without, a root `unlinkDir` event. Chokidar configuration exposes native-versus-polling mode, write stability, polling interval, symlink following, and project watcher capacity. First-party `write` and `edit` tool observations synchronously invalidate a relevant provider, so the next model step sees its own mutation without waiting for host delivery. Watch startup/runtime failures are logged and retried; discovery still returns readable candidates for direct loads but reports an incomplete observation. Teardown closes watchers and ignores late callbacks.
|
||||
|
||||
`@deepseek-ai/dsh-tool-skill` injects the first non-empty complete catalog as a durable sourced `user/message` on the first complete `agent/step` that observes one. At every `agent/step` it applies exact `skill` tool visibility, hashes the exact rendered text between the `<available_skills>` tags, and scans the read-only session events backwards without copying them for the newest recognizable visible catalog from this plugin. A changed digest appends a durable, complete replacement through `agent.inject()`, including an explicit empty catalog when all skills disappear. If no catalog remains visible but a recognizable one exists in historical events, compaction hid it and the next complete observation re-establishes the current catalog, including an empty tombstone. A current empty catalog with no historical publication emits nothing, while an incomplete snapshot preserves the last-good model view. The backward scan normally stops at the newest visible catalog; when compaction hides every catalog it pays an O(session-events) scan to recover that fact.
|
||||
`@deepseek-ai/dsh-tool-skill` contributes the first non-empty complete catalog as a sourced `UserMessage` to the first entering `agent/pre-step` that observes one. At every pre-step it applies exact `skill` tool visibility, hashes the exact rendered text between the `<available_skills>` tags, and scans the read-only session events backwards without copying them for the newest recognizable visible catalog from this plugin. A changed digest contributes a complete replacement to an enter decision, including an explicit empty catalog when all skills disappear; rejection or listener failure records nothing. If no catalog remains visible but a recognizable one exists in historical events, compaction hid it and the next complete entering observation re-establishes the current catalog, including an empty tombstone. A current empty catalog with no historical publication emits nothing, while an incomplete snapshot preserves the last-good model view. The backward scan normally stops at the newest visible catalog; when compaction hides every catalog it pays an O(session-events) scan to recover that fact.
|
||||
|
||||
The TUI consumes the same invalidation as presentation state, not session history. `skills/change` carries no diff; the TUI refetches `snapshot()` for the active session cwd, applies only the latest complete result, and retains the previous commands across incomplete observations. A complete empty result clears stale completions. Because pi-tui closes autocomplete when its provider is replaced, a catalog that arrives while the user is typing a slash-command name also triggers a suggestion-only re-query of the current draft.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ skill 服务将目录成员关系与指令正文加载分离。`ctx.skills.snaps
|
||||
|
||||
系统从缺失根目录最近的现有祖先开始,使用 `fs.watchFile` 每次跟进一层缺失路径片段;真实根目录出现后,再交给 Chokidar。每次发现操作都会在扫描前重新探测所保留的根目录/祖先模式。即使子项移除在根目录 `unlinkDir` 事件之前就触发失效并发布权威空目录,或者该事件根本没有到达,这项独立探测也会在删除后重新建立祖先观察。Chokidar 配置公开原生事件或轮询模式、写入稳定性、轮询间隔、符号链接跟随选项和项目 watcher 容量。第一方 `write` 和 `edit` 工具观察会同步使相关提供方失效,因此下一个模型步骤无需等待宿主事件投递,就能看到自身改动。watcher 启动或运行失败会被记录并触发重试;发现过程仍会返回可读候选项供直接加载,但会报告不完整观测。资源销毁会关闭 watcher,并忽略延迟回调。
|
||||
|
||||
`@deepseek-ai/dsh-tool-skill` 在 `agent/step` 首次观察到非空完整目录时,将该目录注入为一条持久且带来源的 `user/message`。每次 `agent/step`,它都会应用 `skill` 工具的精确可见性,对 `<available_skills>` 标签之间精确渲染的文本计算哈希,并从后向前扫描只读会话事件且不复制,以查找该插件发布的最新一条可识别且仍可见的目录。digest 变化时,插件通过 `agent.inject()` 追加一份持久的完整替换目录;所有 skill 消失时,也会追加显式空目录。如果没有目录仍然可见,但历史事件中存在可识别目录,则说明压缩(compaction)已将其遮蔽,下一次完整观察会重新建立当前目录,包括空 tombstone。如果当前目录为空且历史上从未发布目录,则不发送任何内容;不完整快照则保留最后一次完整的模型视图。反向扫描通常在最新且仍可见的目录处停止;当压缩遮蔽所有目录时,它会以一次 O(session-events) 扫描的成本确认这一事实。
|
||||
`@deepseek-ai/dsh-tool-skill` 在首次观察到非空完整目录且返回 enter 的 `agent/pre-step` 中,贡献一条带来源的 `UserMessage`。每次 pre-step,它都会应用 `skill` 工具的精确可见性,对 `<available_skills>` 标签之间精确渲染的文本计算哈希,并从后向前扫描只读会话事件且不复制,以查找该插件发布的最新一条可识别且仍可见的目录。digest 变化时,插件会向 enter 决策贡献完整替换目录;所有 skill 消失时也包含显式空目录,reject 或监听器失败则不记录任何内容。如果没有目录仍然可见,但历史事件中存在可识别目录,则说明压缩(compaction)已将其遮蔽,下一次完整且进入步骤的观察会重新建立当前目录,包括空 tombstone。如果当前目录为空且历史上从未发布目录,则不发送任何内容;不完整快照则保留最后一次完整的模型视图。反向扫描通常在最新且仍可见的目录处停止;当压缩遮蔽所有目录时,它会以一次 O(session-events) 扫描的成本确认这一事实。
|
||||
|
||||
TUI 将同一失效通知作为界面状态而非会话历史来消费。`skills/change` 不携带 diff;TUI 会为活动会话的 cwd 重新获取 `snapshot()`,仅应用最新的完整结果,并在观测不完整时保留先前命令。完整的空结果会清除陈旧补全项。pi-tui 在其提供方被替换时会关闭自动补全,因此如果目录在用户输入斜杠命令名称期间到达,还会触发一次仅用于更新建议的当前草稿重查。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-tmux-location-context.md
|
||||
2026-07-27-tmux-location-context.md: 9f5e931e56565b0bf3ee219567c6913c40ef1d97
|
||||
2026-07-27-tmux-location-context.zh.md: 154a48e25a5a320187c21f27f542a2b584abcd51
|
||||
2026-07-27-tmux-location-context.md: dc9e559d1ed5cbb419901303a81e69cb0d0ba828
|
||||
2026-07-27-tmux-location-context.zh.md: 9c8a56f12406ff17ce9c25538dcfdf8e7aa06f91
|
||||
|
||||
@@ -14,7 +14,7 @@ tmux exposes this without a daemon: `$TMUX_PANE` names the process's pane, and `
|
||||
|
||||
`@deepseek-ai/dsh-tmux-context` is an opt-in function plugin in `packages/context/tmux-context/`, alongside the other bounded request-context enrichments that define neither a tool nor a service. The shipped TUI mounts it because terminal-multiplexer context is specific to that surface; `dsh-agent-spine-demo` and the Web/headless surfaces stay silent.
|
||||
|
||||
**Pull on the first step of each turn, not a tmux push.** The plugin prepends an `agent/step` listener and acts only when `step === 1`. A pull model needs no background process, no hook installation in the user's tmux, and no teardown; it re-reads current state each turn so a moved, renamed, or re-laid-out pane is picked up naturally. Gating on the first step makes the reading per-turn: a location is stable within a turn, and re-querying every step would add cost without new information. A pane moved mid-turn is reflected on the next turn, which is the accepted tradeoff for the simpler design.
|
||||
**Pull on the first step of each turn, not a tmux push.** The plugin prepends an `agent/pre-step` listener and acts only when `step === 1`. A pull model needs no background process, no hook installation in the user's tmux, and no teardown; it re-reads current state each turn so a moved, renamed, or re-laid-out pane is picked up naturally. Gating on the first step makes the reading per-turn: a location is stable within a turn, and re-querying every step would add cost without new information. A pane moved mid-turn is reflected on the next turn, which is the accepted tradeoff for the simpler design.
|
||||
|
||||
**Read through the `ctx.bash` seam, never raw `child_process`.** The listener runs the tmux/`ps` read commands through `ctx.bash`, so the deployment's sandbox and policy apply and the plugin owns no subprocess code. Absent `ctx.bash`, absent tmux env, a wrong field count, or an empty pane id each make the attempt a no-op, matching how `workspace-context` no-ops without an `fs` provider.
|
||||
|
||||
@@ -36,7 +36,7 @@ The turn preamble is the volatile first line; the two-line state block below it
|
||||
|
||||
### Durability and request reconstruction
|
||||
|
||||
Each reading is a normal surface node until compaction shadows it; the plugin contributes nothing to system-prompt assembly and `request/header` carries no tmux-context text. The reading records a preparation attempt, not a committed step: because the prepended listener runs first, its append may remain when a later `agent/step` listener cancels or fails the attempt, and the append-only log performs no rollback.
|
||||
Each reading is a normal surface node until compaction shadows it; the plugin contributes nothing to system-prompt assembly and `request/header` carries no tmux-context text. The reading records a preparation attempt, not a committed step: because the prepended listener runs first, its append may remain when a later `agent/pre-step` listener cancels or fails the attempt, and the append-only log performs no rollback.
|
||||
|
||||
The published `./invariant` companion registers no runtime check: a reading is a per-turn snapshot of external tmux state, so the session holds no cross-event relation to validate, and scheduling and format stay pinned by the package's pipeline tests.
|
||||
|
||||
@@ -46,7 +46,7 @@ An agent booted inside tmux now receives its own session/window/pane location an
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests pin: first-step injection and source/surface metadata; the `$TMUX_PANE`-keyed command including its `#{pane_tty}`-vs-`ps -o tty=` guard; step-gating; change suppression across turns and re-injection on a moved pane; positive-interval suppression and threshold; every no-op path (no bash, nonzero exit, wrong field count, empty pane id, aborted signal, and a contained executor rejection from either `resolve()` or `run()` that warns instead of failing the turn); prepended ordering before ordinary `agent/step` listeners; resilience to a corrupt prior reading (non-text block, single-line text); and config rejection of negative and non-integer intervals. Per-file coverage is 100%.
|
||||
Unit tests pin: first-step injection and source/surface metadata; the `$TMUX_PANE`-keyed command including its `#{pane_tty}`-vs-`ps -o tty=` guard; step-gating; change suppression across turns and re-injection on a moved pane; positive-interval suppression and threshold; every no-op path (no bash, nonzero exit, wrong field count, empty pane id, aborted signal, and a contained executor rejection from either `resolve()` or `run()` that warns instead of failing the turn); prepended ordering before ordinary `agent/pre-step` listeners; resilience to a corrupt prior reading (non-text block, single-line text); and config rejection of negative and non-integer intervals. Per-file coverage is 100%.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ tmux 无需守护进程即可暴露这些信息:`$TMUX_PANE` 标识进程所
|
||||
|
||||
`@deepseek-ai/dsh-tmux-context` 是位于 `packages/context/tmux-context/` 的可选启用型函数插件,与其他既不定义工具也不定义服务的有界请求上下文增强并列。已交付的 TUI 会挂载它,因为终端复用器上下文是该界面特有的;`dsh-agent-spine-demo` 与 Web/无头界面保持沉默。
|
||||
|
||||
**在每轮的第一个 step 拉取,而非 tmux 推送。** 插件前置注册一个 `agent/step` 监听器,仅在 `step === 1` 时动作。拉取模型无需后台进程、无需在用户的 tmux 中安装 hook、也无需清理;它每轮重新读取当前状态,因此被移动、改名或重新布局的 pane 都会被自然感知。以第一个 step 为门槛使读数按轮次生成:位置在一轮内是稳定的,逐步骤重复查询只会增加成本而不带来新信息。轮次中途移动的 pane 会在下一轮反映,这是换取更简单设计所接受的取舍。
|
||||
**在每轮的第一个 step 拉取,而非 tmux 推送。** 插件前置注册一个 `agent/pre-step` 监听器,仅在 `step === 1` 时动作。拉取模型无需后台进程、无需在用户的 tmux 中安装 hook、也无需清理;它每轮重新读取当前状态,因此被移动、改名或重新布局的 pane 都会被自然感知。以第一个 step 为门槛使读数按轮次生成:位置在一轮内是稳定的,逐步骤重复查询只会增加成本而不带来新信息。轮次中途移动的 pane 会在下一轮反映,这是换取更简单设计所接受的取舍。
|
||||
|
||||
**通过 `ctx.bash` seam 读取,绝不用裸 `child_process`。** 监听器通过 `ctx.bash` 运行 tmux/`ps` 只读命令,从而应用部署方的沙箱与策略,插件不拥有任何子进程代码。`ctx.bash` 缺失、tmux 环境缺失、字段数不符或 pane id 为空,都会使本次尝试成为空操作,与 `workspace-context` 在无 `fs` provider 时的空操作一致。
|
||||
|
||||
@@ -36,7 +36,7 @@ window active=<0|1>, pane active=<0|1>, layout <window-layout>
|
||||
|
||||
### 持久性与请求重建
|
||||
|
||||
每条读数在被压缩遮蔽前都是普通表层节点;插件对系统提示装配毫无贡献,`request/header` 也不携带任何 tmux-context 文本。读数记录的是一次准备尝试,而非已提交的 step:由于前置监听器最先运行,当后续 `agent/step` 监听器取消或失败时其追加可能仍会保留,只追加的日志不做回滚。
|
||||
每条读数在被压缩遮蔽前都是普通表层节点;插件对系统提示装配毫无贡献,`request/header` 也不携带任何 tmux-context 文本。读数记录的是一次准备尝试,而非已提交的 step:由于前置监听器最先运行,当后续 `agent/pre-step` 监听器取消或失败时其追加可能仍会保留,只追加的日志不做回滚。
|
||||
|
||||
发布的 `./invariant` 伴生插件不注册任何运行时检查:读数是外部 tmux 状态的按轮快照,会话中不存在需要校验的跨事件关系,调度与格式由本包的管线测试固定。
|
||||
|
||||
@@ -46,7 +46,7 @@ window active=<0|1>, pane active=<0|1>, layout <window-layout>
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定了:首个 step 的注入及来源/表层元数据;以 `$TMUX_PANE` 为键的命令(含其 `#{pane_tty}` 与 `ps -o tty=` 的比对守卫);step 门槛;跨轮次的变化抑制与 pane 移动时的重新注入;正间隔抑制与阈值;每条空操作路径(无 bash、非零退出、字段数不符、pane id 为空、信号已取消,以及 `resolve()` 或 `run()` 抛出的执行器拒绝被兜住并记录警告而非使该轮失败);前置排序先于普通 `agent/step` 监听器;对损坏的历史读数(非文本块、单行文本)的容错;以及配置对负值与非整数间隔的拒绝。逐文件覆盖率为 100%。
|
||||
单元测试固定了:首个 step 的注入及来源/表层元数据;以 `$TMUX_PANE` 为键的命令(含其 `#{pane_tty}` 与 `ps -o tty=` 的比对守卫);step 门槛;跨轮次的变化抑制与 pane 移动时的重新注入;正间隔抑制与阈值;每条空操作路径(无 bash、非零退出、字段数不符、pane id 为空、信号已取消,以及 `resolve()` 或 `run()` 抛出的执行器拒绝被兜住并记录警告而非使该轮失败);前置排序先于普通 `agent/pre-step` 监听器;对损坏的历史读数(非文本块、单行文本)的容错;以及配置对负值与非整数间隔的拒绝。逐文件覆盖率为 100%。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-trajectory-inspection-ledger.md
|
||||
2026-07-27-trajectory-inspection-ledger.md: cdeaa30ea64f47b0e0110baf566f747a4591a384
|
||||
2026-07-27-trajectory-inspection-ledger.zh.md: df2a3d266161a7c1c4444863971f3d177533af8c
|
||||
2026-07-27-trajectory-inspection-ledger.md: e3fc22234c1df449c99eac90af27de7da0b6f202
|
||||
2026-07-27-trajectory-inspection-ledger.zh.md: abf8fd3bcaeabba6061bd415acbe8ddefef7ac79
|
||||
|
||||
@@ -16,13 +16,16 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
|
||||
- Event kind and content form the two stable columns. Role tags align toward the content, nested subtools receive a small indentation, and CSS truncation preserves the available preview width. Token usage and duration stay in the inspector.
|
||||
- Product prose uses the existing sans stack. Turn ids, token counts, durations, tool calls, raw payloads, and other machine data use the existing code stack.
|
||||
- Existing theme tokens own both light and dark rendering. Neutral borders and surfaces form the structure; distinct low-emphasis role hues support scanning without carrying success or failure meaning, while business blue identifies selection, links, and focus.
|
||||
- The client runtime exposes a read-only history source independent from Session and SessionManager. Each activated source owns its raw entries, paging, live gap repair, and reconnect rebuild; the ordinary conversation snapshot remains the folded Chat projection. Trajectory subscribes to that source, exhausts its paging only while mounted, and lazily derives its event order, context lineage, schema index, and Requests instead of imposing those structures on every conversation consumer.
|
||||
- Ordinary generation and compaction calls form one chronological Request projection, distinguished by purpose rather than separate collections. Effective prompt state and its change ride the Request that introduced them; compaction and prompt changes are not independent inspection entities. Complete history makes global Request numbering and cumulative usage session-wide rather than tail-window-relative.
|
||||
- The client runtime exposes a read-only history source independent from Session and SessionManager. Each activated source owns its raw entries, paging, live gap repair, and reconnect rebuild; the ordinary conversation snapshot remains the folded Chat projection. Trajectory opens the source's tail while mounted and requests one older page when the user reaches the loaded range's top, then lazily derives event order, context lineage, schema index, and Requests instead of imposing those structures on every conversation consumer.
|
||||
- Ordinary generation and compaction calls form one chronological Request projection, distinguished by purpose rather than separate collections. Effective prompt state and its change ride the Request that introduced them; compaction and prompt changes are not independent inspection entities. Request numbering and cumulative usage cover the loaded history window and expand as older pages arrive.
|
||||
- Call schemas come from the active recorded Request header. Keyless snapshot fixtures deliberately replace that catalog with the non-array `{{tools}}` token, which the durable inspection boundary treats as unavailable instead of attempting to project or fabricate schemas.
|
||||
- Selecting a record or Request opens an inspector inside Trajectory. Tabs and Summary sections follow the selected entity: Markdown messages expose rendered, source, provenance, and hierarchy views; tools add JSON payload/result and schema views; Requests add options, usage, timing, and result navigation. Images render as media rather than serialized data.
|
||||
- Selecting a record or Request opens an inspector inside Trajectory. Tabs and Summary sections follow the selected entity: Markdown messages expose rendered, source, provenance, and hierarchy views; tools add JSON payload/result and schema views; Requests add options, usage, timing, and result navigation. Scrollable Summary regions keep their scrollbar thumbs transparent until hover or `focus-within`, while retaining the scrollbar reservation and scroll behavior. Images render as media rather than serialized data.
|
||||
- Turn folding removes all rows after its first record and replaces them with a compact step/tool-call count; Assistant folding applies the same interaction to its tool-call descendants. Global controls fold or expand both levels.
|
||||
- The separate Waterfall tab is removed. A fixed Overview above the ledger projects every record with known `startedAt` onto three semantic timing lanes using its own duration. Finalized Assistant spans divide the recorded interval at the first non-empty token delta, so distinct TTFT and decoding colors retain their actual ratio; incomplete timing falls back to one Assistant color. Hovering for 500 ms exposes exact start/end, total duration, TTFT, and decoding time without relying on the browser's native tooltip delay. Dragging left or right commits an inclusive interval filter: any record whose active interval overlaps either boundary remains visible, records without known timing leave the focused ledger, and clearing the selection restores the full branch. Wheel gestures zoom the time domain. A right-button click clears the interval selection; dragging instead pans an already zoomed viewport without mutating it. The Overview keeps the full time domain while focused so the selection can be resized or cleared without losing orientation.
|
||||
- Live history updates retain the ledger's bottom position only while the user is already following its tail. Scrolling upward clears that follow state, so streamed chunks and newly appended records do not interrupt inspection of earlier rows.
|
||||
- A long ledger initially positions the loaded tail at the bottom and mounts only the viewport's row window plus bounded overscan. Request-only separators join the next measurable virtual item, with a terminal separator retaining its own fixed clearance, so the virtualizer never owns a zero-height item. Semantic DOM-safe row keys and ARIA indexes expose identity independently from mount position. A tail with known older history virtualizes immediately even when its loaded projection is below the ordinary row threshold. Stable-key virtualizer anchoring preserves the visible item across prepends and appends; the manual scroll-height fallback applies only when completing pagination disables virtualization. Selection, timeline focus, folding, search, and bottom following address records by stable event or tool-call identity rather than requiring their DOM rows to exist. An explicit loading row covers records until initial positioning finishes and while an older page is pending. The raw window base sequence detects a prepend even when a page adds no surface-visible node.
|
||||
- The separate Waterfall tab is removed. A fixed Overview above the ledger projects every loaded record with known `startedAt` onto three semantic timing lanes using its own duration. While an older prefix remains unloaded and the viewport includes the loaded domain's start, a neutral ellipsis control covers the truncated edge and loads one earlier page without assigning unknown history a fabricated duration; hovering that control suppresses the ordinary timeline cursor. Finalized Assistant spans divide the recorded interval at the first non-empty token delta, so distinct TTFT and decoding colors retain their actual ratio; incomplete timing falls back to one Assistant color. Hovering for 500 ms exposes exact start/end, total duration, TTFT, and decoding time without relying on the browser's native tooltip delay. Dragging left or right commits an inclusive interval filter: any record whose active interval overlaps either boundary remains visible, records without known timing leave the focused ledger, and clearing the selection restores the full branch. Wheel gestures zoom the time domain. A right-button click clears the interval selection; dragging instead pans an already zoomed viewport without mutating it. The Overview keeps the full time domain while focused so the selection can be resized or cleared without losing orientation.
|
||||
- Live history updates retain the ledger's bottom position only while the user is already following its tail. Scrolling upward clears that follow state, so streamed chunks and newly appended records do not interrupt inspection of earlier rows. Tail following and virtualizer measurement react to row keys and heights rather than content identity, so text-only stream frames neither discard the measurement cache nor repeat a DOM scroll write.
|
||||
- Token streaming reuses the finalized history inspection, layout, Request numbering, Overview projection, and search results. A frame appends only the current partial Assistant cells and searches that partial when a query is active; text and reasoning deltas do not re-fold or rescan the loaded prefix, while message completion, tool lifecycle, compaction, rewrites, and other structural events rebuild the affected projections. Before those rebuilds, the inspection ledger drops completed-step token payloads that no projection reads while retaining the first visible token for timing, every usage chunk for accounting, and every chunk from unfinished or interrupted steps; the independent history source retains the raw entries.
|
||||
- History folding rebases only the loaded surface events into a compact contiguous input for the canonical surface manager, then maps its nodes back to absolute session sequences. Structural events therefore retain canonical replacement validation without replaying token chunks or materializing synthetic events for unloaded sequences.
|
||||
- Trajectory opts into a conversation-owned composer overlay through `data-conversation-composer-overlay`. `ConversationRoot` positions the composer seat and publishes its live height; Trajectory keeps the ledger at full height and reserves that height plus 16 px inside its vertical table and inspector scrollers. Those panes adapt to the available width instead of exposing horizontal scrollbars beneath the overlay.
|
||||
- This local inspector remains independent from the conversation-wide Chat details column. At narrow widths it overlays the ledger and remains dismissible by keyboard or pointer.
|
||||
|
||||
@@ -32,6 +35,12 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
|
||||
|
||||
**Keep one card per Turn and Step.** Rejected: repeated card chrome reduced the number of visible records and made cross-step comparison slower.
|
||||
|
||||
**Mount every projected record in the table.** Rejected: record projection remains useful for search, timing, and navigation, but keeping every row and its descendants in the DOM makes browser rendering scale with the complete session instead of the visible viewport.
|
||||
|
||||
**Exhaust every history page when Trajectory mounts.** Rejected: complete session metrics would be immediately available, but transporting and repeatedly projecting old chunk-heavy pages delays inspection of the current tail. On-demand backward paging makes that cost follow the user's navigation.
|
||||
|
||||
**Rebuild the loaded ledger for every streamed token chunk.** Rejected: virtual rows bound DOM work but do not make repeated history folding cheap. Keeping finalized projections stable makes ordinary deltas proportional to the current partial, while structural events remain the explicit full-rebuild boundary.
|
||||
|
||||
**Flatten every record without Turn or Request boundaries.** Rejected: a trajectory is not merely a log stream; those boundaries preserve the causal structure without consuming dedicated rows.
|
||||
|
||||
**Reuse the global Chat details column.** Rejected: it would couple local inspection to conversation navigation and make a row click unexpectedly change another view's state.
|
||||
@@ -44,4 +53,4 @@ Trajectory has to make prose, machine payloads, token usage, timing, and nested
|
||||
|
||||
## Consequences
|
||||
|
||||
Trajectory shows more useful records per viewport while retaining Turn and Request orientation. Context rewrites and compactions remain inline with their surrounding history, while a rewind begins a successor branch that inherits only the retained prefix. The floating composer leaves the ledger visible to the viewport edge without covering its final rows or hiding horizontal controls. The main ledger omits token usage and duration so content receives the available width; the local inspector exposes those facts together with full payloads, provenance, schemas, and request timing. The Overview uses recorded start/duration and token-boundary facts without fabricating live elapsed time, and its inclusive focus behavior matches the interaction users already know from Chrome DevTools Network. Focused component tests pin tail following, timing projection, delayed detail disclosure, folding, record and interval selection, entity-specific tabs, and running/error semantics; the assembled Web snapshot pins the ledger, Overview timing details, composer overlay geometry, and inspector through the real client composition.
|
||||
Trajectory shows more useful records per viewport while retaining Turn and Request orientation. Context rewrites and compactions remain inline with their surrounding history, while a rewind begins a successor branch that inherits only the retained prefix. The floating composer leaves the ledger visible to the viewport edge without covering its final rows or hiding horizontal controls. The main ledger omits token usage and duration so content receives the available width; the local inspector exposes those facts together with full payloads, provenance, schemas, and request timing. The Overview uses recorded start/duration and token-boundary facts without fabricating live elapsed time, and its inclusive focus behavior matches the interaction users already know from Chrome DevTools Network. Tail-first paging bounds initial transport and projection work, virtualization bounds mounted row elements, incremental partial projection removes loaded-history length from ordinary token-frame work, and completed-step chunk compaction makes structural rebuilds proportional to inspection-relevant entries rather than the raw token count. Focused component tests pin tail-first paging, prepend anchoring and identity retention, the virtual window, tail following, content-only streaming without repeated scroll writes, streaming structural sharing, high-sequence window folding, timing projection, delayed detail disclosure, folding, record and interval selection, entity-specific tabs, and running/error semantics. A real-browser long-ledger contract pins stable prepend geometry, bounded mounting, top/middle/bottom reachability, and bounded scroll writes across a paced stream; the assembled Web snapshot pins the ledger, Overview timing details, composer overlay geometry, and inspector through the real client composition.
|
||||
|
||||
@@ -16,13 +16,16 @@ Status: implemented
|
||||
- 事件类型与内容构成两个稳定列。角色标签朝内容侧对齐,嵌套子工具略微缩进,内容预览使用 CSS 截断以适应可用宽度。token 用量和耗时留在检查器中。
|
||||
- 产品正文使用现有无衬线字体栈。轮次 id、token 数、耗时、工具调用、原始载荷和其他机器数据使用现有代码字体栈。
|
||||
- 现有主题 token 同时负责亮色和暗色渲染。中性边框与表面构成整体结构;区分度较低的角色色帮助扫读而不表达成功或失败语义,业务蓝色则标识选择状态、链接和焦点。
|
||||
- 客户端 runtime 提供独立于 Session 和 SessionManager 的只读历史数据源。每个已激活的数据源自行拥有原始条目、分页、实时缺口修复和重连重建;普通会话快照仍然只是 Chat 所需的折叠投影。Trajectory 订阅该数据源,仅在挂载期间补齐全部历史,并按需派生事件顺序、上下文谱系、schema 索引和请求,避免让所有会话消费者承担这些结构。
|
||||
- 普通生成调用与压缩调用形成一条按时间排序的请求投影,以 purpose 区分而不是放入不同集合。生效的提示词状态及其变化附着在引入它们的请求上;压缩和提示词变化都不是独立检查实体。完整历史使全局请求编号和累计用量以整个会话为范围,而不是相对于末尾窗口。
|
||||
- 客户端 runtime 提供独立于 Session 和 SessionManager 的只读历史数据源。每个已激活的数据源自行拥有原始条目、分页、实时缺口修复和重连重建;普通会话快照仍然只是 Chat 所需的折叠投影。Trajectory 在挂载期间打开该数据源的尾部,当用户到达已加载范围顶部时请求一页更早的历史,再按需派生事件顺序、上下文谱系、schema 索引和请求,避免让所有会话消费者承担这些结构。
|
||||
- 普通生成调用与压缩调用形成一条按时间排序的请求投影,以 purpose 区分而不是放入不同集合。生效的提示词状态及其变化附着在引入它们的请求上;压缩和提示词变化都不是独立检查实体。请求编号和累计用量覆盖已加载的历史窗口,并随更早页面到达而扩展。
|
||||
- 调用 schema 来自当前生效且已记录的请求头。无密钥快照 fixture(测试前置数据)有意将该目录替换为非数组 token `{{tools}}`,持久化检查边界会将其视为不可用,而不是尝试投影或虚构 schema。
|
||||
- 选择记录或请求后,轨迹视图内部会打开检查器,其标签页和概览区域随实体类型变化:Markdown 消息提供渲染、源码、来源和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。图片以媒体形式渲染,而不是显示为序列化数据。
|
||||
- 选择记录或请求后,轨迹视图内部会打开检查器,其标签页和概览区域随实体类型变化:Markdown 消息提供渲染、源码、来源和层级视图;工具提供 JSON 载荷/结果和 schema 视图;请求提供选项、用量、计时和结果跳转。可滚动的概述区域默认保持滚动条滑块透明,直到悬停或 `focus-within` 时才显示,同时保留滚动条预留空间和滚动行为。图片以媒体形式渲染,而不是显示为序列化数据。
|
||||
- 折叠轮次时保留其第一条记录,将后续行替换为紧凑的步骤和工具调用数量;折叠助手时对其工具调用后代应用相同交互。全局控件可以分别折叠或展开这两个层级。
|
||||
- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的记录按各自耗时投影到三条语义计时轨道。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间相交的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整分支。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
|
||||
- 实时历史更新仅在用户已经跟随记录表末尾时保留底部位置。向上滚动会清除跟随状态,因此流式分块和新追加的记录不会打断对旧记录的检查。
|
||||
- 长记录表初始时将已加载尾部置于底部,只挂载视口对应的行窗口及有界的额外缓冲行。仅含请求的分隔行并入下一个具备可测高度的虚拟项,末尾分隔行则保留固定留白,因此虚拟化器不会管理零高度项。可安全用于 DOM 的语义行键与 ARIA 索引使标识不依赖挂载位置。只要已知尾部之前仍有更早历史,即使当前已加载投影低于常规行数阈值,也会立即启用虚拟化。基于稳定键的虚拟化器锚定会在向前补页和尾部追加时保留当前可见项;只有分页完成导致虚拟化停用时,才使用手动滚动高度兜底。选择、时间线聚焦、折叠、搜索和末尾跟随均按稳定的事件或工具调用标识定位,不要求对应 DOM 行已存在。初始定位完成前以及更早页面仍在等待时,明确的加载行会遮住真实记录。原始窗口的基准序号即使在一页未增加任何 surface 可见节点时,也能检测到这次向前补页。
|
||||
- 移除独立的 waterfall(瀑布式事件)标签页。固定在记录表上方的 Overview 区域将所有 `startedAt` 已知的已加载记录按各自耗时投影到三条语义计时轨道。仍有更早前缀尚未加载且 viewport 包含已加载时间域起点时,中性的省略号控件会遮住截断边缘并加载一页更早历史,而不会为未知历史虚构耗时;悬停在该控件上会隐藏普通的时间线光标。已完成的助手时间条以首个非空 token 增量为分界,用不同颜色按真实比例表示 TTFT 与解码时间;计时不完整时退化为单一助手色。悬停 500 ms 后会显示精确起止时刻、总耗时、TTFT 和解码时间,而不依赖浏览器原生 tooltip 的延迟。向左或向右拖动会提交包含边界的区间筛选:任何活动区间与所选区间相交的记录都会保留,计时未知的记录会从聚焦后的记录表中移除,清除选择则恢复完整分支。滚轮手势用于缩放时间域。右键单击会清除区间选择;右键拖动则只会平移已放大的 viewport,不会改变该选区。聚焦后,Overview 区域仍保留完整时间范围,以便在不失去方位的情况下调整或清除选择。
|
||||
- 实时历史更新仅在用户已经跟随记录表末尾时保留底部位置。向上滚动会清除跟随状态,因此流式分块和新追加的记录不会打断对旧记录的检查。末尾跟随与虚拟化器测量仅响应行键和高度,而非内容标识,因此仅含文本的流式帧既不会丢弃测量缓存,也不会重复执行 DOM 滚动写入。
|
||||
- token 流式输出会复用已完成历史的检查结果、布局、请求编号、Overview 投影和搜索结果。每个帧只追加当前未完成助手的单元格,并在查询处于激活状态时搜索这部分内容;文本与推理(reasoning)增量不会重新折叠或扫描已加载前缀,而消息完成、工具生命周期、压缩、`rewrite` 及其他结构事件会重建受影响的投影。在这些投影重建前,检查记录表会丢弃已完成步骤中没有任何投影读取的 token 载荷,但会保留首个可见 token 用于计时、保留所有用量分片用于核算,并保留未完成或中断步骤的所有分片;独立历史数据源仍保留原始条目。
|
||||
- 历史折叠只把已加载的 surface 事件重新编号为紧凑连续的输入并交给规范 surface manager,再将其节点映射回会话绝对序号。因此,结构事件会保留规范的替换校验,而无需重放 token 分片,也不会为未加载的序号实体化合成事件。
|
||||
- Trajectory 通过 `data-conversation-composer-overlay` 启用由会话持有的 composer 浮层模式。`ConversationRoot` 负责定位 composer seat 并发布其实时高度;Trajectory 让记录表保持全高,并在记录表与检查器的纵向滚动容器内预留该高度加 16 px。这两个窗格会根据可用宽度自适应,而不会在浮层下方暴露横向滚动条。
|
||||
- 此局部检查器与会话级 Chat 详情栏相互独立。在窄屏下,检查器会覆盖记录表,并且仍可通过键盘或指针关闭。
|
||||
|
||||
@@ -32,6 +35,12 @@ Status: implemented
|
||||
|
||||
**每个轮次和步骤保留一张卡片。** 不予采纳:重复的卡片框架减少了可见记录数量,并降低了跨步骤比较的速度。
|
||||
|
||||
**在表格中挂载每条投影记录。** 不予采纳:记录投影仍可用于搜索、计时和导航,但把每一行及其后代都保留在 DOM 中,会使浏览器渲染开销随完整会话增长,而非随可见视口增长。
|
||||
|
||||
**Trajectory 挂载时补齐所有历史页面。** 不予采纳:完整会话指标可以立即获得,但传输并反复投影含大量分片的旧页面会延迟对当前尾部的检查。按需向前分页会让这项成本随用户导航产生。
|
||||
|
||||
**每收到一个流式 token 分片就重建已加载记录表。** 不予采纳:虚拟行限制了 DOM 工作量,却不会让反复折叠历史变得低廉。保持已完成投影稳定,可以让普通增量的成本只随当前未完成部分增长,而结构事件仍是显式的完整重建边界。
|
||||
|
||||
**不使用轮次或请求边界,将所有记录完全扁平化。** 不予采纳:轨迹并非普通日志流;这些边界无需占用独立行,也能保留因果结构。
|
||||
|
||||
**复用全局 Chat 详情栏。** 不予采纳:这会让局部检查与会话导航耦合,还会使行点击意外改变另一个视图的状态。
|
||||
@@ -44,4 +53,4 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
轨迹视图在保留轮次与请求定位的同时,每个视口可以显示更多有效记录。上下文 `rewrite` 与压缩保持在周边历史中的原始位置,`rewind` 则建立仅继承保留前缀的后继分支。浮动 composer 让记录表一直显示到视口边缘,同时不会遮住最后几行,也不会隐藏横向控件。主记录表省略 token 用量和耗时,让内容获得可用宽度;局部检查器展示这些数据以及完整载荷、来源、schema 和请求计时。Overview 区域使用记录的开始时间、耗时与 token 边界数据,而不虚构实时流逝时间,其包含边界的聚焦行为与用户熟悉的 Chrome DevTools Network 交互一致。针对性组件测试锁定末尾跟随、计时投影、延迟展示详情、折叠、记录与区间选择、实体特定标签页和运行/错误语义;组装后的 Web 快照则通过真实客户端组合锁定记录表、Overview 计时详情、composer 浮层几何形状与检查器。
|
||||
轨迹视图在保留轮次与请求定位的同时,每个视口可以显示更多有效记录。上下文 `rewrite` 与压缩保持在周边历史中的原始位置,`rewind` 则建立仅继承保留前缀的后继分支。浮动 composer 让记录表一直显示到视口边缘,同时不会遮住最后几行,也不会隐藏横向控件。主记录表省略 token 用量和耗时,让内容获得可用宽度;局部检查器展示这些数据以及完整载荷、来源、schema 和请求计时。Overview 区域使用记录的开始时间、耗时与 token 边界数据,而不虚构实时流逝时间,其包含边界的聚焦行为与用户熟悉的 Chrome DevTools Network 交互一致。尾部优先分页限制初始传输和投影工作量,虚拟化限制已挂载的行元素数量,未完成部分的增量投影让普通 token 帧的工作量不再随已加载历史长度增长,而已完成步骤的分片压缩则让结构重建的工作量与检查所需条目数量成正比,而非与原始 token 数量成正比。针对性组件测试锁定尾部优先分页、向前补页锚定与标识保持、虚拟窗口、末尾跟随、仅含内容的流式输出不会重复写入滚动位置、流式输出的结构共享、高序号窗口折叠、计时投影、延迟展示详情、折叠、记录与区间选择、实体特定标签页和运行/错误语义。真实浏览器中的长记录表契约锁定向前补页时稳定的几何位置、有界挂载、顶部/中部/底部可达性,以及按节奏进行的流式输出中有界的滚动写入;组装后的 Web 快照则通过真实客户端组合锁定记录表、Overview 计时详情、composer 浮层几何形状与检查器。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-web-session-search.md
|
||||
2026-07-27-web-session-search.md: 9a634c586a4793d1c6986a7e7c0b0c1157b5b687
|
||||
2026-07-27-web-session-search.zh.md: 5ec2baf7443aaaaa75abc348ee426df9c14fbaa2
|
||||
2026-07-27-web-session-search.md: 4c49c0e61ca1b15181e767e4f65353312f8a3e3f
|
||||
2026-07-27-web-session-search.zh.md: e9aa00656443e544bad623540d07ff785b1285ef
|
||||
|
||||
@@ -12,7 +12,7 @@ The Web sidebar exposes session titles and Workspace membership but cannot retri
|
||||
|
||||
The shared Web/headless composition mounts [`@deepseek-ai/dsh-session-query-sqlite`](../../../../packages/session-query/session-query-sqlite/README.md) with `openAt: first-search` and an in-memory database. The service is ACTIVE at boot, while its `node:sqlite` module and connection-private handle open only on the first content query. This keeps Node 22 startup output free of SQLite's experimental warning before search is used without promising to suppress the warning when search first imports the module. Each service instance owns its index, preserving the SQLite backend's single-owner contract across parallel CLI or Web invocations without leaving process-scoped derived files behind. The database starts empty and lazily reconciles live and persisted sessions on that first query. It remains a disposable derived index, separate from canonical JSONL persistence.
|
||||
|
||||
The host gateway exposes `session.search` through the existing typed RPC stack. It derives the authorization set from the same visible summaries as `session.list`, asks `ctx.sessionQuery.searchSessions` for globally ranked current-surface `user/message`, `assistant/message`, and `steering/message` matches, and consumes provider pages until it has 20 authorized sessions plus one lookahead or exhausts the stream. The first provider page requests 20 hits; a first-page `SESSION_QUERY_INVALID_LIMIT` halves that size through 10, 5, 2, and 1, retaining the learned size across continuations and stale-generation restarts. Every hit's session id, best-match session id, surface, and event type are revalidated before its snippet leaves the Host. Emitted snippets contain at most 240 Unicode code points; the Host and wire schema share the protocol bounds and code-point-safe truncation helper, while the wire schema independently enforces the snippet bound at client parse. Keeping the potentially large authorization set out of SQLite bindings avoids the portable variable ceiling while preserving global ranking. The response remains one bounded page; `hasMore` tells the UI to ask for a narrower query rather than exposing pagination. A stale continuation discards the current attempt's partial results, deduplication entries, and cursors, then restarts from the first page against the original visibility snapshot. Limit probes and stale retries share the limit of 100 provider calls (and therefore at most 2,000 inspected hits); a page larger than its requested limit, a repeated continuation cursor, or a still-unexhausted stream at that call budget fails closed as an `internal` business error. The carrier signal cancels superseded work, including persistence listing, bounded batches of cold-session metadata stats, and each provider call, and wins over a concurrent limit or stale rejection. A missing query service or an unrecovered indexing/query failure remains a business error and does not mutate the canonical session store.
|
||||
The host gateway exposes `session.search` through the existing typed RPC stack. It derives the authorization set from the same visible summaries as `session.list`, asks `ctx.sessionQuery.searchSessions` for globally ranked current-surface `user/message` and `assistant/message` matches, and consumes provider pages until it has 20 authorized sessions plus one lookahead or exhausts the stream. The first provider page requests 20 hits; a first-page `SESSION_QUERY_INVALID_LIMIT` halves that size through 10, 5, 2, and 1, retaining the learned size across continuations and stale-generation restarts. Every hit's session id, best-match session id, surface, and event type are revalidated before its snippet leaves the Host. Emitted snippets contain at most 240 Unicode code points; the Host and wire schema share the protocol bounds and code-point-safe truncation helper, while the wire schema independently enforces the snippet bound at client parse. Keeping the potentially large authorization set out of SQLite bindings avoids the portable variable ceiling while preserving global ranking. The response remains one bounded page; `hasMore` tells the UI to ask for a narrower query rather than exposing pagination. A stale continuation discards the current attempt's partial results, deduplication entries, and cursors, then restarts from the first page against the original visibility snapshot. Limit probes and stale retries share the limit of 100 provider calls (and therefore at most 2,000 inspected hits); a page larger than its requested limit, a repeated continuation cursor, or a still-unexhausted stream at that call budget fails closed as an `internal` business error. The carrier signal cancels superseded work, including persistence listing, bounded batches of cold-session metadata stats, and each provider call, and wins over a concurrent limit or stale rejection. A missing query service or an unrecovered indexing/query failure remains a business error and does not mutate the canonical session store.
|
||||
|
||||
[`WorkspaceBrowser`](../../../../packages/client/ui-workspace/README.md) keeps metadata and content search deliberately separate. Its default copy is English, and its input plus defensive request path remove NUL and cap queries at the request schema's 500 UTF-16 code units without splitting a surrogate pair. A non-blank query immediately computes case-insensitive title and Workspace substring matches from the Session list, starts a 250 ms debounced content request, aborts the preceding request when the query changes, and ignores stale completions. It merges local matches first in recency order with backend-ranked content-only matches, deduplicates by session id, and renders a flat list regardless of the normal grouping mode. Each row shows the title, Workspace, and an available one-line snippet. Selecting a row opens the Session only and preserves the query; it does not navigate to an exact event.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Web 侧边栏会展示会话标题及其 Workspace 归属,但无法根据只
|
||||
|
||||
Web 与 headless 共用的组合会使用 `openAt: first-search` 和内存数据库挂载 [`@deepseek-ai/dsh-session-query-sqlite`](../../../../packages/session-query/session-query-sqlite/README.md)。服务启动时处于 ACTIVE 状态,而其 `node:sqlite` 模块与连接私有句柄分别要到首次内容查询才会导入和打开。这让 Node 22 的启动输出在使用搜索前不会出现 SQLite 实验性警告,但并不承诺在首次搜索导入该模块时抑制警告。每个服务实例都独占自己的索引,因此并行 CLI 或 Web 调用可维持 SQLite 后端的单一所有者契约,又不会留下进程级派生文件。数据库从空状态启动,并在该首次查询时惰性对齐实时会话与持久化会话。它仍是与规范 JSONL 持久化相互独立的可丢弃派生索引。
|
||||
|
||||
宿主网关通过现有的类型化 RPC 栈公开 `session.search`。它根据 `session.list` 使用的同一组可见摘要推导授权集合,向 `ctx.sessionQuery.searchSessions` 请求全局排序后的当前 surface `user/message`、`assistant/message` 和 `steering/message` 匹配项,并持续消费提供方分页,直到获得 20 个已授权会话及一个前瞻项,或结果流耗尽。首个提供方页面请求 20 个命中;如果第一页返回 `SESSION_QUERY_INVALID_LIMIT`,页面大小会依次折半为 10、5、2、1,并在续传和陈旧世代重启中沿用探测所得的大小。每个命中的会话 id、最佳匹配会话 id、surface 和事件类型都会经过重新校验,其 snippet 才能离开宿主。发出的 snippet 最多包含 240 个 Unicode 码点;宿主与传输 schema 共用协议边界及码点安全的截断辅助函数,而传输 schema 会在客户端解析时独立强制执行 snippet 上限。将可能很大的授权集合排除在 SQLite 绑定之外,可避开可移植变量上限,同时保持全局排序。响应仍只有一个有界页面;`hasMore` 会指示 UI 提示用户缩小查询范围,而不是公开分页能力。陈旧的续传会丢弃当前尝试的部分结果、去重条目和游标,然后依据原始可见性快照从第一页重新开始。上限探测与陈旧重试共用 100 次提供方调用的限制(因此最多检查 2,000 个命中);如果某页命中数超过其请求的上限、续传游标重复,或用尽该调用预算后结果流仍未耗尽,都会直接返回 `internal` 业务错误,不返回部分结果。载体信号会取消已被取代的工作,包括持久化列表枚举、分批受限执行的冷会话元数据 stat,以及每一次提供方调用;即使同时收到上限拒绝或陈旧拒绝,也以取消为准。查询服务缺失或索引/查询故障无法恢复时,仍作为业务错误处理,不会修改规范会话存储。
|
||||
宿主网关通过现有的类型化 RPC 栈公开 `session.search`。它根据 `session.list` 使用的同一组可见摘要推导授权集合,向 `ctx.sessionQuery.searchSessions` 请求全局排序后的当前 surface `user/message` 和 `assistant/message` 匹配项,并持续消费提供方分页,直到获得 20 个已授权会话及一个前瞻项,或结果流耗尽。首个提供方页面请求 20 个命中;如果第一页返回 `SESSION_QUERY_INVALID_LIMIT`,页面大小会依次折半为 10、5、2、1,并在续传和陈旧世代重启中沿用探测所得的大小。每个命中的会话 id、最佳匹配会话 id、surface 和事件类型都会经过重新校验,其 snippet 才能离开宿主。发出的 snippet 最多包含 240 个 Unicode 码点;宿主与传输 schema 共用协议边界及码点安全的截断辅助函数,而传输 schema 会在客户端解析时独立强制执行 snippet 上限。将可能很大的授权集合排除在 SQLite 绑定之外,可避开可移植变量上限,同时保持全局排序。响应仍只有一个有界页面;`hasMore` 会指示 UI 提示用户缩小查询范围,而不是公开分页能力。陈旧的续传会丢弃当前尝试的部分结果、去重条目和游标,然后依据原始可见性快照从第一页重新开始。上限探测与陈旧重试共用 100 次提供方调用的限制(因此最多检查 2,000 个命中);如果某页命中数超过其请求的上限、续传游标重复,或用尽该调用预算后结果流仍未耗尽,都会直接返回 `internal` 业务错误,不返回部分结果。载体信号会取消已被取代的工作,包括持久化列表枚举、分批受限执行的冷会话元数据 stat,以及每一次提供方调用;即使同时收到上限拒绝或陈旧拒绝,也以取消为准。查询服务缺失或索引/查询故障无法恢复时,仍作为业务错误处理,不会修改规范会话存储。
|
||||
|
||||
[`WorkspaceBrowser`](../../../../packages/client/ui-workspace/README.md) 有意将元数据搜索与内容搜索保持独立。其默认界面文案为英文;输入框及防御性请求路径会移除 NUL,将查询限制在请求 schema 规定的 500 个 UTF-16 code unit 内且不会拆分 surrogate pair。非空白查询会立即从会话列表中计算不区分大小写的标题和 Workspace 子串匹配,在 250 ms 防抖后发起内容请求,在查询变化时中止前一请求,并忽略陈旧的完成结果。它先按新近程度排列本地匹配,再合并由后端排序且仅匹配内容的结果,按会话 id 去重;无论常规分组模式如何,最终都渲染为扁平列表。每一行显示标题、Workspace,并在存在时显示一行摘要片段。选择某一行只会打开对应会话,并保留查询条件;不会跳转至确切事件。
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Agent Note: Address pending queue occurrences for edit and removal
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-29-addressable-queue-operations.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web queue rendered pending messages but could not edit or delete one row. `MessageId` was insufficient as an address because callers may enqueue the same immutable message more than once. The browser also inferred queue retirement from turn and status events, so a row operation racing with driver claim had no authoritative outcome.
|
||||
|
||||
## Decision
|
||||
|
||||
**Each accepted FIFO occurrence has its own identity.** AgentLoop mints an opaque `InboxItemId` and publishes an `InboxItem` containing that id, the identified `UserMessage`, and its acceptance-time `queued | steering` placement. Reusing one `MessageId` creates distinct inbox identities. Injection bypasses the FIFOs and receives no inbox identity.
|
||||
|
||||
**Mutation ends at driver claim.** `Agent.updateInbox(id, action)` synchronously searches the pending queued FIFO. Edit replaces frozen content while preserving `InboxItemId`, `MessageId`, source, wake policy, and position. Remove emits the occurrence’s terminal discard. Strict steer transfers the message into an open next-step window as a new steering occurrence; a closed window returns `steer-unavailable` without changing the queued item. Pending steering and driver-claimed occurrences return `not-found`, so later mutations never rewrite active-turn input or durable history.
|
||||
|
||||
**The live ledger is authoritative.** `agent/inbox/enqueue`, `update`, `dequeue`, and `discard` maintain a Host mirror of queued occurrences. A synchronously re-entrant update or terminal event may reach the mirror before its outer enqueue listener; the mirror retains that unseen outcome for the current dispatch and folds it into the enqueue, so listener registration order cannot publish stale content or a ghost row. The wire sends complete `session/queue` snapshots rather than incremental guesses. Reconnect sends the current baseline, and every queued mutation or terminal event replaces it. The client applies no optimistic edit and never retires a row from durable turn events or status changes.
|
||||
|
||||
**Queue addresses require a live ordinary-session Agent.** `session.updateQueue` queries only the mounted Agent registry and never resumes a cold session: an `InboxItemId` is process-local and cannot name work after restart or disposal. A session-backed subagent returns `agent-busy` before inbox access and retains its continuation owner; for ordinary sessions, a missing Agent and a driver-claimed occurrence both return `queue-item-not-found`.
|
||||
|
||||
**Web actions address Queue only.** The placement-aware `session/queue` snapshot carries both queued and pending-steering occurrences; QueueDock selects only queued items, while ChatView projects steering and retains the existing durable transcript path after consumption. QueueDock hides while empty, renders one pending occurrence directly, and defaults two or more occurrences to a collapsed `"<n> 条排队消息"` header that expands or collapses the complete list. The header exposes `aria-expanded` and `aria-controls`; the expanded list scrolls within a 180px height bound. An active edit or mutation keeps its rows visible, and emptying the queue restores the collapsed default for the next queue. Visible rows expose edit, delete, and a running-only strict-steer action. The UI derives queue row and mutation types from the runtime `SessionFace` contract rather than importing the connection plugin, so plugin cooperation continues through services and snapshots. Edit is available only when all content blocks are text; the editor cannot silently drop non-text blocks. An editing row exposes only save and cancel, with Enter and Escape as their keyboard equivalents. Delete removes the exact occurrence, while strict steer preserves every content block and retires the row only through the authoritative snapshot. The Web stop action preserves pending Queue work; AgentLoop claims the next waking occurrence only after the interrupted turn reaches quiescence, and its dequeue event retires that row without a browser resend. The [Web Queue steer action](2026-07-30-web-queue-steer-action.md) owns the strict transfer and pending-projection contract.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Address rows by `MessageId`.** Rejected because one immutable message may be sent repeatedly; editing or deleting by message identity would affect an ambiguous occurrence.
|
||||
|
||||
**Apply optimistic browser mutations.** Rejected because driver claim and another client can win before the Host action. Waiting for the authoritative snapshot makes the ownership boundary visible and lets `queue-item-not-found` report a real race.
|
||||
|
||||
**Allow editing or removal of pending steering.** Rejected because QueueDock only addresses independent queued turns. Once strict steer succeeds, the new steering occurrence belongs to the active turn and remains outside this mutation surface.
|
||||
|
||||
**Expose a protocol-only promotion operation.** Rejected because no product interaction reorders Queue. A public operation without a current consumer would add ordering semantics and tests for speculative use.
|
||||
|
||||
**Resume a cold Agent for a queue operation.** Rejected because durable session identity does not preserve the process-local inbox capability. Resuming can only produce `not-found` after creating unrelated live state.
|
||||
|
||||
## Verification
|
||||
|
||||
AgentLoop contract tests hold prompt admission while editing, removing, and strictly steering exact queued occurrences; they reject mutations of steering occurrences and verify the resulting independent turn and terminal lifecycle events. Host schema and proxy tests cover queued-only authoritative snapshots, synchronous re-entrant mutation order, reconnect, cold-Agent rejection, typed race errors, and the RPC transport. Client runtime and QueueDock tests cover non-optimistic projection, single-row presentation, default multi-row collapse, interaction-forced visibility, reset after emptying, expansion, text-only editing, save and cancel affordances, removal, strict steer, retirement races, and disabled mixed-content editing. Keyless browser scenarios drive all three exposed actions through the built Web composition and real HTTP/SSE wire, then stop consecutive active turns to prove the preserved FIFO advances without clearing its tail.
|
||||
|
||||
## Consequences
|
||||
|
||||
Queued work gains precise row operations without becoming durable session history. Occurrence identity is a live process-local capability and disappears at claim, strict transfer, broad cancellation, disposal, or restart; the Web stop action preserves queued occurrences until a later claim, while reconnect recovers only queued items still held by the live Agent. Editing excludes mixed content until an editor can preserve every block, while pending steering remains outside the projection and operation surface.
|
||||
|
||||
The protocol now carries full queue snapshots on each change. Queues are expected to remain short, so deterministic recovery and multi-client convergence are preferred over an incremental mutation protocol.
|
||||
@@ -1,43 +0,0 @@
|
||||
# Agent Note(agent 决策记录):为待处理队列项提供编辑与移除操作
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-29-addressable-queue-operations.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 队列能够渲染待处理消息,但无法编辑或删除其中某一行。`MessageId` 不足以充当寻址标识,因为调用方可以多次将同一条不可变消息加入队列。浏览器还会根据轮次和状态事件推断队列项已退役,因此当行操作与驱动器认领发生竞态时,系统无法给出权威结果。
|
||||
|
||||
## 决策
|
||||
|
||||
**每次获准进入 FIFO 的项都有独立标识。** AgentLoop 会铸造不透明的 `InboxItemId`,并发布一个 `InboxItem`,其中包含该 id、已有标识的 `UserMessage`,以及接受时确定的 `queued | steering` 放置方式。复用同一个 `MessageId` 会创建不同的 inbox 标识。注入绕过 FIFO,因此不会获得 inbox 标识。
|
||||
|
||||
**变更边界止于驱动器认领。** `Agent.updateInbox(id, action)` 会同步搜索待处理的 queued FIFO。编辑会替换已冻结的内容,同时保留 `InboxItemId`、`MessageId`、来源、唤醒策略和位置。移除会发出该次入队项的终态 discard。严格 steering(中途引导)会把消息作为新的 steering 单次入队项转移到开放的 next-step 窗口;窗口关闭时返回 `steer-unavailable`,且不改变 queued 项。待处理 steering 和已被驱动器认领的项会返回 `not-found`,因此后续变更绝不会改写活动轮次输入或持久历史。
|
||||
|
||||
**实时账本是权威状态。** `agent/inbox/enqueue`、`update`、`dequeue` 和 `discard` 共同维护 queued 入队项的 Host 镜像。同步可重入的 update 或终态事件可能先于外层 enqueue 监听器到达镜像;镜像会在当前分发期间保留这一尚不可见的结果,并在处理 enqueue 时把它合并进去,因此监听器注册顺序不会导致系统发布陈旧内容或不存在的行。协议发送完整的 `session/queue` 快照,而非增量猜测。重连会发送当前基线,每次 queued 变更或终态事件都会整体替换它。客户端不会进行乐观编辑,也绝不根据持久轮次事件或状态变化退役队列行。
|
||||
|
||||
**Queue 寻址要求普通会话的 Agent 存活。** `session.updateQueue` 只查询已挂载的 Agent 注册表,绝不恢复冷会话:`InboxItemId` 属于进程本地标识,无法在重启或资源释放后继续指向工作。由会话支撑的 subagent 会在访问 inbox 前返回 `agent-busy`,并保留其继续执行 owner;对于普通会话,Agent 缺失和单次入队项已被驱动器认领这两种情况都返回 `queue-item-not-found`。
|
||||
|
||||
**Web 操作只面向 Queue。** 带 placement 的 `session/queue` 会同时携带 queued 和待处理 steering;QueueDock 只选择 queued 项,ChatView 则投影 steering,并在消费后沿用既有的持久 transcript(文本记录)路径。QueueDock 在队列为空时隐藏,只有一个待处理项时直接渲染该行,存在两个或更多待处理项时则默认收起为可展开或收起完整列表的 `"<n> 条排队消息"` 表头。表头暴露 `aria-expanded` 和 `aria-controls`;展开后的列表以 180px 为高度上限,并可滚动。存在进行中的编辑或变更时,列表行会保持可见;队列清空后,下一次出现队列时会恢复默认收起状态。可见行暴露编辑、删除以及仅在运行期间可用的严格 steering 操作。UI 从运行时 `SessionFace` 契约派生队列行与变更类型,而不是导入连接插件,因此插件仍通过服务和快照协作。仅当所有内容块都是文本时才提供编辑功能;编辑器不能静默丢弃非文本块。编辑中的行只展示保存和取消操作,对应的键盘操作分别是 Enter 和 Escape。删除会移除对应的精确入队项;严格 steering 会保留每个内容块,并且只通过权威快照退役该行。Web 停止操作会保留待处理 Queue 工作;只有在被中断轮次达到完全停稳后,AgentLoop 才认领下一个可唤醒入队项,其出队事件会退役该行,无需浏览器重发。[Web Queue steering 操作](2026-07-30-web-queue-steer-action.md)负责定义严格转移与待处理投影契约。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**通过 `MessageId` 寻址行。** 不予采纳,因为同一条不可变消息可以重复发送;按消息标识编辑或删除会无法确定应操作哪一次入队。
|
||||
|
||||
**在浏览器中进行乐观变更。** 不予采纳,因为驱动器认领或另一个客户端可能先于 Host 操作完成。等待权威快照可以显式呈现所有权边界,并让 `queue-item-not-found` 报告真实竞态。
|
||||
|
||||
**允许编辑或移除待处理 steering。** 不予采纳,因为 QueueDock 只寻址独立的 queued 轮次。严格 steering 一旦成功,新的 steering 单次入队项就属于活动轮次,并且不再位于此变更接口内。
|
||||
|
||||
**暴露仅协议层的前移操作。** 不予采纳,因为当前没有产品交互会重新排序 Queue。公开一个没有当前消费方的操作,会为了推测性用途引入排序语义和测试。
|
||||
|
||||
**为队列操作恢复冷 Agent。** 不予采纳,因为持久会话标识不会保留进程本地的 inbox 寻址凭据。恢复只能在创建无关的实时状态后得到 `not-found`。
|
||||
|
||||
## 验证
|
||||
|
||||
AgentLoop 契约测试会在编辑、移除和严格 steering 精确 queued 入队项时保持提示词接纳窗口打开,拒绝对 steering 入队项的变更,并验证所得独立轮次及终态生命周期事件。Host schema 与代理测试覆盖仅含 queued 项的权威快照、同步可重入变更顺序、重连、拒绝冷 Agent、类型化竞态错误和 RPC 传输。客户端运行时和 QueueDock 测试覆盖非乐观投影、单行呈现、多行默认收起、交互期间强制保持可见、清空后重置、展开、仅文本编辑、保存与取消入口、移除、严格 steering、退役竞态,以及禁用混合内容编辑。无密钥浏览器场景会通过构建后的 Web 组合和真实 HTTP/SSE 协议执行公开的全部三项操作,随后连续停止活动轮次,证明保留的 FIFO 会继续推进且不清空队尾。
|
||||
|
||||
## 后果
|
||||
|
||||
queued 工作获得精确的行操作,但不会因此成为持久会话历史。单次入队标识是进程本地的实时寻址凭据,会在认领、严格转移、广义取消、dispose 或重启时消失;Web 停止操作会将 queued 入队项保留到后续认领,而重连只能恢复仍由活跃 Agent 持有的 queued 项。编辑会排除混合内容,直至编辑器能够保留每个块;待处理 steering 则不属于此投影和操作接口。
|
||||
|
||||
现在,协议会在每次变更时携带完整队列快照。队列预期保持较短,因此系统优先选择确定性恢复和多客户端收敛,而非增量变更协议。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-queued-manual-compaction.md
|
||||
2026-07-30-queued-manual-compaction.md: 05676ef824bc62ddbdbd8895a325570e91e4bafd
|
||||
2026-07-30-queued-manual-compaction.zh.md: b2e40a42451570887a194c215df07e98aa1bd864
|
||||
2026-07-30-queued-manual-compaction.md: 4b7a905712a01948146b8830dfc037185162eefc
|
||||
2026-07-30-queued-manual-compaction.zh.md: 15a42de3536f2da5304e77ce3cb282029856ba6d
|
||||
|
||||
@@ -8,7 +8,7 @@ English | [中文](2026-07-30-queued-manual-compaction.zh.md)
|
||||
|
||||
Automatic compaction protects the context window, but an interactive user also needs a deterministic way to condense accumulated history before pressure policy fires. Sending `/compact` as prompt text would spend a model turn and let the conversation model reinterpret a direct control action. Implementing it inside one UI would duplicate command discovery, lifecycle logging, cancellation, and backend policy.
|
||||
|
||||
The human command arrives between turns and must summarize asynchronously. A prompt accepted during that wait must keep its ordinary identity, FIFO position, and wakeup behavior, but it must not derive a request from history that compaction is about to replace. A status check is insufficient: a waking send schedules the driver's claim as a microtask, leaving a same-tick interval where status still reads idle even though the prompt already has right of way.
|
||||
The human command arrives between turns and must summarize asynchronously. A prompt accepted during that wait must keep its ordinary identity, FIFO position, and wakeup behavior, but it must not derive a request from history that compaction is about to replace. A separate status check is insufficient because another caller can wake the driver between that check and the compaction operation claiming the idle phase.
|
||||
|
||||
Compaction also needs one mutual-exclusion fact shared by manual, pressure, overflow, and explicit-range entry points. A process-local flag alone cannot explain a crash-recovered log, while a summarize-first transaction leaves no durable evidence during the expensive interval. Conversely, treating marker pairs as exclusive containers would forbid valid idle injection even though injection is explicitly non-waking and immediate between turns.
|
||||
|
||||
@@ -22,15 +22,15 @@ This note extends the [compaction capability seam](2026-06-18-compaction-capabil
|
||||
|
||||
The command plugin tracks each real handler promise independently of the command executor's abort-aware wait. Its composite lifecycle effect unregisters `/compact` before asynchronously draining handlers that already started, so root teardown reaches quiescence only after backend close and flush work settles.
|
||||
|
||||
The seam's `ManualCompactAgentContext` adds only `reserveTurnAdmission()` to the session and routing facts compaction already needs. Retention, balancing, summarization, marker ordering, replacement, and durability remain backend responsibilities.
|
||||
The seam's `ManualCompactAgentContext` adds only `runMaintenance()` to the session and routing facts compaction already needs. Retention, balancing, summarization, marker ordering, replacement, and durability remain backend responsibilities.
|
||||
|
||||
### Idle turn admission is synchronously reservable
|
||||
### Idle maintenance is synchronously claimed
|
||||
|
||||
`Agent.reserveTurnAdmission(): (() => void) | undefined` claims the boundary before the next ordinary turn. It succeeds only when the driver is idle, no reservation exists, and no accepted waking item already owns the next turn, including a wake whose claim is still a pending microtask.
|
||||
`Agent.runMaintenance(task)` starts only from the idle phase and claims that phase before invoking the task. A waking send starts the loop immediately when idle, so whichever operation claims the phase first owns the boundary.
|
||||
|
||||
The reservation does not create a second queue. Later sends keep their `InboxItemId`, placement, FIFO order, and wakeup facts. `acceptsNextStep` remains false, so waking next-step input becomes an ordinary queued follow-up rather than steering. Release is idempotent and re-arms the existing driver path. `inject()` is not withheld.
|
||||
Maintenance does not create a second queue. Later sends keep their `MessageId`, placement, FIFO order, and wakeup facts. Waking input remains queued until maintenance settles, then starts the existing driver path; `inject()` remains non-waking.
|
||||
|
||||
`whenIdle()` treats a reservation as unfinished activity, including when it holds a waking item. Lifecycle teardown still drains the driver's own activity promise rather than awaiting an external operation, so disposal can cancel and unwind without depending on the reservation holder.
|
||||
`whenIdle()` treats maintenance and any waking work released behind it as unfinished activity. Cancellation aborts the agent-owned maintenance signal, and lifecycle teardown drains the same activity boundary before disposal completes.
|
||||
|
||||
### One parameterized transaction owns every bracket
|
||||
|
||||
@@ -81,7 +81,7 @@ That reference also carried client-side replacement-anchor machinery to preserve
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Check `agent.status` without reserving admission.** Rejected because an accepted waking send can still be waiting on its claim microtask while status reads idle.
|
||||
**Check `agent.status` before starting maintenance.** Rejected because the check and phase claim would be separate operations; a waking send could start the driver between them.
|
||||
|
||||
**Queue the command itself.** Rejected because `/compact` is direct control, not model input, and a prompt already accepted first must retain right of way rather than being reordered around a second command queue.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
自动压缩(compaction)可以保护上下文窗口,但交互用户还需要一种确定性方法,在压力策略触发前压缩累积的历史。把 `/compact` 作为提示词文本发送会消耗一个模型轮次,还会让会话模型重新解释一项直接控制操作。在某个 UI 内实现该功能,则会重复命令发现、生命周期日志记录、取消与后端策略。
|
||||
|
||||
面向用户的命令在轮次之间到达,并且必须异步生成摘要。在等待期间获接纳的提示词必须保留普通身份、FIFO 位置与唤醒行为,但不得从即将被压缩替换的历史派生请求。仅检查状态并不足够:唤醒发送会把驱动器的认领安排为 microtask,因此在同一 tick 内存在一段间隔,此时状态仍显示 idle,但提示词已经拥有优先权。
|
||||
面向用户的命令在轮次之间到达,并且必须异步生成摘要。在等待期间获接纳的提示词必须保留普通身份、FIFO 位置与唤醒行为,但不得从即将被压缩替换的历史派生请求。单独检查状态并不足够,因为另一调用方可能在该检查与压缩操作认领 idle phase 之间唤醒驱动器。
|
||||
|
||||
手动、压力、溢出和显式范围入口点还需要共享同一项互斥事实。仅使用进程本地标志无法解释一份崩溃恢复后的日志,而先摘要再记录的事务在开销较大的等待期间不会留下持久证据。反过来,把标记对视为排他容器又会禁止有效的空闲注入,尽管注入按定义不会唤醒,并且会在轮次之间立即执行。
|
||||
|
||||
@@ -22,15 +22,15 @@ Status: implemented
|
||||
|
||||
命令插件会独立跟踪每个实际处理器 promise,不依赖命令执行器的中止感知等待。其复合生命周期 effect 先注销 `/compact`,再异步等待所有已开始的处理器结算,因此根级 teardown 只有在后端的闭合与 flush 工作结算后才会完全停稳。
|
||||
|
||||
该 seam 的 `ManualCompactAgentContext` 只在压缩已需使用的会话与路由事实之上增加 `reserveTurnAdmission()`。保留、平衡、摘要、标记排序、替换与持久性仍由后端负责。
|
||||
该 seam 的 `ManualCompactAgentContext` 只在压缩已需使用的会话与路由事实之上增加 `runMaintenance()`。保留、平衡、摘要、标记排序、替换与持久性仍由后端负责。
|
||||
|
||||
### 可以同步预留空闲轮次接纳
|
||||
### 同步认领空闲维护阶段
|
||||
|
||||
`Agent.reserveTurnAdmission(): (() => void) | undefined` 会在下一个普通轮次之前认领边界。仅当驱动器空闲、没有既存预留,而且尚无已获接纳的唤醒项拥有下一轮次时,它才会成功;仍在等待 microtask 认领的唤醒项也包括在内。
|
||||
`Agent.runMaintenance(task)` 只能从 idle phase 启动,并会在调用任务前认领该 phase。会唤醒的发送会在 idle 时立即启动循环,因此先认领 phase 的操作会拥有该边界。
|
||||
|
||||
该预留不会创建第二个队列。之后发送的项保留其 `InboxItemId`、位置、FIFO 顺序与唤醒信息。`acceptsNextStep` 保持 false,因此唤醒的 next-step 输入会成为普通的排队 follow-up,而不是 steering(中途引导)。释放操作可幂等调用,并重新启用既有驱动器路径。`inject()` 不受阻塞。
|
||||
维护阶段不会创建第二个队列。之后发送的项保留其 `MessageId`、位置、FIFO 顺序与唤醒信息。会唤醒的输入会保持排队,直至维护任务结算,再启动既有驱动器路径;`inject()` 仍然不会唤醒驱动器。
|
||||
|
||||
`whenIdle()` 会把预留视为尚未完成的活动,包括预留持有唤醒项的情况。生命周期 teardown 仍会排空驱动器自身的 activity promise,而不会等待外部操作,因此 dispose(资源释放)可以执行取消并完成退出清理,无需依赖预留持有方。
|
||||
`whenIdle()` 会把维护任务及其结算后释放的所有唤醒工作视为尚未完成的活动。取消会中止 agent 自有的维护信号,生命周期 teardown 则会在 dispose(资源释放)完成前排空同一个活动边界。
|
||||
|
||||
### 一个参数化事务拥有每一对标记
|
||||
|
||||
@@ -81,7 +81,7 @@ DSH 有意在调用摘要器前记录 `compact/start`。缓慢或崩溃的尝试
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**仅检查 `agent.status`,不预留接纳。** 不予采用,因为已获接纳的唤醒发送可能仍在等待其认领 microtask,而状态仍显示 idle。
|
||||
**在启动维护任务前检查 `agent.status`。** 不予采用,因为检查与 phase 认领会成为两个独立操作;会唤醒的发送可能在二者之间启动驱动器。
|
||||
|
||||
**把命令本身加入队列。** 不予采用,因为 `/compact` 是直接控制而非模型输入;先获接纳的提示词必须保留优先权,不能围绕第二个命令队列重新排序。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.md
|
||||
2026-07-30-web-queue-steer-action.md: b5bc3547e0f72ba8211f4338fb4c1f5223a3d0cc
|
||||
2026-07-30-web-queue-steer-action.zh.md: 6edde8e2cc2bee0a52a0b05df8f62f77b3d9ccfa
|
||||
2026-07-30-web-queue-steer-action.md: b04095b81f499982c8680a2d3627d8e98a70d8ac
|
||||
2026-07-30-web-queue-steer-action.zh.md: b04902b8a8a0d727b01aa6ba5562e12cc5d36c92
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-07-30-web-queue-steer-action.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The Web composer originally queued every Enter submission while an agent ran. QueueDock already gives each pending message an addressable row, and the durable transcript already renders consumed `steering/message` events as user-style bubbles, but Web had neither an action connecting those two surfaces nor a direct composer gesture for choosing current-turn steering.
|
||||
The Web composer originally queued every Enter submission while an agent ran. QueueDock already gives each pending message an addressable row, and the durable transcript already renders consumed steer events as user-style bubbles, but Web had neither an action connecting those two surfaces nor a direct composer gesture for choosing current-turn steering.
|
||||
|
||||
Implementing the row action as a client-side delete followed by `session.prompt(mode: 'steer')` would split one user intent across two RPCs. Driver claim could win between them, the steer could fail after deletion, or the existing best-effort `agent.steer()` fallback could silently append a new Queue item after the original occurrence was removed. A send-now action must therefore distinguish current-turn steering from Queue promotion and preserve the original row when steering is no longer possible.
|
||||
|
||||
@@ -16,7 +16,7 @@ Implementing the row action as a client-side delete followed by `session.prompt(
|
||||
|
||||
Each non-editing ordinary-session QueueDock row exposes the upward-arrow action as “插话发送”. The action is enabled only while the session reports a running agent; mixed-content messages remain eligible because steering forwards the complete immutable `UserMessage` rather than the row's text projection. An addressed subagent keeps its Queue projection read-only because its continuation transport does not expose queue mutation.
|
||||
|
||||
Activating the action requests strict current-turn steering for that exact `InboxItemId`. Success removes the Queue row through the authoritative Host snapshot and immediately projects the same pending steering after the `Deep diving...` running-status row; that bubble offers Copy but no Fork because the message has no durable event sequence yet. Once AgentLoop drains it, the existing durable `steering/message` event takes over the same user-style bubble and restores its clock, Copy, and Fork without a separate durable presentation path.
|
||||
Activating the action requests strict current-turn steering for that exact `InboxItemId`. Success removes the Queue row through the authoritative Host snapshot and immediately projects the same pending steering after the `Deep diving...` running-status row; that bubble offers Copy but no Fork because the message has no durable event sequence yet. Once AgentLoop drains it, the existing durable `user/message` event takes over the same user-style bubble and restores its clock, Copy, and Fork without a separate durable presentation path.
|
||||
|
||||
The running bit is only an interaction hint. AgentLoop's `acceptsNextStep` value is authoritative at the synchronous mutation boundary. If that window has closed, the operation leaves the Queue occurrence unchanged and returns a typed `steer-unavailable` error, after which the original waking occurrence proceeds through Queue. If the driver already claimed the occurrence, it returns the existing `queue-item-not-found` error and independent-turn delivery is already underway. The UI treats both races as converged Queue delivery without a failure notice; transport and unknown errors still surface.
|
||||
|
||||
@@ -36,13 +36,13 @@ The action does not run `agent/prompt-submit`: choosing steering intentionally c
|
||||
|
||||
The Host's existing `queuedMirror` remains the sole transient inbox authority. Its `session/queue` snapshot carries every live occurrence with `placement: 'queued' | 'steering'`: QueueDock renders only queued rows, while ChatView renders pending steering at the conversation tail after the `Deep diving...` running-status row, with Copy but without Fork, edit, or delete actions. Reconnect replays the same snapshot, so this visibility does not require client optimism or a second registry.
|
||||
|
||||
When AgentLoop claims pending steering, it emits `agent/inbox/dequeue` immediately before synchronously appending `steering/message`. The Host retires that steering row on the following microtask, allowing the durable session event to enter the linear mux stream first. On the accepted live event, the client Session retires the first matching current steering occurrence before publishing its snapshot; history replay does not consume a later occurrence that reused the same `MessageId`. ChatView therefore renders one authority at a time without scanning durable history, and the durable projection restores the clock, Copy, and Fork against its logged event time and sequence. An append failure still retires the claimed row.
|
||||
When AgentLoop claims pending steering, it emits `agent/inbox/dequeue` immediately before synchronously appending the durable `user/message`. The Host retires that steering row on the following microtask, allowing the durable session event to enter the linear mux stream first. On the accepted live event, the client Session retires the first matching current steering occurrence before publishing its snapshot; history replay does not consume a later occurrence that reused the same `MessageId`. ChatView therefore renders one authority at a time without scanning durable history, and the durable projection restores the clock, Copy, and Fork against its logged event time and sequence. An append failure still retires the claimed row.
|
||||
|
||||
The existing `session.prompt(mode: 'steer')` contract remains best-effort for new primary-session input: outside the next-step window it becomes a waking follow-up. The composer carries an explicit `queue | steer` mode through slash adjudication and reference serialization before calling that contract. A browser-local submission policy owns the persisted busy-Enter preference and resolves plain versus accelerated Enter as complementary gestures only for steer-capable sessions; the Settings row and InputBar share that policy without duplicating storage or delivery-window authority. Only the Queue row action is strict, because either negative result converges through the original Queue occurrence.
|
||||
|
||||
### Verification
|
||||
|
||||
AgentLoop contract coverage holds prompt admission open, converts one exact queued occurrence, and proves the replacement steering occurrence keeps the message value and delivery receipt, drains as `steering/message`, and never starts its former independent turn. It also pins unavailable-window retention, claimed-address rejection, and re-entrant cancellation lifecycle conservation.
|
||||
AgentLoop contract coverage holds prompt admission open, converts one exact queued occurrence, and proves the replacement steering occurrence keeps the message value and delivery receipt, drains as a `user/message`, and never starts its former independent turn. It also pins unavailable-window retention, claimed-address rejection, and re-entrant cancellation lifecycle conservation.
|
||||
|
||||
Host schema and proxy tests cover the new action, both typed errors, placement-aware snapshots and reconnect replay, plus durable-before-retirement ordering. Client tests cover silent convergence of both semantic races, genuine error reporting, read-only subagent rows and Queue-only subagent gestures. Runtime and ChatView tests cover occurrence-aware pending-to-durable handoff, including repeated `MessageId` values, while Web ARIA snapshots cover pending steering after the running-status row with Copy alone and the durable node with clock, Copy, and Fork.
|
||||
|
||||
@@ -66,6 +66,6 @@ The keyless Web steering scenario queues a message through the real composer whi
|
||||
|
||||
## Consequences
|
||||
|
||||
`session/queue` describes a placement-aware transient inbox snapshot rather than a Queue-only list, so every consumer must filter by placement. Pending steering survives reconnect and appears immediately, but remains non-durable until `steering/message` commits. The running bit can also remain true briefly after the strict next-step window closes, so an enabled action may internally return `steer-unavailable` while the product continues through Queue without reporting failure.
|
||||
`session/queue` describes a placement-aware transient inbox snapshot rather than a Queue-only list, so every consumer must filter by placement. Pending steering survives reconnect and appears immediately, but remains non-durable until the durable `user/message` commits. The running bit can also remain true briefly after the strict next-step window closes, so an enabled action may internally return `steer-unavailable` while the product continues through Queue without reporting failure.
|
||||
|
||||
The explicit action changes delivery from an independently admitted turn to current-turn steering, so prompt-admission plugins do not process the converted message. Enqueue-before-discard lifecycle publication remains required for re-entrant cancellation safety; focused regression coverage protects that ordering.
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
Web composer 原本会在 agent 运行期间把所有 Enter 提交作为 Queue 入队。QueueDock 已经为每条待处理消息提供可寻址的行,持久 transcript(文本记录)也已能把消费后的 `steering/message` 事件渲染为用户样式气泡,但 Web 既没有连接这两个界面的操作,也没有让用户从 composer 直接选择当前轮次 steering 的手势。
|
||||
Web composer 原本会在 agent 运行期间把所有 Enter 提交作为 Queue 入队。QueueDock 已经为每条待处理消息提供可寻址的行,持久 transcript(文本记录)也已能把消费后的 steer 事件渲染为用户样式气泡,但 Web 既没有连接这两个界面的操作,也没有让用户从 composer 直接选择当前轮次 steering 的手势。
|
||||
|
||||
如果 Web 先在客户端删除该行,再调用 `session.prompt(mode: 'steer')`,就会把用户的一次意图拆分到两个 RPC 中。驱动器可能在两次调用之间先认领该项,steering 投递也可能在删除后失败;现有尽力而为的 `agent.steer()` 回退还可能在原单次入队项被移除后,静默追加一个新的 Queue 项。因此,立即发送操作必须区分当前轮次 steering 与 Queue 前移,并在 steering 已不可用时保留原行。
|
||||
|
||||
@@ -16,7 +16,7 @@ Web composer 原本会在 agent 运行期间把所有 Enter 提交作为 Queue
|
||||
|
||||
普通会话中每个非编辑态的 QueueDock 行都会提供名为“插话发送”的向上箭头操作。仅当会话报告 agent 正在运行时,该操作才会启用;包含混合内容的消息仍可使用,因为 steering 会转发完整且不可变的 `UserMessage`,而非该行的文本投影。已寻址 subagent 的 Queue 投影保持只读,因为其继续执行传输不提供 Queue 变更。
|
||||
|
||||
触发该操作会针对对应的 `InboxItemId` 请求严格的当前轮次 steering。操作成功后,权威 Host 快照会移除 Queue 行,并在 `Deep diving...` 运行状态行之后立即投影同一条待处理 steering;该气泡提供复制,但消息尚无持久事件序号,因此不提供 fork。AgentLoop 排空该项后,现有持久 `steering/message` 事件会接管同一个用户样式气泡,并恢复时钟、复制和 fork,无需另建持久展示路径。
|
||||
触发该操作会针对对应的 `InboxItemId` 请求严格的当前轮次 steering。操作成功后,权威 Host 快照会移除 Queue 行,并在 `Deep diving...` 运行状态行之后立即投影同一条待处理 steering;该气泡提供复制,但消息尚无持久事件序号,因此不提供 fork。AgentLoop 排空该项后,现有持久 `user/message` 事件会接管同一个用户样式气泡,并恢复时钟、复制和 fork,无需另建持久展示路径。
|
||||
|
||||
running 标志位只用于提示交互状态。在同步变更边界上,AgentLoop 的 `acceptsNextStep` 值才是权威依据。如果该窗口已经关闭,操作会保持 Queue 单次入队项不变并返回类型化的 `steer-unavailable` 错误,随后原唤醒单次入队项会经 Queue 继续执行。如果驱动器已经认领该项,则返回现有的 `queue-item-not-found` 错误,且独立轮次投递已经开始。UI 会把两种竞态都视为已收敛的 Queue 投递,不显示失败通知;传输和未知错误仍会显示。
|
||||
|
||||
@@ -36,13 +36,13 @@ Composer 对新输入采用另一套尽力而为契约。所寻址会话空闲
|
||||
|
||||
Host 仍以现有 `queuedMirror` 作为唯一的瞬态 inbox 权威。`session/queue` 快照会携带所有存活单次入队项及其 `placement: 'queued' | 'steering'`:QueueDock 只渲染 queued 行,ChatView 则在会话流末尾、`Deep diving...` 运行状态行之后渲染待处理 steering,提供复制操作,但不提供 fork、编辑或删除操作。重连会重放同一份快照,因此这项可见性既不依赖客户端乐观展示,也不需要第二个 registry。
|
||||
|
||||
AgentLoop 认领待处理 steering 时,会在同步追加 `steering/message` 之前立即发出 `agent/inbox/dequeue`。Host 会等到下一个微任务才退役该 steering 行,让持久 session 事件先进入线性 mux 流。客户端 Session 接纳该实时事件时,会在发布快照前退役第一个匹配的当前 steering 单次入队项;历史回放不会消费后来复用同一 `MessageId` 的单次入队项。因此,ChatView 无需扫描持久历史就能每次只渲染一份权威,持久投影则会根据已记录的事件时间与序号恢复时钟、复制与 fork 操作。追加失败时,已认领行仍会退役。
|
||||
AgentLoop 认领待处理 steering 时,会在同步追加持久 `user/message` 之前立即发出 `agent/inbox/dequeue`。Host 会等到下一个微任务才退役该 steering 行,让持久 session 事件先进入线性 mux 流。客户端 Session 接纳该实时事件时,会在发布快照前退役第一个匹配的当前 steering 单次入队项;历史回放不会消费后来复用同一 `MessageId` 的单次入队项。因此,ChatView 无需扫描持久历史就能每次只渲染一份权威,持久投影则会根据已记录的事件时间与序号恢复时钟、复制与 fork 操作。追加失败时,已认领行仍会退役。
|
||||
|
||||
现有 `session.prompt(mode: 'steer')` 对主会话新输入仍采用尽力而为的契约:在 next-step 窗口之外,它会变为唤醒 agent 的后续轮次。Composer 会让显式 `queue | steer` 模式经过 slash 裁决与引用序列化,再调用该契约。浏览器本地的提交策略拥有持久化的繁忙态 Enter 偏好,并且只为支持 steering 的会话把普通 Enter 与加速 Enter 解析为互补手势;Settings 行和 InputBar 共享该策略,不重复实现存储或投递窗口权威。只有 Queue 行操作采用严格语义,因为任一种负面结果都会经原 Queue 单次入队项收敛。
|
||||
|
||||
### 验证
|
||||
|
||||
AgentLoop 契约覆盖保持提示词接纳窗口打开,转换一个精确的 queued 单次入队项,并证明替代它的 steering 单次入队项保留消息值和投递回执、以 `steering/message` 的形式排空,且绝不启动原本的独立轮次。该覆盖还钉住窗口不可用时保留原项、拒绝已被认领的地址,以及可重入取消下的生命周期守恒。
|
||||
AgentLoop 契约覆盖保持提示词接纳窗口打开,转换一个精确的 queued 单次入队项,并证明替代它的 steering 单次入队项保留消息值和投递回执、以 `user/message` 的形式排空,且绝不启动原本的独立轮次。该覆盖还钉住窗口不可用时保留原项、拒绝已被认领的地址,以及可重入取消下的生命周期守恒。
|
||||
|
||||
Host schema 和代理测试覆盖新操作、两种类型化错误、带 placement 的快照与重连重放,以及先持久化再退役的顺序。客户端测试覆盖两种语义竞态的静默收敛、真实错误报告、只读 subagent 行和仅支持 Queue 的 subagent 手势。运行时与 ChatView 测试覆盖按单次入队项完成的待处理到持久交接,包括重复的 `MessageId` 值;Web ARIA 快照则覆盖位于运行状态行之后且仅有复制的待处理 steering,以及带时钟、复制和 fork 的持久节点。
|
||||
|
||||
@@ -66,6 +66,6 @@ Host schema 和代理测试覆盖新操作、两种类型化错误、带 placeme
|
||||
|
||||
## 后果
|
||||
|
||||
`session/queue` 表示带 placement 的瞬态 inbox 快照,而不只是 Queue 列表,因此每个消费方都必须按 placement 过滤。待处理 steering 会在界面中立即出现并能在重连后恢复,但在 `steering/message` 提交前仍不持久。严格 next-step 窗口关闭后,running 标志位仍可能短暂保持为 true,因此已启用的操作可能会在内部返回 `steer-unavailable`,而产品仍经 Queue 继续执行且不显示失败。
|
||||
`session/queue` 表示带 placement 的瞬态 inbox 快照,而不只是 Queue 列表,因此每个消费方都必须按 placement 过滤。待处理 steering 会在界面中立即出现并能在重连后恢复,但在持久 `user/message` 提交前仍不持久。严格 next-step 窗口关闭后,running 标志位仍可能短暂保持为 true,因此已启用的操作可能会在内部返回 `steer-unavailable`,而产品仍经 Queue 继续执行且不显示失败。
|
||||
|
||||
这项显式操作会把投递方式从经独立接纳的轮次改为当前轮次 steering,因此提示词接纳插件不会处理转换后的消息。为保证可重入取消安全,生命周期事件仍必须先发布 enqueue 再发布 discard;有针对性的回归覆盖会保护这一顺序。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-02-pwsh-tool-bash-parity.md
|
||||
2026-08-02-pwsh-tool-bash-parity.md: bf40c440b9f7f330412d8949f59c54d541152d45
|
||||
2026-08-02-pwsh-tool-bash-parity.zh.md: bc67dac29900eacf9e615e76fe947e3e642a3979
|
||||
2026-08-02-pwsh-tool-bash-parity.md: 945d2d5243162fe8e7fb3f76cbc3bcf0b5c2fdee
|
||||
2026-08-02-pwsh-tool-bash-parity.zh.md: f537e313a0c895927c6e2319b11b98619a70d461
|
||||
|
||||
@@ -16,7 +16,7 @@ The first Windows-native foundation shipped `dsh-tool-pwsh` as a deliberately mi
|
||||
- **`run_in_background` is wired through the generic task runtime** exactly like the bash tool: preflight, owner registration, `task_output`/`task_kill` control, and the same outcome mapping. `pwsh-local`'s already-mirrored `start()` handle backs it.
|
||||
- **The `DSH_*` environment is shared, not duplicated**: `BashEnvRegistry` moved out of `dsh-tool-bash` into a new tool-independent `@deepseek-ai/dsh-bash-env` package (`ctx.bashEnv` + built-ins + the session-persistence contributor), and both shell tools inject it. Contributors apply to pwsh calls exactly as they do to bash calls, resolving the bash tool's `FIXME(bash-env-ownership)`.
|
||||
- **Windows reality is pinned where bash has no analog**: every command runs under a UTF-8 output preamble so the Windows PowerShell 5.1 fallback cannot garble non-ASCII output through the UTF-8-decoding collector, and the prompts teach that Windows forced termination settles as exit 1 without a signal marker.
|
||||
- **Out of scope, unchanged**: sandbox escalation (waits for a Windows-confining executor), persistent PTY shells (backends are Linux/macOS-only; ConPTY is roadmap work), and pwsh-specific TUI/GUI presentation (generic/terminal cards stay; a PowerShell-aware terminal card with an exit pill is roadmap work).
|
||||
- **Out of scope, unchanged**: sandbox escalation (waits for a Windows-confining executor) and persistent PTY shells (backends are Linux/macOS-only; ConPTY is roadmap work). The pwsh-specific terminal card with an exit pill shipped separately in the [pwsh UI presentation matches bash](2026-08-05-pwsh-ui-bash-parity.md) decision.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -33,4 +33,4 @@ The first Windows-native foundation shipped `dsh-tool-pwsh` as a deliberately mi
|
||||
- `@deepseek-ai/dsh-bash-env` is a new shipped package; `dsh-tool-bash`'s `dshHome` config moved there, so compositions mounting the shell tools must also mount `bash-env` (the spine bundles do).
|
||||
- Windows-only semantics (CRLF normalization, forced-termination exit-1/signal-null, POSIX-only self-signal) remain pinned by tests as before.
|
||||
- The pwsh tool's per-file coverage gate rides on the scriptable fake-executor suite (`tests/tools.spec.ts`); the real-pwsh integration and Loader-composition suites self-skip where `pwsh` is absent, mirroring the bash suites' division of labor.
|
||||
- The roadmap proposal's parity stage is delivered; its remaining stages are the Windows default composition and pwsh TUI/GUI rendering.
|
||||
- The roadmap proposal's parity stage is delivered; the terminal-card presentation stage shipped in the [pwsh UI presentation matches bash](2026-08-05-pwsh-ui-bash-parity.md) decision (the TUI itself was removed), leaving the Windows default composition as the remaining stage.
|
||||
|
||||
@@ -16,7 +16,7 @@ Status: implemented
|
||||
- **`run_in_background` 经通用任务运行时接线**,与 bash 工具完全一致:预检、owner 注册、`task_output`/`task_kill` 控制与相同的结果映射。其背后是 `pwsh-local` 早已镜像好的 `start()` 句柄。
|
||||
- **`DSH_*` 环境共享而非复制**:`BashEnvRegistry` 从 `dsh-tool-bash` 迁入新的工具无关包 `@deepseek-ai/dsh-bash-env`(`ctx.bashEnv` + 内置事实 + session-persistence contributor),两个 shell 工具都注入它。contributor 对 pwsh 调用与 bash 调用一视同仁,并消化了 bash 工具的 `FIXME(bash-env-ownership)`。
|
||||
- **Windows 现实在 bash 无对应处钉死**:每条命令都在 UTF-8 输出 preamble 下运行,使 Windows PowerShell 5.1 兜底无法经 UTF-8 解码的 collector 破坏非 ASCII 输出;prompt 教授 Windows 强制终止以无 signal 的 exit 1 结算。
|
||||
- **范围外,不变**:sandbox 升级(等待 Windows-confining 执行器)、持久 PTY shell(后端仅限 Linux/macOS;ConPTY 属路线图)、pwsh 专属 TUI/GUI 呈现(维持 generic/terminal 卡;带退出 pill 的 PowerShell 感知 terminal 卡属路线图)。
|
||||
- **范围外,不变**:sandbox 升级(等待 Windows-confining 执行器)与持久 PTY shell(后端仅限 Linux/macOS;ConPTY 属路线图)。带退出 pill 的 pwsh 专属 terminal 卡已随 [pwsh UI 呈现与 bash 对齐](2026-08-05-pwsh-ui-bash-parity.md) 决策另行交付。
|
||||
|
||||
## 备选方案
|
||||
|
||||
@@ -33,4 +33,4 @@ Status: implemented
|
||||
- `@deepseek-ai/dsh-bash-env` 成为新的交付包;`dsh-tool-bash` 的 `dshHome` 配置迁往那里,因此挂载 shell 工具的组合也必须挂载 `bash-env`(spine bundle 已如此)。
|
||||
- Windows 专属语义(CRLF 归一化、强制终止 exit-1/signal-null、仅 POSIX 的自信号)一如既往由测试钉住。
|
||||
- pwsh 工具的 per-file 覆盖门禁由可脚本化的 fake-executor 套件(`tests/tools.spec.ts`)承担;真实 pwsh 的集成与 Loader 组合套件在无 `pwsh` 的宿主自跳过,与 bash 套件的分工一致。
|
||||
- 路线图提案的 parity 阶段已交付;其余阶段是 Windows 默认组合与 pwsh TUI/GUI 渲染。
|
||||
- 路线图提案的 parity 阶段已交付;terminal 卡呈现阶段随 [pwsh UI 呈现与 bash 对齐](2026-08-05-pwsh-ui-bash-parity.md) 决策交付(TUI 本身已移除),剩余阶段是 Windows 默认组合。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-04-web-context-source-and-steer-marks.md
|
||||
2026-08-04-web-context-source-and-steer-marks.md: dca08d003df810932a2b5565e42d167781d593f7
|
||||
2026-08-04-web-context-source-and-steer-marks.zh.md: b0dc4c880778df86965e2732a69aaddd0c85d80f
|
||||
2026-08-04-web-context-source-and-steer-marks.md: 9070ea6ed34fffecd9fd2b90275bd31155100c75
|
||||
2026-08-04-web-context-source-and-steer-marks.zh.md: 9d7c7c0a34587071e281ff8b2cb77e359a1580c1
|
||||
|
||||
@@ -8,7 +8,7 @@ English | [中文](2026-08-04-web-context-source-and-steer-marks.zh.md)
|
||||
|
||||
Everything a producer adds to the model-facing conversation reached the Web transcript as one of two anonymous shapes. Every logged non-user `user/message` — the skill catalog, the runtime snapshot, reconciled `AGENTS.md` instructions, a guard notice, a subagent report, a cross-session snapshot — collapsed into one identical `上下文注入` row, so a reader could not tell what had been added without expanding each row and reading raw JSON. Mid-turn steering was worse: it rendered in exactly the bubble a turn-opening prompt uses, leaving the transcript unable to say which message interrupted a running turn.
|
||||
|
||||
The distinctions are already durable. `user/message.source` is the merge-extensible provenance every producer must supply, and steering has its own `steering/message` event; only the presentation discarded them. The terminal transcript this Web UI replaced did name each card's producer, so the Web surface was a regression for the same log.
|
||||
The distinctions are already durable. `user/message.source` is the merge-extensible provenance every producer must supply, while `agent/inbox/spliced` records whether an identified message entered and left `next-turn` or `next-step`; only the presentation discarded them. The terminal transcript this Web UI replaced did name each card's producer, so the Web surface was a regression for the same log.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -20,7 +20,7 @@ The transcript names all three roles a non-prompt message can play — injected
|
||||
|
||||
`recall` covers `session-reference` because that is the one shipped source that lifts another session's material into this one. No Web leaf mounts `dsh-session-reference` today — it had only a terminal host — so the arm exists for log portability rather than for a bundled producer, and it is exercised by unit coverage rather than an assembled Web scenario.
|
||||
|
||||
`MessageItem` captions durable and pending steering bubbles with `插话`. This reverses one clause of [no steer entry or interjection chrome](../simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md), which removed the badge because the composer could not steer and the label named a gesture users could not perform. The composer gained a Steer gesture afterwards without amending that note; this decision supplies the product decision its reintroduction clause required, and corrects the stale facts left in it. The caption is the only steering chrome here: composer modes, the Queue dock's strict-steer action, and pending-steering lifecycle stay with their own owners.
|
||||
`MessageItem` captions durable and pending steering bubbles with `插话`. The runtime replays durable `agent/inbox/spliced` events and projects a user-origin `user/message` as `SteeringMessageNode` when that same message identity was claimed from `next-step`; a queued-turn claim stays a `UserMessageNode`, and a non-user next-step message stays context. This reverses one clause of [no steer entry or interjection chrome](../simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md), which removed the badge because the composer could not steer and the label named a gesture users could not perform. The composer gained a Steer gesture afterwards without amending that note; this decision supplies the product decision its reintroduction clause required, and corrects the stale facts left in it. The caption is the only steering chrome here: composer modes, the Queue dock's strict-steer action, and pending-steering lifecycle stay with their own owners.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -36,7 +36,7 @@ The transcript names all three roles a non-prompt message can play — injected
|
||||
|
||||
## Testing
|
||||
|
||||
- `packages/client/runtime` unit coverage pins each provenance arm, the label fallbacks when a name field is missing, empty, or wrongly typed, and the unnamed degradation for a source with no readable kind.
|
||||
- `packages/client/runtime` unit coverage pins each provenance arm, the label fallbacks when a name field is missing, empty, or wrongly typed, the unnamed degradation for a source with no readable kind, and steering reconstruction on reset and live append paths.
|
||||
- `packages/client/ui-conversation` jsdom coverage pins the role title, the producer label beside it, the label's survival while expanded, the roleless header, and the steering caption on both durable and pending bubbles.
|
||||
- The keyless assembled-Web goldens carry the named header and the steering caption, so the assembled transcript — not only component tests — proves the marks.
|
||||
|
||||
@@ -45,4 +45,5 @@ The transcript names all three roles a non-prompt message can play — injected
|
||||
- A reader can attribute every non-prompt message in the transcript at a glance, and the header stays honest for logs this client version has never seen a producer for.
|
||||
- Producer names in the UI are package-shaped (`dsh-tool-skill`, `@deepseek-ai/dsh-system-prompt`) wherever the source carries only a plugin id. That is the cost of refusing a client-side name table; a producer that wants a better name records better provenance.
|
||||
- `ContextMessageNode` gains a required field, so every constructed node — including test fixtures — must supply it.
|
||||
- `SteeringMessageNode` remains a distinct presentation node even though the agent loop now records admitted steering as `user/message`; its identity comes from the durable inbox history rather than a separate message event.
|
||||
- The `recall` arm has no producer in a shipped Web leaf until a host mounts `dsh-session-reference`; it is reachable only through logs written elsewhere.
|
||||
|
||||
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
生产方向模型侧对话补充的一切内容,进入 Web transcript(文本记录)后只剩两种匿名形态。每一条已记录的非用户 `user/message`——skill 目录、运行时快照、经过对账的 `AGENTS.md` 指令、guard 提示、子 agent 汇报、跨会话快照——都塌缩成同一行 `上下文注入`,读者不逐行展开去读原始 JSON 就无从知道究竟注入了什么。steering(中途引导)的情况更糟:它渲染成与开轮提示完全相同的气泡,于是 transcript 无法说明哪一条消息打断了正在运行的轮次。
|
||||
|
||||
这些区分本来就是持久事实。`user/message.source` 是每个生产方都必须提供的可合并扩展来源,steering 另有自己的 `steering/message` 事件;把它们丢掉的只有呈现层。被这套 Web UI 取代的终端 transcript 本来会写出每张卡片的生产者,因此面对同一份日志,Web 侧是一次倒退。
|
||||
这些区分本来就是持久事实。`user/message.source` 是每个生产方都必须提供的可合并扩展来源,`agent/inbox/spliced` 则记录有身份的消息是从 `next-turn` 还是 `next-step` 进入和离开;把这些事实丢掉的只有呈现层。被这套 Web UI 取代的终端 transcript 本来会写出每张卡片的生产者,因此面对同一份日志,Web 侧是一次倒退。
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -20,7 +20,7 @@ transcript 为非提示消息可能承担的三种角色分别命名:注入上
|
||||
|
||||
`recall` 覆盖 `session-reference`,因为它是当前唯一会把另一个会话的材料搬进本会话的已发布来源。今天没有任何 Web 叶子挂载 `dsh-session-reference`——它此前只有终端宿主——因此该分支的存在是为了日志可移植性,而不是为了某个已打包的生产方,其覆盖来自单元测试而非组装后的 Web 场景。
|
||||
|
||||
`MessageItem` 为持久与待处理的 steering 气泡加上 `插话` 标注。这推翻了[取消 steer 入口与插话装饰](../simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md)中的一条结论——当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。
|
||||
`MessageItem` 为持久与待处理的 steering 气泡加上 `插话` 标注。runtime 会重放持久 `agent/inbox/spliced` 事件;如果一条用户来源的消息以相同身份从 `next-step` 被领取,后续 `user/message` 就投影为 `SteeringMessageNode`。从排队轮次领取的消息仍是 `UserMessageNode`,非用户来源的 next-step 消息仍是上下文。这推翻了[取消 steer 入口与插话装饰](../simplification/2026-07-31-web-ui-no-steer-entry-or-interjection-chrome.md)中的一条结论。当时移除徽章,是因为 composer 无法 steer,标签指向了用户做不到的动作。此后 composer 获得了 Steer 手势,却没有同步修订那份 note;本决策提供了它在「重新引入」条款中要求的产品决策,并订正了其中留下的过时事实。标注是这里唯一的 steering 装饰:composer 模式、Queue dock 的严格 steer 操作、待处理 steering 的生命周期仍归各自的所有者。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -36,7 +36,7 @@ transcript 为非提示消息可能承担的三种角色分别命名:注入上
|
||||
|
||||
## Testing
|
||||
|
||||
- `packages/client/runtime` 单元覆盖钉住每个来源分支、名称字段缺失/为空/类型不符时的回退,以及来源没有可读 kind 时的无名降级。
|
||||
- `packages/client/runtime` 单元覆盖钉住每个来源分支、名称字段缺失/为空/类型不符时的回退、来源没有可读 kind 时的无名降级,以及 reset 和实时 append 路径上的 steering 重建。
|
||||
- `packages/client/ui-conversation` 的 jsdom 覆盖钉住角色标题、标题旁的生产者名称、展开后该名称的留存、无名时的标题形态,以及持久与待处理气泡上的 steering 标注。
|
||||
- 无密钥的组装 Web 黄金基线携带带名称的标题栏与 steering 标注,因此证明这些标识的是组装后的 transcript,而不只是组件测试。
|
||||
|
||||
@@ -45,4 +45,5 @@ transcript 为非提示消息可能承担的三种角色分别命名:注入上
|
||||
- 读者一眼即可归因 transcript 中每一条非提示消息;即便面对本客户端版本从未见过其生产者的日志,标题栏依然如实。
|
||||
- 只要来源仅携带插件 id,UI 中的生产者名称就呈现为包名形态(`dsh-tool-skill`、`@deepseek-ai/dsh-system-prompt`)。这是拒绝客户端名称表的代价;想要更好名称的生产者应当记录更好的来源。
|
||||
- `ContextMessageNode` 增加了一个必填字段,因此每一处构造该节点的代码——包括测试 fixture——都必须提供它。
|
||||
- 即使 agent loop 现在把已经接纳的 steering 记录为 `user/message`,`SteeringMessageNode` 仍是独立的呈现节点;它的身份来自持久 inbox 历史,而不是独立消息事件。
|
||||
- 在某个宿主挂载 `dsh-session-reference` 之前,`recall` 分支在已发布的 Web 叶子中没有生产者,只能通过别处写入的日志抵达。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-context-form-vocabulary.md
|
||||
2026-08-05-context-form-vocabulary.md: 506fbd0ec9eff25c2369a2c127c5149efd1cd96e
|
||||
2026-08-05-context-form-vocabulary.zh.md: d2f92d0734e33e4cbf3ef97e5138b625ef6205b4
|
||||
2026-08-05-context-form-vocabulary.md: 618c11b925208d09a62d10101656e3358e327ddb
|
||||
2026-08-05-context-form-vocabulary.zh.md: 66278b261f23601461c96f08e87ea58766b2bfc2
|
||||
|
||||
@@ -31,7 +31,7 @@ That move also relocates catalog **identity**: the republish digest now covers t
|
||||
|
||||
**`snapshot`** — current state that a later snapshot from the same producer supersedes. The runtime-context snapshot, `time-context`, and `tmux-context` declare it. `renderContextSections()` exposes the assembly's named contributions, which `renderContextSnapshot()` already joined for the model, so the body attributes each part to the subsystem that produced it without re-splitting joined prose. The two single-contribution producers record one section each. The cleared runtime-context marker has no contributions left and declares no form.
|
||||
|
||||
**`notice`** — a one-off account of something that just happened. `tool-tasks`, `goal` state changes, `tool-goal` wrap-up, `plan-mode` switches, and `repeat-tool-guard` reminders declare it with a `summary`, which rides the **collapsed** row: a notice is meant to be read without expanding at all. The summary is bounded where its inputs are caller text (a task's label and status detail have no length of their own).
|
||||
**`notice`** — a one-off account of something that just happened. `tool-tasks`, `tool-goal` wrap-up, `plan-mode` switches, and `repeat-tool-guard` reminders declare it with a `summary`, which rides the **collapsed** row: a notice is meant to be read without expanding at all. The summary is bounded where its inputs are caller text (a task's label and status detail have no length of their own). Goal state changes remain domain-owned `goal/change` events rather than model context, so they declare no form.
|
||||
|
||||
**`relay`** — a message another agent addressed to this one. Both subagent-addressed sources declare it; the sender is shown as the opaque session id the source already records, because this client cannot resolve it to a title.
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Status: implemented
|
||||
|
||||
**`snapshot`**——会被同一生产方后续快照取代的当前状态。运行时快照、`time-context`、`tmux-context` 声明它。`renderContextSections()` 暴露出装配时的具名贡献——`renderContextSnapshot()` 本来就是把它们拼给模型的——因此内容区能把每一段归属到产生它的子系统,而不必去切分已经拼好的散文。两个单贡献生产方各记录一段。运行时快照的「已清空」标记没有任何贡献可归属,因此不声明形态。
|
||||
|
||||
**`notice`**——刚刚发生了什么的一次性说明。`tool-tasks`、`goal` 状态变更、`tool-goal` 收尾、`plan-mode` 切换与 `repeat-tool-guard` 提醒都带 `summary` 声明它,而该摘要出现在**折叠态**行上:notice 的全部意义就是不展开也能读完。摘要在其输入是调用方文本时自行封顶(任务的 label 与状态 detail 本身没有长度约束)。
|
||||
**`notice`**——刚刚发生了什么的一次性说明。`tool-tasks`、`tool-goal` 收尾、`plan-mode` 切换与 `repeat-tool-guard` 提醒都带 `summary` 声明它,而该摘要出现在**折叠态**行上:notice 的全部意义就是不展开也能读完。摘要在其输入是调用方文本时自行封顶(任务的 label 与状态 detail 本身没有长度约束)。Goal 状态变更仍是由领域层持有的 `goal/change` 事件,而非模型上下文,因此不声明 form。
|
||||
|
||||
**`relay`**——另一个 agent 发给本 agent 的消息。两个子 agent 定向来源都声明它;发送方以来源已记录的不透明会话 id 呈现,因为本客户端无法把它解析成标题。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-addressable-queue-operations.md
|
||||
2026-07-29-addressable-queue-operations.md: 04bcaa26be2f4f70c4003f6cc2fce4798499d205
|
||||
2026-07-29-addressable-queue-operations.zh.md: a16bf8667294de8d08da54ddb0d317bf9938e4eb
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md
|
||||
2026-08-05-pwsh-ui-bash-parity.md: 6bbdb0e6bc69ef1af03a6a9146f83b84754cb2a6
|
||||
2026-08-05-pwsh-ui-bash-parity.zh.md: 75f3a3ddec002acaa1755c81114b0f122ab80593
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: pwsh UI presentation matches bash
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-05-pwsh-ui-bash-parity.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2 — but the TUI package was removed ([`ed30088adb`](https://github.com/deepseek-harness/deepseek-harness/commit/ed30088adb)), leaving the Web surface as the only UI the gap affects.
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-tool-pwsh`'s `presentResult` now mirrors `dsh-tool-bash`'s call-for-call: a completed foreground result is a `terminal` card whose output body is the marker-free rendered text and whose exit-status pill is the parsed `exitCode`/`signal`; background acknowledgements and `isError` results stay generic `console`-fenced cards; non-single-text-block results stay untouched (`undefined`).
|
||||
|
||||
The parse is shared, not duplicated: `parseExitStatus`/`ParsedExitStatus` moved from `dsh-tool-bash`'s private render module into the `@deepseek-ai/dsh-bash` seam package (exported from its index), and `dsh-tool-bash`'s `render.ts` re-exports it so its source-plane consumers keep one import root. Both tools' renderers emit the same `[exit code: N]` / `[killed by signal: X]` markers, so one seam-owned inverse can never drift between the twins — the same "shared, not duplicated" shape the [bash-env extraction](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) used for the `DSH_*` registry.
|
||||
|
||||
The Web UI needs no per-tool code for the card itself: the client's terminal-card bridge maps any `card: 'terminal'` result view (`terminal-card-model` in `dsh-client-ui-conversation`), so the pwsh presenter change flows through the same rendering path bash already has. The collapsed tool row does get one client classification entry: `classifyTool('pwsh')` now yields the shell-family row (`bash` variant, its own `Pwsh` title) instead of the generic `others` "Tool call" row. A keyless browser lane (`apps/web/tests/pwsh-terminal.e2e.ts`) seeds a session whose pwsh call/result is presented by the real tool on replay — the api-proxy recomputes views from logged args/result content — and pins the terminal card golden, including the exit pill and the run-state dot.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Import `parseExitStatus` from `@deepseek-ai/dsh-tool-bash/src/render.ts`.** Rejected: workspace imports stay external in the built bundles, so `tool-pwsh` would gain a hard runtime dependency on `tool-bash` in every consumer closure (including compositions that deliberately mount the pwsh twin without bash), and a sibling tool depending on its twin for one function inverts the package relationship. The seam move keeps the shared contract on a package both tools already depend on.
|
||||
|
||||
**A new dedicated presentation package (e.g. `@deepseek-ai/dsh-shell-present`).** Rejected: a new package costs manifests, module-graph/catalog regeneration, and README surface for a single pure function; `@deepseek-ai/dsh-bash` is already in both tools' closures and already owns the `BashRunResult` facts the parse reconstructs.
|
||||
|
||||
**Duplicate the parse into `tool-pwsh`'s render module (a third twin).** Rejected: the parity review's core finding was that copied text contracts drift without a shared implementation; the parse and the marker emission must co-evolve in one place, and the parse is exactly the contract the UI pill depends on.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A Windows composition using `dsh-tool-pwsh` now shows its shell calls exactly as bash calls look in the Web UI: cwd-headed terminal card, raw output, exit-status pill, run-state dot, and the red failure treatment on non-zero exits.
|
||||
- `parseExitStatus` becomes public seam surface on `@deepseek-ai/dsh-bash`; `dsh-tool-bash/src/render.ts` keeps re-exporting it, so no bash-tool consumer changes.
|
||||
- The roadmap's stage 2 shrinks: the TUI is removed (EOL), and the terminal-card counterpart now ships on the Web surface. The Windows default composition (stage 1) remains the outstanding stage.
|
||||
- Verification: `dsh-bash` owns the parse edge cases under the per-file coverage gate; `tool-pwsh`'s presenter suite mirrors `tool-bash`'s (clean/non-zero/signal/timeout round-trip, marker-like output, background/error generics, multi-block fallback); the client row-model suite pins the `Pwsh` shell-family row; the web `pwsh-terminal` lane is the assembled keyless scenario.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: pwsh UI presentation matches bash
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-05-pwsh-ui-bash-parity.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2——但 TUI 包已被移除([`ed30088adb`](https://github.com/deepseek-harness/deepseek-harness/commit/ed30088adb)),Web 表面成为该缺口唯一影响的 UI。
|
||||
|
||||
## Decision
|
||||
|
||||
`dsh-tool-pwsh` 的 `presentResult` 现在逐调用镜像 `dsh-tool-bash`:完成的前台结果是 `terminal` 卡,输出正文为去 marker 的渲染文本,退出状态 pill 为解析出的 `exitCode`/`signal`;后台 ack 与 `isError` 结果保持通用 `console` 围栏卡片;非单一文本块结果保持不变(`undefined`)。
|
||||
|
||||
解析是共享而非复制:`parseExitStatus`/`ParsedExitStatus` 从 `dsh-tool-bash` 的私有 render 模块迁入 `@deepseek-ai/dsh-bash` seam 包(由其 index 导出),`dsh-tool-bash` 的 `render.ts` 再导出它,使源平面消费方保持单一导入根。两个工具的渲染器发出相同的 `[exit code: N]` / `[killed by signal: X]` marker,因此一个由 seam 拥有的逆解析永远不会在孪生之间漂移——与 [bash-env 抽取](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 处理 `DSH_*` 注册表时相同的 "共享而非复制" 形态。
|
||||
|
||||
Web UI 的卡片本身不需要任何按工具编写的代码:客户端的 terminal 卡桥接(`dsh-client-ui-conversation` 的 `terminal-card-model`)映射任意 `card: 'terminal'` 结果视图,因此 pwsh presenter 变更直接流经 bash 已有的同一渲染路径。折叠的工具行有一处客户端分类条目:`classifyTool('pwsh')` 现在归入 shell 家族行(`bash` variant,自有 `Pwsh` 标题),而非通用的 `others` "Tool call" 行。一条 keyless 浏览器通道(`apps/web/tests/pwsh-terminal.e2e.ts`)播种一个会话,其 pwsh 调用/结果在回放时由真实工具呈现(api-proxy 从已记录的 args/result 内容重新计算视图),并钉住 terminal 卡 golden,包括退出 pill 与运行状态点。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**从 `@deepseek-ai/dsh-tool-bash/src/render.ts` 导入 `parseExitStatus`。** 否决:workspace 导入在构建产物中保持外部引用,因此 `tool-pwsh` 会在每个消费方闭包中新增对 `tool-bash` 的硬运行时依赖(包括刻意只挂 pwsh 孪生、不挂 bash 的组合),且兄弟工具为其单个函数依赖孪生会颠倒包间关系。seam 迁移把共享契约放在两个工具本就依赖的包上。
|
||||
|
||||
**新建专用呈现包(如 `@deepseek-ai/dsh-shell-present`)。** 否决:为一个纯函数新建包要付出 manifest、module-graph/目录再生成与 README 面的成本;`@deepseek-ai/dsh-bash` 已在两个工具的闭包中,且已拥有该解析重建的 `BashRunResult` 事实。
|
||||
|
||||
**把解析复制进 `tool-pwsh` 的 render 模块(第三个孪生)。** 否决:parity 评审的核心发现正是"复制的文本契约缺少共享实现就会漂移";解析与 marker 发出必须在同一处共同演化,而解析恰恰是 UI pill 依赖的契约。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 使用 `dsh-tool-pwsh` 的 Windows 组合现在在 Web UI 中显示的 shell 调用与 bash 调用完全一致:cwd 头的 terminal 卡、原始输出、退出状态 pill、运行状态点,以及非零退出时的红色失败处理。
|
||||
- `parseExitStatus` 成为 `@deepseek-ai/dsh-bash` 的公开 seam 表面;`dsh-tool-bash/src/render.ts` 继续再导出它,bash 工具消费方零改动。
|
||||
- 路线图阶段 2 收窄:TUI 已移除(EOL),terminal 卡对应物现已在 Web 表面交付。Windows 默认组合(阶段 1)仍是未完成的阶段。
|
||||
- 验证:`dsh-bash` 在逐文件覆盖率门禁下拥有解析边界用例;`tool-pwsh` 的 presenter 套件镜像 `tool-bash` 的(干净/非零/信号/超时往返、marker 状输出、后台/错误 generic、多块回退);客户端行模型套件钉住 `Pwsh` shell 家族行;web `pwsh-terminal` 通道是组装后的 keyless 场景。
|
||||
Reference in New Issue
Block a user