@thincoder/core 0.10.2 → 0.10.4

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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/advisor/citations.mjs +40 -4
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/convergence.mjs +1 -1
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +5 -4
  8. package/advisor/run.mjs +5 -1
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/completion.mjs +3 -1
  12. package/agent/digest-account.mjs +188 -0
  13. package/agent/dispatch-run.mjs +29 -12
  14. package/agent/family-tools.mjs +11 -11
  15. package/agent/helpers.mjs +8 -0
  16. package/agent/record-results.mjs +10 -1
  17. package/agent/run-stages.mjs +27 -4
  18. package/agent/run-start.mjs +49 -3
  19. package/agent/spawn-child.mjs +2 -1
  20. package/agent/subagent-event.mjs +71 -0
  21. package/agent/suspension.mjs +60 -12
  22. package/agent/turn-loop.mjs +31 -2
  23. package/agent/write-gate.mjs +13 -25
  24. package/agent-tools/advisor-async.mjs +25 -7
  25. package/agent-tools/advisor-settle.mjs +7 -9
  26. package/agent-tools/advisor.mjs +34 -33
  27. package/agent-tools/async-settle.mjs +29 -5
  28. package/agent-tools/batch-lifecycle.mjs +29 -7
  29. package/agent-tools/batch-paths.mjs +22 -2
  30. package/agent-tools/batch-read.mjs +36 -0
  31. package/agent-tools/batch-skeleton.mjs +59 -33
  32. package/agent-tools/batch.mjs +16 -6
  33. package/agent-tools/design-token.mjs +76 -16
  34. package/agent-tools/escalate-async.mjs +4 -2
  35. package/agent-tools/review-facts.mjs +143 -16
  36. package/agent-tools/subagent-actions-query.mjs +13 -6
  37. package/agent-tools/subagent-async.mjs +16 -4
  38. package/agent-tools/subagent-panel.mjs +121 -37
  39. package/agent-tools/subagent-spawn.mjs +1 -1
  40. package/agent-tools/subagent.mjs +5 -3
  41. package/agent-tools/verify.mjs +5 -0
  42. package/agent-tools/zero-write-watch.mjs +77 -0
  43. package/agent.mjs +1 -1
  44. package/auto-think.mjs +26 -2
  45. package/config-presets.mjs +8 -0
  46. package/config.mjs +30 -0
  47. package/conventions.mjs +26 -6
  48. package/declaration.mjs +26 -2
  49. package/embedding.mjs +35 -3
  50. package/escape.mjs +1 -1
  51. package/i18n.mjs +5 -0
  52. package/ledger-cmd.mjs +13 -21
  53. package/ledger-db.mjs +34 -1
  54. package/ledger-migrate.mjs +4 -3
  55. package/ledger-read.mjs +94 -0
  56. package/ledger-surface.mjs +15 -46
  57. package/ledger-tools.mjs +148 -85
  58. package/ledger-variant-notice.mjs +47 -0
  59. package/ledger.mjs +38 -64
  60. package/manifest-schema.mjs +19 -7
  61. package/manifest.mjs +32 -11
  62. package/mcp/transport-http.mjs +52 -4
  63. package/mcp/transport-stdio.mjs +33 -8
  64. package/mcp.mjs +13 -5
  65. package/memory/code-search.mjs +5 -3
  66. package/memory/core.mjs +6 -4
  67. package/memory/docs.mjs +5 -3
  68. package/memory/schema.mjs +2 -0
  69. package/package.json +1 -1
  70. package/prompts/advisor-design.md +3 -2
  71. package/prompts/advisor-round1.md +1 -0
  72. package/prompts/advisor-round2.md +8 -3
  73. package/prompts/advisor-round3.md +7 -2
  74. package/prompts/common.md +2 -2
  75. package/prompts/discipline-normal.md +2 -1
  76. package/prompts/persona-engineering.md +7 -4
  77. package/provider/anthropic.mjs +14 -2
  78. package/provider/core.mjs +10 -4
  79. package/provider/errors.mjs +23 -0
  80. package/provider/google.mjs +31 -4
  81. package/provider/normalize.mjs +57 -0
  82. package/provider/responses-request.mjs +3 -1
  83. package/provider/responses.mjs +38 -4
  84. package/provider/sse.mjs +45 -13
  85. package/proxy.mjs +13 -3
  86. package/session.mjs +13 -2
  87. package/shell-identity.mjs +107 -0
  88. package/stream-destroy.mjs +62 -0
  89. package/tool-docs/advisor.md +1 -1
  90. package/tool-docs/bash.md +1 -1
  91. package/tool-docs/batch.md +1 -1
  92. package/tool-docs/delete.md +7 -5
  93. package/tool-docs/ledger.md +6 -0
  94. package/tool-docs/subagent.md +10 -0
  95. package/tool-docs/verify.md +1 -1
  96. package/tools/bash.mjs +9 -4
  97. package/tools/edit-batch.mjs +7 -2
  98. package/tools/index.mjs +1 -4
  99. package/tools/patch.mjs +8 -2
  100. package/tools/write-path.mjs +6 -4
  101. package/undo-stack.mjs +23 -9
  102. package/tool-docs/ledger_add.md +0 -1
  103. package/tool-docs/ledger_close.md +0 -1
  104. package/tool-docs/ledger_count.md +0 -1
  105. package/tool-docs/ledger_query.md +0 -1
  106. package/tool-docs/ledger_update.md +0 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,71 @@
3
3
  All notable changes to the core package are documented here.
4
4
  Format: Keep a Changelog · 中文 · 号在发布时定(CalVer——见 `docs/RELEASE.md` §4)。
5
5
 
6
+ ## [Unreleased]
7
+
8
+ > 0.10.3 → 0.10.4(月内 +1——发布时定号)
9
+
10
+ ### Added
11
+
12
+ - **零落笔看门狗(#934)**:eng-coder ∥ eng-designer ∥ 评审面连续 50 轮零文本产出 ⇒ 自动上游提醒(阈值单源叶 `zero-write-watch.mjs`)。
13
+ - **工程模式状态档写通路(#941)**:`PROJECT-MANIFEST.json` ∥ `.thincoder/conventions.json` 合规写入面(`state` 第五值——「恰执行一次」语义;`src` 段仍拒)。
14
+ - **空目录清理通路(#943)**:`delete` 空目录臂(非空照拒 ∥ 嵌套空树自底向上——`delete` 描述面同拍)。
15
+ - **台账变体键首启提示(#935)**:盘符大小写变体旧库检测 + `migrate` 指引(不再静默分裂)。
16
+ - **工具面首用可发现性(#938)**:子代理派单六字段骨架进描述面 ∥ batch 头参 ∥ 占位域修正。
17
+ - **`ledger` 统一入口(#923)**:五工具合 1(action 分派;五旧名 = 弃壳零装配)。
18
+
19
+ ### Changed
20
+
21
+ - **消化账务语义(#930)**:投递-结算两分 ∥ 重试上限 + 升级 ∥ 未结可见。
22
+ - **模型可见面三笔(#950)**:派单六字段骨架 ∥ 消化签收纪律线 ∥ 停摆轮引用清理。
23
+ - **bash 执行器身份(#922)**:运行时 shell 探针 + 注入执行器行(描述与真实执行器不再相左)。
24
+ - **advisor 主循环拆分(#951)**:时间线记录 ∥ 压缩协作外提(行为零变——结构面)。
25
+
26
+ ### Fixed
27
+
28
+ - **消化账务取件窗(#939)**:多轮 run 首轮签收跨轮存活(假重投根治)。
29
+ - **设计令牌裁决闸(#940)**:评审回执无 `VERDICT: pass` 不签发(机械判 ∥ 回显径)。
30
+ - **评审串台面(#949)**:并发/排队评审按本评审实例定域 + 投递水印(不再借他场轮次)。
31
+ - **引文核验装饰剥离(#928)**:包裹引号 ∥ 行内注记并入捕获串不再误报失配(候选集 c1–c4;零模糊)。
32
+ - **评审 documents 域锚祖先链(#945)**:容器相对形在子仓锚下可解析(受理判据零放宽)。
33
+ - **批次档路径多落点歧义(#942)**:无仓前缀相对形不再静默嵌套 + 歧义显式拒。
34
+ - **docRoot 空值哨兵(#944)**:`null` = 本面无(不再被默认值补回)。
35
+ - **responses 适配面健壮性(#907)**:SSE error 帧双分支 ∥ 错误码映射 ∥ `max_output_tokens` 显式发送。
36
+ - **评审文档路径基面(#921)**:按候选项目根解析 + 诊断拒语 + 七处基面声明在位。
37
+
38
+ ## [0.10.3] — 2026-10-04
39
+
40
+ > 0.10.2 → 0.10.3(月内 +1——发布时定号)
41
+
42
+ ### Added
43
+
44
+ - **OpenCode Go 内置预设(#906)**:`opencode-go`(OpenAI 协议侧)∥ `opencode-go-anthropic`(Anthropic 协议侧)——开箱可选渠道(双协议混装建模)。
45
+ - **ACP 结构化子代理事件(#862/#843)**:`session_info_update` 携 `_meta["thincoder.dev/subagent"]`(role ∕ id ∕ 状态 ∕ 进度)——ACP 客户端可呈现子代理活动;判据单源叶 `subagent-event.mjs`。
46
+ - **manifest 未知键告警(#802)**:读档返回 `unknownKeys` + 恰一行可见告警(含键名;非拒——拼错 / 版本错位的声明键不再静默失效)。
47
+ - **台账只读路径**:只读数据库打开面(CLI `ledger list` ∥ ACP `ledger/*` 接口共用)。
48
+ - **崩溃防护两面(#865/#866)**:流响应体错误护(`stream-destroy.mjs` 四消费点)∥ win32 杀树同步化(退出阶段不漏杀)。
49
+
50
+ ### Changed
51
+
52
+ - **模型切换解锁 · 核侧(#918)**:会话槽三键(provider ∕ model ∕ effort)**单写者**——回合关联落盘(`prefsSeedOnly`)不携三键(槽在场值赢 ∥ 缺播种)——忙期选定不再可能被旧快照覆写。
53
+ - **环边界中止前置(#793)**:回合环头 ∥ 环尾两检查点——中止态(用户 Stop)恒以 AbortError 收束,不再产出撞帽续跑询问(需求 F5「Stop 优先」兑现)。
54
+ - **多项目台账合计(#882)**:歧义根(容器根)下状态行台账段按族合计——多项目求和不显单一。
55
+
56
+ ### Fixed
57
+
58
+ - **MCP 会话过期自愈(#850)**:HTTP 404 ⇒ 清会话 + 重新 initialize + 重试一次(单飞重建);二次失败透传 + `sessionDead` 入存活判据。
59
+ - **杀树相位(#877)**:退出阶段 POSIX 组杀 + SIGKILL 兜底(忽略 SIGTERM 的子进程不再泄漏)。
60
+ - **SSE 重复帧守卫(#856)**:message 形状重复帧前缀补差 + tool_calls 覆盖(MiniMax v2 类端点二次追加不再叠加)。
61
+ - **直连断流 abort 通道(#878)**:fetch 直连路径 120s 看门狗恢复生效(含遗漏路径收口)。
62
+ - **图片字节预算(#853)**:历史图片累计驱逐(最老先出)+ 注入时同判 + 413 可操作化。
63
+ - **embedding 毒行隔离(#859)**:孤立代理项清洗 + 单毒行不阻后续批。
64
+ - **auto-think 守卫(#860)**:思考型模型分类调用——恒发守卫 + 失败一次可见(不再 100% 静默回退)。
65
+ - **子代理模型形态防线(#861)**:非法 `agent.subagentModel`——加载期清洗 + 运行期明确错误(不再裸崩)。
66
+ - **大工具回执汇总行(#796)**:批量编辑回执末行恒置 `edit batch: N/N entries — <路径清单>`——大回执(offload 截中段)也可读到结论。
67
+ - **TUI-OOM 余项(#863)**:/undo 快照双上界 ∥ console 采集 cap ∥ advisor 关闭轻量化。
68
+ - **台账幽灵根发现(#899)**:歧义根不再被空壳库遮蔽(族发现跳过 + 容器根合计恢复)。
69
+ - **设计令牌回显链(#884)**:截断回显容错 + 常量标记剥离 + 正则单源(工程模式签发可靠性)。
70
+
6
71
  ## [0.10.2] — 2026-10-03
7
72
 
8
73
  > 0.10.1 → 0.10.2(月内 +1——发布时定号)
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * 第 11 批(C / F14 / `ADVISOR-GUARDS.md §3`):解析候选 = cwd + **评审对象声明范围派生根**(声明仓根 /
9
9
  * 声明文件目录 / 声明目录——纯路径派生,零扫描、零 git);命中判据三条件全中(围栏内 ∧
10
- * 可读 ∧ 目标行含引文内容)⇒ 零新增假命中。失败原因三分(file unreadable /
11
- * content mismatch @ path / path traversal)——父侧不再人肉复核。
10
+ * 可读 ∧ 目标行含**任一内容候选**——c1 原捕获 / c2 剥外层装饰 / c3 首引号后段 / c4 首成对引号内层;
11
+ * 零模糊、零内容级归一(装饰剥离候选集 = `ADVISOR-GUARDS.md` §3)。失败原因四类(file unreadable / content mismatch @ path / path traversal / 非连续引文(省略号形——形状 vs 内容造假分列))——父侧不再人肉复核。
12
12
  */
13
13
  import { readFileSync, realpathSync } from "node:fs"
14
14
  import { resolve, relative, dirname, extname, sep, isAbsolute, join } from "node:path"
@@ -21,6 +21,10 @@ import { resolve, relative, dirname, extname, sep, isAbsolute, join } from "node
21
21
  // advisory for the parent agent, never a crash path.
22
22
  const CITATION_RE = /([\w./\\-]+\.(?:mjs|cjs|js|ts|jsx|tsx|mts|cts|py|rs|go|c|h|cpp|hpp|java|rb|php|sh|bash|json|md|markdown|mdx|yaml|yml|toml|css|html)):(\d+):\s*([^`\n]{4,})/g
23
23
 
24
+ // 非连续引文判据(`ADVISOR-GUARDS.md §3` · 2026-10-03):引文内容含省略号(`…` / `...`)⇒ 缩略形,
25
+ // 非所引行的连续子串——失败分类专类(形状不符与内容造假分列;先于 `content mismatch` 判定)。
26
+ const ELLIPSIS_RE = /…|\.\.\./
27
+
24
28
  /** Extract `file:line: content` citations from a review text. */
25
29
  export function extractCitations(text) {
26
30
  const out = []
@@ -75,12 +79,18 @@ function resolveCitation(citation, roots, base) {
75
79
  } catch {
76
80
  continue // 存在但读不了(目录等)——按未命中处理,试下一候选
77
81
  }
78
- if (line.includes(citation.content)) {
82
+ if (citationCandidates(citation.content).some((c) => line.includes(c))) {
79
83
  return { matched: true, root, resolved: relative(base, real).split(sep).join("/") }
80
84
  }
81
85
  mismatch ??= real
82
86
  }
83
- if (mismatch) return { matched: false, reason: `content mismatch @ ${relative(base, mismatch).split(sep).join("/")}` }
87
+ if (mismatch) {
88
+ const rel = relative(base, mismatch).split(sep).join("/")
89
+ // 判定优先级(`ADVISOR-GUARDS.md §3`):含省略号且不连续 ⇒ 非连续引文类——先于 mismatch。
90
+ return ELLIPSIS_RE.test(citation.content)
91
+ ? { matched: false, reason: `not a contiguous citation (ellipsis) @ ${rel} — quote one contiguous excerpt` }
92
+ : { matched: false, reason: `content mismatch @ ${rel}` }
93
+ }
84
94
  if (traversal) return { matched: false, reason: "path traversal" }
85
95
  return { matched: false, reason: "file unreadable" }
86
96
  }
@@ -137,3 +147,29 @@ export function appendCitationReport(text, cwd, opts = {}) {
137
147
  }
138
148
  return text + lines.join("\n")
139
149
  }
150
+
151
+ /** 装饰剥离候选集(`ADVISOR-GUARDS.md` §3 · 2026-10-05 批 · 台账 #928):评审引风常含包裹引号 ∥ 行内
152
+ * 注记(`file:line: "…引文…" — 注记`)——捕获串带装饰时字节包含判失败 ⇒ 误报 content mismatch。
153
+ * 候选(任一为所引行**连续子串**即命中——零模糊、零内容级归一):
154
+ * c1 原捕获(现行行为——零回归,不设下限)∥ c2 反复剥「引号族 + 空白」的先导/尾随 ∥ c3 首个引号
155
+ * 字符后段(剥前注记)∥ c4 首个成对引号段内层文本;派生候选下限 2 字符(空引号 ∥ 单字符垃圾形
156
+ * 不成候选)。 */
157
+ function citationCandidates(raw) {
158
+ const out = [raw] // c1——原捕获(零回归)
159
+ const add = (s) => { if (s.length >= 2 && !out.includes(s)) out.push(s) }
160
+ const DECOR = /^[\s"'“”「」『』]+|[\s"'“”「」『』]+$/g
161
+ let s = raw
162
+ for (;;) { const t = s.replace(DECOR, ""); if (t === s) break; s = t }
163
+ add(s) // c2——剥离外层装饰(反复至不动点)
164
+ const qi = raw.search(/["'“”「」『』]/)
165
+ if (qi >= 0) add(raw.slice(qi + 1).trim()) // c3——首个引号字符后段(剥前注记)
166
+ const spans = []
167
+ for (const [open, close] of [['"', '"'], ["“", "”"], ["「", "」"]]) {
168
+ const a = raw.indexOf(open)
169
+ if (a < 0) continue
170
+ const b = raw.indexOf(close, a + 1)
171
+ if (b > a + 1) spans.push({ a, inner: raw.slice(a + 1, b) })
172
+ }
173
+ if (spans.length) add(spans.sort((x, y) => x.a - y.a)[0].inner) // c4——首个成对引号段内层文本
174
+ return out
175
+ }
@@ -7,9 +7,10 @@
7
7
  * re-attach — F13/`ADVISOR-GUARDS.md §2` #3); renderTimeline moved verbatim too, so the tail
8
8
  * GENERATOR and the tail CLASSIFIER stay in one file with the assembler they
9
9
  * feed (`ADVISOR-GUARDS.md §1` 谓词 ↔ §4 结构化尾——同族单源)。拆分线 = 行数硬帽实测(见批次档 §5)。
10
+ * advisor-loop-split 批:loop 的压缩检查点归位本档(`compactContextIfNeeded`——`context_limit` 尾串生成点同迁;时间线面 ⇒ `advisor/timeline.mjs`)。
10
11
  */
11
12
 
12
- import { providerSpec } from "../config.mjs" // 第 25 批:预算派生(与 loop.mjs:11 同源导入)
13
+ import { providerSpec } from "../config.mjs" // 第 25 批:预算派生(与 loop.mjs:12 同源导入)
13
14
  // B4(群 B 批,ADVISOR-GUARDS.md §9——F32):CJK 加权单源(provider/rate.mjs 叶子向无环)
14
15
  import { estimateText } from "../provider/rate.mjs"
15
16
 
@@ -82,13 +83,31 @@ export function compactMessages(messages, pinned = null) {
82
83
  ...recent)
83
84
  }
84
85
 
86
+ /** 压缩检查点(自 advisor/loop.mjs 迁出——advisor-loop-split 批):超 `budget.compactAt` ⇒ `onText` 通知 +
87
+ * `compactMessages` 原地重写(F13 pinned 重挂随它);重写后仍超 `budget.limit` ⇒ 返回上下文超限尾串
88
+ * (调用点收尾——`context_limit` 生成点),否则 `null`。 */
89
+ export function compactContextIfNeeded(messages, budget, pinned, onText) {
90
+ // Check context window and compact if needed
91
+ const currentTokens = estimateTokens(messages)
92
+ if (currentTokens > budget.compactAt) {
93
+ onText(`\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n`)
94
+ compactMessages(messages, pinned)
95
+ if (estimateTokens(messages) > budget.limit) {
96
+ // Report the POST-compaction count — the pre-compaction currentTokens
97
+ // is stale by the time compaction has run.
98
+ return `Advisor: context window limit reached (${estimateTokens(messages)} tokens). Review incomplete — too many tool calls. Try a narrower scope.`
99
+ }
100
+ }
101
+ return null
102
+ }
103
+
85
104
  // ─────────────────────────────────────────────────────────────────────────────
86
105
  // 不完整判定族(A / F16 共用单谓词——`ADVISOR-GUARDS.md §1`;六 kind = 宿主尾族)
87
106
  // ─────────────────────────────────────────────────────────────────────────────
88
107
 
89
108
  /** 宿主尾族六 kind 的块首行逐字前缀(`ADVISOR-GUARDS.md §1` 表)。变量段(token 数 / 秒数 / 工具轮数)
90
109
  * 不入前缀——取各尾的固定字面部分;`review_failed` = run.mjs catch 的字符串 resolve
91
- * 形态(不 throw),其余五条 = renderTimeline 尾(loop.mjs)。 */
110
+ * 形态(不 throw);`context_limit` 生成点 = 本档 `compactContextIfNeeded`,其余四条 = renderTimeline 尾(loop.mjs)。 */
92
111
  const ADVISOR_INCOMPLETE_PREFIXES = [
93
112
  ["context_limit", "Advisor: context window limit"],
94
113
  ["turn_cap", "Advisor: stopped after"],
@@ -24,7 +24,7 @@ export function buildConvergenceInstructions(round, scopeFiles = null) {
24
24
  "2. STALE-CONTEXT WARNING: any diff or file content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.",
25
25
  "3. You have no git tool; git output in earlier messages is historical and untrustworthy (committed fixes never show in a diff).",
26
26
  "4. `read` the files named in the prior review output (or the review surface above) in full — ALWAYS. Batch reads/greps in a single reply.",
27
- "5. Evidence rule: every 'Unfixed'/'New' finding MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may be stale or fabricated. Findings without a fresh quoted line are treated as unverified and will not be accepted.",
27
+ "5. Evidence rule: every 'Unfixed'/'New' finding MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(() => {`). Line numbers alone are NOT evidence — they may be stale or fabricated. Findings without a fresh quoted line are treated as unverified and will not be accepted.",
28
28
  "6. Produce your verification table. Do not re-read content you already have.",
29
29
  round === 2
30
30
  ? "7. You may flag obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style)."
@@ -17,13 +17,15 @@ const DEFAULT_CRITERIA = `Review the code changes, focusing on:
17
17
  /**
18
18
  * Extract the agent's response table (| # | Action | Detail |) — the fix-claims
19
19
  * reference for convergence rounds.
20
- * Semantics (decision 2026-08-08): without sinceIdx, scan BACKWARD for the
21
- * MOST RECENT response table (no prior-table index is carried anymore — the
22
- * agent response is a focus aid only; format drift falls back to the
23
- * no-response text and never drives control flow). With sinceIdx, scan
24
- * FORWARD from it (legacy callers/tests).
20
+ * Semantics: with sinceIdx, scan FORWARD from it — the review instance passes
21
+ * its delivery watermark (`agent._advisorResponseAnchor`), so the table taken
22
+ * is the one written after THIS review's delivery. Without sinceIdx (legacy
23
+ * direct callers — no watermark), fall back to scanning BACKWARD for the most
24
+ * recent table. The agent response is a focus aid only — format drift falls
25
+ * back to the no-response text and never drives control flow (decision
26
+ * 2026-08-08).
25
27
  * @param {Array} history — message history
26
- * @param {number} [sinceIdx] — legacy: start scanning forward from this index
28
+ * @param {number} [sinceIdx] — delivery watermark: start scanning forward from this index
27
29
  * @returns {string|null} the response table content, or null
28
30
  */
29
31
  export function extractAgentResponseTable(history, sinceIdx) {
package/advisor/loop.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  * advisor/loop.mjs — advisor tool loop: chat → execute tools → repeat, plus the
3
3
  * review timeline (split out of advisor/run.mjs, 第 11 批 — run.mjs was 498/500
4
4
  * 硬帽;拆分保持既有 import 面:run.mjs 继续 re-export 本文件导出)。
5
+ * 自本档再拆(advisor-loop-split 批):时间线记录面 ⇒ `advisor/timeline.mjs`;压缩协作面 ⇒ `advisor/compaction.mjs`(`compactContextIfNeeded`)。
5
6
  *
6
7
  * 第 11 批(F15/`ADVISOR-GUARDS.md §4`):每次 chat 调用携带硬墙信号(`AbortSignal.any([signal,
7
8
  * AbortSignal.timeout(remaining)])`;墙判定绑信号状态——抛错 / partial 两形态同判),
@@ -13,10 +14,11 @@ import { toOpenAISchema } from "../tools/index.mjs"
13
14
  import { truncateAdvisorResult } from "./truncate.mjs"
14
15
  import { batchTool } from "../agent-tools/batch.mjs"
15
16
  import {
16
- estimateTokens, compactMessages, shouldBudgetNudge, budgetNudgeText, timeoutTail, renderTimeline,
17
+ shouldBudgetNudge, budgetNudgeText, timeoutTail, renderTimeline, compactContextIfNeeded,
17
18
  MAX_ADVISOR_TURNS, advisorContextBudget, TOOL_TIMEOUT_MS, REVIEW_TIMEOUT_MS, MAX_RESULT_CHARS,
18
19
  ADVISOR_THINKING_PLACEHOLDER,
19
20
  } from "./compaction.mjs"
21
+ import { createTimelineRecorder } from "./timeline.mjs"
20
22
 
21
23
  const { readTool, globTool, grepTool, lsTool } = await import("../tools/index.mjs")
22
24
  const { lspTool } = await import("../tools/lsp.mjs")
@@ -68,21 +70,7 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
68
70
  // 未注入 ⇒ 不发进度行(核内零端名分支——契约 5 / 10)。端在装配层传入自己的格式化器。
69
71
  const describeArgs = seams.describeArgs ?? (() => "")
70
72
  // 第 11 批硬墙 / 预算 / 尾:实现注解见下方各点;守卫函数与 renderTimeline 在 compaction.mjs。
71
- // Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
72
- // Every chunk is ALSO recorded into an ordered timeline — the persisted record
73
- // must show the review process (thinking ↔ tool progress ↔ final text) at its
74
- // real positions, not a summary appended at the end. Same-kind consecutive
75
- // chunks merge (token streams); kind flips start a new entry.
76
- const timeline = []
77
- const record = (kind, text) => {
78
- const last = timeline.at(-1)
79
- if (last && last.kind === kind) last.text += text
80
- else timeline.push({ kind, text })
81
- }
82
- const emit = (kind) => (text) => { record(kind, text); onOutput?.({ kind, text }) }
83
- const onThink = emit("think")
84
- const onText = emit("text")
85
- const onTool = emit("tool")
73
+ const { timeline, onThink, onText, onTool } = createTimelineRecorder(onOutput)
86
74
  // toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
87
75
  // set with controllable timing/errors.
88
76
  const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent, reviewType, batchDoc)
@@ -90,10 +78,20 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
90
78
  let toolCallCount = 0
91
79
  let reviewTextProduced = false
92
80
  let budgetNudged = false
81
+ // #934(零落笔看门狗——§6.32.2 ②):连续无文本产出轮计数(循环局部——单次评审运行内计数,
82
+ // 修复轮 = 新运行 ⇒ 重新计数)+ 单次运行通报闩(文本产出后不再触发——「连续无产出」条件不回头)
83
+ // + 本轮有无文本标记(轮顶结上一轮用)。
84
+ let silentRounds = 0
85
+ let stallAlerted = false
86
+ let roundHadText = false
93
87
  const startTime = now()
94
88
  // 第 25 批(`ADVISOR-GUARDS.md §8`):上下文预算跟随评审模型窗口——`providerSpec`(模型规格表 × provider 级
95
89
  // context 覆盖)派生;函数体内、while 轮次外一次性(provider 全场不变),两档消费见下守卫。
96
90
  const budget = advisorContextBudget(provider)
91
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.2 ②):阈值单源(叶档 agent-tools/zero-write-watch.mjs)——
92
+ // 函数域动态 import:模块域会与既有环(叶 → parent-channel → async-settle ↔ advisor-async → 本档)
93
+ // 互锁(加载期 TLA 死锁);函数运行时图已就绪,模块缓存 ⇒ 每次评审一次代价。
94
+ const { ZERO_WRITE_ALERT_ROUNDS } = await import("../agent-tools/zero-write-watch.mjs")
97
95
 
98
96
  while (true) {
99
97
  // Interrupted (Ctrl+I) — stop immediately instead of spinning a fresh uncancellable signal
@@ -120,19 +118,23 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
120
118
  if (++turns > MAX_ADVISOR_TURNS) {
121
119
  return renderTimeline(timeline, "Advisor: stopped after " + MAX_ADVISOR_TURNS + " tool rounds — the review appears to be looping. You may retry with a narrower scope.")
122
120
  }
123
-
124
- // Check context window and compact if needed
125
- const currentTokens = estimateTokens(messages)
126
- if (currentTokens > budget.compactAt) {
127
- onText(`\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n`)
128
- compactMessages(messages, pinned)
129
- if (estimateTokens(messages) > budget.limit) {
130
- // Report the POST-compaction count — the pre-compaction currentTokens
131
- // is stale by the time compaction has run.
132
- return renderTimeline(timeline, `Advisor: context window limit reached (${estimateTokens(messages)} tokens). Review incomplete — too many tool calls. Try a narrower scope.`)
121
+
122
+ // #934(零落笔看门狗——§6.32.2 ② / §6.32.6 触发接线②):轮顶结上一轮(首轮无上一轮可结)——
123
+ // 上一轮无文本 ⇒ silentRounds += 1;越阈且未通报 ⇒ 恰一次通报(可选钩子——缺省零行为;
124
+ // 钩子恒零抛:抛错不得上抛为评审 failed 面——实现面兜底捕获兑现)。
125
+ if (turns > 1) {
126
+ if (!roundHadText) silentRounds += 1
127
+ roundHadText = false
128
+ if (!stallAlerted && silentRounds >= ZERO_WRITE_ALERT_ROUNDS) {
129
+ stallAlerted = true
130
+ try { seams.onStallRound?.(silentRounds) } catch { /* #934:恒零抛 */ }
133
131
  }
134
132
  }
135
133
 
134
+ // 压缩协作(迁出 compaction.mjs `compactContextIfNeeded`):超限尾 ⇒ 收尾
135
+ const contextTail = compactContextIfNeeded(messages, budget, pinned, onText)
136
+ if (contextTail) return renderTimeline(timeline, contextTail)
137
+
136
138
  // LLM generation silence: the reasoning phase produces no SSE bytes for
137
139
  // seconds to tens of seconds (server-side prefill on large contexts, per
138
140
  // tool-round LLM return). A placeholder keeps the panel visibly working.
@@ -155,7 +157,7 @@ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, c
155
157
  messages,
156
158
  tools: toolSchemas,
157
159
  signal: callSignal,
158
- onToken: (t) => { if (String(t ?? "").trim()) reviewTextProduced = true; onText(t) },
160
+ onToken: (t) => { if (String(t ?? "").trim()) { reviewTextProduced = true; silentRounds = 0; roundHadText = true } onText(t) },
159
161
  onReasoning: onThink,
160
162
  // LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
161
163
  // TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
@@ -48,12 +48,13 @@ export function buildObjectDeclarationBlock(object = null) {
48
48
  */
49
49
  export function buildDesignApprovalBlock(designToken, designId) {
50
50
  const echo = designId
51
- ? `If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}] and this exact designId: ${designId}. Copy BOTH values verbatim.`
52
- : `If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]`
51
+ ? `If — and ONLY if — no unresolved 🔴 (Critical) issue remains, end your reply with this exact token: [DESIGN-TOKEN:${designToken}] and this exact designId: ${designId}. Copy BOTH values verbatim — every character, including the colon and the digits after the uuid inside the token brackets; a shortened echo is flagged as truncated.`
52
+ : `If — and ONLY if — no unresolved 🔴 (Critical) issue remains, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]. Copy it verbatim — every character, including the colon and the digits after the uuid; a shortened echo is flagged as truncated.`
53
53
  return [
54
54
  "## Approval Signal",
55
55
  echo,
56
- "🟡 (Advisory) and 🔵 (Note) findings do NOT block approval — list them if present, but still include the token. If there are any 🔴 issues, do NOT include the token.",
56
+ "The verdict line is checked mechanically: without a `VERDICT: pass` line, or with a `VERDICT: changes-required` line, no token is issued even when the token is echoed.",
57
+ "🟡 (Advisory) and 🔵 (Note) findings do NOT block approval — list them if present, but still include the token. If any 🔴 (Critical) issue remains unresolved, do NOT include the token. A reasoned non-fix accepted by the reviewer does not block (🔴 only — 🟡/🔵 never block approval); anything else stays unresolved.",
57
58
  ].join("\n")
58
59
  }
59
60
 
@@ -195,7 +196,7 @@ function buildAdvisorUserMessageInner(agent, prior, reviewType, designToken = nu
195
196
  // conversation can quote literal "\x"/"\u" sequences (server 400 risk).
196
197
  if (p && (agent._advisorRound || 0) > 0) {
197
198
  const scopeFiles = resolveScopeFiles(agent, paths)
198
- const response = extractAgentResponseTable(agent.history)
199
+ const response = extractAgentResponseTable(agent.history, agent._advisorResponseAnchor ?? undefined)
199
200
  || (scopeFiles?.length
200
201
  ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
201
202
  : "(Agent did not provide a response table — perform a fresh review of the files named in the system prompt context)")
package/advisor/run.mjs CHANGED
@@ -140,7 +140,11 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
140
140
  const pinned = buildPinnedBrief(reviewType, documents, object, designToken, designId)
141
141
 
142
142
  try {
143
- const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned)
143
+ // #934(零落笔看门狗——§6.32.6 触发接线②):评审族闸位钩子转发(callbacks 不供 `onStall` ⇒
144
+ // `onStallRound: undefined` ⇒ 环路零行为——同步径(advisor 工具)不供钩子)。
145
+ const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned, {
146
+ onStallRound: callbacks?.onStall,
147
+ })
144
148
 
145
149
  // Host-verified citations (decision d698434): mechanically check every
146
150
  // `file:line: content` reference in the review against the CURRENT file
@@ -0,0 +1,26 @@
1
+ /**
2
+ * advisor/timeline.mjs — 评审环路的有序时间线记录器(自 advisor/loop.mjs 迁出——advisor-loop-split 批):
3
+ * 记录序 / 同类合流 / 三 kind 包装一体;下游消费 = renderTimeline(compaction.mjs)。
4
+ * 零 import 叶(对照 agent/relay-prefix.mjs 先例家族)。
5
+ *
6
+ * @param {Function} [onOutput] — 实时 chunk 出口;缺省 ⇒ 只记录不出口。
7
+ * @returns {{ timeline: Array, onThink: Function, onText: Function, onTool: Function }}
8
+ */
9
+ export function createTimelineRecorder(onOutput) {
10
+ // Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
11
+ // Every chunk is ALSO recorded into an ordered timeline — the persisted record
12
+ // must show the review process (thinking ↔ tool progress ↔ final text) at its
13
+ // real positions, not a summary appended at the end. Same-kind consecutive
14
+ // chunks merge (token streams); kind flips start a new entry.
15
+ const timeline = []
16
+ const record = (kind, text) => {
17
+ const last = timeline.at(-1)
18
+ if (last && last.kind === kind) last.text += text
19
+ else timeline.push({ kind, text })
20
+ }
21
+ const emit = (kind) => (text) => { record(kind, text); onOutput?.({ kind, text }) }
22
+ const onThink = emit("think")
23
+ const onText = emit("text")
24
+ const onTool = emit("tool")
25
+ return { timeline, onThink, onText, onTool }
26
+ }
package/advisor.mjs CHANGED
@@ -124,7 +124,9 @@ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
124
124
  * NOTE: the caller (prepareAdvisorMessages) applies escapeLiteralEscapes to
125
125
  * the return value — direct callers must do the same (the prior table and
126
126
  * agent response can quote literal "\x"/"\u" sequences).
127
- * @param {Object} agent — the parent agent (history used for the response table)
127
+ * @param {Object} agent — the parent agent (history used for the response table:
128
+ * scanned forward from `agent._advisorResponseAnchor` — this instance's delivery
129
+ * watermark; absent → legacy backward scan)
128
130
  * @param {Object|null} prior — prior issue table (extracted from history when null)
129
131
  * @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
130
132
  * @param {Object|null} [object] — review-object declaration (AGENT-LOOP-ASYNC-POOL.md §6.18): mechanically
@@ -156,7 +158,7 @@ export function buildAdvisorFollowUp(agent, prior, scopeFiles = null, object = n
156
158
  const noResponseFallback = scopeFiles?.length
157
159
  ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
158
160
  : "(Agent did not provide a response table — perform a fresh full review; the review surface is unknown, ask the user for the file list)"
159
- const response = extractAgentResponseTable(agent.history) || noResponseFallback
161
+ const response = extractAgentResponseTable(agent.history, agent._advisorResponseAnchor ?? undefined) || noResponseFallback
160
162
  const round = (agent._advisorRound || 0) + 1
161
163
  // Review-object declaration FIRST (T-OA2 — round 2+ stays anchored, no re-archaeology).
162
164
  const declaration = buildObjectDeclarationBlock(object)
@@ -28,10 +28,12 @@ const MAX_EMPTY_RETRIES = 2
28
28
  * @param {object} callbacks - { onTurnEnd, onSubTurnBreak }
29
29
  */
30
30
  export function handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks) {
31
- if (!response.content) {
31
+ if (String(response.content ?? "").trim() === "") {
32
32
  // Transient empty response (reasoning exhausted / output truncated): instead of
33
33
  // aborting the whole turn, inject a reminder and let the model respond again.
34
34
  // Bounded — after MAX_EMPTY_RETRIES consecutive empties, surface the original error.
35
+ // §6.31.8 F-DA6(#929 同族 · 批 digest-accounting):空判加宽为**空白判**(纯空白内容
36
+ // 原过判 truthy ⇒ 静默当正常收尾);既有重试(≤2)/ 抛错骨架零改。
35
37
  const retries = agent._emptyRetries ?? 0
36
38
  if (retries < MAX_EMPTY_RETRIES) {
37
39
  agent._emptyRetries = retries + 1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * agent/digest-account.mjs — 消化账务单件(AGENT-LOOP-ASYNC-POOL.md §6.31 · 批 digest-accounting ·
3
+ * 台账 #930):投递 ≠ 销账——报告不许无声消失。
4
+ *
5
+ * 三角色(§6.31.2):**投递**(注入但留容器)∥ **见账**(消化轮收尾逐条机检账目覆盖)∥
6
+ * **销账**(覆盖才离容器 + 释放 report)。本档承载:常量(`DA_RETRY_LIMIT`)/ 回合分类
7
+ * (`isDigestRound`)/ 投递门控(`shouldDeliverEntry`)/ 账目合同(`coversDigestId` 解析 ∥
8
+ * `digestAccountRequirement` 要求行)/ 见账 pass(`accountDigestRound`——销账 ∥ 重投标记 ∥
9
+ * 升级)/ 读面(`unsettledRows` · `unsettledRow` · `unsettledCount`)/ 落痕清位 ∕ 取件助手
10
+ * (`armAccountRound` · `harvestAccountOutput`)。
11
+ *
12
+ * 载体(§6.31.2):`_daSession`(会话标记——挂起会话置 / 复位,`agent/suspension.mjs`)与
13
+ * `_unsettledDigests`(升级账本——经 `async-settle.mjs` 写助手);账目态挂条目字段
14
+ * (`_daDelivered` / `_daRetry` / `_daFailures`)。**fallback 面**(无 `_daSession`——headless /
15
+ * 直连 `runAgent`)零账目动作(不销账 / 不重投 / 不升级):缺省条逐字沿用取尽语义(先离容后注入);
16
+ * 已投递未销账条同判据跳过且留容器(§6.31 出口缝② · 父侧裁定 2026-10-05)。
17
+ *
18
+ * 边界(§6.31.12):不做处理质量机检(只保「不许无声」);升级账本无 TTL(会话期面——
19
+ * 中止清、idle 存续)。
20
+ */
21
+ import { logEvent } from "../log.mjs"
22
+ import { pushReal } from "../context.mjs"
23
+ import { carrierField, releaseSettledEntry, appendUnsettledDigest } from "../agent-tools/async-settle.mjs"
24
+ import { DIGEST_ACCOUNT_DOMAIN } from "./helpers.mjs"
25
+
26
+ /** 同条重投上限(§6.31.5 KD-DA4):重投 ≤ DA_RETRY_LIMIT ⇒ 总投递 ≤ 3 次;超限 ⇒ 升级。 */
27
+ export const DA_RETRY_LIMIT = 2
28
+
29
+ /**
30
+ * 回合分类单点(§6.31.2):账目只属**消化轮**(autoTurn ∧ 非上行唤醒轮 ∧ 非 timer 轮)——
31
+ * timer / 上行轮投递不销账、不重投(其账目在后续消化轮)。
32
+ */
33
+ export function isDigestRound(opts) {
34
+ return opts?.autoTurn === true && opts?.upstreamTurn !== true && opts?.timerTurn !== true
35
+ }
36
+
37
+ /**
38
+ * 投递门控(§6.31.4 单点谓词):
39
+ * - `!_daDelivered` ⇒ 投(首次投递——任意回合,沿既有「user + auto」面);
40
+ * - `_daRetry` ∧ 本回合 = 消化轮 ⇒ 投(重投经消化轮回路);
41
+ * - 其余 ⇒ 跳过(防重复注入)。
42
+ */
43
+ export function shouldDeliverEntry(entry, digestRound) {
44
+ if (entry?._daDelivered !== true) return true
45
+ return entry._daRetry === true && digestRound === true
46
+ }
47
+
48
+ /** 账目要求行(§6.31.3):核常量(helpers.mjs)+ 未销账 id 清单动态拼(`<ids>` 占位替换)。 */
49
+ export function digestAccountRequirement(ids) {
50
+ return DIGEST_ACCOUNT_DOMAIN.replace("<ids>", ids.map((id) => `#${id}`).join(", "))
51
+ }
52
+
53
+ /**
54
+ * 账目覆盖机检(§6.31.3):输出文本内**存在一行**同时含标记 `[digest-ack` 与 `#<id>`
55
+ * (id 词界 = 后随非数字)⇒ 该条覆盖;disposition / 要点 / 原因**不参与机检**。
56
+ * 无标记 / 无 id / 部分覆盖各按本条判(零工具轮可满足——纯文本判据)。
57
+ * 注:行内扫**全部**出现位(长 id 首现不吻合不弃行——如 id=1 行内先见 `#12` 后见 `#1`)。
58
+ */
59
+ export function coversDigestId(output, id) {
60
+ const key = `#${id}`
61
+ for (const line of String(output ?? "").split(/\r?\n/)) {
62
+ const marker = line.indexOf("[digest-ack")
63
+ if (marker < 0) continue
64
+ for (let from = marker; ;) {
65
+ const at = line.indexOf(key, from)
66
+ if (at < 0) break
67
+ const after = line[at + key.length]
68
+ if (after === undefined || !/[0-9]/.test(after)) return true
69
+ from = at + 1 // 词界不吻合 ⇒ 续扫下一出现位(同长 id 前缀不吞后随正例)
70
+ }
71
+ }
72
+ return false
73
+ }
74
+
75
+ /**
76
+ * 见账 pass(§6.31.5——消化轮收尾 `finalizeAgentTurn` 单点调用;调用方已判 `isDigestRound ∧
77
+ * `_daSession` ∧ 非会话停止径):
78
+ * - 投递账 = pending ∩ `_daDelivered`;覆盖 ⇒ 离容器(splice)+ `releaseSettledEntry`(含 report 释放);
79
+ * - 未覆盖 ⇒ `_daFailures += 1`;≤ `DA_RETRY_LIMIT` ⇒ `_daRetry = true`(下轮重投);超限 ⇒ **升级**
80
+ * (离容器 + 入 `_unsettledDigests` + 恰一条提醒(pushReal)+ 恰一条 `ev:unsettled`)。
81
+ * 缺痕(output == null——抛错 / 撞帽 / 未收口)⇒ 全条未覆盖(安全方向——假阴可重投;假阳是事故本相)。
82
+ */
83
+ export function accountDigestRound(carrier) {
84
+ const pending = carrierField(carrier, "_pendingAsyncResults")
85
+ if (!Array.isArray(pending)) return { covered: 0, retried: 0, unsettled: 0 }
86
+ const output = harvestAccountOutput(carrier)
87
+ const covered = []
88
+ const retried = []
89
+ const upgraded = []
90
+ for (const entry of [...pending]) {
91
+ if (entry?._daDelivered !== true) continue
92
+ if (output != null && coversDigestId(output, entry.id)) { covered.push(entry); continue }
93
+ entry._daFailures = (entry._daFailures ?? 0) + 1
94
+ if (entry._daFailures <= DA_RETRY_LIMIT) { entry._daRetry = true; retried.push(entry) }
95
+ else upgraded.push(entry)
96
+ }
97
+ for (const entry of covered) {
98
+ const i = pending.indexOf(entry)
99
+ if (i >= 0) pending.splice(i, 1)
100
+ releaseSettledEntry(entry)
101
+ }
102
+ for (const entry of upgraded) {
103
+ const i = pending.indexOf(entry)
104
+ if (i >= 0) pending.splice(i, 1)
105
+ appendUnsettledDigest(carrier, {
106
+ id: String(entry.id),
107
+ role: entry.role ?? "subagent",
108
+ failures: entry._daFailures,
109
+ report: entry.report ?? null,
110
+ at: Date.now(),
111
+ })
112
+ }
113
+ // 升级:恰一条提醒(机器线 + 人读线——`pushReal`)+ 恰一条留痕(同点;字段单源 = LOGGING.md §6.2)。
114
+ // 注:`ids` 以 join 串落盘——`logEvent` 只收标量(数组字段会被静默丢弃——先例 `ev:discarded`)。
115
+ if (upgraded.length > 0) {
116
+ const ids = upgraded.map((entry) => String(entry.id))
117
+ pushReal(carrier, { role: "user", content: unsettledReminder(upgraded.length, ids) })
118
+ logEvent("ev:unsettled", { n: upgraded.length, ids: ids.join(", ") })
119
+ }
120
+ return { covered: covered.length, retried: retried.length, unsettled: upgraded.length }
121
+ }
122
+
123
+ /** 升级提醒行(§6.31.5——逐字 = 父侧定稿:含 n / 轮数(= 重投上限 + 1)/ id 清单与接手指引)。 */
124
+ export function unsettledReminder(n, ids) {
125
+ return `[System reminder: ${n} background report(s) were delivered but never accounted after ${DA_RETRY_LIMIT + 1} digest rounds — automatic re-delivery has stopped and they are marked UNSETTLED: ${ids.map((id) => `#${id}`).join(", ")}. Their content was delivered in this conversation; process them there or re-spawn the work. Readable via subagent action:'status'.]`
126
+ }
127
+
128
+ /**
129
+ * 读面(§6.31.6——status 段 ∥ 端面残余行共用单点):行 = `{ id, role, state, attempts }`;
130
+ * state ∈ `awaiting-digest`(在途未投递 ∥ 已投递未判)/ `retrying`(判过未覆盖)/
131
+ * `unsettled`(已升级——随携 `preview` ≤400 字符)。
132
+ */
133
+ export function unsettledRows(carrier) {
134
+ const rows = []
135
+ const pending = carrierField(carrier, "_pendingAsyncResults")
136
+ for (const entry of Array.isArray(pending) ? pending : []) {
137
+ const failures = entry?._daFailures ?? 0
138
+ rows.push({
139
+ id: String(entry.id),
140
+ role: entry.role ?? "subagent",
141
+ state: failures > 0 ? "retrying" : "awaiting-digest",
142
+ attempts: failures,
143
+ })
144
+ }
145
+ const ledger = carrierField(carrier, "_unsettledDigests")
146
+ for (const u of Array.isArray(ledger) ? ledger : []) {
147
+ rows.push({
148
+ id: String(u.id),
149
+ role: u.role ?? "subagent",
150
+ state: "unsettled",
151
+ attempts: u.failures ?? 0,
152
+ preview: String(u.report ?? "").slice(0, 400),
153
+ })
154
+ }
155
+ return rows
156
+ }
157
+
158
+ /** 单查 id 同解析(§6.31.6 单查序第三 / 四站):命中 pending / 升级账本 ⇒ 行形同上;未命中 ⇒ null。 */
159
+ export function unsettledRow(carrier, id) {
160
+ const key = String(id)
161
+ return unsettledRows(carrier).find((row) => row.id === key) ?? null
162
+ }
163
+
164
+ /** 端面残余行判据(§6.31.6):pending 中 `_daFailures > 0` 条数(升级条已离容器——由升级提醒承载)。 */
165
+ export function unsettledCount(carrier) {
166
+ const pending = carrierField(carrier, "_pendingAsyncResults")
167
+ let n = 0
168
+ for (const entry of Array.isArray(pending) ? pending : []) if ((entry?._daFailures ?? 0) > 0) n += 1
169
+ return n
170
+ }
171
+
172
+ /** 落痕清位(§6.31.5——`run-start.mjs` 会话态起跑即清;fallback 零动作):父字段 ∥ history 双清。
173
+ * 注:核内写点落**父字段**(`turn-loop.mjs` 的 `agent._lastRunOutput`——逐轮追加,本批 #939
174
+ * 收正;抗 `history` 整体替换——`context.mjs` `applyCompression`);`history` 级为兼容读形
175
+ * (`harvestAccountOutput` 回退分支)——该痕不属跨 run 字段集,§6.31.5 边界句。 */
176
+ export function armAccountRound(carrier) {
177
+ if (!carrier || typeof carrier !== "object") return
178
+ carrier._lastRunOutput = null
179
+ if (carrier.history && typeof carrier.history === "object") carrier.history._lastRunOutput = null
180
+ }
181
+
182
+ /** 落痕取件(§6.31.5——载体吸收读:父字段优先 / 回退 `history`);读值 = 本 run 多轮累积文本(#939);
183
+ * 缺痕(本 run 零输出落痕——零模型轮 / 起跑即断)⇒ null。 */
184
+ export function harvestAccountOutput(carrier) {
185
+ const own = carrier?._lastRunOutput
186
+ if (own !== undefined && own !== null) return own
187
+ return carrier?.history?._lastRunOutput ?? null
188
+ }