@migor/agentia 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +917 -34
  2. package/README.en.md +6 -4
  3. package/README.md +4 -3
  4. package/dist/AGENTS.md +106 -33
  5. package/dist/core/limits.d.ts +33 -1
  6. package/dist/core/limits.js +37 -1
  7. package/dist/core/limits.js.map +1 -1
  8. package/dist/core/run.d.ts +2 -2
  9. package/dist/core/timeout.d.ts +5 -5
  10. package/dist/core/timeout.js +5 -5
  11. package/dist/core/tool.d.ts +2 -2
  12. package/dist/core/trace.d.ts +10 -1
  13. package/dist/core/trace.js +10 -1
  14. package/dist/core/trace.js.map +1 -1
  15. package/dist/engine/concurrency.d.ts +1 -1
  16. package/dist/engine/concurrency.js +1 -1
  17. package/dist/engine/errors.d.ts +2 -2
  18. package/dist/engine/errors.js +2 -2
  19. package/dist/engine/loop-result.d.ts +2 -2
  20. package/dist/engine/loop.js +91 -56
  21. package/dist/engine/loop.js.map +1 -1
  22. package/dist/engine/mcp-server-http.js +69 -36
  23. package/dist/engine/mcp-server-http.js.map +1 -1
  24. package/dist/engine/mcp-server-stdio.d.ts +0 -13
  25. package/dist/engine/mcp-server-stdio.js +42 -25
  26. package/dist/engine/mcp-server-stdio.js.map +1 -1
  27. package/dist/engine/mcp-server.d.ts +15 -0
  28. package/dist/engine/mcp-server.js +39 -8
  29. package/dist/engine/mcp-server.js.map +1 -1
  30. package/dist/engine/resume-input.d.ts +1 -1
  31. package/dist/engine/resume-input.js +1 -1
  32. package/dist/engine/retry.d.ts +15 -0
  33. package/dist/engine/retry.js +51 -2
  34. package/dist/engine/retry.js.map +1 -1
  35. package/dist/engine/span-scope.d.ts +15 -0
  36. package/dist/engine/span-scope.js +8 -1
  37. package/dist/engine/span-scope.js.map +1 -1
  38. package/dist/engine/spec.d.ts +5 -1
  39. package/dist/engine/spec.js.map +1 -1
  40. package/dist/engine/tracer.d.ts +8 -4
  41. package/dist/engine/tracer.js +24 -17
  42. package/dist/engine/tracer.js.map +1 -1
  43. package/dist/engine/turn.d.ts +2 -2
  44. package/dist/engine/turn.js +19 -4
  45. package/dist/engine/turn.js.map +1 -1
  46. package/dist/engine/types.d.ts +8 -3
  47. package/dist/eval/export.js +2 -2
  48. package/dist/eval/export.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +2 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/integrations/anthropic.d.ts +1 -1
  53. package/dist/integrations/anthropic.js +1 -1
  54. package/dist/integrations/anthropic.js.map +1 -1
  55. package/dist/integrations/file-sink.d.ts +2 -2
  56. package/dist/integrations/file-sink.js +2 -2
  57. package/dist/integrations/mcp-http.js +15 -88
  58. package/dist/integrations/mcp-http.js.map +1 -1
  59. package/dist/integrations/mcp-protocol.d.ts +86 -10
  60. package/dist/integrations/mcp-protocol.js +147 -1
  61. package/dist/integrations/mcp-protocol.js.map +1 -1
  62. package/dist/integrations/mcp-stdio.js +87 -52
  63. package/dist/integrations/mcp-stdio.js.map +1 -1
  64. package/dist/integrations/mcp.d.ts +1 -1
  65. package/dist/integrations/mcp.js +19 -3
  66. package/dist/integrations/mcp.js.map +1 -1
  67. package/dist/integrations/metrics-otlp.d.ts +1 -1
  68. package/dist/integrations/metrics-otlp.js +8 -2
  69. package/dist/integrations/metrics-otlp.js.map +1 -1
  70. package/dist/integrations/metrics-render.js +1 -1
  71. package/dist/integrations/metrics-render.js.map +1 -1
  72. package/dist/integrations/metrics-state.d.ts +2 -2
  73. package/dist/integrations/metrics-state.js +4 -4
  74. package/dist/integrations/metrics-state.js.map +1 -1
  75. package/dist/integrations/metrics.d.ts +2 -2
  76. package/dist/integrations/openai.d.ts +4 -1
  77. package/dist/integrations/openai.js +25 -6
  78. package/dist/integrations/openai.js.map +1 -1
  79. package/dist/integrations/otlp.d.ts +29 -6
  80. package/dist/integrations/otlp.js +128 -7
  81. package/dist/integrations/otlp.js.map +1 -1
  82. package/dist/integrations/report.d.ts +14 -2
  83. package/dist/integrations/report.js +39 -6
  84. package/dist/integrations/report.js.map +1 -1
  85. package/dist/runtime/memory.d.ts +5 -0
  86. package/dist/runtime/memory.js.map +1 -1
  87. package/dist/store/fsStore.js +9 -5
  88. package/dist/store/fsStore.js.map +1 -1
  89. package/dist/store/record.d.ts +2 -2
  90. package/dist/store/record.js +1 -1
  91. package/dist/store/sqliteStore.d.ts +2 -2
  92. package/dist/store/sqliteStore.js +3 -3
  93. package/dist/store/sqliteStore.js.map +1 -1
  94. package/dist/store/store.d.ts +32 -6
  95. package/dist/store/store.js +36 -5
  96. package/dist/store/store.js.map +1 -1
  97. package/dist/toolkit/collect.d.ts +11 -5
  98. package/dist/toolkit/collect.js +10 -4
  99. package/dist/toolkit/collect.js.map +1 -1
  100. package/dist/toolkit/module.js +78 -1
  101. package/dist/toolkit/module.js.map +1 -1
  102. package/dist/toolkit/prompt.d.ts +1 -1
  103. package/dist/toolkit/prompt.js +2 -2
  104. package/dist/toolkit/prompt.js.map +1 -1
  105. package/dist/toolkit/subagent.js +17 -3
  106. package/dist/toolkit/subagent.js.map +1 -1
  107. package/dist/transport/approval-policy.js +1 -1
  108. package/dist/transport/approval-policy.js.map +1 -1
  109. package/dist/transport/approval-supervisor.d.ts +8 -0
  110. package/dist/transport/approval-supervisor.js +23 -3
  111. package/dist/transport/approval-supervisor.js.map +1 -1
  112. package/dist/transport/async.d.ts +5 -4
  113. package/dist/transport/async.js +166 -145
  114. package/dist/transport/async.js.map +1 -1
  115. package/dist/transport/http-endpoints.js +1 -1
  116. package/dist/transport/http-route.js +2 -2
  117. package/dist/transport/http-route.js.map +1 -1
  118. package/dist/transport/http-shapes.d.ts +1 -1
  119. package/dist/transport/http-shapes.js +1 -1
  120. package/dist/transport/resume-scanner.d.ts +7 -0
  121. package/dist/transport/resume-scanner.js +40 -9
  122. package/dist/transport/resume-scanner.js.map +1 -1
  123. package/dist/transport/run-call-options.d.ts +77 -0
  124. package/dist/transport/run-call-options.js +76 -0
  125. package/dist/transport/run-call-options.js.map +1 -0
  126. package/dist/transport/run-outcome.d.ts +109 -0
  127. package/dist/transport/run-outcome.js +100 -0
  128. package/dist/transport/run-outcome.js.map +1 -0
  129. package/dist/transport/scheduler.js +5 -0
  130. package/dist/transport/scheduler.js.map +1 -1
  131. package/dist/transport/signal-supervisor.d.ts +10 -2
  132. package/dist/transport/signal-supervisor.js +15 -2
  133. package/dist/transport/signal-supervisor.js.map +1 -1
  134. package/dist/transport/wake-policy.d.ts +1 -1
  135. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,887 @@
5
5
  (0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
6
6
  决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
7
7
 
8
+ ## [0.10.3] - 2026-10-14
9
+
10
+ > 本版主题(窗口 `0.10.2 → 0.10.3`):**把「可观测」从「能导出」推进到「跨进程接得上、agent 自己查得到」**,
11
+ > 外加五轮深评的静默项收口与一轮门禁加固。三块 —— ① **链路**:MCP 桥按 SEP-414 出入站携带
12
+ > `traceparent`(跨进程 trace 第一次接得上;载体是 `params._meta` 的**裸键**,缺失/畸形静默降级)、
13
+ > OTLP 导出对齐 semconv(三个缓存/压缩键 + 可选的 `execute_tool` 投影,**默认关**);
14
+ > ② **可读面**:CLI `agentia report` 加会话 / 任务维度、交接与护栏四个 `agentia.*` 属性
15
+ > (**additive**,不造新 span kind)、两个 MCP 只读工具 —— 让 agent 自己查「上一轮慢在哪」;
16
+ > ③ **缺陷与门禁**:深评静默项收口(retry 坏值不再谎报「用户取消」、嵌套能力重复计数、护栏行在真实
17
+ > HITL 流程零产出、`sessionId` 静默失效改为出声)+ 门禁加固(文档预算闸 / 副本闸 / verify-all 分片 /
18
+ > pre-push 钩子)+ 决策引用从「日期 + 圈号」迁到「日期 + slug」(302 处,守卫只认 slug)。
19
+ >
20
+ > **无破坏性变更 ⇒ 既有使用者不需要任何动作、无需迁移**:公共导出面**零增删改**(`src/index.ts`
21
+ > 本版只动版本常量那一行);`McpClientLike.callTool` 第三参与 `TaskStore` 的新方法都是
22
+ > **可选 / 有默认实现**的加法。
23
+ > ⚠️ **但有三条读数会变(变小),升级后看到数字变小不是坏了**:① 嵌套 capability 的 tokens / cost
24
+ > 此前在**外层与内层各计一次**(列不可加总),现在**只认顶层**、内层显示 `-` —— 影响
25
+ > `buildRunReport`、`@migor/trace-view` 能力排行、CLI `agentia report`、`agentia dev` 面板与官网
26
+ > playground(五处共用同一份实现);② `agentia report` 按 `traceId` 去重,append-only 导出里
27
+ > 同一棵 trace 的多行不再翻倍;③ `retry` 的配置坏值不再被归类成「用户取消」。
28
+
29
+ ### 修复:retry 配置坏值被误报成「用户取消」+ 入口段抛错时 run 根 span 不合拢(2026-10-13 ①②)
30
+
31
+ - **病灶①(深评 A1)**:`retry: { baseDelayMs: NaN }` + 一次 429 ⇒ 退避算出 NaN ⇒
32
+ `sleep` 抛 RangeError ⇒ `turn.ts` 的裸 `catch` 吞成 `stopReason: 'aborted'`(配置错被
33
+ 谎报成用户取消);`maxAttempts: NaN` 则让重试被**静默关闭**(`!(NaN >= 1)` 成立)。
34
+ - **修法①**:`RetryOptions` 四个数值旋钮登记进 `core/limits.ts` 真源表并加构造期校验
35
+ (`resolveRetry` 抛 `TypeError`,报错文案插 `zeroClauseOf`;`runAgent` 入口同款校验,
36
+ 与 labels / fallbacks 同落点 —— 配置错响亮抛,不收成失败的 run);退避处 catch 只认
37
+ AbortError,其他错误原样抛出归类;`backoffDelay` 上界夹在 `MAX_TIMER_DELAY_MS`
38
+ (正向抖动曾能把它推过 Node 定时器上限)。0 的读法:`maxAttempts: 0` = 关闭重试、
39
+ `baseDelayMs: 0` = 不退避、`maxDelayMs: 0` = 封顶为零、`jitter: 0` = 不加抖动。
40
+ - **病灶②(深评 A2)**:`runAgent` 入口段(菜单摘要 / 模型链 / 标签校验)抛错时 run 根
41
+ span 已开却永不 end —— 交付 sink 的 trace 里 `status='error'` 而根 span 是 ok、无
42
+ endedAt、无结构化 error,违反「非正常收尾必带结构化 error」。
43
+ - **修法②**:入口段包 try/catch,catch 给根 span 收尾(error 状态 + 结构化 error)
44
+ 后原样 rethrow —— 对外行为不变(照样抛),trace 不再说谎。
45
+ - **验证**:`tests/engine/retry.test.ts` +6 条(含深评实测路径的入口校验用例与 catch 判别
46
+ 用例)、`tests/engine/loop.test.ts` +1 条(两个真实触发点的 trace 形状)、
47
+ `tests/limits.test.ts` +4 探针 + 4 文案对账 case。
48
+ ### 装配期静默缺口收口:静态方法装饰期拒 + 子菜单重名查重(2026-10-13 ③④)
49
+
50
+ - **③ @Tool/@SubAgent/@Skill 装饰静态方法 → 装饰期即抛**(此前能力静默从菜单消失:
51
+ 装饰器正常登记、装配零报错,但收集只扫实例原型链,静态方法挂在构造函数上永远扫不到)。
52
+ 与私有方法守卫同款落点(`assertMethodTarget` 加 `context.static` 判定);`@Prompt` 是唯一
53
+ 例外(纯文本资产,静态收集是它的文档化形态,经 `allowStatic` 显式放行)。**非破坏性**:
54
+ 静态装饰这三类此前本就是死资产,现在只是从「静默失踪」变成「类定义期响亮报错」。
55
+ - **④ 子菜单重名查重**:主菜单的「菜单能力重名」查重此前管不到 @SubAgent/@Skill 的
56
+ `tools` 引用展开结果(运行期展开)—— 两个 provider 各有同名能力被同一能力整片引用时
57
+ 装配全绿、第一次模型调用才由 API 400 暴露。现在装配期逐引用片展开查重,报错指到
58
+ 「哪个能力的哪几片引用」(`子菜单能力重名:@SubAgent "x" 的 tools 引用展开后 "search"
59
+ (来自 "a" + "b")`),修法引导(能力级路径收窄 / 改名)写进文案。
60
+ - **依据**:`docs/spec.md` §10 2026-10-13 ref-reject-static-methods ref-submenu-name-check;usage-guide §3(装饰器只接实例方法)、
61
+ §4 `tools` 行与 §8 常见错误行(重名查重口径补「主菜单+子菜单」)同步。
62
+
63
+ ### 四处低等静默/口径收口:报告去重下沉、握手簿记回收、metrics 失败出声、Span.name 注释(2026-10-13 ⑤⑥)
64
+
65
+ - **`mergeRunReports` 按 `traceId` 去重下沉到框架公共面**:2026-10-12 ⑧ 的去重落在 CLI 层,
66
+ 框架公共 API 喂同样的 append-only 导出照样翻倍计数。现在框架函数自身去重(同口径:
67
+ 只留最后一份、保序;`merged(n runs)` 是合成 id 不参与去重)。CLI 侧不动 —— 它作用在
68
+ 原始 trace 行上,与框架函数不是同一份代码。
69
+ - **stdio 连接器握手 / tools/list 超时后 pending 簿记回收**:guard 只是「放弃等待」,
70
+ 超时的 initialize 条目此前永远留在 pending(server 活着但不回包时每超时一次泄漏一条)。
71
+ 握手与 list 各持一个 AbortController,失败分支 abort 复用 `send` 里既有的 abandoned 清理。
72
+ - **metrics OTLP 导出失败缺省落 `console.warn`**(此前无 `onExportError` 时被 `?.` 吞掉、
73
+ 零信号),与 traces 侧 `flushSinks` 同款(每次失败一条)。
74
+ - **`core/trace.ts` 的 `Span.name` 注释对齐实现**:capability span 的 name 是**裸能力名**
75
+ (无 `subagent:` 前缀;区分类型看 attributes,见 `capabilityKindOf`)—— 旧注释的
76
+ `${capabilityType}:${capabilityName}` 口径曾让 otlp.ts 按前缀判、生产上一条 gen_ai.* 都没发出去。
77
+ - **依据**:`docs/spec.md` §10 2026-10-13 ref-merge-reports-dedupe ref-stdio-metrics-silent;usage-guide(metricsSink `onExportError` 行 /
78
+ 已知边界行 / §6 `mergeRunReports` 速查行)同步。
79
+ ### 守卫射程轮:CLI 纳入 exactOptionalPropertyTypes、e2e:mcp:server 进 CI、三处货架补齐(2026-10-13 ⑦)
80
+
81
+ - **D2(CLI 类型收紧)**:`packages/cli` 的 `tsconfig.json` 与 `tsconfig.templates.json`
82
+ 此前**没开** `exactOptionalPropertyTypes`(根包 2026-09-18 起就开着),「显式 undefined
83
+ 覆盖缺省」那类防线在 CLI 代码上无声缺位。现两份配置(连同发给用户的模板
84
+ `templates/tsconfig.json`)全部开启,并修掉暴露的 6 处类型错:结果记录型
85
+ (`SpanDiff` / `ToolIoEvent` / `ExportRecordMeta`)改必填 `T | undefined`(与框架侧
86
+ 移植真源逐字对齐)、内部管道 `HarvestRow.messages` 改 `?: T | undefined`、公共入参
87
+ `harvestEvalCase` 签名不动、调用点条件展开。`tsconfig-strictness` 守卫射程扩到四份配置。
88
+ - **D1(e2e:mcp:server 进 CI)**:MCP 反向桥端到端此前全仓零自动化;现为 CI 独立
89
+ **必需检查** job `e2e-mcp-server`(离线零网络、不需要 uv),并登记进 `verify-all.sh`
90
+ 的 `ci_only` 与 CONTRIBUTING / AGENTS.md 的对应抄写。verify-all 仍是 8 步(不加第 9 步)。
91
+ - **E2(inspector 白名单门禁)**:新增 W5 守卫 —— `dist/inspector` 里每个 `.js`/`.css`
92
+ 都必须在 STATIC 白名单里(双向集合相等),trace-view 拆出新文件而白名单没跟上时
93
+ 构建即红(此前表现为面板 404 白屏)。白名单本身保持显式人批。
94
+ - **低等三项**:CI 各 job 补 `timeout-minutes`;guards-registry 货架补 `tests/scripts`;
95
+ `layering.test.ts` 的 import 解析改走 `lib/source-scan.ts` 遮蔽器(注释 / 模板里的
96
+ `from './x.js'` 不再造假依赖边)。
97
+ - 依据与逐项核对结论见 `docs/spec.md` §10 2026-10-13 ref-guard-scope-round4。
98
+ ### 修复:engine 非正常路径口径对齐(深度评审 2026-10-13 低等项五件)(spec §10 2026-10-13 ref-deep-review-minor)
99
+
100
+ - **`max_iterations` 出口保留已产出文本**:循环耗尽时 `finalText` 此前恒为 `''`,与
101
+ `max_tokens` / `refusal` / `budget_exceeded` 的「文本不丢」口径不一致;现在耗尽出口
102
+ 带上最后一回合的模型文本。
103
+ - **「消息已收、工具未跑」窗口内取消不再执行工具**:`streamTurn` 成功返回后、
104
+ `executeTurnTools` 之前补 `signal.aborted` 检查 —— abort 落在该窗口时整批工具的副作用
105
+ 一个都不发生(与续跑入口「已取消不执行任何工具」同口径),run 以 `aborted` 收尾。
106
+ - **trace 截断摘要多源漂移修复**:`snapshot()` 的收尾摘要(`trace.truncated` 的
107
+ `{ droppedEvents, limit }` 那笔)此前每次调用都新建时间戳,多次 snapshot 互不一致、
108
+ 也都与增量流里那一条对不上;现在只生成一次、同一对象复用于两条缝
109
+ (「按 seq 折回 == snapshot」的折叠契约在截断场景重新成立)。
110
+ - **stdio 反向桥监听器泄漏修复**:`process.stdout` 的 EPIPE 吞错监听随 `close()` 摘除
111
+ (此前只挂不摘,多次 `createMcpServer` 累积进程级监听器)。
112
+ - **MCP 反向桥补 trace 闸**:`createMcpServer` 的选项新增 `traceLimits`(每次
113
+ `tools/call` 那棵 trace 的事件数上限,构造期校验、坏值抛 TypeError)与
114
+ `maxEventChars`(事件正文截断上限,`false` = 不截断)—— 缺省与引擎缺省逐字一致,
115
+ 子 agent 类工具在桥内长跑时 trace 体积不再无界。
116
+ ### 修复:TaskStore 副本契约坐实 + cancel 与推进路径互斥(五路深评批次二,2026-10-13 ⑨–⑫)
117
+
118
+ - **B2 · store 副本契约(⑨)**:`InMemoryTaskStore` / `FileTaskStore` 的 `save` 入库与
119
+ `get` / `byIdempotency` / `list` 出站统一走 `cloneTaskRecord`(深拷纯数据、AbortSignal /
120
+ 函数等进程内对象按引用保留)。此前契约注释要求副本、实现交活引用 —— 「改 rec →
121
+ `await save`」的纪律下,save 失败时变异已污染内存(approve 重试撞 409、任务搁死)。
122
+ **行为变化**:终态落库失败时库里诚实停在最后一次落库成功的状态(此前被引用语义
123
+ 原地推进遮掉);`submit` / 幂等去重返回从浅拷贝升级为深拷贝(HITL 挂起会整体替换
124
+ `spec.messages`,浅拷贝快照照样被穿透)。
125
+ - **B1 · cancel 互斥闸(⑩)**:新增 per-task `#cancelInflight`,与 approve / signalTask /
126
+ 到期唤醒 / 审批超时四条推进路径互为对侧(先登记者赢,无死锁);cancel 临界段落库改
127
+ **当场发生**(不再经微任务延迟);`#executeInner` 起跑前加**取消意图闸**(意图在场
128
+ ⇒ 绝不起跑,记账归 cancel),`#cancelSettled` 覆盖面随之补齐。修掉两扇真实竞态窗:
129
+ cancel×approve 并发「取消后照常执行 + onFinished 双发」、cancel×槽位释放并发打破
130
+ 「被取消的排队任务绝不起跑」。
131
+ - **B3 + 低等 · 恢复路径落库失败(⑪)**:`resumePending` 认领 save 接同步抛错(不再逃出
132
+ 启动路径;不派发、不计入推进数、warn);认领 / 审批超时 / 到期唤醒三处落库失败的
133
+ catch 不再静默留尸,各补 `console.warn`。
134
+ - **低等 · Scheduler drain 感知(⑫)**:runner 停机后 tick 静默跳过(此前每 tick 刷一条
135
+ `console.error`)。
136
+ - **依据与取舍**(含副本克隆的成本评估):`docs/spec.md` §10 2026-10-13 ref-taskstore-copy-contract ref-cancel-mutex ref-resume-save-guard ref-scheduler-aware-drain。
137
+ - **验证**:新增 `tests/store/taskStore-copy.test.ts` / `tests/transport/cancel-race.test.ts` /
138
+ `tests/transport/resume-scanner.test.ts` 三份套件 + scheduler drain 用例;既有依赖
139
+ 「改 poll 返回值即生效」的三处用例改为显式 `save`(副本契约的推论)。
140
+ ### 修复:能力排行嵌套能力重复计数(E1)+ 文档 drift 收口(2026-10-13 ⑬)
141
+
142
+ - **病灶**:`@migor/trace-view` 的 `summarizeTrace` 对每个 capability span 累加 usage,
143
+ 而框架侧 capability.usage 是其**子孙 llm.turn 的聚合** ⇒ `@SubAgent` 套 `@Skill` 时
144
+ 同一批 token 在外层与内层行**各计一次**,tokens / costUsd 列不可加总(与
145
+ `core/trace.ts`「capability 聚合不进 totalUsage」的纪律冲突)。CLI `agentia report`、
146
+ `agentia dev` 面板、官网 playground 三处共用同一份实现,同漏。
147
+ - **修法**:能力排行的 usage **只认顶层 capability**(没有 capability 祖先的);内层行仍计
148
+ calls / errors / 耗时(那是它自己这次调用的真账),tokens / cost 显示 `-` ⇒ 各行可加总。
149
+ 引擎与 tracer 行为不动。依据见 `docs/spec.md` §10 2026-10-13 ref-ranking-top-capability。
150
+ - **同批修复**:`examples/deploy` 补上脚手架模板同款的 PORT 校验(`PORT=''` 空串曾被
151
+ `Number('')` 静默变成 0 = 随机端口);usage-guide 的「CLI 三件套」统一为四件套口径
152
+ (补 `export`),能力排行补「嵌套只认顶层」口径说明。
153
+ - **同批守护**:doctor 的 `ENTRY_CANDIDATES`(CLI 侧双写)与框架 `discover.ts` 加源码级
154
+ 逐字对拍;AGENTS.md 十条 drift 对账修正(数字改为指向真源);trace-view 结构棘轮
155
+ summary.js 130 → 153。
156
+ - **验证**:嵌套夹具 + 变异验证(去掉顶层判定 ⇒ 新用例红);`verify-all.sh` 8/8。
157
+
158
+ ### 修复:`buildRunReport` 的嵌套能力重复计数(2026-10-13 ⑭)
159
+
160
+ - **病灶**:⑬ 修的是 trace-view 展示层;框架公共 API `buildRunReport`
161
+ (`src/integrations/report.ts`)有同款嵌套累加 —— `@SubAgent` 套 `@Skill` 时同一批
162
+ token 在外层与内层行各计一次,能力排行的 tokens/costUsd 不可加总。消费方
163
+ `examples/observability-mcp` 的 `report_trace` 工具同漏。
164
+ - **修法**:与 ⑬ 完全同款 —— 沿 `parentSpanId` 上爬判 capability 祖先(带防环),
165
+ 嵌套行的 usage 不累加(`tokens`/`tokensTotal`/`costUsd` 为 `null`),calls/errors/耗时
166
+ 照计 ⇒ 各行可加总。依据见 `docs/spec.md` §10 2026-10-13 ref-build-report-dedupe。
167
+ - **验证**:`tests/integrations/report.test.ts` 新增嵌套夹具(去掉顶层判定即红)。
168
+
169
+ ### 工程:引用完整性轮 —— 22 个圈号归位 + 体量闸头注表自审 + 圈号引用守卫(2026-10-14)
170
+
171
+ - **病灶①(引用层)**:全仓指回 `docs/spec.md` §10 的 `(日期 圈号)` 写法**没有任何守卫**。
172
+ 实测三类真实形态共 **22 行 / 23 处引用**:**(a) 重排后未同步(14 行 / 15 处)** —— 批次二自己的
173
+ 分支已把四件事编成 ⑨⑩⑪⑫,**同一分支**的 `tests/**` 与 `AGENTS.md` 仍写 ①②③④(偏差恒为
174
+ +8,**落地当天就自相矛盾**,与合并无关);另一轮同理,`spec.md` 内一处 ① 应作 ⑧。
175
+ **(b) 幽灵锚点(7 处)** —— `src/` 的注释与 `export` 的移植副本引该日 ⑧ 之后的号,而该日
176
+ 只有 ①–⑧(`git log -S` 限 `docs/spec.md` 查过:从来没有过);另一处在本文件里 ——
177
+ 有个已发布的版本节标题引了该日的 ⑲,而 ⑲ 在 §10 **从未存在过**(还做过正对照:`-S` 搜
178
+ 同一日的 ⑳ 能命中发版提交,搜 ⑲ 为空)。
179
+ **(c) 给字母编号的日子硬加圈号(1 处)** —— `spec.md` 内一处引该日条目号时写成了圈号形式,
180
+ 而**2026-09-11 的 §10 条目用 R1–R6**(不是圈号);**同一个提交**(#40)里另一处已经写着
181
+ 「见 §10 2026-09-11 R1 条」⇒ 两处自相矛盾,从写下那天起就不成立。
182
+ - **病灶②(读数层)**:`source-size.test.ts` 头注表写 `async.ts` **627** 代码行,实测 **680**;
183
+ 而 `AGENTS.md` 的导览图明确写着「当前真值以这张表为准、别在这里数」。根因**不是**「抄了旧纸」:
184
+ 改这张表的那个 PR 落地时,**它自己分支上的 `async.ts` 已经是 680**(另一批先合)——
185
+ **答案就在磁盘上**。这是「假账」的典型形状:语义上没撒谎,数字没现算。
186
+ - **修法**:① 22 行圈号按**主题**认锚点归位(不按位置猜:幽灵号分别落到当日的反向桥条目、
187
+ 导出条目;字母编号那处改写成 `R1`;CHANGELOG 那处**删掉幽灵括号**);② 头注表按实测重算
188
+ (680 / 524 / 493),「留多少余量」不再在正文另记一份数;
189
+ `AGENTS.md` 那行**去掉读数、只留指针**(它烂过两次:行数差一、代码行被同日抽件改掉)。
190
+ ⚠️ 本轮**零语义改动**(全部落在注释 / 文档 / 测试),故不写 §10。
191
+ - **新守卫(两条,都做过反向验证)**:
192
+ - **头注表自审**(`tests/architecture/source-size.test.ts`):实测与表里的数逐行对拍、口径复用
193
+ 同一个 `codeLineCount`。变异:表里 680→679 ⇒ **恰好那条红**(体量闸本体仍绿 ⇒ 两条断言
194
+ **独立**);表行去掉反引号 ⇒ 真空护栏红(`只从表里解析到 2 行`)。
195
+ - **圈号引用**(`tests/docs/circle-refs.test.ts`,新):射程内「日期在 §10 **有条目** ⇒ 圈号必须
196
+ 存在」。「条目」看**行首形态**(`### 日期` / `**日期` / `- 日期:`)、**不看有没有编号** ——
197
+ 取「有条目」而非「有圈号锚点」是有意的:后者会把「日期有条目、但条目用字母编号」那一类
198
+ (即上面 (c))整片漏出射程。引用按「日期 + **紧跟的一整串**圈号」吃(`(日期 ②③)` 指两条
199
+ 决策,只认第一个会让后面那些**变裸的**;实测多校验 14 处、新增违规 0)。变异 5 轮:源码注释
200
+ 改回幽灵号 ⇒ 恰好报出该行(`该日共 8 个锚点`);把锚点正则打瞎 ⇒ 真空护栏红(`只解析出
201
+ 6 个锚点`);`spec.md` 那处改回圈号 ⇒ **恰好只报 `docs/spec.md:1236` 一行**;把「条目」判据的
202
+ 列表项分支打瞎 ⇒ 真空护栏红(`只解析出 1 个「有条目但无圈号锚点」的日期`);把 `resume-scanner`
203
+ 那串的**第二个**号改成不存在的(`⑩⑱`)⇒ 恰好报出 `resume-scanner.test.ts:9` 那处的 `⑱`
204
+ (只认第一个的版本对它是瞎的)。⚠️ 它只判「**号在不在**」、判不了「**号指得对不对**」
205
+ —— 指向错主题但号存在的(如把 ⑩⑪ 写成 ②③,而 ②③ 也是该日的号)机器判不了 ⇒ **本仓
206
+ 最大的一类错引(14 行)从来不是本守卫的功劳**,是人逐条读出来的。CHANGELOG 只查带 `§10`
207
+ 标记的引用:其裸标签**两节两种惯例**(`[0.10.0]` 照抄 §10 的号、`[0.10.2]` 用本节自己的
208
+ 条目序号),全查会在后者误报。已登记 `docs/guards.md` §1.4。
209
+ - **顺带记录**:§10 同日编号有 9 处不连续 —— 3 处是「同日首条不编号」的风格、
210
+ 4 处是 09-18 的 ③–⑥ 跳号,另有 09-14 缺 ①、09-29 缺 ⑲(成因未定)。其中 **09-29 缺 ⑲ 只被
211
+ CHANGELOG 引过一次**(PR #203 把「推广稿重写成定稿」那条按 §10 圈号写成了 ⑲,而 §10 从未
212
+ 为它立条目)⇒ 本轮**删掉那个幽灵括号**(该条日期由所属版本节标题 `[0.10.0] - 2026-09-29`
213
+ 给出,不丢信息)。**不做「同日编号必须连续」这条更硬的闸**:它要求回头给这些段补号,
214
+ 而补号本身会撞死引用(本轮修的正是这类撞伤)——收益抵不过风险,登记为已知边界。
215
+ - **验证**:`verify-all.sh` 8/8;两条新守卫共 7 轮变异逐条咬中后 `sha256` 逐字节还原。
216
+
217
+ ### 工程:门禁加固轮 —— 文档预算闸 + 副本闸 + verify-all 分片 + pre-push 钩子(2026-10-14)
218
+
219
+ - **出发点**是一个方法论问题:多模型参与后守卫的**数量**在涨,但守卫的**有效性**没涨。归纳出三个
220
+ 病根:**① 守卫声称的「理由」没人守** —— 活证据是 `circle-refs.test.ts` 的「档 B」原写着「CHANGELOG
221
+ 的圈号是它自己的条目序号」,实测是错的(`[0.10.0]` 那一节的 15 条与 §10 逐条对得上);**写错的
222
+ 理由,守卫照样绿**。**② 同一事实多处副本**(本轮三个实例:`source-size.test.ts` 头注表读数 /
223
+ `CHANGELOG` 两节圈号惯例 / `spec.md` 里同一天两种编号写法),此前**零守卫**。**③ 编号是「位置号」,
224
+ 位置会变** —— 上一轮修的 14 行错引**全部**由「按天编号 + 合并重排」产生(偏差恒为 +8),**落地
225
+ 当天即自相矛盾**。下面 ①–⑤ 对治前两条,⑥ 是 ③ 的根因消除。
226
+ - **① 常驻文档预算闸**(`tests/docs/doc-budget.test.ts`,新)—— 对标业界做法里的 `verify-doc-budgets`。
227
+ 口径是**码点**而不是字节、也不是行:中文一字三字节 ⇒ 按字节设闸读数没有可比性;而按**行**设闸对
228
+ `docs/guards.md` 完全无效(504 行里躺着 125KB,平均 250 字符/行、最长一行 3423)。两个维度:
229
+ `AGENTS.md` / `CONTRIBUTING.md` **整份**设上限(它们是散文、必须从头读到尾 ⇒ 总长就是读取成本),
230
+ `guards.md` 这类**注册表的单行**设上限(行数随守卫数增长是**设计**,只有单条长度才是病灶)。
231
+ 头注明写**三类刻意不设上限**:`spec.md`(决策日志,只增不改)/ `usage-guide.md`(完备性已由
232
+ `boundary-table.test.ts` 守)/ `docs/plans`·`docs/reviews`(带日期的快照)—— 三者共有的性质是
233
+ 「它们的长度不是读取成本,而是完备性」。超了有两条路且**必须显式**:精简(首选)或提上限(改的
234
+ 就是本文件里那个数字 ⇒ review 时一定看得见)。
235
+ - **②+③ 副本闸**(`tests/architecture/duplicated-blocks.test.ts`,新)—— 滚窗找**生产源码**里的
236
+ 成段逐字重复。归一化丢空行与整行注释、**折缩进**(跨文件副本常因所在层级不同整块差一级,不折就
237
+ 对「抄了一个方法到另一个类里」完全瞎)。`WINDOW = 12` 是**量出来的**不是拍的:同一量具在
238
+ N = 8 / 12 / 16 / 20 各跑一遍 —— N=8 得 7 对(多出 `skill.ts ↔ subagent.ts`、`mcp-http.ts ↔
239
+ mcp-stdio.ts` 这类**结构性同形**)、**N=12 得 5 对**(全是**真会一起改**的那种:三份移植副本 +
240
+ 两份共享样板)、N=16 得 4、N=20 得 3(`store.ts ↔ fsStore.ts` 那份 12 行的同源实现会漏出去)。
241
+ `KNOWN` 表 5 条各带 `why`(≥ 30 字)+ 可选的 `syncedBy × symbol` **联合核对** —— 点**符号名**
242
+ 而不是文件名(本仓移植副本的守卫文件点的是 `exportRun` / `diffTraces` 这类导出名;第一版按文件名
243
+ 判,两条都误红)。三条护栏防「闸变哑」(文件数下限 / 已知的 diff 移植副本必须在探测结果里 /
244
+ 重复对数下限),外加一条**幽灵豁免**(登记了但实际已不重复 ⇒ 红)与「未登记重复 ⇒ 红」互为对侧。
245
+ - **④ verify-all 分片**(`scripts/verify-all.sh`)—— 加 `shard="${1:-all}"` + 新增 `step_names`
246
+ 数组与 `steps` 一一对应(条数不等即 exit 2),**按名字选步、不按下标**(下标会在插入/重排时
247
+ **静默错位**,而分片照样打印「全绿」)。`rapid` 片 16–20 秒(全链约 18 分钟,卡在 `npm test`)。
248
+ ⚠️ **`steps=(` 数组形状逐字未动** —— 它既是 `verify-all-wiring.test.ts` 的解析锚点,也是 CI job 名
249
+ (分支保护的必需状态检查)里那 8 步的来源。两条自证:未知分片 exit 2;**选到 0 步也 exit 2**
250
+ (「它会跑完什么都不做然后报绿」,正是 pre-push 钩子最坏的失效形态)。分片**不打印「N/N 全绿」**
251
+ 那个形状(本仓多处文档抄写的就是它),改打「分片 rapid:3/8 步全绿(其余 5 步未跑)」。
252
+ - **⑤ pre-push 钩子**(`.githooks/pre-push`,新,约 20 秒)—— 调 `rapid` 片;`SKIP_VERIFY=1` 逃生门;
253
+ `node_modules` 缺失时**响亮警告后放行**(不静默 skip)。已在本仓用 `git config core.hooksPath
254
+ .githooks` 启用,`CONTRIBUTING.md` 写了怎么开(git **不会**自动启用仓库里的钩子,不写出来别人
255
+ 克隆下来永远不会跑它)。
256
+ - **⑥ 编号脱位设计稿**(`docs/plans/2026-10-14-ref-slug-migration.md`,新,**P1 未开工**)——
257
+ `(日期 圈号)` → `(日期 + slug)`:圈号是**位置号**,重排即腐烂;slug 由标题派生、可校验。
258
+ 射程实测 **119 个锚点 / 508 处引用 / 500+ 行跨约 40 文件**,分 P1(加名)/ P2(换引用)/ P3(收口)
259
+ 三步走。⚠️ 设计稿里**刻意用占位符** —— 论证「位置号会腐烂」的文档,自己不该先写一个位置号。
260
+ - **三处登记同步**:`docs/guards.md` §1.1 加副本闸行、§1.4 加文档预算闸行、§2 待守加一行
261
+ (`cli/harvest.ts ↔ cli/export.ts` 的样板重复**尚无同步机制** —— 免得「登记过」被读成「已经安全」)。
262
+ - **验证**:`verify-all.sh` 8/8;四个守卫 `doc-budget` 3/3 · `guards-registry` 3/3 ·
263
+ `duplicated-blocks` 5/5 · `verify-all-wiring` 15/15,`biome` 全仓 514 文件 0 error;
264
+ **15 项变异逐条咬中**(每个新断言各一:必读集超预算 / 注册表单行超限 / 注册表变空 / 删登记项 /
265
+ 加幽灵项 / `symbol` 写错一个字母 / `WINDOW` 放宽到 8 / 射程清空 / 两处清单条数不等 / 分片名写错 /
266
+ 文档删掉「怎么开钩子」那条 / 钩子去掉可执行位 / 未知分片 / 选到 0 步 / `steps` 与 `step_names`
267
+ 条数不等),恢复后 `sha256` 逐字节一致、钩子权限回到 755。
268
+ - ⚠️ **如实标注射程**:副本闸只判**逐字**重复,判不了**语义副本**(本仓三次实例那种「同一事实两种
269
+ 写法」一行都不重复 ⇒ 它挡不住);分片**不是门禁**(`rapid` 全绿 ≠ 可以推 PR,它只覆盖静态三步)。
270
+
271
+ ### 工程:决策锚点加 slug(P1 加名)—— 把「位置号」换成稳定地址(2026-10-14)
272
+
273
+ - **接上一条的 ⑥**:`(日期 + 圈号)` → `(日期 + slug)` 的迁移,**P1(加名)已落地**。设计稿里
274
+ 那 4 个开放问题在它的 §7.1 逐条定稿,锚点形态定在 §7.2。
275
+ - **改了什么**:§10 的 **120 行**带圈号锚点行,各在「日期 + 圈号」之后插一个 `ref-` 前缀的 slug
276
+ (形态 `### <日期> <圈号> ref-<名>:<标题>`);**引用一处未动**(P1 的铁律)。加上本条目自己在 §10
277
+ 的那一行也带 slug ⇒ 落地后共 **121 行**有 slug。§10 章节头加了一段
278
+ 读者指引,它自带全仓**第一条**真 slug 引用。`docs/spec.md` §10 另加本决策条目 —— 引用形态是对外
279
+ 约定的一部分(`AGENTS.md` 与 PR 模板里都出现过),按 `CONTRIBUTING.md` 的判据这是**语义变更** ⇒ 要写。
280
+ - **守卫**(`tests/docs/circle-refs.test.ts`)**同时放宽与收紧**。放宽:引用键接受「一串圈号」或
281
+ 「一个 slug」。收紧四条:**有圈号必有 slug**(挡「只加 slug、圈号被误删」的半迁移)、**slug 全局
282
+ 唯一**、**slug 锚点数下界**(119 —— 是**量级**护栏、**不是**逐条计数;单条丢失改由另两条路咬,
283
+ 见守卫注释)、**射程内 slug 引用 ≥ 1**(后者是「新形态真的通了」的活证据 —— 掉了就说明新支路成了
284
+ 死代码)。**退出条件(P3 删掉圈号那一支)写在代码里**,不靠谁记得。
285
+ - **实测**:带圈号的锚点**定义行 121 行**、圈号**唯一键 120** —— 差的那一个正是 `2026-09-14 ②` 的
286
+ **同日重号**(slug 能区分它、圈号不能,这本身就是位置号失效的一个具体证据;该重号在射程内的引用
287
+ 数 = **0** ⇒ 不产生 P2 换引用的歧义)。§10 另有 **4 条无编号锚点**与 **45 条** `- 日期:…` 条目行
288
+ **不在 P1 射程**(无圈号 ⇒ 无锚点可引 ⇒ 不给它起名)。
289
+ - **验证**:`circle-refs` 2/2(新增一条锚点侧用例);**6 项变异逐条咬中** —— 改某条锚点的 slug 名 ⇒
290
+ **恰好**引用它的那两行红;造重名 slug ⇒ 唯一性红;打瞎 `SLUG_ANCHOR` 正则 ⇒ 真空护栏红(**不是**
291
+ 「全绿」);把 3 处 slug 引用全打瞎 ⇒ 「新形态通了」红;删某条锚点的 slug ⇒ 半迁移红;删某条锚点
292
+ 的圈号 ⇒ 那一行滑出锚点射程、自己成了一条查不到的 slug 引用 ⇒ `bad` 红。恢复后 `sha256` 逐字节一致。
293
+ - ⚠️ **自审逮到一处「守卫理由腐烂」**(正是本轮要防的病根 1):下界一开始写成「取紧值 120 ⇒ 能挡住
294
+ 『有人删掉某条锚点的圈号』」——**这句话在本轮内就过期了**:我随后往 §10 加了一条决策(它自己也带
295
+ 圈号 + slug),实测变 121,下界于是有了 1 的余量,那句理由不再成立。⇒ 已改成**量级**下界(119)
296
+ 并明写「它**挡不住**单条丢失,那由 `bad` / `anchorNoSlug` 咬」。教训:**紧值型断言会把「理由」一起
297
+ 钉死**,加一条数据就失效,而失效时**没有任何东西会响**。
298
+ - ⚠️ **落地过程本身踩了三次同一个坑**(如实记):守卫头注 → 设计稿 §3.2 的示例 → §10 条目的示例,
299
+ 都因为**把示例写成「具体日期 + 具体 slug」**而变成一条指向不存在锚点的**真引用**,被守卫当场咬红。
300
+ ⇒ 规矩:这类**形态示例一律用占位符**(`<日期>` / `ref-<名>`),本守卫**自己也在射程内**。
301
+ - ⚠️ **射程边界**:slug 只解决「地址会漂」,**不解决「地址指错了」** —— 键存在但指错主题的仍然只能
302
+ 人读(守卫挡的是「查不到」,不挡「查错了」)。**P2(换引用)与 P3(守卫只认 slug)均未开工**;
303
+ P2 的射程**现算**(`circle-refs` 守卫口径、逐圈号、**只算 git 跟踪文件**):
304
+ **483 处引用 / 473 行 / 100 个文件**(P1 落地前是 474 / 464 / 99)。⚠️ 设计稿 §4 原记的
305
+ 「508 处 / 约 40 个文件」**用任何探针都复现不出来**(守卫口径在写稿那棵树上是 474 / 464 / 99,
306
+ 其标注出处的探针是 412)⇒ 已在设计稿标为「不可复现」并以守卫口径为准。
307
+ ⚠️ **同一份读数必须连口径一起写**:射程里混着 `packages/cli/CHANGELOG.md`(48 处)等
308
+ **gitignored 生成物**,它们只在**本机**存在(CI / 新克隆 / `git worktree` 里都没有)⇒ 拿「本机读数」
309
+ 与「对照树读数」相减会凭空多出 48 处,**看起来正好像引用被改了**(本轮据此差点把对的判成错的)。
310
+
311
+ ### 修复:护栏行在真实 HITL 主流程完全不产出(trace-view summary 的扫面不限 span kind)(2026-10-12 ⑨)
312
+
313
+ - **病灶**:真实审批流是「挂起 → 人批 → 带 approvals 恢复」;恢复段的工具执行走续跑入口
314
+ (`loop.ts` 把 **run 根 id 当 turnId**),`approval.decided` 记在 **run 根** span 上。
315
+ 而 summary 聚它时只扫 `kind === 'llm.turn'` ⇒ 所有真实审批用法下 `guardrail:*` 行
316
+ **一行都没有**,只有「首段同步带 approvals」(基本只有测试这么干)才出行。
317
+ CLI `agentia report` 与 `agentia dev` 面板与 summary 同源,同样漏。
318
+ - **修法**:`approval.decided` 的聚合**不限 span kind**(`tool.output` 仍限 `llm.turn`)。
319
+ 落点因段而异是记账事实不是缺陷(与 `tool.input` / `tool.output` 同一纪律),事件体自带
320
+ `tool`;**一次决定只记一笔**(记账点只有 `executeOneTool` 一处,跨段是另一条 trace),
321
+ 扫面放大不会双计。引擎行为不动。
322
+ - **顺带订正文档口径**:`usage-guide.md` 的 `agentia.guardrail.*` 属性表原写「发起它的
323
+ `llm.turn` span」⇒ 与续跑再挂起场景矛盾,改为「与 `approval.requested` 同 span:首段
324
+ `llm.turn`、续跑段 run 根」。依据与「为什么不该限 kind」见 `docs/spec.md` §10 ⑨。
325
+ - **验证**:`packages/trace-view/test/summary.test.js` 保留首段形状用例、补恢复段形状
326
+ (decided 在 run 根 ⇒ 行在场且数值对)与「两处都聚」;**变异验证**(恢复 kind 限制
327
+ ⇒ 两条新用例红;复原绿)。summary.js 棘轮 126 → 130(净 +4 全是注释,代码行零增长)。
328
+ ### observability-mcp 示例复审收口:分组键对齐 CLI、root=`/` 特判、sessions 截断(2026-10-12 ⑩)
329
+
330
+ - **是什么**:`examples/observability-mcp/` 的三处低危修复(框架与 CLI **零改动**):
331
+ ① 会话归组从两级补成**三级回退**(run 根 `session.id` → 外层记录的 `TaskRecord.taskId` →
332
+ `(无会话)`),与 CLI `agentia report` 逐字同口径 —— 此前 `readTraceFile` 丢掉外层 wrapper,
333
+ FileTaskStore / SqliteTaskStore 导出的记录在示例侧全坍进 `(无会话)`;
334
+ ② 只读边界的 root=`/` 特判(`root + sep` 变成 `//` 曾把根下**所有**文件误拒);
335
+ ③ `report_trace` 的 `sessions` 加截断天花板(`MAX_SESSION_ROWS = 40`,与 diff 侧同档),
336
+ 新增 `sessionGroups` / `sessionsTruncated` 两个字段如实报出截断(**加法**,兼容)。
337
+ - **守护形态**:这份移植(本仓第四份)配的是**源码级双钉**
338
+ (`tests/observability-mcp-parity.test.ts`:三段 canonical 行在两份源码里逐字都在场),
339
+ 而不是前三份的运行时逐字对拍 —— 示例必须可整目录拷走单用、且示例 dist 在 `npm test`
340
+ 时未构建,两个对拍落点都不成立(理由见 `docs/spec.md` §10 2026-10-12 ref-g8-example-closeout)。
341
+ - **验证**:示例 `npm run verify` 17 → 26 条 check(新增 §8 三级回退 / §9 root=`/` /
342
+ §10 截断);**3 条变异各咬住预期断言**(去 taskId 层 / 去特判 / 去截断,各红)。
343
+ README 边界节补两条如实标注(TOCTOU 时间窗;入站 `traceparent` 缝合是引擎层白得的、
344
+ 本示例未做端到端断言)。
345
+
346
+ ### 观测面给 agent 的入口:两个 MCP 只读工具(G8)(2026-10-12 ⑦)
347
+
348
+ - **是什么**:新示例 `examples/observability-mcp/`(本地小包,`private`)用现成的反向桥
349
+ `createMcpServer` 把调优报告包成两个只读工具:`report_trace`(能力排行 + 会话维度的成功率
350
+ 与「每成功任务成本」)与 `diff_traces`(两条 run 的结构化差异)。任何 MCP 宿主(编辑器 /
351
+ 桌面客户端 / 另一个 agent)接上来,agent 就能自己查「我上一轮慢在哪 / 贵在哪」。
352
+ **核心零改动**:两个工具是 `buildRunReport` / `mergeRunReports` / `diffTraces` 的薄壳。
353
+ - ⚠️ **这是分发面,不是功能面**(本仓瓶颈在产品面 / 分发面):验收标准是「外部客户端接得上」,
354
+ 不是「新增了多少能力」。`npm run verify` 起真 HTTP server + 出厂连接器 + `mcpTools`
355
+ 连回去跑通 17 条 check(report / diff / 缺侧 / 截断 / 越界拒绝)。
356
+ - **三条口径**:① 只读 **trace 根**(`AGENTIA_TRACE_ROOT`,缺省 cwd)**内**的文件 —— 工具参数
357
+ 由模型给 ⇒ 不设边界等于对外发一个任意文件读原语,判据是 realpath 比前缀(不是 `startsWith`,
358
+ 后者挡不住 `../` 与同前缀兄弟目录);② 进模型上下文的东西有天花板(差异 span 最多 40 条,
359
+ 截掉多少如实报出);③ 读不出就说读不出(一条 trace 都认不出 ⇒ 报错而不是空表;成本给
360
+ `null` 而不是 0)。
361
+ - **门禁**:示例进了 `tsconfig.tests.json`(按仓库根那套严格开关类型检查,含
362
+ `exactOptionalPropertyTypes`)与 `e2e:examples` 第 10 轮;`examples/README.md` 表行由
363
+ `examples-table` 守卫双向核。
364
+
365
+ ### 交接与护栏:四个 `agentia.*` 属性 + 护栏进能力排行(G7)(2026-10-12 ⑥)
366
+
367
+ - **是什么**:交接(agent → agent)在这个框架里就是一条 capability span 本身,护栏(审批)则
368
+ **只是事件**(`approval.requested` / `approval.decided`)—— 两者对外部 OTLP 消费者都要解析
369
+ trace 才看得见。现在各补一个 additive 属性当入口(**不造 span kind**,核心零改动):
370
+ `agentia.handoff.from` / `.to` 写在被调 `@SubAgent` 的 capability span 上;
371
+ `agentia.guardrail.name`(待决的**首个**工具名)/ `.triggered`(`true`)写在发起审批的
372
+ `llm.turn` span 上。名字来源:run 根用 `runName`(与 OTLP 的 `gen_ai.agent.name` 同源)、
373
+ 子 agent 用它注册的 `name`;嵌套交接**逐层换名**。
374
+ - **视图**:`@migor/trace-view` 的 summary(CLI `agentia report` 与 `agentia dev` 面板同源)把
375
+ `approval.decided` 聚成 `guardrail:<工具名>` 一行 —— `calls` = 走到决定的次数、`err` =
376
+ **被拒**次数、`total` / `max` = 等审批时长。此前「哪个工具卡在审批上、被拒了几次」在排行上
377
+ **完全看不见**。
378
+ - ⚠️ **只发一半是口径不是遗漏**:`guardrail.*` **只在触发时发**,不设 `false`(与
379
+ `gen_ai.conversation.compacted` 同款);`handoff.*` 拿不到调用方身份时(宿主手搓 recorder +
380
+ ctx 直调 `subagentToTool`)**两个键都不发**。⚠️ 护栏行**只聚 `decided`**:只有
381
+ `approval.requested` 而始终没恢复的挂起不产出行;缺 `waitedMs` 记 0 不编造。
382
+ - ⭐ **一处实现上的补空(设计稿没说清)**:「调用方 agent 名」在调用点**拿不到** ——
383
+ `RecorderBackend` **只写不读**(查不了 span 树)、`ctx.parentSpanId` 反查不出名字。唯一来源是
384
+ 「当前调用期」作用域 ⇒ `SpanScope` 加可选 `agentName`,并把 `withCurrentSpan` 从**整条替换**
385
+ 改成**合并**(夹在中间的 `llm.turn` 谁都不知道身份,替换会把名字抹掉)。收窄语义逐字不变。
386
+ - **影响面**:`src/engine/{span-scope,loop,turn}.ts`、`src/toolkit/subagent.ts`、
387
+ `packages/trace-view/src/summary.js`(棘轮 106 → 126)。公共导出面 `src/index.ts` **零变化**。
388
+ 约定与读法写进 `docs/usage-guide.md` §6.4「交接与护栏」。
389
+ - **验证**:`tests/toolkit/subagent.test.ts`(嵌套逐层换名 / 无作用域时不发)、
390
+ `tests/engine/approval.test.ts`(在场 + 不在场两态)、`packages/trace-view/test/summary.test.js`
391
+ (护栏行三态)。**变异 4 条:3 条当场咬住,第 4 条先没咬住 → 抓出的是我自己的自证样本**
392
+ (`approved` 一真一假时,把判据取反仍是 1)⇒ 补样本后才咬住,已写进用例注释。
393
+
394
+ ### CLI `agentia report` 加会话 / 任务维度(G6)(2026-10-12 ⑤)
395
+
396
+ - **是什么**:`agentia report` 此前只有**能力维度**(`capabilities[]`)+ run 级**计数**
397
+ (`runs` / `failed`),而 `failed` 是**次数不是分母** ⇒ 「这一段跑得好不好」的第一问
398
+ (成功率)在输出上**根本算不出来**。现在另打一张按**分组键**归组的表,`--json` 里是
399
+ `sessions: SessionRow[]`(**加法**,兼容)。
400
+ - **分组键三选一、不混排**(一条 run 只进一个组):run 根 span 的 `session.id`
401
+ (宿主传 `session: { store, id }` 才写)→ 记录里的 `TaskRecord.taskId` → `(无会话)` 桶。
402
+ ⚠️ 会话一般聚多条 run,而 `taskId` **逐任务唯一** ⇒ 后一路通常是「一组一条」,它是给单任务
403
+ 批跑逐条标成本,**不是**多轮聚合。
404
+ - **口径**:`runs` / `ok` / `fail` 三个数**一起出列**(只报百分比在小组成上是噪声);
405
+ ⚠️ **成功数为 0 时 `cost-ok` 是 `-`(JSON 里 `null`),不是 0** —— 「除不了」不等于「不花钱」。
406
+ 成本取该 run 的 `totalUsage.costEstimate`(= 各 `llm.turn` 之和、**不含** capability 聚合
407
+ ⇒ `@SubAgent` 里套 `@Skill` 不会把同一批 token 数两遍)。⚠️ 设计稿原写「该组各能力行
408
+ `costUsd` 相加」,落地时**改掉**了 —— 理由见 `docs/spec.md` §10 2026-10-12 ref-report-session-dimension。
409
+ - **影响面**:`packages/cli/src/report.ts`(194 → 339 行,棘轮账 LINE/TOTAL 同步);
410
+ `docs/usage-guide.md` §6.4 的 `--json` 形状列表加 `sessions`。**框架侧 `src/` 零改动。**
411
+ - **验证**:`report.test.mjs` +6 条(归组 / `(无会话)` 桶 / `taskId` 回退键 / 降序 /
412
+ **「成功数为 0 ⇒ `-`」的反向断言** / 成本读不出 ⇒ `-`);**变异 4 条各咬住预期的那一条**
413
+ (`> 0` → `>= 0` / 丢掉 `taskId` 回退 / 把「读不出」写成 `0` / `session.id` 读成 `sessionId`)。
414
+
415
+ ### OTLP 导出:三个缓存/压缩 semconv 键 + 可选的 execute_tool span 投影(2026-10-12 ②)
416
+
417
+ - **G3(additive、双发不替换)**:`llm.turn` 缓存读数 > 0 时另发
418
+ `gen_ai.usage.cache_read.input_tokens` / `gen_ai.usage.cache_write.input_tokens`
419
+ (0 = 没走缓存 ⇒ 不留键);父 span 上有早于本回合开始的 `context.budget` 事件 ⇒ 发
420
+ `gen_ai.conversation.compacted: true`(上游口径:只在能可靠判定时置真,不设 false)。
421
+ 键名逐字核自 `semantic-conventions-genai @ 6fd0d763` 的 `client-inference.md`。
422
+ - **G2(`OtlpExporterOptions.projectToolSpans`,默认关)**:开启后把 span 上按 `tool_use_id`
423
+ 配对的 `tool.input` / `tool.output` 事件投影成 `execute_tool <工具名>` child span
424
+ (`gen_ai.operation.name=execute_tool` + `gen_ai.tool.name`;start/end 取两事件的真实记账时刻;
425
+ `ok:false` → status Error + `error.type`)。**导出侧投影**:engine 记账点不动、本地 trace
426
+ 形状不变、原事件保留在父 span 上;关闭时导出产物与既往一致。孤儿 input
427
+ (`traceLimits.maxEvents` 截断可造出)→ 不写 `endTimeUnixNano` / status +
428
+ `agentia.projection.orphan: true`,不编零时长。
429
+ - 顺带:`context.budget` 事件名上移为 `core/trace.ts` 的 `CONTEXT_BUDGET_EVENT` 单源常量
430
+ (第一次有了跨层消费方)。
431
+
432
+ ### run 级 `sessionId` 的静默失效改成出声(2026-10-12 ③)
433
+
434
+ - **是什么**:程序内 `app.run(messages, { sessionId })`(不带 `session`)此前**静默丢弃** ——
435
+ run 根没有 `session.id`(OTLP 的 `gen_ai.conversation.id` 也就不会有)、会话历史接不上、
436
+ 零信号(可观测深钻 §2 行 7)。现在 `AgentApp.run` 入口 `console.warn` 一次,文案指出正确写法:
437
+ 要会话维度 + 历史用 `session: { store, id }`;只要 trace 标记不要历史,直连
438
+ `executeRun` / `runAgent` 的引擎级 `sessionId`(那一层本就生效)。
439
+ - **内部链路不误伤**:`AsyncRunner` 的续跑段刻意不注入 `session`,`buildRunCallOptions`
440
+ 不再把任务里的 `sessionId` 透传进 `app.run`(它在本层已被消费 —— 换成 `session` 注入,
441
+ 或没配 `sessionStore` 时在 submit 已响亮报错)。
442
+ - **无破坏性变更**:行为面只多一条告警;`sessionId` 的类型与异步宿主语义逐字未变。
443
+ 依据与备选方案(为什么不是「让 `app.run` 转发它」)见 `docs/spec.md` §10 2026-10-12 ref-session-id-warn。
444
+
445
+ ### MCP 桥接 SEP-414 trace 上下文(`params._meta` 裸键)(2026-10-12 ④)
446
+
447
+ - **出站**:run 内经 MCP 桥调 `tools/call` 时,两个出厂连接器自动把当前 span 的 W3C
448
+ `traceparent` 放进 `params._meta`(SEP-414 定的载体:裸键、不加前缀、**不进 `arguments`**)——
449
+ 认出它的 server(MCP Python SDK、agentia 反向桥等)就能把这次调用链回你的 trace。
450
+ 不在 run 内(桥脱离引擎单用)则不带:不造空上下文。
451
+ - **入站**:`createMcpServer` 认 `tools/call` 的 `params._meta.traceparent`,合法即记成该次
452
+ trace run 根的一条 `links`(与 HTTP `traceparent` 头同款缝合);缺失 / 畸形静默当作
453
+ 没有上游 —— 不报错、不影响调用成败。
454
+ - **无破坏性变更**:`McpClientLike.callTool` 第三参新增可选 `traceparent` 字段(鸭子类型,
455
+ 只实现两个参数的旧客户端照常兼容,只是链路断在它的传输上);公共导出面零变化。
456
+ 决策与三条纪律(静默降级 / 不动 `arguments` / 不造空上下文)见 `docs/spec.md` §10 2026-10-12 ref-sep414-trace-context。
457
+
458
+ ### 可观测文档:四条「缝外配方」类缺口收口(2026-10-12)
459
+
460
+ 依据 `docs/reviews/2026-10-12/observability-deep-dive.md` 的 G9 / G10 / G4 / G5,**纯文档**,`src/` 一行未动:
461
+
462
+ - `docs/observability.md` 新增四节:**§6 投递纪律**(sink 内攒批 + 定时 flush 的骨架、关停时
463
+ 先 `handler.drain()` 再 `sink.drain()`、后端不可达就丢遥测不丢可用性;并明说**为什么不把
464
+ `flushSinks` 改成 fire-and-forget** —— sink 的语义是「有兜底、会投递」,改了等于删掉一条
465
+ 已有承诺);**§7 OTLP 只发 JSON ⇒ 直打 Phoenix 会 415**(已知边界 + collector 转码配方,
466
+ 含最小可用配置;为什么不内建 protobuf 编码器);**§8 步数与环**(纯 trace 函数配方:
467
+ 数 `llm.turn` span、按回合归属成本、`tool_use_id` 配对、识别「同一工具同参数连调 N 次」);
468
+ **§9 轨迹评测**(确定性检查优先 —— 签名相等 / 步数上限,`agentia diff` / `harvest` /
469
+ `defineEval` 拼合,LLM-judge 殿后)。
470
+ - **顺带订正一处事实错误**:§2.6 原把 Arize Phoenix 列进「平台支持 OTLP ⇒ 用内置导出器即可」
471
+ —— Phoenix 只收 `application/x-protobuf`,内置导出器(只发 `application/json`)打不进去。
472
+ - `docs/usage-guide.md` §7 已知边界表新增一行(OTLP trace 导出只发 JSON ⇒ 直打 Phoenix 会 415,
473
+ 按 choice 登记);`tests/docs/boundary-table.test.ts` 登记表同步(93 条:pin 57 / choice 11 /
474
+ gap 25)。
475
+
476
+ ### `agentia report` 按 `traceId` 去重:append-only 导出不再翻倍计数(2026-10-12 ⑧)
477
+
478
+ - **修什么**:`FileTaskStore` 是 append-only(每次 save 追加一行),审批流任务的导出里同一棵
479
+ 挂起段 trace 会出现多行(挂起 / approve / 接管各 save 一次),而 `report` 按行计 ⇒
480
+ runs / 成功数 / 成本全部翻倍(实测实际 2 段 0.02 的任务报出 runs=3、cost=0.03)。
481
+ 现在按 **`traceId` 去重、只留最后一行**(append-only 里后面的是新状态);能力表与 runs 总计
482
+ 也走同一份去重后的输入(存量问题一并修)。去重键是 traceId 而不是 taskId:一个任务可跨多段
483
+ run,每段一棵新 trace —— 按 taskId 去重会把不同段错并成一条。
484
+ - **无破坏性变更**:`ReportJson` 形状未动,只有计数口径修正(同 traceId 的重复行此前本就是
485
+ 同一棵 trace 的重复书写);无 traceId 的行(手写 trace)照原样全保留。
486
+ - **文档同步**:usage-guide §6.4 的 FileTaskStore 段落点明去重口径;会话维度段落点名
487
+ 「**挂起计入 ok**」(与引擎「挂起不是失败」同口径 ⇒ 全挂审批上会报成功率 100%,刻意取舍
488
+ 但需写明)。决策依据见 `docs/spec.md` §10 2026-10-12 ref-report-dedupe-traceid。
489
+
490
+ ### 工程:决策引用换成 slug + 守卫收口(P2 / P3)(2026-10-14)
491
+
492
+ - **接上一条的 ①**:`(日期 + 圈号)` → `(日期 + slug)` 的三步迁移**已走完**(设计稿
493
+ `docs/plans/2026-10-14-ref-slug-migration.md`):上一轮 P1 给 §10 的每条锚点加了 slug(引用一处未动),
494
+ 本轮 P2 把引用换成 slug、P3 把守卫收成只认 slug。
495
+ - **P2 换引用**:射程 = 守卫射程 − 带日期的**快照**(`docs/plans/**` · `docs/reviews/**`,写于迁移之前
496
+ = 历史)− §10 **锚点定义行**(圈号在那里是显示编号)− **日期在 §10 无条目**的引用(那个号指别的文档
497
+ 自己的编号)⇒ **302 处 / 84 个文件**,按文件分 5 批提交(便于 review 与单独 revert)。替换**按内容
498
+ 不按行号**(§10 加了章节头,其后行号全漂过)。脚本带一条硬规则:**同日重号拒绝自动选**(源序先到
499
+ 先得 = 脚本替引用挑一个含义,正是本设计要消灭的东西)⇒ 源头给一行显式覆盖表。
500
+ - **守恒验证**:84 个文件**归一化后逐字节相同** ⇒ 除引用形态之外**一个字符都没动**;另有按
501
+ `(文件, 日期, 键串)` 的前后对拍(P1 的「引用一处未动」也这么验)。
502
+ - **P3 收口**:守卫从「圈号**或** slug 都能解析」(P1 起的**有期限**放宽)收成**只认 slug**,并新增
503
+ 判据「射程内(**除定义行、除快照、日期有条目**)的『日期 + 圈号』引用数 == 0」。**这不是新决定** ——
504
+ 它是 P1 时就写在代码里的退出条件到期执行。三项排除写成**减法**(各自指向另一条已写明的规则),
505
+ 而不是「给 `isDefLine` 加个开关」:读判据的人要能顺着三项去查。
506
+ - **一处口径修正(如实记)**:`packages/cli/CHANGELOG.md` **移出射程** —— 它是构建产物、只在**本机
507
+ 磁盘**存在(CI / 新克隆 / `git worktree` 里没有)。P3 首跑报「49 处残留」,其中 **46 处来自它**
508
+ ⇒ 校验它的后果就是**本机红、CI 绿**。去掉它不丢覆盖(根 `CHANGELOG.md` 那一档覆盖同一份内容)。
509
+ - **另登记一条待守缺口(本轮量出、不在本轮做)**:**不带日期的引用**(`§10 ⑧` / `同日 ⑧` —— 日期写在
510
+ 上一句里)**从来不在守卫射程**(它的形态要求日期**紧邻**键)。实测 **35 处**(探针
511
+ `.workbuddy/probes/2026-10-14-dateless-circ.mjs`,git 跟踪文件口径、可复跑)⇒ 已登记进
512
+ `docs/guards.md` §2 的「待守」表(含「为什么还没有守卫」与两种可能的守卫形状)。
513
+ 「引用已全换成 slug」这句只对**带日期**的那一类成立 —— 这行缺口就是它的射程注脚。
514
+ - **「怎么写引用」到 P3 才写**(设计稿 §7.1 第 4 问的定稿):`AGENTS.md` 的「设计决策」与
515
+ `CONTRIBUTING.md` 的坑表各补一句。P1 期两种写法并存,那时教谁都是半对。
516
+ - **验证**:`circle-refs` 3/3;**P3 变异 8/8 逐条咬中** —— 改锚点 slug 名 ⇒ **恰好**引它的那几行红;
517
+ 造重名 slug ⇒ 唯一性红;把引用 / 锚点正则**分别**打瞎 ⇒ **真空护栏**红(**不是**「全绿」);塞一处
518
+ 「日期 + 圈号」⇒ 残留判据红;把引用的日期改错 ⇒ 「该 slug 属于」红;删 slug 留圈号 ⇒ 半迁移红;
519
+ **正对照**(M6):「圈号是**可去掉**的显示编号」这条设计**真的生效** —— 删掉某条锚点的显示圈号后
520
+ 锚点仍可引、守卫仍绿。每项都在输出里点到名,恢复后 `sha256` 逐字节一致。
521
+ - **无使用者面变更**:本轮只动文档与一个守卫(`src/` 一个文件都没碰)⇒ 既有使用者不需要任何动作。
522
+
523
+ ## [0.10.2] - 2026-10-11
524
+
525
+ > 本版主题(窗口 `0.10.1 → 0.10.2`):**对外那一面各做实一格 —— 一个人能不能找到它、
526
+ > 能不能 5 分钟跑起来**;对内把框架最深的两块体积债还清。三块 —— ① **交付面**:脚手架生成的
527
+ > 新工程默认入口就是**可上线的 HTTP 服务**(`npm start` 直接起服务,不再是需要自己接线的批处理
528
+ > 样例);② **分发面**:官网全站**英文版**(`/en`,五页逐节对译 + 英文 404 + 语言自动检测与手动
529
+ > 切换)—— 对外检索面第一次有英文落点;③ **内部债**:两轮**纯结构**重构(方法级体量闸与三处巨型体
530
+ > 外移;`#executeInner` 的结果记账外移 + 补 14 条直接单测),顺带把 c8 分支覆盖率棘轮从 88 收到 89。
531
+ > **无破坏性变更**:脚手架是**生成物**,已生成的工程有自己的 `package.json` / `src/`,不会被本版动到
532
+ > ⇒ 既有使用者**不需要任何动作**;`@migor/agentia` 的公共 API 与 `exports` **逐字未变**(本版唯一
533
+ > 碰 `src/index.ts` 的是版本常量那一行)。
534
+
535
+ ### 官网 · 全站英文版(`/en`)+ 语言自动检测与手动切换(2026-10-08 ①)
536
+
537
+ - **是什么**:站点从中文单语扩成中 / 英两版 —— `index / docs / api / playground / tradeoffs`
538
+ 五页**逐节对译** + 英文 404,挂在 `/en/*`(中文仍在根路径,是默认入口)。此前整站只有中文
539
+ (`fragments/` 下没有 `en/`、也没有 `/en` 路由)⇒ 对外检索面**没有英文落点**,而这正是本仓
540
+ 「瓶颈在产品面 / 分发面」那句话里最直接的一格。
541
+ - **语言怎么定**:`<head>` 最前的一段 `is:inline` 脚本,三条判据 —— ① 先读
542
+ `localStorage['agentia-lang']`(用户**显式**切过);② 否则看 `navigator.languages` 里有没有
543
+ 中文;③ 拿不到语言信息就**不跳**(宁可停在默认的中文版,也不猜)。⚠️ 自动跳转**只读不写**
544
+ localStorage —— 写了就等于把「自动」记成「用户选择」,用户之后手动点回中文会被**立刻弹走**;
545
+ 只有 Nav 的切换控件才写。手动切换在导航右侧(`中 / EN`),指向**同页**的另一种语言。
546
+ - **URL 形态**:`build.format: 'file'` 下英文首页的产物是**扁平的 `en.html`** ⇒ 首页地址是
547
+ `/en`(无尾斜杠);`public/_worker.js` 加一条 `/en/` → `/en` 的 308 收敛(访客看到
548
+ `/en/docs` 会顺手猜 `/en/`)。站点地址一律干净形态(`.html` 会被 Cloudflare Pages 308)。
549
+ - **SEO / agent 面**:两版各自 canonical 指自身,`hreflang` 三处互指(`x-default` 指中文版);
550
+ `sitemap.xml.ts` 增 5 条 `/en/*`;`llms.txt.ts` 加「Documentation (English)」节;
551
+ `scripts/check-website-agent-readiness.mjs` 与 `scripts/build-md-variants.mjs` 的页面枚举改成
552
+ **递归** —— 不改则英文页**全部在产物守卫射程外**,且拿不到 `.md` 变体(GEO 档 C/D 直接退化)。
553
+ - **英文 docs 页不摆第二份「已知边界」表**:那张表是构建期从**中文** `docs/usage-guide.md` §7
554
+ 抠的,英文页手写第二份必然漂移 ⇒ 改成「一句说明 + 指回单源」;`llms-full.txt` 保持中文单源不动。
555
+ - **客户端脚本文案**(首屏 trace 自播 / 回放面板 / BYOK 真实模式)是**运行期**插进 DOM 的,
556
+ 构建期无从替换 ⇒ 新增 `packages/website/src/scripts/lang.js` 的 `pt(zh, en)` 接缝,中英**贴在一起写**
557
+ (不抽两份平行数组 —— 那会漂移,而且没有任何东西看得出来);语言取 `<html lang>`
558
+ (服务端已经定下的事实),**不读 `localStorage`**(否则「服务端渲了英文页、脚本文案出中文」
559
+ 这种错配就有了可能)。
560
+ - **守卫 `tests/docs/website-i18n.test.ts`**:页面集合一一对应 / 中英 fragment 的 `id` 集合逐页
561
+ 相等 + 剧本骨架(span id / 事件类型 / 顺序 / `usage`)逐例相等 / 英文面 0 CJK(fragment **与
562
+ 真跑客户端脚本**)/ `<html lang>` 映射与三处 `hreflang` / 检测脚本**只读不写** localStorage /
563
+ 五条 `/en` 路由已登记进 sitemap + llms.txt + `_worker.js`。**反向验证过 19 条**(各恰好点名
564
+ 那条,复原后 `sha256` 逐字节一致)—— 其中第一版有一条**假守卫**(裸正则被文件头注释里的
565
+ `lang='en'` 满足),是变异验证当场抓出来的。
566
+ - ⚠️ 两处如实标注的**射程**:① 该守卫**不看 `dist/`** —— `npm test` 在 `verify-all` 里早于官网
567
+ 构建(步 6 vs 步 8);产物侧仍由步 8 的 `scripts/check-website-agent-readiness.mjs` 守。
568
+ ② 「英文面 0 CJK」走**行为断言**(子进程喂假 DOM + 假 `fetch` 真跑,见
569
+ `tests/fixtures/website-i18n-harness.mjs`)而非源码扫描:本仓 `typescript` 是 7.x(tsgo),
570
+ 主入口已不再导出经典 parser API,而手写扫描器会被 `playground.js` 里**带反引号 / 引号的正则**
571
+ (`/^\s*```/`、`/[&<>"']/g`)带偏(实测 24 处假命中)⇒ 它能抓「新增一段没包 `pt()` 的中文」,
572
+ 抓不到「**永远跑不到的死分支**」里漏包。
573
+ - 官网**不随包发布**(`packages/website` 是 `private: true`)⇒ 本版**无包的对外面变更**,
574
+ 既有使用者不需要任何动作。
575
+
576
+ ### 官网 · 手机版重点特性卡的 `Core` 徽章压住标题(2026-10-08 ②)
577
+
578
+ - **病例**:390px 手机截图 —— 英文首页「Built for production」那张重点卡的 `Core` 徽章叠在
579
+ 标题 `Observable: trace as a first-class citizen` 的首行上(暗色小字下像两个字叠印)。
580
+ - **根因**:`.feature-flag` 是 `position: absolute; top: 16px; right: 18px`。桌面端这张卡
581
+ `grid-column: span 2` 跨两列、标题一侧有富余,撞不上;**单列窄屏下标题换行**,首行末端
582
+ 直接钻进徽章底下。中英共用同一套 CSS ⇒ 不是「英文站专有」,标题一长就复现。
583
+ - **修法**:在**已有的单列断点**(`@media (max-width: 560px)`,即那条把 `.feature--key` 的
584
+ `grid-column` 收回 `auto` 的地方)把徽章放回正常流:`position: static` +
585
+ `display: inline-block` + `margin-bottom`。⚠️ **只写 `position: static` 不够** ——
586
+ `<span>` 默认 `display: inline`,内联元素的 `padding` / `border` 不参与行高计算,
587
+ 徽章框照样会叠到下一行去。
588
+ - **守卫**:`tests/docs/website-css.test.ts` 增两条 —— ①「单列断点里 `.feature-flag` 必须
589
+ 收回正常流(且不是内联)」;②「该断点须覆盖手机宽度(≥480px)」。**反向验证过 4 条**
590
+ (删掉整条规则 / `static`→`absolute` / 去掉 `display` / 断点 `560→360`),各点名对应断言,
591
+ 复原后 `sha256` 逐字节一致。⚠️ 断言前**先剥注释**:本仓踩过「守卫被同文件注释满足」的假绿。
592
+ - 顺手把 `docs/guards.md` §1 里 `website-css.test.ts` 那行补准:**2026-09-30 加的
593
+ 「折叠菜单 CTA 边框」守卫一直没登记**(那行仍写着「反向验证过 3 条」)。本次给它补做
594
+ 4 条变异验证,该行一并更新。同样**不涉及包**(改的是 `packages/website` 的样式与网站守卫)。
595
+
596
+ ### 官网 · 部署后的 agent 打分不再在半传播状态下跑;`/en/` 的 308 补缓存头(2026-10-08 ③)
597
+
598
+ - **修掉一条腐烂的判据(真缺陷,非本次引入)**:`ci.yml` 的 `deploy-website` 在打分前要「等新
599
+ 版本真上线」,而那条判据是「`robots.txt` 里出现 `Sitemap: ` 行」—— 注释写着「它是本轮新增的
600
+ 产物,只有新版才可能有」。**那一行自 PR #119 起每一版都有** ⇒ 条件恒为真、从不等待。实测当场
601
+ 中了:第 1 次探测就报「已在线上」,而 AFDocs 读到的是**上一版的 sitemap**,据此刷出两条 **假
602
+ FAIL**(`llms-txt-links-resolve` 报「2 条坏链」、`llms-txt-coverage` 报「5 links not in
603
+ sitemap」—— 而 11 条链接逐条复测全是 200)。
604
+ ⇒ 判据换成「**本次运行的提交**」:站点侧由 `packages/website/src/pages/robots.txt.ts` 在构建期
605
+ 写下 `# build: <GITHUB_SHA>`(本地构建**不写** —— 「这一行在不在」本身就是「这份产物是不是 CI
606
+ 构建的」的判据,不给它编假缺省值),CI 侧轮询等线上出现**同一个 SHA**。等不到不再静默:轮询
607
+ 超时留 `::warning::`,而构建产物里没有该行则 `::error::` 并**跳过打分**。
608
+ ⚠️ **教训(写进 `ci.yml` 与 `robots.txt.ts` 的注释)**:判据不能是「某个只有新版才有的字面量」
609
+ —— 它会随版本变旧、然后恒真;只能是「本次运行自己的身份」。
610
+ - **`/en/` 的 308 补上缓存头**:`public/_worker.js` 原先用 `Response.redirect()`,它只给一个
611
+ `location`、**不带任何 cache 头**(Pages 自己那条尾斜杠 308 同样不带)—— 而这条规则由我们接手
612
+ 的理由**正是响应头**(否则没有任何地方能补)。改成显式 `new Response(...)` 并带上与站内其余响应
613
+ 同一档的 `public, max-age=0, must-revalidate`。行为用例增 3 条(`/en/` ⇒ 308 + 目标 + **缓存头**
614
+ + 不劳烦 `ASSETS`;带 `Accept: text/markdown` 仍 308;`/en` 本体不拦),**反向验证 2 条**
615
+ 各点名对应断言。
616
+ ⚠️ 两处更正:① 那条注释原先写着「Pages 对 `/en/` 只会回 404」,**是错的**(实测 Pages 会把
617
+ `/docs/`、`/en/docs/` 这类「产物里存在 `x.html`」的目录形态自己 308 到干净形态);② 上一轮我曾把
618
+ 那次 `cache-header-hygiene` FAIL 的账算在**这条 308** 头上 —— **也不是它**(AFDocs 会跟随重定向,
619
+ 逐端点复测静态资源 / `.md` / llms / sitemap 全合格)。那次的真因就是上面那条**腐烂的判据**。
620
+ 补这个头是显式的**卫生**,不是那次红的解药。
621
+ - **守卫加固(并更正一处误判)**:`tests/docs/website-i18n.test.ts` 里「检测脚本用了
622
+ `location.replace`」这条断言原先直接匹配脚本原文 —— 一条**写着同样字面量的注释**就能把它喂绿。
623
+ 现在先剥**整行注释**再断言,并给剥壳器加了自证断言。⚠️ 上一轮我把风险说成「靠那段 HTML 注释
624
+ 用了全角括号才没爆」—— **不对**:那段注释在 `<script>` 块**之外**、根本不在被断言的字符串里;
625
+ 真实(也较窄)的风险是**脚本块内**的注释。同一守卫里另一条「`_worker.js` 的 `/en/` 规范化必须是
626
+ 308」原先钉死了整条 `Response.redirect(...)` 表达式 ⇒ 上面那次**行为等价**的改写把它打红了;已改成
627
+ 只核**登记**(分支在场 + 是 308),行为交给真跑用例 —— **钉实现的字面量会把「等价改写」判成回归**。
628
+ - **新增元守卫**:`tests/scripts/verify-all-wiring.test.ts` 增一条 —— 把「等新版上线」判据的
629
+ **两端契约**钉住(`ci.yml` 侧必须按本次提交比;生产侧**真跑** `robots.txt.ts`,按打桩的
630
+ `GITHUB_SHA` 核**渲染出来的产物**,并钉住「没给 SHA 时不输出这一行」)。写这条守卫时**我自己先
631
+ 写出两条假守卫**(判据侧匹配整个 job body ⇒ 被同一 job 里另一处同样的串满足;生产侧核源码文本
632
+ ⇒ 被文件自己的注释满足),都是变异验证当场抓到的,已收紧。
633
+ - 同样**不涉及包**(改的是 `.github/workflows/ci.yml`、`packages/website` 与 `tests/`)。
634
+
635
+ ### 官网 · 语言跳转从客户端 JS 搬到服务端 302(2026-10-09 ①)
636
+
637
+ - **病例(真 FAIL,非判据抖动)**:AFDocs(`agentdocsspec.com` 的 0.20.0 打分器)的
638
+ `redirect-behavior` **逐页**判 `js-redirect` 而 FAIL —— 这是 CI 上唯一一条**真** FAIL
639
+ (`cache-header-hygiene` 那条是部署半传播的连带假红,早前已排除)。根因:语言自动检测是
640
+ `Base.astro` 里一段 `is:inline` 脚本(`location.replace`),而它**留在每一页 HTML 里**
641
+ (英文页也有,只是运行期不触发);AFDocs 判的是「这份 HTML 里有没有 JS 跳转」,不区分它会不会真跑。
642
+ - **搬到服务端**:`packages/website/public/_worker.js`(Pages **advanced mode** worker,源码 ==
643
+ 线上行为)按 `Accept-Language` 对**中文页面路径**回 **302** 到 `/en` 等价路径,并带
644
+ **`Vary: Accept-Language`**(少了它 CDN 会把中文那份 200 回给英文访客 —— 跳转直接失效)。
645
+ 客户端那段脚本连同 `localStorage['agentia-lang']` 一起下线。⚠️ 设计稿
646
+ `docs/plans/2026-10-08-website-i18n.md` §7 那条「静态托管没有服务端、JS 是唯一手段」当场被推翻
647
+ (它没把 `_worker.js` 算进去),已在原处划掉并补 §9。
648
+ - **显式选择优先于自动检测(防循环的那把锁)**:`Nav.astro` 的中 / EN 切换控件改写成
649
+ **`hl=zh` / `hl=en` cookie**(`path=/`、一年有效);`/` 见到 `hl=zh` 就**不跳** —— 没有它,
650
+ 英文浏览器点「中」落回 `/` 后会被自动检测再次弹回 `/en`,用户**出不来**。
651
+ - **四条刻意边界**(写进 `_worker.js` 注释 + 设计稿 §9.3):① 只从中文路径跳到 `/en`、**不反向**
652
+ (`/en` 是本体、永不拦;代价如实标注:中文偏好访客循外链落到 `/en` 时**不会**被送回 `/`,
653
+ 旧 JS 会 —— 有意收窄);② **只在非 markdown 请求上做**(带 `Accept: text/markdown` 的 agent
654
+ 要它请求的那个 URL 的 `.md`);③ 只处理 `GET` / `HEAD`;④ 302 **不给长 `max-age`**
655
+ (与站内其余响应同档 `public, max-age=0, must-revalidate`)。
656
+ - **守卫**:新增 `tests/docs/website-lang-negotiation.test.ts`(**真跑** `_worker.js`,喂假
657
+ `env.ASSETS`)11 条 —— 用户点名的五条(`hl=zh` 不跳 / 无 cookie + `Accept-Language: en` ⇒
658
+ 302 到 `/en` / `zh-CN` 不跳 / `/en` 本体不被拦 / **手动切回中文后不再被弹回**)外加 `Vary` +
659
+ 缓存头、深层页 query 保留、`POST` 与 markdown 不跳、`/en/` 308 与内容协商**不回归**。
660
+ `tests/docs/website-i18n.test.ts` 两条改写(客户端跳转**已下线**:断言打在**两层剥壳**后的源码上;
661
+ Nav 改核 **`hl` cookie**、且只认真实 `onclick` 处理器里的写入 —— Nav 注释里就写着 `localStorage`)。
662
+ **反向验证过 8 条**(各恰好点名那条,复原后 `sha256` 逐字节一致):整段停用 ⇒ **4 条** /
663
+ 忽略 cookie ⇒ **2 条** / `Vary` 换成 `Accept`、去掉 `/en` 排除、去掉「非 markdown」闸、
664
+ 去掉方法闸 ⇒ **各 1 条**(服务端);Nav 退回 `localStorage`、Base 加回客户端脚本 ⇒ **各 1 条**。
665
+ - **验收读数(本地真跑,8 页 curated 样本)**:`redirect-behavior` 由 **FAIL(js-redirect,逐页)
666
+ 翻成 PASS**(「No redirects detected across 8 pages」),`URL Stability and Redirects` 100/100;
667
+ 同一份产物在**退回旧实现**时该检查重新 FAIL —— 红→绿两向都实测过。
668
+ - ⚠️ **一处自己踩到的坑(顺手记下)**:第一版把「已下线」的说明写在 `<head>` 的 **HTML 注释**里,
669
+ 而 Astro 会把它**渲进每一页**(`location.replace` 字面量又回到了产物 HTML 里 —— 正是这条检查
670
+ 在看的字面量);已把说明移进**构建期才执行的 frontmatter** 注释,`<head>` 只留一句不含该字面量的短注。
671
+ - 官网**不随包发布**(`packages/website` 是 `private: true`)⇒ 本版**无包的对外面变更**,
672
+ 既有使用者不需要任何动作。
673
+
674
+ ### 深度评审收口:框架记账 / patrol 软链边界 / soak 假红假绿 / i18n 守卫记录面(2026-10-09 ②)
675
+
676
+ > 对 0.9.5 → 0.10.1 全窗口 + 在制品做了三轮深度评审(框架内核 / CLI / exports / soak /
677
+ > terminal-bench / patrol / 官网 i18n / CI),本节是发现的修复批次。每处修法都带变异验证
678
+ > (改坏 → 点名断言红 → 复原核 diff)。
679
+
680
+ - **`createOpenAIClient` 接 DeepSeek 缓存计量**(`src/integrations/openai.ts`):DeepSeek 的
681
+ `prompt_cache_hit_tokens` 此前被整个丢弃且 cache 恒记 0 —— agent loop 前缀缓存命中率常达
682
+ 80–95%,成本读数系统性高估数倍。现映射进 `cache_read_input_tokens`,且因 DeepSeek 口径
683
+ `prompt_tokens` **已含**命中部分(与 Anthropic 相反),`input_tokens` 拆出未命中部分防双计;
684
+ 无这两个字段的端点行为逐字不变。决策依据见 spec §10 2026-10-09 ref-deepseek-cache-usage。
685
+ **对包的对外面**:DeepSeek 端点的记账口径变准(原先恒 0)—— 修正而非破坏;其余无公共 API 变更。
686
+ - **patrol 示例的边界校验补上 realpath**(`examples/patrol/src/workspace.ts`):原实现只有词法判定,
687
+ 根内指向外部的符号链接可读出界(数据外泄给模型)、`quarantine_path` 可把界外真文件移进隔离区
688
+ (审批人只见 benign 相对路径);头注却声称「软链逃逸一律抛错」。现为词法 + 真身两道校验,
689
+ walk 不跟随软链,注释/README 与实现对齐。
690
+ - **terminal-bench 适配器五处**:基础设施错误(限流/5xx/超时/连接/上下文超长)不再落进能力分母
691
+ (新增 exit 4 桶);bash 哨兵锚定整行(`set -x` 不再污染);`harbor_agent.py` 版本从真值派生
692
+ (不再硬编码);任务转换不再静默丢 `docker-compose.yaml`(单服务提取 ENV/WORKDIR/ENTRYPOINT,
693
+ 做不到的响亮告警)与原版 `run-tests.sh`(保留原文 + 尾部接 reward 落盘,测试依赖安装不再丢失);
694
+ 容器内静态 node 包补 SHASUMS256 校验。
695
+ - **soak 验证工具**:`e2e-soak-suspend` 看门狗余量从真实预算逐轮派生(旧写死 +30s 在长档必假红)、
696
+ approve 臂补「人工批落地 ≥1」闸(微档不再空转恒绿)、续跑段 link 校验从「存在」升级为
697
+ 「指向上一段」;`e2e-soak-app` 头注改为如实表述(假端点只回 echo,subagent/skill 链路本档未压到)。
698
+ - **官网 i18n 守卫补记录面**(`tests/fixtures/website-i18n-harness.mjs`):`placeholder`/`value`/
699
+ `title`/`aria-label`/`setAttribute` 通道此前不被记录 —— 英文页漏包 `pt()` 直接显示中文也全绿;
700
+ 假 fetch 剧本补 429 与网络错误分支。`_worker.js` 的 `/en/` → 308 补上 query/hash 保留。
701
+ ⚠️ 本批另有两条**在 rebase 时被 ①(服务端 302)吸收**:客户端检测脚本已删除 ⇒
702
+ 「404 页跳过跳转」与「只读 localStorage 断言加强」两条随之失效(对象不存在了);
703
+ 404 形态的残留(坏路径被 302 到 `/en/<坏路径>` 再 404)记为已知边界,见 PR #222 描述。
704
+ - 契约注释两条(不改行为):`MemorySnapshot.rev` 声明「水合成功时不得为 undefined」;
705
+ `TaskStore.get` 声明「必须返回副本而非内部活引用」(transport 的「先落库再派发」纪律依赖它)。
706
+
707
+ ### 脚手架 README:「`npm start` 部署/Docker 用这条」的误导表述(2026-10-09 ③)
708
+
709
+ - **病例**:`agentia create` 生成的 README 写「`npm start` ……(部署/Docker 用这条)」,但脚手架的
710
+ `main.ts` 是**一次性批处理入口**(`app.run` → 打印 → 退出:无端口、无 `/healthz`、无 drain)——
711
+ 那句话会读成「脚手架能直接上线」。全仓此前没有一处明说「脚手架给的是 CLI 形态,服务看
712
+ `examples/deploy/`」。
713
+ - **修法**:模板 README 的「构建与生产运行」节把 `npm start` 如实标为 Job 形态(适合 cron / CI /
714
+ 容器一次性任务),并给出服务化的三个指引:`createHttpHandler` + `AsyncRunner` 的配方在
715
+ `examples/deploy/`、上线清单在 `docs/deployment.md`、API 见项目内 `AGENTS.md` 的「触发与宿主」节。
716
+ - 只改文案,不改生成逻辑与脚本。CLI 套件 210/210 绿。
717
+
718
+ ### 脚手架默认入口 = 可上线的 HTTP 服务(`npm start`)(2026-10-09 ④)
719
+
720
+ - **病例**:`agentia create` 生成的项目只有 **Job 形态**(`npm start` = 跑一次就退出的
721
+ `main.ts`),而框架的交付承诺是「build 产物可以直接上线当服务」—— 上一版
722
+ (2026-10-09 ③)只把 README 那句误导文案改诚实了,服务化仍要用户自己去
723
+ `examples/deploy/` 抄配方再接线。
724
+ - **形态(对齐 NestJS 口径)**:`npm run build && npm start` 起来的就是**一个在监听的 HTTP
725
+ 服务**。为此把两个入口调了个个儿 —— **`src/main.ts` = 服务**(NestJS 的 `main.ts` 就是那个
726
+ 在 `listen` 的入口),原来的薄入口挪成 **`src/batch.ts`**(`npm run start:batch -- "你的问题"`)。
727
+ 脚本表:`start` / `start:prod`(**同物**,显式生产名)/ `start:batch`;**不再有 `start:server`**
728
+ —— 它的角色被 `start` 吸收,留一个同物的旧名只会让人以为服务还有另一种起法。
729
+ 决策记录(含「换骨 vs 换皮」「SQLite vs FileTaskStore」两个取舍)见
730
+ `docs/plans/2026-10-09-scaffold-default-service.md` 与 `docs/spec.md` §10 2026-10-09 ref-scaffold-default-service。
731
+ - **服务面**:`src/main.ts` 复用 `src/app.ts` 的 `createAgentApp()` 工厂(dev 环与生产共享
732
+ 同一份装配,旁路会被 `templates.test.mjs` 的形状断言挡住),接上 `createHttpHandler` +
733
+ `AsyncRunner` + `SqliteTaskStore`:`/healthz`、同步 `POST /run`(含 SSE)、异步
734
+ `POST /tasks`、**崩溃续跑**(`resumePending`)、**优雅停机**(SIGTERM/SIGINT →
735
+ `drain({ timeoutMs: 15_000 })` → `server.close`,二次信号直接退出)。配套:模板
736
+ `package.json` / `.gitignore` / README 三节重写、`create` 的后续步骤文案、
737
+ `docs/usage-guide.md` §2.1 文件分工表两行。
738
+ - **鉴权取舍**:可选,不强制 —— 设了 `AGENTIA_TOKEN` 就启用 Bearer 校验(`/healthz`
739
+ 除外);**不设则启动时打一条响亮警告**(「HTTP 面无任何鉴权,仅应监听回环/内网」)。
740
+ 与 `examples/deploy` 同档:框架「鉴权只是缝」(策略是宿主/反代的事),patrol 那种
741
+ 强制模式对第一天的新工程太重。
742
+ - **两处顺手修掉的静默**(评审实报,都不是新功能):① `AGENTIA_DB` 的缺省从 **cwd 相对**
743
+ 改成按本文件位置落**工程根** —— 服务形态常换工作目录启动(systemd / 容器 `-w`),
744
+ cwd 相对会让同一个工程用上两个库,而 `resumePending()` 静默找不到上一个进程的任务
745
+ (表面看像「任务丢了」);② `PORT=`(空串)此前经 `Number('')` ⇒ `0` ⇒ **静默随机端口**,
746
+ 现在显式当缺省,非整数 / 越界则响亮报错(⚠️ **不能**改成 `|| 3000` —— 那会把冒烟测试
747
+ 依赖的 `PORT=0` 一起吃掉)。另把 `PORT` / `AGENTIA_DB` / `AGENTIA_TOKEN` 补进 `.env.example`
748
+ 的变量清单。
749
+ - **Node ≥ 22.5 边界如实标注**:`SqliteTaskStore` 用 `node:sqlite`,更低版本**起服务时**
750
+ 构造期抛可读报错(框架对它延迟加载)。默认入口变成服务之后,这条从「只有 `start:server`
751
+ 需要」变成「**脚手架默认路径就需要**」—— README、`docs/usage-guide.md` 与上线清单
752
+ `docs/deployment.md` §1 三处都写明,并给出 `FileTaskStore` 这条 Node 18 的退路(同一套
753
+ API,代价是单写者前提)。框架**库**的 `engines: >=18` 与 `import-floor` job **不变**
754
+ —— 抬起来的是脚手架默认路径的下限,不是包的下限。
755
+ - **只影响新建工程**:脚手架是**生成物**(`agentia create` 的那一刻写出去),
756
+ 已生成的工程有自己的 `package.json` / `src/`,不会被这一版动到 ⇒ **既有使用者不需要任何动作**
757
+ (不构成破坏性变更)。
758
+ - **交付面同步(补做)**:这次翻转改的是「哪个文件是默认入口」,而**散文面当时没跟着改** ——
759
+ 官网中英两版 docs 页、仓库根 `README.md` 与 `packages/cli/README.md`(**随 npm 发布**)
760
+ 都还写着翻转前的说法,而 `tsc`、单测与八步门禁**全绿**:它们扫源码与产物,不看散文。
761
+ 已补齐,并加了 `tests/docs/scaffold-entry-contract.test.ts`(四层判据 + 三条防真空下限,
762
+ 12 条变异验证)。⚠️ **射程如实写在该文件头**:目录树里「一行只写一个文件」的列举、以及
763
+ 散文把两个入口名折到相邻两行的写法**不在射程内** —— 硬判会误红,而误报的门禁会被关掉。
764
+ ⚠️ 这次漂移此前**零守卫**:`check-website-agent-readiness.mjs` 只核 `llms-full.txt` 与单源
765
+ `docs/usage-guide.md` 逐字节相等,**官网 fragments 整片不在它的射程内**。
766
+ - **同一类病的第二例(顺手修掉)**:2026-09-22 把装配拆成 `src/app.ts` 时,官网两版 api 参考
767
+ 仍把读 `.env` 说成由默认入口 `src/main.ts` 首行做(它实际在 `src/app.ts`)—— 比本次翻转还早,
768
+ 停了半个月。已一并改掉,并给上面那条守卫补了第 ④ 层判据(同一行不得把读 `.env` 的 API
769
+ 挂在默认入口上、却不提装配模块)。⚠️ 这次也**零守卫**:`templates.test.mjs` 的同一条
770
+ 不变量只守模板源码,守不到散文面。
771
+ - **验证**:`templates.test.mjs` 的服务面形状断言改钉 `main.ts`(从 `./app.js` 取工厂、不许
772
+ 旁路 `createApp`、AsyncRunner + createHttpHandler + SqliteTaskStore + resumePending +
773
+ `drain({` + SIGTERM、缺省无 token 必须有警告文案),原 `main.ts` 那两条**平移**到 `batch.ts`
774
+ (薄入口 + `result.error` 非零退出);`scripts/e2e-cli.ts` 一步真起 `node dist/main.js`
775
+ (PORT=0、临时 DB、假 Anthropic 端点)—— `/healthz` 200、无 token `POST /run` 401、
776
+ 带 token 真跑一轮 200、SIGTERM 干净退出,另一条从**两个 cwd** 真跑 `dist/batch.js`。
777
+ 零 `src/` 框架改动。
778
+
779
+ ### 重构(纯结构,零行为变化)· 方法级体量闸 + 三处巨型体外移(2026-10-11 ①)
780
+
781
+ 来源:一轮框架深度评审的 §8 建议 ①②③。**零语义变更**(下游行为一字未改)⇒ 按
782
+ `CONTRIBUTING.md:59` 的判据**不写 `docs/spec.md` §10**;决策留档在
783
+ `docs/plans/2026-10-11-method-size-guard-and-debt.md`。
784
+
785
+ - **新增方法级体量闸 `tests/architecture/method-size.test.ts`**(`src/**` 单个可调用体 ≤
786
+ **240** 行)+ 自带扫描器 `tests/architecture/lib/callable-spans.ts`。文件级已有闸
787
+ (`source-size`,单文件「含代码行」≤ 750),但**方法级没有** —— 而文件级闸的盲区正好是
788
+ 最大那一档的生长方式:一个工厂把周边全吞进自己的花括号,**文件总行数几乎不变**。
789
+ ⚠️ **判据是行数、不是比例** —— 评审报告建议的「比例」判据经**实测后否掉**:抽件在减分子的
790
+ 同时也减分母(`mcp-http.ts` 抽走 74 行后仍在 78%),比例的下限被文件的脚手架行数顶住,
791
+ 要当时就咬只能落在离阈值 1~4pt 处 ⇒ 常态化误报(违 `guards.md` §3 纪律①)。
792
+ 扫描器**刻意不是 AST**:`typescript` 7.0.2 是 tsgo、**已无经典 compiler API**(实测
793
+ `import * as ts` 只剩 `{default, version, versionMajorMinor}`)⇒ 走文本,并与结构体检技能
794
+ 的独立尺对拍(13 行读数**逐字相等**)。
795
+ - **三笔债按新闸的射程外移**(判据:这段代码能不能**局部问答**,能才搬):
796
+ - `integrations/mcp-http.ts` 的 `createStreamableHttpMcpConnector` **265 → 191 行**
797
+ (文件 316 → 245)—— 两个连接器**各自抄了一份**的协议面(JSON-RPC 结果提取 / HTTP 错误
798
+ 包装 / `tools` 清单校验 / `isError` 拒绝 / 会话自愈判定,共 7 个)收进既有的
799
+ `mcp-protocol.ts`(158 → 294)。同一条错误文案曾存在两份,**改一处漏一处没有用例看得见**。
800
+ - `integrations/mcp-stdio.ts` 的 `createStdioMcpConnector` **252 → 213 行** —— 子进程退出
801
+ 观察面(`trackChildExit`)与收尾(`reapChild`)提出去;stdout 分帧派发提出 `dispatchLine`。
802
+ - `engine/mcp-server-http.ts` 的 `startHttpTransport` **192 → 145 行** —— 五个纯件
803
+ (`sendJson` / `readBody` / `jsonRpcIdOf` / `startListening` / `closeOwnServer`)提到
804
+ **同文件模块级**;`mcp-server-stdio.ts` 同款提出 `dispatchLine`(那个体本来就在 240 以下,
805
+ 属顺手对齐,不是欠债)。
806
+ - `transport/async.ts` 的 `#executeInner` **257 → 217 行**(文件 1460 → 1419)—— 42 行 run
807
+ 选项装配 + 幂等去重的终态采纳搬到**新文件** `transport/run-call-options.ts`。⚠️ 搬迁
808
+ **逐字**:那些 `...(cond ? {k: v} : {})` 的形状是承重的(本仓开着
809
+ `exactOptionalPropertyTypes`,「没这个键」与「键是 undefined」下游语义不同)。
810
+ - ⚠️ **反直觉读数,写明免得下次误判**:外移让这七个文件的**总行数上升**(2569 → 2801,
811
+ **+232**)。原因不是多写了代码,而是**注释是证据载体** —— 搬走的每一块都带着自己的
812
+ 「为什么外移」。这个方向**没有守卫会被触发**(`source-size` 是单文件闸,没有全仓总量闸)。
813
+ - ⚠️ **本轮自己踩到并修掉的两个「守卫骗人」**:① 第一版自证样本把花括号放在**单独一行**,
814
+ 而收尾判据**对整体深度平移是免疫的** ⇒ **摘掉遮蔽照样绿**(假守卫)—— 已换成「花括号与体的
815
+ 开括号**同一行**」的真病灶形态。② 外移时漏掉**第三个写者**:基线 `markExited` 挂在
816
+ `error` / `exit` / `close` **三个**监听器上,只搬两个 ⇒ 表现为「关不掉」或「多等一个宽限期」,
817
+ **用例全绿、`tsc` 全绿、67 个 MCP 用例全过**。这正是 `guards.md` 附 B.3 第 1 条问的东西,
818
+ 它是**被机械对拍(字面量 `'exit'` 计数 1→2)带出来的线索**,已补回并保持 1:1 映射。
819
+ - **验证**:变异电池六条(4 条扫描器口径 + 棘轮 + 真空护栏)**咬住 6 / 漏网 0 / 还原异常 0**,
820
+ 每条都是精确字面替换(命中数 ≠ 1 即中断)、备份先于一切变异、还原后 `sha256` 逐字节回到基线;
821
+ 其中最强的一条是**把 `mcp-http.ts` 用 `git show HEAD` 退回拆分前** ⇒ 断言原文点名
822
+ `mcp-http.ts:52 createStreamableHttpMcpConnector = 265 行(占文件 84%,该文件可调用体 1 个)`
823
+ —— 即**本闸本来就能在拆分前咬住那笔债**。另按附 B.3 第 2 条做**字符串字面量机械对拍**
824
+ (163 种 / 300 处 → 164 种 / 302 处,5 处丢失逐条核过,只有三条 MCP 错误文案 2→1,
825
+ 而那是本次的**目的**)。`guards.md` §1.1 补登记行(含两条已知偏离:**比例盲区** 与
826
+ 「多行调用传对象字面量实参 `foo({` 会被判成一个体」——全量实测 538 个体里 4 个、最大 9 行)。
827
+
828
+ ### 重构(纯结构,零行为变化)· ③ 做全:`#executeInner` 的结果记账外移 + 直接单测(2026-10-11 ②)
829
+
830
+ - **先更正上一份评审的口径**:它把 ③ 写成「`async.ts` 的 **drain 协调**(`#executeInner` 257 行含
831
+ **4 个嵌套 `try`**)」—— **两半都不准**。① drain 的**等待闸**早已抽成 `src/transport/drain-gate.ts`
832
+ (**在飞计数刻意不搬**:它同时是 `/healthz` 的 `inFlight` 口径、由任务生命周期加减);
833
+ ② 现测是 **6 个 `try {`**(`catch` 只有 3 个,另 3 个是 `finally`;缩进 profile 4/6/8/8/10/8)
834
+ ⇒ **最深四级 `try` 嵌套**,不是 4 个。**根因是 `AGENTS.md` 自相矛盾**:`transport/`
835
+ 导览一处写「只剩 drain 协调未抽」,几行之后就是 `drain-gate.ts` 的说明。⇒ 本轮**就地更正那句话**
836
+ 并留下后果说明(正是它让人把已完成的抽件报成未偿的债)。
837
+ - **做了什么**:抽 `src/transport/run-outcome.ts`(同族的第五件,前四件是 slot-pool →
838
+ approval-policy → drain-gate → run-call-options)——`readClaimed`(认领前重读;**读失败不升级成
839
+ 「任务起不来」**,这条分支此前只能透过 runner 行为间接观察)+ `applySuspendedOutcome` /
840
+ `applyTerminalOutcome`(挂起与终态两支的**记录映射**)。逐字搬迁,判据与注释一并搬走。
841
+ - **纯的边界**:实例副作用以**返回值**交回调用点。其中「用户输入快照必须在 `rec.spec` 被覆盖
842
+ **之前**取」是**承重的顺序事实** —— 取错时刻是**静默的错**(会话历史翻倍、token 复利),
843
+ 所以它必须是一条类型上看得见的返回值,而不是藏在函数体里的回调。
844
+ - **读数**:`#executeInner` **217 → 176 行**;其中的 `try` **6 → 5**、`catch` **3 → 2**
845
+ (`readClaimed` 摘掉一个 `try`+`catch`)。⚠️ **最深 `try` 嵌套没变,仍是四级** —— 去掉的那个与
846
+ run 段**平级**,不在 4→6→8→10 那条最深链上。⇒ 本轮降的是**体量与 `try` 个数**,不是嵌套深度;
847
+ `async.ts` **1419 → 1379 行**。⚠️ **剩下的 5 个各有不同语义**(最外层挡「异步 store 任何一处
848
+ reject 逃逸成 unhandled rejection」/ 认领终态闸 / run 段错误映射(`cancelIntent` 必须声明在
849
+ `try` **外**才能被 catch 看见)/ 收尾摘监听器 / 落库失败也要释放槽位)⇒ **抽平它们只会改变语义
850
+ 或把 `try` 搬个地方**。所以本轮的判据是「**能抽的抽干净 + 把剩下的为什么不能抽写下来**」,
851
+ 不是「把 `try` 数降到 1」。另**如实记一条**:本轮抽件**没有降低嵌套深度**(4 级 → 4 级),
852
+ 降的是**体量与 `try`/`catch` 个数**;「抽走了一个 `try`」≠「嵌套浅了一层」。
853
+ - **新增单测 `tests/transport/run-outcome.test.ts`**(14 个 `it`):快照的三条判据**各缺一条都拿不到
854
+ 快照**、`eventsDelivered` **两档都断**(只断一档的话「无条件清」也能绿)、终态 5 个挂起痕迹字段
855
+ + 返回值 3 组合(`isResume × run.status`,且非取消那支**用 `failed` 试**——只用 `succeeded`
856
+ 的话两种写法都绿)、「取消但已有 error ⇒ 不覆盖」、以及**异步 store**(`get` 返回 Promise)那一支。
857
+ fixture **逐字段给值**、不用 `{} as AgentRunResult` —— 本件真正读的就是那几个字段,空对象 cast
858
+ 会让「字段名写错」在 `tsc` 下全绿、运行时读成 `undefined`(本仓第 5 条踩过的原样形态)。
859
+ transport 套件 **353 → 367** 全绿(零行为变化的行为侧判据)。
860
+ - **变异电池 16 条**(`.workbuddy/probes/2026-10-11-run-outcome-mutation-battery.py`):咬住 **16** /
861
+ 漏网 **0**。⚠️ 被改的文件是**本轮新增、尚未入库**的(`git status` 里是 `??`)⇒ 脚本按本仓第 15 条
862
+ **第四形态**写:备份先于一切变异、**开跑就把恢复点打印出来**、装 SIGTERM/SIGINT 处理器先还原再退
863
+ (`try/finally` 挡不住 SIGTERM:默认处置是立即终止、不展开栈)。
864
+ - **覆盖率棘轮:分支阈值 88 → 89**。阈值按 `docs/guards.md` 附 B.2 那条推 —— **阈值 = 水位 − ~3pt**。
865
+ **改前基线**(在 `HEAD`(`303c870`)上开独立 `git worktree` 跑,里面只有改前那 353 条用例):
866
+ `All files` 行 98.57 / **分支 92.82** / 函数 98.44,其中 `transport/async.ts` 分支 **94.91**;
867
+ **改后现测**(367 条用例):行 98.58 / **分支 92.85** / 函数 98.45,`transport/async.ts` 分支
868
+ **95.08**,新件 `run-outcome.ts` **100/100/100/100**(那 14 条单测确实把它的分支走遍了)。
869
+ ⇒ 88 的余量 4.85pt 超出「~3pt」的必要范围,收口到 **89(余量 3.85pt)**;⚠️ **收 90 只剩 2.85pt
870
+ (低于 3pt)故不取** —— 阈值离水位太远会放掉一整个模块的测试,太近会把正常抖动当退化报。
871
+ **行 / 函数刻意不动** —— 行 95 的余量 3.58pt、函数 95 的余量 3.45pt,本来就在 ~3pt 上。
872
+ ⚠️ 基线那次跑有 2 条红,**与代码无关**,如实记下:`worktree` 里没有 `dist/` ⇒ `tests/docs` 的
873
+ 记分牌量不了**发布体积**(`dist/index.js 不在场`)。那两条不执行 `src/` 代码、也不影响覆盖率读数。
874
+ - **忠实性按 `docs/guards.md` 附 B.3 六条复核过**:字面量多重集对拍 `async.ts` **72 种 → 72 种**
875
+ (少的 3 处是 `'cancelled'` / `'suspended'` / `'succeeded'` 各 1 处,**都落在新件里**;多的 1 处是
876
+ `'./run-outcome.js'` 这条 import)⇒ **1:1**;新件**零模块级状态**;依赖单向(新件不 import `async.ts`,
877
+ `RunSettled` 是**结构镜像**,真类型漂移时调用点直接报错)。
878
+
879
+ ### 依赖
880
+
881
+ - 开发依赖:`@anthropic-ai/sdk` → 0.132.0、`@types/node` → 26.6.4 —— **不影响运行时面**:
882
+ 两个都在 devDependencies(根 `dependencies` 是 `{}`)。SDK 只在
883
+ `tests/types/message-compat.types.ts` 做「自有公共消息类型族 ↔ SDK 类型」的结构兼容门禁,
884
+ 另在 `tests/engine/errors.test.ts` / `tests/engine/loop.test.ts` 里真 `new` 出 SDK 的错误类
885
+ 断言引擎映射 ⇒ 两道都在 `verify-all` 射程内(第 4 步 / 第 5 步),本版是绿的。
886
+ - 官网(不随包发布):`astro` → 7.3.7。构建与产物形状由 `verify-all` 第 8 步
887
+ (`build:website` + 按产物形状核 404 / robots / sitemap / llms.txt)在 PR 阶段验证,
888
+ 真实部署只在 main 上跑 —— 本版已随 main 的 CI 部署通过。
8
889
  ## [0.10.1] - 2026-10-05
9
890
 
10
891
  > 本版主题(窗口 `0.10.0 → 0.10.1`):**从「看得见一次 run」到「留得下、比得出」**,外加给两块
@@ -180,7 +1061,7 @@
180
1061
  `tests/scripts/release-scripts.test.ts` **10/10 仍绿** —— 它的两条断言锚的本就是那段骨架标记
181
1062
  与「输出含 TODO」,精确化之后照样成立。
182
1063
 
183
- ### 文档 · 推广稿重写成「可直接发出去的定稿」(2026-09-29 ⑲)
1064
+ ### 文档 · 推广稿重写成「可直接发出去的定稿」
184
1065
 
185
1066
  - **是什么**:`docs/promotion.md` 此前是**素材清单**(Product Hunt / X / 短版 / awesome 条目 /
186
1067
  视频旁白,各一段),且口径是「配合 30 秒演示视频」的草稿 ⇒ 真要发的人还得自己组织成文。
@@ -1122,7 +2003,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1122
2003
 
1123
2004
  ### 新增
1124
2005
 
1125
- - **run 事件投入口(spec §10 2026-09-28 ⑥,定案 A3+B1)**:外部系统可以把一条**事件**
2006
+ - **run 事件投入口(spec §10 2026-09-28 ref-run-event-input,定案 A3+B1)**:外部系统可以把一条**事件**
1126
2007
  投给一条**挂起**的 run —— `AsyncRunner.signalTask(taskId, event)` 与
1127
2008
  `POST /tasks/:id/events`(与 approve/cancel 对称),醒来时事件进消息历史。
1128
2009
  - 🔒 **投毒面焊成窄的**:事件体是白名单 `{ eventId?, type, payload }`(全是字符串,
@@ -1143,7 +2024,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1143
2024
  / `TaskRecord.deliveredEventIds` / `AgentRunResult.eventsDelivered`(全部加法,无破坏性)。
1144
2025
  `eventsDelivered` 是引擎对「本段把事件真注入了没有」的如实报告 —— 宿主据此清簿记,
1145
2026
  保证注入后再次挂起也不会在下次续跑**重复注入**(复审探针抓出的缝,已闭合)。
1146
- - **sqlite 到期索引(spec §10 2026-09-28 ⑤ 落地)**:`TaskStore` 新增**可选**方法
2027
+ - **sqlite 到期索引(spec §10 2026-09-28 ref-due-index-measure 落地)**:`TaskStore` 新增**可选**方法
1147
2028
  `listDue?(before)`(接口加法,不破自定义 store),`SqliteTaskStore` 实现它
1148
2029
  (派生列 `wake_at` + `(status, wake_at)` 索引;存量库构造期就地迁移并回填,
1149
2030
  列漂移读时自愈)。`AsyncRunner` 的「到期唤醒」那一半扫描有索引走索引
@@ -1152,7 +2033,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1152
2033
  ⚠️ `resumePending` 的其余三条职责(挂起读数重建 / 审批超时 / 孤儿认领)仍以全表
1153
2034
  `list()` 为输入 —— listDue 只替代到期唤醒那一半。
1154
2035
 
1155
- - **时间挂起(durable timer,spec §10 2026-09-28 ②)**:工具可以在**执行期**调
2036
+ - **时间挂起(durable timer,spec §10 2026-09-28 ref-defer-until)**:工具可以在**执行期**调
1156
2037
  `ctx.deferUntil(at)` 说「现在还不是时候,T 之后再问我」—— 引擎把该回合收尾成挂起
1157
2038
  (`stopReason: 'suspended'` + `suspendedReason: 'timer'` + 目标时刻 `wakeAt`),
1158
2039
  到点由宿主续跑并**重跑这一批**工具。用途:等批处理作业、等限流窗口、等外部系统回填。
@@ -1181,7 +2062,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1181
2062
  `RunMeta.wakeAt`、`TaskRecord.wakeAt`、`HealthResponse.suspended`。另顺手修两处:
1182
2063
  `resumePending` 跳过挂起记录时的原因不再是 `'terminal'` 而是 `'suspended'`(诊断不说错话);
1183
2064
  时间挂起醒来时的续跑段**不再**重复注入会话历史、且 trace 会 link 上一段 run。
1184
- - **旧任务记录的读时归一(迁移垫片,spec §10 2026-09-28 ③)**:store 读回记录时会把 0.9.5 之前
2065
+ - **旧任务记录的读时归一(迁移垫片,spec §10 2026-09-28 ref-read-normalize-shim)**:store 读回记录时会把 0.9.5 之前
1185
2066
  落库的旧形状归一 —— 状态值 `awaiting_approval` → `suspended` 并补 `suspendedReason: 'approval'`,
1186
2067
  挂起时刻 `approvalPendingSince` → `suspendedSince`(旧键删掉)。**升级不再需要宿主手工改库**:
1187
2068
  此前那种记录读回来会被 `isTerminalTask` 判成终态、`resumePending` 按 `'terminal'` 跳过 ⇒
@@ -1192,7 +2073,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1192
2073
  - `SqliteTaskStore` 额外把派生列 `status` 拉回与 json 一致 —— 否则外部/DBA 的
1193
2074
  `SELECT status, count(*) FROM tasks GROUP BY status` 会**继续**报旧值。
1194
2075
  - 归一失败/坏 JSON 的取舍与各 store 既有口径一致;本改动**无公共 API 变化**。
1195
- - **取消 API(spec §10 2026-09-28 ④)**:`AsyncRunner.cancel(taskId)` 与 `POST /tasks/<id>/cancel`
2076
+ - **取消 API(spec §10 2026-09-28 ref-cancel-api)**:`AsyncRunner.cancel(taskId)` 与 `POST /tasks/<id>/cancel`
1196
2077
  —— 在跑的**真中断**、在睡的**不再醒**、在排队的**绝不起跑**,落库 `status: 'cancelled'`
1197
2078
  (**新状态**,与 `failed` 分开:取消不是失败)。
1198
2079
  - 机制是同一条 abort signal,差别在**意图**:`runTimeoutMs` 超时仍然落 `failed`
@@ -1205,12 +2086,12 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1205
2086
  (去重不再把 `cancelled` 当「已有」)。
1206
2087
  - 另:`awaitTask` 的终态集合改成 `isTerminalTask`(原先手写两值 ⇒ 新终态被漏掉,
1207
2088
  症状是「取消后一直等到超时」)。
1208
- - **菜单漂移不再静默(R8 候选 3,spec §10 2026-09-27 ⑧)**:挂起段之后**续跑**时,未决
2089
+ - **菜单漂移不再静默(R8 候选 3,spec §10 2026-09-27 ref-menu-drift-signals)**:挂起段之后**续跑**时,未决
1209
2090
  tool_use 引用的工具若已不在当前菜单(删了 / 改名了),框架把这件事记成**三处信号** ——
1210
2091
  `menu.drift` 事件(`{ missing, tool_use_ids, menu_size }`;时间线与
1211
2092
  `GET /tasks/:id/stream` 都看得到)、父 span 的 `menu.drift` attribute(如 `missing:danger`)、
1212
2093
  一条 `console.warn`。⚠️ **run 照常收尾**(不判失败:挂起是合法态、改代码是发布常态 ——
1213
- 理由与「严格模式为什么不给」见 spec §10 2026-09-27 ⑧)。另新增 run 根 attribute
2094
+ 理由与「严格模式为什么不给」见 spec §10 2026-09-27 ref-menu-drift-signals)。另新增 run 根 attribute
1214
2095
  `tools.names` / `tools.menuHash`(装配后菜单的名字清单 + 名字与**输入 schema** 的摘要,
1215
2096
  与 `prompts.versions` 同动机:质量回归能定位到具体菜单版本)。口径:判据只看名字;签名与
1216
2097
  菜单顺序、schema 键序无关,`description` **不参与**。**公共 API 零变化**。
@@ -1225,7 +2106,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1225
2106
  capability span + 与引擎同形状的 `tool.input`/`tool.output` 事件)投递 `opts.sinks`;
1226
2107
  结果映射与正向桥方向对称(抛错 → 协议层成功 + `isError: true`)。公共面新增
1227
2108
  `createMcpServer` 与 `McpServerApp` / `McpServerOptions` / `McpServer` 类型。
1228
- 决策见 spec §10 2026-09-27 ⑦。
2109
+ 决策见 spec §10 2026-09-27 ref-mcp-reverse-bridge。
1229
2110
  - **租户归因 labels(`labels: Record<string, string>`,R8-P4)**:`createApp` 缺省 +
1230
2111
  `app.run` 单次覆盖(**整体替换**不合并)+ `runAgent` 直连三层同语义。落 run 根的
1231
2112
  `labels.<key>` 属性(trace 侧无基数问题;与框架自写的 `source` 触发来源审计正交);
@@ -1236,7 +2117,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1236
2117
  `snapshot().droppedLabelValues` 与 `dropped_keys{kind="label:<key>"}`;四个 run 级
1237
2118
  家族(`runs_total` / `runs_failed_total` / `tokens_total` / `cost_usd_total`)在全局
1238
2119
  样本外追加带标签样本(⚠️ 开了以后 `sum(agentia_runs_total)` 会重复计数,总量用
1239
- 不带标签的序列)。公共面新增 `RunLabelMetrics` 类型。决策见 spec §10 2026-09-27 ⑥。
2120
+ 不带标签的序列)。公共面新增 `RunLabelMetrics` 类型。决策见 spec §10 2026-09-27 ref-tenant-labels。
1240
2121
  - **CLI `agentia export`:trace 落盘文件 → 训练数据集**(JSONL,一行一份
1241
2122
  `{ messages, meta }`;R8-P3b)—— harvest(产回归用例)的孪生。开了
1242
2123
  `traceContent: 'full'` 的 run 导出带真 assistant 文本的完整对话;没开的导出工具
@@ -1244,7 +2125,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1244
2125
  过滤:`--ok-only` / `--min-score n`(没带分数的 run 被排除:没判过 ≠ 及格);
1245
2126
  `--out` 落盘(stdout 是产物的纪律同 harvest)。框架侧 `src/eval/export.ts` 的
1246
2127
  `exportRun` 是 module 级(不进公共面,同 harvestEvalCase 纪律),CLI 侧为去类型
1247
- 移植副本 + 逐字对拍守护。决策见 spec §10 2026-09-27 ⑤。
2128
+ 移植副本 + 逐字对拍守护。决策见 spec §10 2026-09-27 ref-export-training-data。
1248
2129
  - **opt-in 记录 assistant 文本进 trace(`traceContent: 'full'`)**:每回合的模型文本落
1249
2130
  该 llm.turn span 的 `output.text` 属性(多块 `\n` 连接;过 `maxEventChars` 同一道
1250
2131
  截断闸 —— 它管「多长」,`traceContent` 管「记不记」)。缺省不记,现状逐字不变;
@@ -1252,7 +2133,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1252
2133
  run 根快照记 `config.traceContent`。⚠️ 实测代价:3 回合、每回合约 1600 字符输出的
1253
2134
  run,trace 体积 5 564 → 11 010 字节(约 2×);且模型输出从此进入要脱敏的面 ——
1254
2135
  出库前走 `docs/observability.md` 配方 2.4。这是「trace → 训练数据导出」(R8-P3b)
1255
- 的引擎侧前提。决策见 spec §10 2026-09-27 ④。
2136
+ 的引擎侧前提。决策见 spec §10 2026-09-27 ref-trace-content-full。
1256
2137
  - **模型 fallback 链(引擎级,`fallbacks: [{ model, client? }]`)**:`createApp` 缺省 +
1257
2138
  `app.run` 单次覆盖 + `runAgent` 直连三层同语义。主模型本回合最终失败(含其
1258
2139
  `maxRetries` 用尽)且错误可换(`classifyError` 的 retryable 类:rate_limit / server /
@@ -1262,7 +2143,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1262
2143
  护栏:`aborted` 永不换(用户取消不是故障)、本回合吐过字不换(与 retry 的
1263
2144
  `!emitted` 同一护栏)、每回合从主环重新起;子 agent / skill 子循环不继承。
1264
2145
  链环 `client` 缺省复用本次 run 的 client(同端点换模型是主用例);run 入口校验
1265
- 坏环/死 client(持久化反序列化空壳)响亮抛 TypeError。决策见 spec §10 2026-09-27 ③。
2146
+ 坏环/死 client(持久化反序列化空壳)响亮抛 TypeError。决策见 spec §10 2026-09-27 ref-model-fallback-engine。
1266
2147
  - **`jsonlTraceSink({ path })`:JSONL 文件 sink 进框架** —— CLI 三件套(`agentia report` /
1267
2148
  `diff` / `harvest`)消费 `trace.jsonl`,而产出侧此前要用户手写 `appendFileSync`(usage-guide
1268
2149
  曾这么教)。现在一行接入:`createApp({ sinks: [jsonlTraceSink({ path: 'trace.jsonl' })] })`。
@@ -1281,7 +2162,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1281
2162
  ### 变更
1282
2163
 
1283
2164
  - **挂起改成「一个状态 + 一个原因」:`awaiting_approval` → `suspended` + `suspendedReason`**
1284
- (spec §10 2026-09-28 ①;为 durable timer 铺路):`RunStatus` 与 `AgentStopReason` 的成员名
2165
+ (spec §10 2026-09-28 ref-suspend-state-reason;为 durable timer 铺路):`RunStatus` 与 `AgentStopReason` 的成员名
1285
2166
  都改成 `'suspended'`,「为什么挂起」由新字段承载 —— `'approval'`(等人工决定)/
1286
2167
  `'timer'`(等一个时刻)。两条判据因此落在**原因**上:`approvalTimeoutMs` 只对 `approval`
1287
2168
  成立、`approve` 对 `timer` 挂起一律 409 —— 否则一条等时刻的 run 会被「审批超时」提前叫醒
@@ -1291,10 +2172,10 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1291
2172
  新增内置正则预设(Bearer / JWT / AWS access key / LLM `sk-` key / 邮箱 / 手机号),
1292
2173
  **缺省全开**(拷走即用),`presets` 可开子集或 `false` 全关;预设命中的替换文案带类别
1293
2174
  标签(`[REDACTED:email]`),自定义 `patterns` 与 `keys` 命中的仍是裸 `[REDACTED]`。
1294
- 脱敏不内建进框架是已锁定决策(spec §9.3 / §10 2026-09-14 ⑥),这是配方层的升级。
2175
+ 脱敏不内建进框架是已锁定决策(spec §9.3 / §10 2026-09-14 ref-event-truncation),这是配方层的升级。
1295
2176
  - **sink 投递失败不再完全静默**:`flushSinks` 吞掉 sink 异常的纪律不变(观测不击穿业务),
1296
2177
  但吞之前现在会落一条 `console.warn`(文案含「trace sink」,可 grep)——「观测的观测」
1297
- 此前是零信号:sink 天天挂、面板一切如常。决策见 `docs/spec.md` §10 2026-09-27 ②。
2178
+ 此前是零信号:sink 天天挂、面板一切如常。决策见 `docs/spec.md` §10 2026-09-27 ref-sink-failure-warn。
1298
2179
 
1299
2180
  ### 重构(纯结构,零行为变化)· 结构收口(2026-09-28,底盘见 `docs/reviews/2026-09-28/SRC-STRUCTURE-2026-09-28.md` 的落地顺序 1–4)
1300
2181
 
@@ -1361,7 +2242,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1361
2242
  连接器反向取 helper(值)是一条**真实的值环**(靠 ESM 函数提升侥幸无恙)。抽后成单向 DAG,
1362
2243
  **公共面不变**(`src/index.ts` 未改;`mcp.ts` 继续转出这些符号)。
1363
2244
  - ⚠️ 盘点报告初稿写「两条环的回边都是 `import type`」**是错的** —— 实测 `mcp.ts:350/352` 是
1364
- **re-export 值**。结论与修法见 `docs/spec.md` §10 2026-09-28 ⑩ 第 4 条。
2245
+ **re-export 值**。结论与修法见 `docs/spec.md` §10 2026-09-28 ref-structure-closeout 第 4 条。
1365
2246
 
1366
2247
  ### 迁移
1367
2248
 
@@ -1369,7 +2250,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1369
2250
  `RedisTaskStore` 里已写好的 JSON):**本版内置兼容读,宿主不需要做任何动作** —— store 读回记录
1370
2251
  时会把旧形状归一(`awaiting_approval` → `suspended` 并补 `suspendedReason: 'approval'`;
1371
2252
  `approvalPendingSince` → `suspendedSince`,旧键删掉),`SqliteTaskStore` 还会把派生列 `status`
1372
- 一并拉正。细节与门禁见 `docs/spec.md` §10 2026-09-28 ③。
2253
+ 一并拉正。细节与门禁见 `docs/spec.md` §10 2026-09-28 ref-read-normalize-shim。
1373
2254
  ⚠️ 本小节此前写的是「框架**不**内置兼容读、请宿主在升级前手工改库」—— 垫片落地后那段要求
1374
2255
  **已作废**(手工处置仍可行,但不再是必须;在等审批的 run 不会再被判成终态)。
1375
2256
  `SuspendedReason` 仍然是**类型**而不是别名:不为旧值引入第二个运行时名字。
@@ -1471,7 +2352,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1471
2352
  (Promise 二次 `resolve` 是 no-op,少这行行为完全一样),为它写断言就是真空变绿。
1472
2353
  ⚠️ 同一轮查清一个会误导人的读数:`src/engine/mcp-server-stdio.ts` 报 **22.22% 行覆盖**不是缺口,
1473
2354
  是**采集假象**(用例走真子进程,c8 只采父进程;实测该用例 22/22 全过),且该数字拆分前就存在。
1474
- 变异验证 17/17 逐条咬人。详见 `docs/spec.md` §10 2026-09-28 ⑩ 第 5 条。
2355
+ 变异验证 17/17 逐条咬人。详见 `docs/spec.md` §10 2026-09-28 ref-structure-closeout 第 5 条。
1475
2356
 
1476
2357
  - **补 7 条「正常输入下可达、却从没走过」的功能分支**(2026-09-28,同轮尾巴;跨 3 个测试文件 +8 条用例):
1477
2358
  先纠正一个读法 —— c8 的 branch **不是 if/else 两条臂**,而是「**代码块**」(全仓条目**全是
@@ -1484,7 +2365,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1484
2365
  **分母不变大的前提下**:语句 295 → **287** 未覆盖、块 347 → **338**、分支 91.82% → **92.04%**,
1485
2366
  用例 1465 → **1473**。函数那一档**刻意不动**(那 11 个「从未被调用的函数」全是只读访问器,
1486
2367
  另有一个 `TaskEventStreams.forget` 是**未导出的内部死方法** —— 为它写测试等于锁死死代码)。
1487
- 变异验证 7/7 逐条咬人。详见 `docs/spec.md` §10 2026-09-28 ⑩ 第 6 条。
2368
+ 变异验证 7/7 逐条咬人。详见 `docs/spec.md` §10 2026-09-28 ref-structure-closeout 第 6 条。
1488
2369
 
1489
2370
  - **`packages/cli/test/inspector.test.mjs` 新增 ⑫「选完文件夹 → 透传给 run 的就是所选目录」**:
1490
2371
  2026-09-24 真用户反馈「选完之后透传给 agent 的不是所选文件夹」,逐环核实后**链路是通的**
@@ -1872,7 +2753,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1872
2753
 
1873
2754
  ### 修复(复核轮:四条窄窗口 / 口径缺陷 + 两条顺手抓出的)
1874
2755
 
1875
- 同一天对这轮 dev 环改动做了一次逐条回读代码的复核(决策链见 `docs/spec.md` §10 2026-09-22 ⑦)。
2756
+ 同一天对这轮 dev 环改动做了一次逐条回读代码的复核(决策链见 `docs/spec.md` §10 2026-09-22 ref-review-round-fixes)。
1876
2757
  下面每条都补了**反向验证**(把修复摘掉 ⇒ 新加的门禁真的会红):
1877
2758
 
1878
2759
  - **并发闸没盖住「换能力选择 ⇒ 重启」那条路径**(双击运行会真并发两次)。受理到 run 真正发出去之间
@@ -1929,12 +2810,12 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
1929
2810
  (prompt 回显模型看不见;对话历史真的进上下文,且只在开了多轮时出现)。
1930
2811
  §2.2 另补一条「**看哪里**」—— 此前只写了「看什么扩展名」,没写监视根(`<项目根>/src` 整棵树
1931
2812
  + 项目根单独的 `.env` / `.env.local`)⇒ 读者无从知道「改项目根的 `README.md` 不触发重启」。
1932
- - `docs/spec.md` §10 2026-09-22 ②:本轮决策记录,含「**P0–P2 全程零框架改动**」的逐项核对。
1933
- §10 2026-09-22 ③:真跑探针抓出的两个缺陷与三条修复(含反向验证记录)。
1934
- §10 2026-09-22 ④:拿功能文档逐项对照做审计 —— 又抓出两处「文档里有、代码里没有」,
2813
+ - `docs/spec.md` §10 2026-09-22 ref-dev-loop-drive-run:本轮决策记录,含「**P0–P2 全程零框架改动**」的逐项核对。
2814
+ §10 2026-09-22 ref-dev-loop-real-run:真跑探针抓出的两个缺陷与三条修复(含反向验证记录)。
2815
+ §10 2026-09-22 ref-doc-vs-code-audit:拿功能文档逐项对照做审计 —— 又抓出两处「文档里有、代码里没有」,
1935
2816
  以及「同一条事实的第三个影子」(中止的 `ok` 是 `false`)。
1936
- §10 2026-09-22 ⑤:追一次瞬时红 → `WATCH_SKIP` 的半实现 + 那条「守了另一件事」的假守卫。
1937
- §10 2026-09-22 ⑥:拿 ⑤ 的「监视根」去核每条判据 → `.env` 是一条**够不着**的判据
2817
+ §10 2026-09-22 ref-watch-skip-half:追一次瞬时红 → `WATCH_SKIP` 的半实现 + 那条「守了另一件事」的假守卫。
2818
+ §10 2026-09-22 ref-dotenv-unreachable:拿 ⑤ 的「监视根」去核每条判据 → `.env` 是一条**够不着**的判据
1938
2819
  (含「判据的正确性 vs 可达性」与「探针别用写死序号」两条可复用的教训)。
1939
2820
  - `docs/guards.md` §1.4:新增五条守卫登记(面板纯逻辑 + watch 允许清单 / dev 环鉴权与 HTTP 面 /
1940
2821
  生成物能力名一致性 / `agentia dev` 整链真跑 / `.env` 接线成对断言),并随 ④ 扩面
@@ -2373,7 +3254,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
2373
3254
  - 复核结论一条「查过但不改」:`interruptibleSleep(0, 已中止的 signal)` 仍 **resolve** ——
2374
3255
  它与 `withTimeout(p, 0)` 的「非正数 = 机制关掉」是同一口径,不是缺口(本轮曾误改,
2375
3256
  被既有用例拦下后回退;那条用例的断言已从隐式 `await` 改成显式 `assert.doesNotReject`
2376
- 并写明理由,见 spec §10 2026-09-19 ④)。
3257
+ 并写明理由,见 spec §10 2026-09-19 ref-budget-guard-cheap-usage)。
2377
3258
 
2378
3259
  **迁移(自己实现这两个结构面时)**
2379
3260
 
@@ -2389,7 +3270,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
2389
3270
  自己接上的**四处**(deadline / 取消 → `signal`、`metadata` 的 `traceparent` → `traceContext`、
2390
3271
  框架错误 → gRPC 状态码、trace → sink),四处漏掉**都不报错**。判别规则只有一条 ——
2391
3272
  客户端是不是标准库(MCP 用 `spawn` + `fetch` 所以能内置,gRPC / Kafka 要引第三方客户端),
2392
- 决策见 spec §10 2026-09-18 ⑪。
3273
+ 决策见 spec §10 2026-09-18 ref-host-integration-recipe。
2393
3274
  - `docs/usage-guide.md` §6.2 新增「gRPC 宿主」配方;`examples/grpc-host/` 给了 proto + 宿主 +
2394
3275
  客户端 + README(一元 / 服务端流 / 异步投递 / 查任务,`PORT=0` 自报端口、无抢占窗口);
2395
3276
  `scripts/e2e-grpc.ts` 真构建真起宿主、用它自带的客户端跑四个 RPC,并入 `npm run e2e` 第四步。
@@ -2456,7 +3337,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
2456
3337
  而仓库里有 5 处注释把它当既成事实引用(含 `src/index.ts` 的公共面注释)—— 现在全部改正。
2457
3338
  为什么反:F7 的理由「守住零运行时依赖」不成立(该口径 = 不依赖**第三方包**;`spawn` / `fetch` 都是
2458
3339
  标准库),且 `store/` 的三层形状里「只用标准库的平台能力」一律内置(`FileTaskStore` / `SqliteTaskStore` /
2459
- HTTP 宿主),独立包才是那个例外。完整论证与可逆性判据见 `docs/spec.md` §10 **2026-09-18 ⑨**。
3340
+ HTTP 宿主),独立包才是那个例外。完整论证与可逆性判据见 `docs/spec.md` §10 **2026-09-18 ref-mcp-builtin**。
2460
3341
  - **连接器替你兜住三件只有它能做的事**(此前只活在 `scripts/e2e-mcp.ts` 那段内联副本里,无门禁守着):
2461
3342
  ① spawn 失败的 `'error'` 是**异步事件**,不接住就是未捕获异常(真实宿主进程直接崩,没有 try/catch
2462
3343
  接得住);② stdout 必须按 `\n` **攒包**(一条报文可能跨多个 chunk);③ **协议层 `isError: true`
@@ -2629,7 +3510,7 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
2629
3510
  `traceparent`(W3C)现在会记成 run 根 span 的一条 **`links`**(新类型 `SpanLink`),
2630
3511
  `createOtlpExporter` 映射为 OTLP **span links** —— 于是「这条 run 是被谁触发的」在跨进程 / 跨服务
2631
3512
  时也可查。**不改 `traceId == runId` 的 1:1 不变量**:run 仍是自洽的一棵新树,上游是被**链接**
2632
- 而不是被**继承**成父 span(理由与取舍见 `docs/spec.md` §10 2026-09-17 ⑤)。
3513
+ 而不是被**继承**成父 span(理由与取舍见 `docs/spec.md` §10 2026-09-17 ref-cross-process-links)。
2633
3514
  - 新增 `parseTraceparent(value)` 导出:把 `traceparent` 头解析成 `TraceContext`。
2634
3515
  **非法 / 缺失 / 版本 `ff` / 全零 id / 位宽不符一律返回 `undefined`**(不抛)——
2635
3516
  链路是观测行为,不该把业务请求打成 400。`createHttpHandler` 在 `POST /run` 与 `POST /tasks`
@@ -2674,20 +3555,20 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
2674
3555
  超时合成信号;以及任何 `code === 'timeout'` 的错误)现在返回 **`type: 'timeout'`**,不再归进 `connection`。
2675
3556
  **`retryable` 保持 `true`** ⇒ **自动重试行为不变**(超时本来就是可重试故障);变的是**记账**:
2676
3557
  按 `span.error.type` 分流的看板 / 告警会把超时类从 `connection` 挪到 `timeout`,`trace-diff` 比对旧 trace
2677
- 时超时会显示为「类型变了」。取证与决策见 `docs/spec.md` §10 2026-09-17 ②。
3558
+ 时超时会显示为「类型变了」。取证与决策见 `docs/spec.md` §10 2026-09-17 ref-timeout-error-type。
2678
3559
  - 顺带把 `isTimeoutError` 的契约写清(三条判据,全鸭子类型):框架 `TimeoutError` 实例 /
2679
3560
  `code === 'timeout'` / `name === 'TimeoutError'`;引擎的工具级 catch 用它 ⇒ 工具自判的超时与引擎判的
2680
3561
  超时记同一类账(`errorKind='timeout'`)。
2681
3562
  - ⚠️ **更正**:此前一版说明里「模型调用超时在 `span.error` 上是 `type:'unknown'`(不可重试)」是**错的** ——
2682
3563
  它一直判 `connection` + 可重试(`engine/errors.ts` 的 `isConnectionError` 专门认 `name === 'TimeoutError'`)。
2683
- 该错误说明已从 `docs/spec.md` 删除,以 2026-09-17 ② 为准。
3564
+ 该错误说明已从 `docs/spec.md` 删除,以 2026-09-17 ref-timeout-error-type 为准。
2684
3565
 
2685
3566
  ### 变更(MCP 超时单源化 —— 一次调用只有一个裁判)
2686
3567
 
2687
3568
  - **原语单源**:`TIMED_OUT` / `withTimeout` 下沉到 `src/core/timeout.ts`,`engine/concurrency.ts`
2688
3569
  原样再导出(`import` 路径与名字对使用者与测试都不变)。MCP 桥的 `withDeadline` 改为它的**薄封装** ——
2689
3570
  此前桥自带一份**纯竞速**实现,于是 2026-09-14 的「超时是硬的」收紧只落进引擎,桥能把**超预算**的
2690
- MCP 调用记成成功(确定性可复现;取证与决策见 `docs/spec.md` §10 2026-09-17 ①)。
3571
+ MCP 调用记成成功(确定性可复现;取证与决策见 `docs/spec.md` §10 2026-09-17 ref-mcp-timeout-single-source)。
2691
3572
  - **⚠️ 行为变更(迁移注意)**:引擎设了 `toolTimeoutMs` 时,`mcpTools({ timeoutMs })` **不再参与判定**
2692
3573
  (即使桥的 `timeoutMs` 更短)—— 一次调用只有一个裁判,此前「谁短谁生效」让同一件事在 trace 里
2693
3574
  落成两种账。要收紧某个 MCP server 的时限,请设 `toolTimeoutMs`(或把该工具单独包一层)。
@@ -2879,7 +3760,9 @@ toolkit 142 / runtime 55 / engine 403 / container 10 / store 55 —— 全绿。
2879
3760
  首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
2880
3761
  框架本体单包;CLI 独立成包(workspaces)。
2881
3762
 
2882
- [Unreleased]: https://github.com/retrychx/agentia/compare/v0.10.1...HEAD
3763
+ [Unreleased]: https://github.com/retrychx/agentia/compare/v0.10.3...HEAD
3764
+ [0.10.3]: https://github.com/retrychx/agentia/releases/tag/v0.10.3
3765
+ [0.10.2]: https://github.com/retrychx/agentia/releases/tag/v0.10.2
2883
3766
  [0.10.1]: https://github.com/retrychx/agentia/releases/tag/v0.10.1
2884
3767
  [0.10.0]: https://github.com/retrychx/agentia/releases/tag/v0.10.0
2885
3768
  [0.9.5]: https://github.com/retrychx/agentia/releases/tag/v0.9.5