Merge codex/invariant-service-seam into codex/invariant-service-review-fixes
# Conflicts: # .agents/notes/implemented/architecture/2026-07-19-package-invariant-runtime-contracts.i18n.yaml # scripts/test-invariants.spec.ts # scripts/test-invariants.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
|
||||
2026-07-19-package-invariant-runtime-contracts.md: ab4301a80b01ac2b4b2870f02c232ab321a6fab1
|
||||
2026-07-19-package-invariant-runtime-contracts.zh.md: 2e0816b2f381fd8fa51df51d8c482d6d4adf48b4
|
||||
2026-07-19-package-invariant-runtime-contracts.md: aa67747878654d8958e52357f96f3654ca64df39
|
||||
2026-07-19-package-invariant-runtime-contracts.zh.md: c0d308f95061a9f579201a72a4b011f98f79f891
|
||||
|
||||
@@ -27,7 +27,7 @@ The central `dsh-invariants` service owns only configuration, registration uniqu
|
||||
|
||||
### Implemented checks
|
||||
|
||||
The current 94-package workspace has 19 executable companions and 75 justified empty companions.
|
||||
The current 95-package workspace has 20 executable companions and 75 justified empty companions.
|
||||
|
||||
| Owner | Runtime relationship |
|
||||
|---|---|
|
||||
@@ -43,6 +43,7 @@ The current 94-package workspace has 19 executable companions and 75 justified e
|
||||
| `dsh-hook-protocol` | Hook invocation/result correlation, dialect, identity, and duration constraints. |
|
||||
| `dsh-sandbox-policy` | Durable `sandbox/mode` events use the closed sandbox-mode vocabulary. |
|
||||
| `dsh-fs` | Filesystem decision/observation events carry usable target and version identities. |
|
||||
| `dsh-goal` | Durable goal snapshots preserve source attribution, rendered content, revisions, lifecycle and timestamp relationships, and sequential admitted rounds. |
|
||||
| `dsh-subagent` | Provider add/remove and child start/end events preserve identity and pairing. |
|
||||
| `dsh-permission` | Durable permission decisions name a preset in the active permission table. |
|
||||
| `dsh-user-approval` | Approval asked/decided records pair by call and use valid outcomes and policies. |
|
||||
|
||||
@@ -27,7 +27,7 @@ Status: implemented
|
||||
|
||||
### 已实施的检查
|
||||
|
||||
当前 94 个包的 workspace 包含 19 个可执行 companion 和 75 个有理由的空 companion。
|
||||
当前 95 个包的 workspace 包含 20 个可执行 companion 和 75 个有理由的空 companion。
|
||||
|
||||
| 所有者 | 运行时关系 |
|
||||
|---|---|
|
||||
@@ -43,6 +43,7 @@ Status: implemented
|
||||
| `dsh-hook-protocol` | hook invocation/result 的关联、dialect、身份和 duration 约束。 |
|
||||
| `dsh-sandbox-policy` | 持久化 `sandbox/mode` 事件必须使用封闭的 sandbox-mode 词表。 |
|
||||
| `dsh-fs` | 文件系统决策/观测事件必须携带可用的 target 和 version 身份。 |
|
||||
| `dsh-goal` | 持久化目标快照保持来源归属、渲染内容、修订号、生命周期和时间戳关系,并保证已准入的目标回合连续编号。 |
|
||||
| `dsh-subagent` | provider add/remove 和 child start/end 事件必须保持身份与配对。 |
|
||||
| `dsh-permission` | 持久化 permission 决策必须引用当前 permission 表中的 preset。 |
|
||||
| `dsh-user-approval` | approval asked/decided 记录按 call 配对,并使用有效 outcome 和 policy。 |
|
||||
|
||||
@@ -0,0 +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
|
||||
2026-07-19-persisted-same-session-goal-domain.md: b0149016ab1b2a21c6d21798bf8b2117472a0f6c
|
||||
2026-07-19-persisted-same-session-goal-domain.zh.md: 33f44136da3f0045d9f4baad5154797a6e746bcc
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Note: Persisted same-session goal domain
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-19-persisted-same-session-goal-domain.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A long-running objective outlives one prompt, turn, or model request. Treating that objective as an in-memory loop variable loses it on process restart, while putting it only in UI state makes model behavior impossible to reconstruct. Treating every session turn as progress also charges unrelated human messages against an automatic-work budget.
|
||||
|
||||
Durable lifecycle and permission to continue are different facts. A session may retain an active objective after restart or fork, but silently starting work when a user opens that session is surprising. The domain needs replayable state without persisted auto-execution authority, and it must remain a plugin on the public agent/session seams rather than a special case in the concrete loop.
|
||||
|
||||
## Decision
|
||||
|
||||
`@deepseek-ai/dsh-goal` in `packages/goal/goal/` owns one current same-session goal through `ctx.goals`. A goal has a branded id, objective, durable phase, compare-and-set revision, and `maxGoalRounds`. `defaultMaxGoalRounds` is a validated deployment setting with default `256`; `create()` materializes it internally before mutation rather than exposing resolution as another service verb.
|
||||
|
||||
The durable phases are `active`, `paused`, `blocked`, and `complete`. A blocked snapshot includes a policy-owned lower-kebab-case code and a normalized free-form message, so usage limits, round caps, execution failures, and human-input dependencies share one lifecycle state without losing their cause. A separate live activation is `armed` or `disarmed`. Creation and explicit resume arm activation; pause, completion, blocking, and clear disarm it. Edits preserve activation and any blocker reason; resume and completion clear that reason. Activation is never part of the persisted snapshot.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### Lifecycle and live activation
|
||||
|
||||
At most one goal is current. Create requires no current non-complete goal and always generates a revision-one id not used earlier in the session; a completed goal may be replaced. Every other mutation carries the expected `GoalRef`, and stale ids or revisions reject. Resume accepts a paused or blocked phase, or a disarmed active goal, only when the round cap has remaining capacity. The domain validates blocker reason shape but deliberately leaves reason codes and the decision to block to policy consumers.
|
||||
|
||||
A cache built from any seed starts disarmed, and every `agent/session-start` edge disarms it again. Resume and fork therefore preserve the durable objective and history but never initiate work on their own. A later human prompt can be interpreted by the model, whose policy surface may explicitly call resume and arm the goal.
|
||||
|
||||
### 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`.
|
||||
|
||||
## 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 disarming and 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.
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
## Consequences
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Known limitations and deferred work
|
||||
|
||||
- This domain records state but does not schedule goal rounds, cancel active turns, or classify abnormal stops.
|
||||
- The actor that records `complete` or `blocked` is authoritative; an independent evaluator or completion certificate is deferred to a policy consumer.
|
||||
- There is one current goal per session; parallel objective graphs and cross-session goal storage are absent.
|
||||
- Plugins share one trusted process boundary. Direct session writers can counterfeit goal records; strict replay detects inconsistency and fails goal access at the offending record, but does not isolate plugins or repair the log.
|
||||
- `GOAL_CHANGE_VERSION` has no pre-release compatibility promise or migration path.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Agent Note: 持久的同会话目标领域
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-19-persisted-same-session-goal-domain.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
长时间运行的目标会跨越单个提示词、轮次或模型请求。若把该目标视为内存中的循环变量,进程重启时就会丢失;若只存放在 UI 状态中,又无法重建模型行为。若把会话中的每个轮次都视为目标进度,与自动工作无关的人类消息也会消耗预算。
|
||||
|
||||
持久生命周期与继续执行的权限是两个不同事实。会话在重启或 fork(派生)后可以保留活跃目标,但用户打开会话时静默启动工作并不符合直觉。该领域需要可回放的状态,却不能持久化自动执行权限;它还必须作为公共 agent(智能体)与会话接缝上的插件存在,而不是具体循环中的特例。
|
||||
|
||||
## 决策
|
||||
|
||||
位于 `packages/goal/goal/` 的 `@deepseek-ai/dsh-goal` 通过 `ctx.goals` 管理一个当前的同会话目标。目标包含品牌化 id、目标描述、持久阶段、比较并交换修订号和 `maxGoalRounds`。`defaultMaxGoalRounds` 是经过校验的部署配置,默认值为 `256`;`create()` 在变更前于内部将其解析为完整值,而不会把解析过程暴露为额外的服务动词。
|
||||
|
||||
持久阶段包括 `active`、`paused`、`blocked` 和 `complete`。阻塞快照包含由策略提供的 kebab-case 小写代码和规范化自由文本消息,因此用量限制、回合上限、执行失败和等待人工输入可以共享一个生命周期状态而不丢失原因。独立的实时激活态为 `armed` 或 `disarmed`。创建与显式恢复会激活目标;暂停、完成、阻塞和清除都会解除激活。编辑保留激活态及阻塞原因;恢复和完成会清除该原因。持久快照绝不包含激活态。
|
||||
|
||||
### 持久记录与回放
|
||||
|
||||
每次非清除变更都通过 `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 会继承目标记录,而无需另设数据库或头字段。
|
||||
|
||||
回放折叠会校验 JSON 形状、来源归属、渲染内容、新 id、修订连续性、生命周期转换、计数器以及单个目标内单调递增的时间戳。目标回合是当前活跃修订上带正数且连续编号的 `user/message` 来源,且不能超过 `maxGoalRounds`;普通会话轮次不会影响该计数器。当前格式的畸形记录会使回放失败,而不会被忽略或修复。
|
||||
|
||||
当 `Agent.inject()` 在活跃工具批次中延迟变更时,服务会在进程内叠加已接受的载荷,使后续变更可以使用新的修订号。FIFO 追加可见后,协调过程只移除完全匹配的载荷;重入的追加观察器对每次变更只投影一次。增量回放会在每个有效事件后推进游标,并停留在首个损坏事件处,因此后续读取会报告同一个持久故障。重启后仍以持久日志为准。
|
||||
|
||||
### 生命周期与实时激活态
|
||||
|
||||
最多只有一个当前目标。创建要求不存在未完成的当前目标,并始终生成该会话此前未使用过、修订号为一的 id;已完成目标可以被替换。其他每次变更都携带预期的 `GoalRef`,陈旧的 id 或修订号会被拒绝。仅当回合上限仍有余量时,暂停或阻塞阶段以及已解除激活的活跃目标才能恢复。领域层校验阻塞原因的形状,但会把原因代码和是否阻塞的决策留给策略消费者。
|
||||
|
||||
从任何种子构建的缓存都以未激活状态开始,每次 `agent/session-start` 边沿也会再次解除激活。因此,恢复和 fork 会保留持久目标与历史,但绝不会自行启动工作。后续人类提示词可由模型解释,其策略表面可以显式调用恢复操作并激活目标。
|
||||
|
||||
### 服务边界
|
||||
|
||||
服务只接受在对应 id 下注册的同一个实时 `Agent` 对象。成功注入变更后,它会发出带作用域的 `goal/changed` 事件,并隔离监听器失败。策略消费者通过本服务、公共 `Agent` 接口和 `agent/*` 事件工作;目标领域既不导入也不修改 `dsh-agent-loop`。
|
||||
|
||||
## 测试
|
||||
|
||||
单元测试固定创建默认值、精确实时 agent 校验、比较并交换拒绝、所有生命周期转换、阻塞原因校验与保留、恢复时的上限执行、清除与替换、种子回放和 `SessionStore.fork()` 继承、会话启动时解除激活与活跃目标重新激活、FIFO 延迟变更协调、重入追加观察、注入拒绝回滚、损坏事件的稳定回放、服务与监听器销毁、监听器隔离、挂钟后退钳制、严格记录解码、生命周期连续性、来源与内容一致性,以及连续目标回合归属。无密钥 Loader/stdio 进程测试通过测试专用 `cordis.yml` 挂载服务与生命周期消费者,再从外部读取持久 JSONL,以验证模型可见快照以及不存在未经请求的目标回合。包源码受仓库逐文件 100% 覆盖率门禁约束。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **把目标存入独立数据库或会话头**——不予采纳,因为会话日志已经提供顺序、持久化、fork 前缀与可重建性;第二份存储会引入原子性和谱系问题。
|
||||
- **使用模型不可见的纯日志事件**——不予采纳,因为会改变后续模型行为的持久状态必须满足仓库日志不变量,保持模型可见且可重建。
|
||||
- **持久化激活态并自动重启**——不予采纳,因为打开或恢复会话时必须等待人类输入;持久阶段记录状态,而不是再次消耗资源的授权。
|
||||
- **把所有会话轮次都计为目标回合**——不予采纳,因为同一会话可以包含人类澄清、检查和无关工作;只有归属于目标的继续执行轮次才消耗该预算。
|
||||
- **向 `dsh-agent-loop` 添加目标状态或通用循环抽象**——不予采纳,因为状态与继续执行策略可以通过现有插件、`Agent` 动词和事件组合,而无需赋予默认循环实现特权。
|
||||
|
||||
## 后果
|
||||
|
||||
- 目标历史作为普通会话数据,在持久化、恢复、无关节点压缩和会话 fork 后继续保留。
|
||||
- 恢复与 fork 会暴露同一持久阶段,但在显式恢复变更激活目标前不会执行任何操作。
|
||||
- 完整快照便于检查和严格回放,但在压缩隐藏它们之前,会在模型历史中重复目标描述与状态字段。
|
||||
- 修订号与生命周期校验会尽早拒绝遭篡改、部分写入或生产者不一致的目标记录。
|
||||
- 回合上限只约束继续执行次数;当回合、token、费用、时间或提供方限制停止工作时,策略消费者会把它们映射为不同的阻塞原因。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- 本领域记录状态,但不调度目标回合、不取消活跃轮次,也不分类异常停止。
|
||||
- 记录 `complete` 或 `blocked` 的参与者具有最终权威;独立评估器或完成证书延期到策略消费者中实现。
|
||||
- 每个会话只有一个当前目标;不存在并行目标图和跨会话目标存储。
|
||||
- 插件共享同一个受信任的进程边界。直接写入会话的插件可以伪造目标记录;严格回放会检测不一致并在违规记录处使目标访问失败,但不会隔离插件或修复日志。
|
||||
- `GOAL_CHANGE_VERSION` 在首次发布前不承诺兼容性,也不提供迁移路径。
|
||||
@@ -12,9 +12,9 @@ The boundary bought package metadata, workspace and tsconfig references, module-
|
||||
|
||||
## Decision
|
||||
|
||||
The helper lives in `@deepseek-ai/dsh-stdio` as the terminal-channel plugin (`packages/ui/stdio/src/index.ts`): `createStdioChat`, its `StdioRuntime` test seam, and its unit tests (`packages/ui/stdio/tests/stdio.spec.ts`, `readline.spec.ts`) moved with it, so EOF handling, rendering, disposal, and piped-vs-TTY behavior stay unit-covered under the per-file coverage gate without hijacking process globals. The module keeps the named `name`/`inject`/`Config`/`apply` export shape — the contract the app's `ctx.plugin(uiStdio, …)` mount consumes — and the keyless Loader-path smokes in `examples/echo-agent` and `examples/repl-agent` keep proving the composed tree boots through the real Loader (the stdio package's plugin-shape unit suite pins the explicit `unwrapExports` assertion, since a bundle without `inject` would boot past a stray default rather than crash).
|
||||
At the time, the helper moved into `@deepseek-ai/dsh-stdio` as the terminal-channel plugin. `createStdioChat`, its `StdioRuntime` test seam, and its unit tests moved with it, keeping EOF handling, rendering, disposal, and piped-vs-TTY behavior under the per-file coverage gate without hijacking process globals. The module kept the named `name`/`inject`/`Config`/`apply` export shape consumed by the app mount, while the then-current Echo and REPL Loader smokes proved the composed tree and the plugin-shape suite pinned explicit `unwrapExports` behavior. The superseding removal note above owns the current package and example state.
|
||||
|
||||
The `packages/support/ui-stdio` package is gone: manifest, tsconfig references, module-graph rows, and README rows deleted; the doc comments that named the package (the example e2e module docs, `packages/README.md`, the support and todo READMEs, [the ui group README](../../../../packages/ui/README.md)) describe the in-package module.
|
||||
The earlier support helper package was removed: its manifest, tsconfig references, module-graph rows, and README rows disappeared, while the remaining documentation described the in-package module.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
Reference in New Issue
Block a user