docs(rfc): pin the fold's read-path rule for a resumed log with an edited config

A folded mode name the current config no longer defines behaves as default
plus one boundary notice naming the dropped definition — set()'s loud
validation covers only the write path.
This commit is contained in:
kingwl
2026-07-07 16:58:30 +08:00
parent 9b8a661df9
commit ac54703ceb
3 changed files with 4 additions and 4 deletions

View File

@@ -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-07-plan-mode.md: 0897f66b7bdaa789c0cae2bc348690da77c6f42c
2026-07-07-plan-mode.zh.md: 9c7b1fe6e9cb4416e67fcf242bd80d00a3e0cc45
2026-07-07-plan-mode.md: b28984bfad7e9a1adb8bbaaa4daa79a5c00fdf65
2026-07-07-plan-mode.zh.md: 2b9be1cea702362ec3852c3da48909e6acdf25cf

View File

@@ -72,7 +72,7 @@ The allowlist is deliberately the degenerate form of a future per-tool decision
### The fold, the service, and the flush
`foldMode(events)` is pure (exported for reconstructors and tests); the service tracks it per session with a lazy cursor in a `WeakMap<Session, { cursor, mode }>` — O(new events) per read, never invalidated, because the log is append-only and `mode/set` is not a surface node (compaction cannot rewrite it). `ctx.modes` (a cordis Service, key `modes`) exposes `list()` — the synthetic `default` entry plus the configured definitions, for pickers — `get(agent): { current, pending? }`, and `set(agent, mode)`, which validates the name against config, drops a no-op (target equals pending ?? current), and otherwise records the intent in a `WeakMap<Session, string>`. A contained `session/event` listener ([defensive patterns](../../../defensive-patterns.md): a policy plugin must not kill the feed) flushes the pending intent as a `mode/set` append on the next `turn/start` or `step/end` — both sit outside the step's tool-execution window, so the executions of a step always run under the mode its assembly folded — and, when the flushed mode differs from the fold at the last `request/header`, appends the one coalesced `context/message` notice in the same frame. Seeding rides `agent/created`: a declaration-merged `AgentOptions.mode` becomes a pending intent, so explicit options beat the logged baseline on create AND resume — the same precedence the call-config seed follows — while a fork child needs no mechanism at all (the parent's `mode/set` is inside the seeded prefix).
`foldMode(events)` is pure (exported for reconstructors and tests); the service tracks it per session with a lazy cursor in a `WeakMap<Session, { cursor, mode }>` — O(new events) per read, never invalidated, because the log is append-only and `mode/set` is not a surface node (compaction cannot rewrite it). `ctx.modes` (a cordis Service, key `modes`) exposes `list()` — the synthetic `default` entry plus the configured definitions, for pickers — `get(agent): { current, pending? }`, and `set(agent, mode)`, which validates the name against config, drops a no-op (target equals pending ?? current), and otherwise records the intent in a `WeakMap<Session, string>`. A contained `session/event` listener ([defensive patterns](../../../defensive-patterns.md): a policy plugin must not kill the feed) flushes the pending intent as a `mode/set` append on the next `turn/start` or `step/end` — both sit outside the step's tool-execution window, so the executions of a step always run under the mode its assembly folded — and, when the flushed mode differs from the fold at the last `request/header`, appends the one coalesced `context/message` notice in the same frame. Seeding rides `agent/created`: a declaration-merged `AgentOptions.mode` becomes a pending intent, so explicit options beat the logged baseline on create AND resume — the same precedence the call-config seed follows — while a fork child needs no mechanism at all (the parent's `mode/set` is inside the seeded prefix). One read-path rule closes the resume-with-edited-config gap: a folded mode name the current config no longer defines behaves as the default mode plus one boundary notice naming the dropped definition — never a silent substitute restriction, never a bricked session; `set()`'s loud validation covers only the write path, and a resumed log answers to the config it finds.
### The soft layer: a computed section and a post-`next()` filter

View File

@@ -72,7 +72,7 @@ allowlist 刻意是未来按工具决定映射(`allow | deny | ask`)的退
### fold、服务与落账
`foldMode(events)` 是纯函数(导出给重构器与测试);服务用 `WeakMap<Session, { cursor, mode }>` 里的惰性游标按会话跟踪它——每次读取 O(新事件数),永不失效,因为日志仅追加且 `mode/set` 不是 surface 节点compaction 改写不了它)。`ctx.modes`cordis Service`modes`)暴露 `list()`——合成的 `default` 条目加上配置的定义集,给选择器——`get(agent): { current, pending? }`,以及 `set(agent, mode)`:对配置校验名字、丢弃 no-op目标等于 pending ?? current、否则把意图记进 `WeakMap<Session, string>`。一个被收容的 `session/event` listener[防御模式](../../../defensive-patterns.md):策略插件不得杀死事件流)在下一个 `turn/start``step/end` 把待落账意图作为 `mode/set` 追加落账——两处都在 step 的工具执行窗口之外,所以一个 step 的执行永远运行在其组装所 fold 的模式下——并且当落账模式与最后一条 `request/header` 处的 fold 不同时,在同一帧内追加那条合并的 `context/message` 通知。播种骑 `agent/created`:声明合并的 `AgentOptions.mode` 变成待落账意图,于是显式选项在 create 与 resume 上都压过日志基线——与调用配置种子相同的优先级——而 fork 子代理完全不需要机制(父的 `mode/set` 就在种子前缀里)。
`foldMode(events)` 是纯函数(导出给重构器与测试);服务用 `WeakMap<Session, { cursor, mode }>` 里的惰性游标按会话跟踪它——每次读取 O(新事件数),永不失效,因为日志仅追加且 `mode/set` 不是 surface 节点compaction 改写不了它)。`ctx.modes`cordis Service`modes`)暴露 `list()`——合成的 `default` 条目加上配置的定义集,给选择器——`get(agent): { current, pending? }`,以及 `set(agent, mode)`:对配置校验名字、丢弃 no-op目标等于 pending ?? current、否则把意图记进 `WeakMap<Session, string>`。一个被收容的 `session/event` listener[防御模式](../../../defensive-patterns.md):策略插件不得杀死事件流)在下一个 `turn/start``step/end` 把待落账意图作为 `mode/set` 追加落账——两处都在 step 的工具执行窗口之外,所以一个 step 的执行永远运行在其组装所 fold 的模式下——并且当落账模式与最后一条 `request/header` 处的 fold 不同时,在同一帧内追加那条合并的 `context/message` 通知。播种骑 `agent/created`:声明合并的 `AgentOptions.mode` 变成待落账意图,于是显式选项在 create 与 resume 上都压过日志基线——与调用配置种子相同的优先级——而 fork 子代理完全不需要机制(父的 `mode/set` 就在种子前缀里)。一条读路径规则补上「resume 时配置已改」的缺口fold 出的模式名若当前配置已不再定义,按默认模式行事,并在下一个边界追加一条点名被移除定义的通知——绝不静默替换一个别的限制,也绝不让会话变砖;`set()` 的大声校验只覆盖写路径,恢复出来的日志面对的是它所遇到的配置。
### 软层:计算型 section 与 post-`next()` 过滤器