@thincoder/core 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.
- package/CHANGELOG.md +46 -0
- package/advisor/citations.mjs +13 -3
- package/advisor/convergence.mjs +1 -1
- package/advisor/messages.mjs +3 -3
- package/agent/dispatch-run.mjs +29 -12
- package/agent/record-results.mjs +6 -1
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +35 -9
- package/agent/turn-loop.mjs +6 -0
- package/agent-tools/advisor-async.mjs +11 -1
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/batch-lifecycle.mjs +3 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +28 -0
- package/agent-tools/batch.mjs +5 -3
- package/agent-tools/design-token.mjs +56 -16
- package/agent-tools/subagent-async.mjs +6 -1
- package/agent-tools/subagent-panel.mjs +119 -36
- package/agent-tools/subagent.mjs +4 -2
- package/agent-tools/verify.mjs +5 -0
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +42 -8
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- package/ledger-cmd.mjs +13 -21
- package/ledger-db.mjs +34 -1
- package/ledger-migrate.mjs +4 -3
- package/ledger-read.mjs +94 -0
- package/ledger-surface.mjs +15 -46
- package/ledger.mjs +36 -62
- package/manifest-schema.mjs +13 -3
- package/manifest.mjs +27 -8
- package/mcp/transport-http.mjs +52 -4
- package/mcp/transport-stdio.mjs +33 -8
- package/mcp.mjs +13 -5
- package/memory/code-search.mjs +5 -3
- package/memory/core.mjs +6 -4
- package/memory/docs.mjs +5 -3
- package/memory/schema.mjs +2 -0
- package/model-ref.mjs +96 -3
- package/package.json +1 -1
- package/prompts/advisor-design.md +3 -2
- package/prompts/advisor-round1.md +1 -0
- package/prompts/advisor-round2.md +8 -3
- package/prompts/advisor-round3.md +7 -2
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +3 -1
- package/provider/core.mjs +10 -4
- package/provider/google.mjs +18 -2
- package/provider/normalize.mjs +57 -0
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session-lifecycle.mjs +21 -12
- package/session.mjs +13 -2
- package/stream-destroy.mjs +62 -0
- package/tool-docs/verify.md +1 -1
- package/tools/edit-batch.mjs +7 -2
- package/undo-stack.mjs +23 -9
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
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
|
+
## [0.10.3] — 2026-10-04
|
|
7
|
+
|
|
8
|
+
> 0.10.2 → 0.10.3(月内 +1——发布时定号)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **OpenCode Go 内置预设(#906)**:`opencode-go`(OpenAI 协议侧)∥ `opencode-go-anthropic`(Anthropic 协议侧)——开箱可选渠道(双协议混装建模)。
|
|
13
|
+
- **ACP 结构化子代理事件(#862/#843)**:`session_info_update` 携 `_meta["thincoder.dev/subagent"]`(role ∕ id ∕ 状态 ∕ 进度)——ACP 客户端可呈现子代理活动;判据单源叶 `subagent-event.mjs`。
|
|
14
|
+
- **manifest 未知键告警(#802)**:读档返回 `unknownKeys` + 恰一行可见告警(含键名;非拒——拼错 / 版本错位的声明键不再静默失效)。
|
|
15
|
+
- **台账只读路径**:只读数据库打开面(CLI `ledger list` ∥ ACP `ledger/*` 接口共用)。
|
|
16
|
+
- **崩溃防护两面(#865/#866)**:流响应体错误护(`stream-destroy.mjs` 四消费点)∥ win32 杀树同步化(退出阶段不漏杀)。
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **模型切换解锁 · 核侧(#918)**:会话槽三键(provider ∕ model ∕ effort)**单写者**——回合关联落盘(`prefsSeedOnly`)不携三键(槽在场值赢 ∥ 缺播种)——忙期选定不再可能被旧快照覆写。
|
|
21
|
+
- **环边界中止前置(#793)**:回合环头 ∥ 环尾两检查点——中止态(用户 Stop)恒以 AbortError 收束,不再产出撞帽续跑询问(需求 F5「Stop 优先」兑现)。
|
|
22
|
+
- **多项目台账合计(#882)**:歧义根(容器根)下状态行台账段按族合计——多项目求和不显单一。
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **MCP 会话过期自愈(#850)**:HTTP 404 ⇒ 清会话 + 重新 initialize + 重试一次(单飞重建);二次失败透传 + `sessionDead` 入存活判据。
|
|
27
|
+
- **杀树相位(#877)**:退出阶段 POSIX 组杀 + SIGKILL 兜底(忽略 SIGTERM 的子进程不再泄漏)。
|
|
28
|
+
- **SSE 重复帧守卫(#856)**:message 形状重复帧前缀补差 + tool_calls 覆盖(MiniMax v2 类端点二次追加不再叠加)。
|
|
29
|
+
- **直连断流 abort 通道(#878)**:fetch 直连路径 120s 看门狗恢复生效(含遗漏路径收口)。
|
|
30
|
+
- **图片字节预算(#853)**:历史图片累计驱逐(最老先出)+ 注入时同判 + 413 可操作化。
|
|
31
|
+
- **embedding 毒行隔离(#859)**:孤立代理项清洗 + 单毒行不阻后续批。
|
|
32
|
+
- **auto-think 守卫(#860)**:思考型模型分类调用——恒发守卫 + 失败一次可见(不再 100% 静默回退)。
|
|
33
|
+
- **子代理模型形态防线(#861)**:非法 `agent.subagentModel`——加载期清洗 + 运行期明确错误(不再裸崩)。
|
|
34
|
+
- **大工具回执汇总行(#796)**:批量编辑回执末行恒置 `edit batch: N/N entries — <路径清单>`——大回执(offload 截中段)也可读到结论。
|
|
35
|
+
- **TUI-OOM 余项(#863)**:/undo 快照双上界 ∥ console 采集 cap ∥ advisor 关闭轻量化。
|
|
36
|
+
- **台账幽灵根发现(#899)**:歧义根不再被空壳库遮蔽(族发现跳过 + 容器根合计恢复)。
|
|
37
|
+
- **设计令牌回显链(#884)**:截断回显容错 + 常量标记剥离 + 正则单源(工程模式签发可靠性)。
|
|
38
|
+
|
|
39
|
+
## [0.10.2] — 2026-10-03
|
|
40
|
+
|
|
41
|
+
> 0.10.1 → 0.10.2(月内 +1——发布时定号)
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **统一 provider 解析(#841 · 三端归一)**:核导出 `resolveProviderPlan`(回退链 = 槽渠道(持 key)→ `defaultModel` 渠道 → 首个持 key 渠道;模型面 = 槽模型 → `defaultModel`(同渠道)→ 渠道单值 → `null`;三态 = `ok` ∕ `fallback` ∕ `invalid` + `reason`)与 `resolveChannelModel`(单值转口面)。
|
|
46
|
+
- **`providerState` ∕ `providerStateReason` 两键**:`loadConfig()` 落(`provider` = 解析入选渠道)+ 槽面 key 门(`applySession`——槽无 key ⇒ 跳过且不写槽)。
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **「无效」语义收窄**:仅「无 provider ∥ 无 key」为 `invalid`(不可运行 ⇒ 引导配置);「可运行而无有效 `defaultModel`」为 `fallback`(**可运行 + 必明示**——不再当无效拦下)。
|
|
51
|
+
|
|
6
52
|
## [0.10.1] — 2026-10-03
|
|
7
53
|
|
|
8
54
|
> 0.9.5 → 0.10.1(月切换——十月首发,发布时定号)
|
package/advisor/citations.mjs
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 第 11 批(C / F14 / `ADVISOR-GUARDS.md §3`):解析候选 = cwd + **评审对象声明范围派生根**(声明仓根 /
|
|
9
9
|
* 声明文件目录 / 声明目录——纯路径派生,零扫描、零 git);命中判据三条件全中(围栏内 ∧
|
|
10
|
-
* 可读 ∧ 目标行含引文内容)⇒
|
|
11
|
-
*
|
|
10
|
+
* 可读 ∧ 目标行含引文内容)⇒ 零新增假命中。失败原因四类(file unreadable / content mismatch @ path /
|
|
11
|
+
* 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 = []
|
|
@@ -80,7 +84,13 @@ function resolveCitation(citation, roots, base) {
|
|
|
80
84
|
}
|
|
81
85
|
mismatch ??= real
|
|
82
86
|
}
|
|
83
|
-
if (mismatch)
|
|
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
|
}
|
package/advisor/convergence.mjs
CHANGED
|
@@ -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(
|
|
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)."
|
package/advisor/messages.mjs
CHANGED
|
@@ -48,12 +48,12 @@ export function buildObjectDeclarationBlock(object = null) {
|
|
|
48
48
|
*/
|
|
49
49
|
export function buildDesignApprovalBlock(designToken, designId) {
|
|
50
50
|
const echo = designId
|
|
51
|
-
? `If — and ONLY if —
|
|
52
|
-
: `If — and ONLY if —
|
|
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
|
|
56
|
+
"🟡 (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
57
|
].join("\n")
|
|
58
58
|
}
|
|
59
59
|
|
package/agent/dispatch-run.mjs
CHANGED
|
@@ -13,6 +13,11 @@ import { snapshotForUndo } from "../undo-stack.mjs"
|
|
|
13
13
|
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites, markClaimNoted } from "../peer-domains.mjs"
|
|
14
14
|
import { logToolError, noteExecutedMutation } from "./dispatch-gates.mjs"
|
|
15
15
|
|
|
16
|
+
/** console 回显预算(AGENT-LOOP.md §6.4 · #863):采集端 cap = 与 helpers.mjs `TOOL_RESULT_OFFLOAD_LIMIT`
|
|
17
|
+
* 同值(65536 字符)——累计到限停收(超出丢弃);发生丢弃 ⇒ 回显段尾恰一行截断标记(逐字——两路径同款)。 */
|
|
18
|
+
const CONSOLE_CAPTURE_LIMIT = 64 * 1024
|
|
19
|
+
const CONSOLE_TRUNCATED_MARK = "[console truncated at 65536 chars]"
|
|
20
|
+
|
|
16
21
|
/** 单条 prepared 项的执行(Phase 2 体——原 `dispatch.mjs` runOne 闭包;自由变量 agent ∕ depth ∕
|
|
17
22
|
* signal ∕ callbacks 经形参代入,语义逐字保持:console 捕获 ∕ routed 短路 ∕ mutation 记账 ∕
|
|
18
23
|
* 错误收口零改)。 */
|
|
@@ -39,6 +44,16 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
|
|
|
39
44
|
// 捕获分离(父恢复原始后子的拦截期间父捕获停止、子恢复后父继续)——正确。
|
|
40
45
|
// 声明在 try 之外:catch 块(异常路径)也要访问(报错前的探查输出回显)。
|
|
41
46
|
const capturedConsole = []
|
|
47
|
+
// 采集端有界(#863):累计达限停收——超出丢弃并置截断位(段尾恰一行标记的判据;采集端单点)。
|
|
48
|
+
let capturedChars = 0
|
|
49
|
+
let capturedTruncated = false
|
|
50
|
+
const capturedPush = (t) => {
|
|
51
|
+
const room = CONSOLE_CAPTURE_LIMIT - capturedChars
|
|
52
|
+
if (room <= 0) { capturedTruncated = true; return }
|
|
53
|
+
if (t.length + 1 > room) { t = t.slice(0, room - 1); capturedTruncated = true }
|
|
54
|
+
capturedChars += t.length + 1
|
|
55
|
+
capturedConsole.push(t)
|
|
56
|
+
}
|
|
42
57
|
// LOGGING(LOGGING.md):tool:* 事件——仅真实执行(pre-gate 拦截项在下方早退分支不入事件)。
|
|
43
58
|
// 参数值永不落盘(NF-L3——工具事件不记 args);child=子代理 id(agent._logId,spawn 时 stamp)。
|
|
44
59
|
const toolT0 = Date.now()
|
|
@@ -72,8 +87,8 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
|
|
|
72
87
|
}
|
|
73
88
|
const origConsoleLog = console.log
|
|
74
89
|
const origConsoleErr = console.error
|
|
75
|
-
console.log = (...a) =>
|
|
76
|
-
console.error = (...a) =>
|
|
90
|
+
console.log = (...a) => capturedPush(a.map(String).join(" "))
|
|
91
|
+
console.error = (...a) => capturedPush("[err] " + a.map(String).join(" "))
|
|
77
92
|
let rawResult
|
|
78
93
|
// ctx 对象提升为变量(docs/cli/design/TUI.md §6.8):subagent 阻塞 execute 返回前在 ctx 上留
|
|
79
94
|
// _subagentKey(relayPrefix 去尾)——runOne 在 execute 返回后读它作 onToolResult
|
|
@@ -120,20 +135,22 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
|
|
|
120
135
|
if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
|
|
121
136
|
noteExecutedMutation(agent, item.tool, item.args)
|
|
122
137
|
}
|
|
138
|
+
// 2026-08-31 工具输出回显 ∥ #863:console 段 ∥ 结果本体拼接先于 offload——并入 64K 判定
|
|
139
|
+
// (超限 ⇒ 落盘 + 预览);采集端发生过丢弃 ⇒ 段尾恰一行截断标记(逐字——错误路径同款)。
|
|
140
|
+
const consoleSegment = capturedConsole.length > 0
|
|
141
|
+
? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}${capturedTruncated ? `\n${CONSOLE_TRUNCATED_MARK}` : ""}`
|
|
142
|
+
: ""
|
|
143
|
+
const spliced = raw + consoleSegment
|
|
123
144
|
// Multimodal tools keep the raw result (base64 images ride the multimodal
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// 2026-08-31:工具执行期间捕获的 console 输出附在结果后回显(模型视野)
|
|
128
|
-
const resultWithConsole = capturedConsole.length > 0
|
|
129
|
-
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
130
|
-
: result
|
|
145
|
+
// channel); everything else offloads oversized text to disk. Flag-driven, not
|
|
146
|
+
// name-driven (consult P3, 2026-08-30).
|
|
147
|
+
const result = item.tool?.multimodal ? spliced : await offloadToolResult(spliced, item.toolCall.id)
|
|
131
148
|
// R10 L3:冲突 / 认领软提示附在工具结果末尾(模型可见——不阻止写;提示附加成功才落去重标记)
|
|
132
149
|
const notedOk = Boolean(peerNote) && !raw.startsWith("Error:")
|
|
133
150
|
if (notedOk) markClaimNoted(agent, peerNote.keys)
|
|
134
151
|
const resultForModel = notedOk
|
|
135
|
-
? `${
|
|
136
|
-
:
|
|
152
|
+
? `${result}\n${peerNote.text}`
|
|
153
|
+
: result
|
|
137
154
|
callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
|
|
138
155
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
139
156
|
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
@@ -160,7 +177,7 @@ export async function runPreparedItem(item, agent, depth, signal, callbacks) {
|
|
|
160
177
|
const ctx = ctxParts.length > 0 ? ` [${ctxParts.join(", ")}]` : ""
|
|
161
178
|
// 2026-08-31:异常路径同样回显捕获的 console(工具报错前的探查输出最有价值)
|
|
162
179
|
const consolePart = capturedConsole.length > 0
|
|
163
|
-
? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
180
|
+
? `\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}${capturedTruncated ? `\n${CONSOLE_TRUNCATED_MARK}` : ""}`
|
|
164
181
|
: ""
|
|
165
182
|
return { ...item, result: `Error: ${error.message}${ctx}${consolePart}`, ok: false }
|
|
166
183
|
}
|
package/agent/record-results.mjs
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { pushReal } from "../context.mjs"
|
|
27
27
|
import { specForModel } from "../config.mjs"
|
|
28
28
|
import { FILE_MUTATORS, toolTouchPaths } from "./helpers.mjs"
|
|
29
|
+
import { capHistoryImages } from "../provider/normalize.mjs"
|
|
29
30
|
import { resolve } from "node:path"
|
|
30
31
|
import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
|
|
31
32
|
import { looksLikeReviewOutput, advisorIncompleteMarker } from "../advisor/run.mjs"
|
|
@@ -130,7 +131,9 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
130
131
|
// F2e: the sync settle's engine-approved suffix (stored on the
|
|
131
132
|
// run by settleDesignReview) is stripped with exact truncation — the
|
|
132
133
|
// prior never carries the raw token / designId.
|
|
133
|
-
|
|
134
|
+
// #863 关闭点轻量化(父侧批·五裁定㈡——与 settle 面同族守卫):closed 实例零消费面
|
|
135
|
+
// (续跑仅取 open)——关闭后不写回 priorOutput(否则关闭点释放被覆盖)。
|
|
136
|
+
if (run.open && looksLikeReviewOutput(result)) {
|
|
134
137
|
run.priorOutput = stripApprovedSuffix(result, run.approvedSuffix)
|
|
135
138
|
}
|
|
136
139
|
} else {
|
|
@@ -174,4 +177,6 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
174
177
|
if (real) pushReal(agent, msg)
|
|
175
178
|
else agent.history.push(msg)
|
|
176
179
|
}
|
|
180
|
+
// #853 D-PR32:注入面同函数同预算——history 本体驱逐最老图片(防无界膨胀;发送前另有副本面同函数)。
|
|
181
|
+
if (deferredUserMsgs.some((d) => d.real)) agent.history = capHistoryImages(agent.history)
|
|
177
182
|
}
|
package/agent/spawn-child.mjs
CHANGED
|
@@ -25,7 +25,8 @@ import { appendCappedText } from "../text-budget.mjs"
|
|
|
25
25
|
export { RELAY_PREFIX_RE, parseRelayPath, relayPrefixOf } from "./relay-prefix.mjs"
|
|
26
26
|
|
|
27
27
|
/** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
|
|
28
|
-
|
|
28
|
+
import { EVENT_SENTINEL } from "./subagent-event.mjs" // 2026-10-04 批(ACP-CLIENT.md §12.4):定义位迁入零依赖叶 `subagent-event.mjs`;再导出 ⇒ 既有 import 面零改(先例 = 同档 `child-marks.mjs`)
|
|
29
|
+
export { EVENT_SENTINEL }
|
|
29
30
|
const RS = "\x1e"
|
|
30
31
|
|
|
31
32
|
// 报告文本锚点(turn-cap / stopped-by-user)2026-09-20 下沉零依赖叶 `child-marks.mjs`(端壳
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-event.mjs — 子代理事件 token 文法机读单源(`⟦ev⟧<小写名>` + RS 分隔字段)。
|
|
3
|
+
*
|
|
4
|
+
* 收编自 `spawn-child.mjs`(2026-10-04 ACP 协议面补全批——设计档 `docs/cli/design/
|
|
5
|
+
* ACP-CLIENT.md` §12.4):哨兵定义位迁入本叶 + 形态判据与逐事件投影单源——消费方
|
|
6
|
+
* (本批 = ACP 桥 `thincoder-cli/src/acp/bridge.mjs`)经本叶判据剥离/投影,不再自持
|
|
7
|
+
* 平行表达式(同族先例 = `relay-prefix.mjs` / `child-marks.mjs`;`spawn-child.mjs`
|
|
8
|
+
* import + 再导出哨兵 ⇒ 既有 import 面零改)。
|
|
9
|
+
*
|
|
10
|
+
* 零 import——任意层可引、无环。
|
|
11
|
+
*
|
|
12
|
+
* 文法:`⟦ev⟧<小写名>\x1e<字段…>`(字段以 RS 分隔)。终止符约束 load-bearing:正文含
|
|
13
|
+
* `⟦ev⟧` 而无 RS 终止符 ⇒ 非事件形态 ⇒ null ⇒ 消费方照常转发(不吞真实正文)。
|
|
14
|
+
* 逐事件字段序 = 发射侧单源(各发射点既有构造——本批零改发射侧);投影映射以样本逐字
|
|
15
|
+
* 钉住(批内件 T-862-1…6)——发射侧改字段序 = 契约面变更(设计档 §12.3)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 事件 token 哨兵串(D1)——LLM 正常内容混淆概率极低;字段分隔用 RS (\x1e)。 */
|
|
19
|
+
export const EVENT_SENTINEL = "⟦ev⟧"
|
|
20
|
+
|
|
21
|
+
/** 形态判据(**单源——唯一表达式**):`⟦ev⟧<小写名>` + RS 终止符同现。
|
|
22
|
+
* D13 负向边界(load-bearing):放宽为无终止符形态会吞真实正文(先例教训见
|
|
23
|
+
* `thincoder-cli/src/tui/render.mjs:250-252`)。形态域 = 开集——未知名同判(客户端
|
|
24
|
+
* SHOULD 容忍未知 state;新增事件名不破坏契约)。 */
|
|
25
|
+
const EVENT_FORM_RE = /^⟦ev⟧([a-z]+)\x1e/
|
|
26
|
+
const RS = "\x1e"
|
|
27
|
+
|
|
28
|
+
/** 数值字段解析(§12.2):无值(缺省 ∥ 空串)⇒ 键省略;`Number` 转换非有限值 ⇒ 键省略
|
|
29
|
+
* (防御面——发射侧恒为整数)。 */
|
|
30
|
+
const num = (v) => {
|
|
31
|
+
if (v == null || v === "") return undefined
|
|
32
|
+
const n = Number(v)
|
|
33
|
+
return Number.isFinite(n) ? n : undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 解析子代理事件 token(**形态命中必非 null**——剥离判据与投影载荷由此同一调用决定)。
|
|
38
|
+
* @param {string} text 事件 token 原文(relay 前缀已由消费方剥离——前缀解析非本叶职责)
|
|
39
|
+
* @returns {{ name: string, progress?: { turn?: number, maxTurns?: number, position?: number }, detail?: string } | null}
|
|
40
|
+
* null = 非事件形态(消费方照常转发)
|
|
41
|
+
*/
|
|
42
|
+
export function parseSubagentEvent(text) {
|
|
43
|
+
const m = EVENT_FORM_RE.exec(String(text))
|
|
44
|
+
if (!m) return null
|
|
45
|
+
const name = m[1]
|
|
46
|
+
const fields = String(text).slice(m[0].length).split(RS)
|
|
47
|
+
// 逐态投影(字段序 = 发射侧单源——样本映射见设计档 §12.3):
|
|
48
|
+
// turn / approval ⟦ev⟧turn\x1e{turn}\x1e{maxTurns}\x1e{phase}\x1e[detail]
|
|
49
|
+
// queued ⟦ev⟧queued\x1e{kind}\x1e{position}\x1equeued\x1e[detail](kind 不承载)
|
|
50
|
+
// async / cancelled / stopped / settled / done = 零字段态(stopped 等第 2/3 位 0 系
|
|
51
|
+
// 发射侧填充,不承载——state 逐字即全部载荷)。
|
|
52
|
+
if (name === "turn" || name === "approval") {
|
|
53
|
+
const ev = { name }
|
|
54
|
+
const progress = {}
|
|
55
|
+
const turn = num(fields[0])
|
|
56
|
+
const maxTurns = num(fields[1])
|
|
57
|
+
if (turn !== undefined) progress.turn = turn
|
|
58
|
+
if (maxTurns !== undefined) progress.maxTurns = maxTurns
|
|
59
|
+
if (Object.keys(progress).length > 0) ev.progress = progress
|
|
60
|
+
if (name === "approval" && fields[3]) ev.detail = fields[3]
|
|
61
|
+
return ev
|
|
62
|
+
}
|
|
63
|
+
if (name === "queued") {
|
|
64
|
+
const ev = { name }
|
|
65
|
+
const position = num(fields[1])
|
|
66
|
+
if (position !== undefined) ev.progress = { position }
|
|
67
|
+
if (fields[3]) ev.detail = fields[3]
|
|
68
|
+
return ev
|
|
69
|
+
}
|
|
70
|
+
return { name } // 零字段态 ∥ 未知名(形态域开集)
|
|
71
|
+
}
|
package/agent/suspension.mjs
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* - abortSignal 会话中止信号(兜底监听 + abort 清场判据)
|
|
30
30
|
* - hooks.onCounts(counts) 计数变化通知({ running, queued, pending, done })
|
|
31
31
|
* - hooks.onDigest(phase, counts) 消化轮边界(start / end)
|
|
32
|
-
* - hooks.reclaim(consumed)
|
|
32
|
+
* - hooks.reclaim(consumed) 回收(消化完成 ∥ 残差离容 ∥ 丢弃三类离容逐条补发 done——
|
|
33
|
+
* 恒达窗:正常 ∥ Abort-continue ∥ 异常 ∥ 退出残差四窗同达)
|
|
33
34
|
* - hooks.freezeAll() 退出冻结(兜底残项)
|
|
34
35
|
* - injectResidual(entry) 残余注入面(idle 退出清场;宿主装配端注入器——CLI =
|
|
35
36
|
* 逐族注入分发 / VSC = pending 单容器注入器)
|
|
@@ -112,6 +113,12 @@ function consultLiveCount(carrier) {
|
|
|
112
113
|
* 单点,与核 run-stages 回合尾中止同式;存活 ∕ 已 settle 条目留池——settled 报告沿后续
|
|
113
114
|
* 回合边界注入到达,不静默丢)+ pending 单容器清 + 会诊会话清理标记;
|
|
114
115
|
* - idle:残余直注入(极端竞态残项——结果零丢失;注入器由宿主注入,缺省 no-op 保残项)。
|
|
116
|
+
*
|
|
117
|
+
* 返回 `{ injected, left, error }`(子代理面板批 · 2026-10-04 —— `hooks.reclaim` 恒达窗的退出半):
|
|
118
|
+
* `left` = **离容全量**(splice 全取——注入失败条不缩水),供调用点 `reclaim(left)` 补发
|
|
119
|
+
* (「消费 ⇒ 必发 done」不变量——C2 零发射口);注入循环 try/catch 收口 ⇒ `injected` = 成功
|
|
120
|
+
* 前缀、`error` = 首错(调用点补发毕后重抛——fail-loud 保持);abort ∥ 无注入器 ∥ 无残差 ⇒ 三键空值。
|
|
121
|
+
* 既有 C3/C4 快照径(中止冻结快照)零改。
|
|
115
122
|
*/
|
|
116
123
|
export async function finishSuspension(carrier, { aborted = false, injectResidual = null } = {}) {
|
|
117
124
|
if (aborted) {
|
|
@@ -124,12 +131,17 @@ export async function finishSuspension(carrier, { aborted = false, injectResidua
|
|
|
124
131
|
const { cleanupConsultSessions } = await import("../agent-tools/consult.mjs")
|
|
125
132
|
cleanupConsultSessions(carrier)
|
|
126
133
|
}
|
|
127
|
-
return
|
|
134
|
+
return { injected: [], left: [], error: null }
|
|
128
135
|
}
|
|
129
136
|
const residual = carrier._pendingAsyncResults
|
|
130
|
-
if (residual?.length
|
|
131
|
-
|
|
137
|
+
if (!residual?.length || typeof injectResidual !== "function") return { injected: [], left: [], error: null }
|
|
138
|
+
const left = residual.splice(0)
|
|
139
|
+
const injected = []
|
|
140
|
+
for (const e of left) {
|
|
141
|
+
try { await injectResidual(e) } catch (error) { return { injected, left, error } } // 首错收口(前缀 = injected)
|
|
142
|
+
injected.push(e)
|
|
132
143
|
}
|
|
144
|
+
return { injected, left, error: null }
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
/** 等待下一次 settle(池 waiter——核内异步面 settle 尾部唤醒)/ 宿主唤醒(wake——用户输入)/
|
|
@@ -179,7 +191,9 @@ function waitForSettleOrWake(carrier, abortSignal, latch, { deadline = null, tim
|
|
|
179
191
|
* 注入由宿主 runTurn 首行完成——单注入点);
|
|
180
192
|
* 3. 池空(无 running / queued / 未注入)→ 自然退出;
|
|
181
193
|
* 4. 等下一 settle / 宿主唤醒(handle.wake)/ timer 到期(opt-in——兑现真 ⇒ timer 轮)。
|
|
182
|
-
* 每轮消化 / 用户回合后:hooks.reclaim(consumed)
|
|
194
|
+
* 每轮消化 / 用户回合后:hooks.reclaim(consumed) 逐条回收(不等池空)——恒达窗形(子代理面板批 ·
|
|
195
|
+
* 2026-10-04):正常 ∥ Abort-continue ∥ 异常抛三径同达,退出残差另经 `finishSuspension` 返回面补发
|
|
196
|
+
* (「消费 ⇒ 必发 done」不变量;发射不去重,归档/记录恰一次住幂等层)。
|
|
183
197
|
*/
|
|
184
198
|
export function startSuspension(ctx) {
|
|
185
199
|
const {
|
|
@@ -221,8 +235,10 @@ export function startSuspension(ctx) {
|
|
|
221
235
|
await runTurn(item)
|
|
222
236
|
} finally {
|
|
223
237
|
carrier._suspended = true
|
|
238
|
+
// 回收恒达窗(子代理面板批 · 2026-10-04 · §2.2):正常 ∥ 异常 ∥ 中止三径同达
|
|
239
|
+
// (「消费 ⇒ 必发 done」不变量)。
|
|
240
|
+
hooks.reclaim?.(afterRun())
|
|
224
241
|
}
|
|
225
|
-
hooks.reclaim?.(afterRun())
|
|
226
242
|
hooks.onCounts?.(backgroundCounts(carrier))
|
|
227
243
|
continue
|
|
228
244
|
}
|
|
@@ -242,9 +258,12 @@ export function startSuspension(ctx) {
|
|
|
242
258
|
continue
|
|
243
259
|
}
|
|
244
260
|
throw e
|
|
261
|
+
} finally {
|
|
262
|
+
// 回收恒达窗(子代理面板批 · 2026-10-04 · §2.2):正常 ∥ Abort-continue ∥ 异常抛三径同达;
|
|
263
|
+
// 中止径 catch 的 `onDigest("end")` 先行(continue 触 finally——既有语义序保持)。
|
|
264
|
+
hooks.reclaim?.(afterRun())
|
|
245
265
|
}
|
|
246
266
|
hooks.onDigest?.("end", backgroundCounts(carrier))
|
|
247
|
-
hooks.reclaim?.(afterRun())
|
|
248
267
|
hooks.onCounts?.(backgroundCounts(carrier))
|
|
249
268
|
continue
|
|
250
269
|
}
|
|
@@ -267,8 +286,10 @@ export function startSuspension(ctx) {
|
|
|
267
286
|
// 中止路径零边界、不补发轮后序钩子;池空 / pending 空自然退出——与消化支同判,§6.30.10)。
|
|
268
287
|
if (e?.name === "AbortError" && !abortSignal?.aborted) continue
|
|
269
288
|
throw e
|
|
289
|
+
} finally {
|
|
290
|
+
// 回收恒达窗(子代理面板批 · 2026-10-04 · §2.2):正常 ∥ Abort-continue ∥ 异常抛三径同达。
|
|
291
|
+
hooks.reclaim?.(afterRun())
|
|
270
292
|
}
|
|
271
|
-
hooks.reclaim?.(afterRun())
|
|
272
293
|
hooks.onCounts?.(backgroundCounts(carrier))
|
|
273
294
|
continue
|
|
274
295
|
}
|
|
@@ -282,8 +303,13 @@ export function startSuspension(ctx) {
|
|
|
282
303
|
// abort 以 AbortError 抛出、循环顶检查不会再执行——不合并会把清场误走 idle 注入)。
|
|
283
304
|
const abortedNow = aborted || Boolean(abortSignal?.aborted)
|
|
284
305
|
aborted = abortedNow
|
|
285
|
-
await finishSuspension(carrier, { aborted: abortedNow, injectResidual })
|
|
306
|
+
const flushed = await finishSuspension(carrier, { aborted: abortedNow, injectResidual })
|
|
307
|
+
// 残差离容补发(子代理面板批 · 2026-10-04 · §2.2——C2 零发射口):补发面 = 离容全量
|
|
308
|
+
// (抛错径同覆);与 freezeAll 双探针幂等(归档/记录恰一次住幂等层)。
|
|
309
|
+
hooks.reclaim?.(flushed.left)
|
|
286
310
|
hooks.freezeAll?.()
|
|
311
|
+
// 注入器首错重抛(fail-loud 保持——清场两探针已行毕)。
|
|
312
|
+
if (flushed.error != null) throw flushed.error
|
|
287
313
|
}
|
|
288
314
|
}
|
|
289
315
|
return { reason: aborted ? "aborted" : "idle", residualInput: pendingInput.splice(0) }
|
package/agent/turn-loop.mjs
CHANGED
|
@@ -57,6 +57,9 @@ export async function runTurnLoop(agent, {
|
|
|
57
57
|
agent._turnFilesMark = agent._touchedFiles.length
|
|
58
58
|
|
|
59
59
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
60
|
+
// #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环头检查点——中止恒以
|
|
61
|
+
// AbortError 收束、不开启新轮(环头注入族 / 压缩 / 模型调用零触)。
|
|
62
|
+
if (signal?.aborted) throw abortError(signal, "agent", "turn-head")
|
|
60
63
|
// 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):编号帧——
|
|
61
64
|
// `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
|
|
62
65
|
// (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
|
|
@@ -240,5 +243,8 @@ export async function runTurnLoop(agent, {
|
|
|
240
243
|
// #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
|
|
241
244
|
// 覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
|
|
242
245
|
if (maxTurns > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
|
|
246
|
+
// #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环尾检查点——中止恒以
|
|
247
|
+
// AbortError 收束(不落 ContinueError——用户 Stop 优先于继续提示;消费契约 = 外层按 reason 判定)。
|
|
248
|
+
if (signal?.aborted) throw abortError(signal, "agent", "turn-tail")
|
|
243
249
|
throw new ContinueError(maxTurns)
|
|
244
250
|
}
|
|
@@ -135,6 +135,11 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
|
|
|
135
135
|
// the same id (no slot residue — T14: the old token is then rejected at
|
|
136
136
|
// the gate); old slots die at TTL only, kept as the fail-safe fallback.
|
|
137
137
|
const prior = [...runs.values()].reverse().find((r) => r.reviewType === "design" && r.docSetKey === key)
|
|
138
|
+
// #863 实例回收(§6.10):同 docSetKey 保最新一条 closed(= prior——F2h 只取 newest,语义零变);
|
|
139
|
+
// 余者去重回收(实例随删释放——closed 零消费面)。
|
|
140
|
+
for (const r of [...runs.values()]) {
|
|
141
|
+
if (r !== prior && r.reviewType === "design" && !r.open && r.docSetKey === key) runs.delete(r.reviewId)
|
|
142
|
+
}
|
|
138
143
|
run = {
|
|
139
144
|
reviewId, reviewType, designId: prior?.designId ?? reviewId,
|
|
140
145
|
round: 0, priorOutput: null, stale: false, open: true, docSetKey: key,
|
|
@@ -144,6 +149,10 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
|
|
|
144
149
|
} else {
|
|
145
150
|
run = openCodeRun(agent)
|
|
146
151
|
if (!run) {
|
|
152
|
+
// #863 实例回收(§6.10):新 code 实例创建 ⇒ closed 实例清除(openCodeRun 仅取 open——零消费)。
|
|
153
|
+
for (const r of [...runs.values()]) {
|
|
154
|
+
if (r.reviewType === "code" && !r.open) runs.delete(r.reviewId)
|
|
155
|
+
}
|
|
147
156
|
const reviewId = randomUUID()
|
|
148
157
|
run = {
|
|
149
158
|
reviewId, reviewType, designId: null,
|
|
@@ -474,7 +483,8 @@ export function closeOpenCodeAdvisorRuns(agent) {
|
|
|
474
483
|
const runs = advisorRunsRead(agent)
|
|
475
484
|
if (runs instanceof Map) {
|
|
476
485
|
for (const r of runs.values()) {
|
|
477
|
-
|
|
486
|
+
// #863 关闭点轻量化:闭合同拍释放 priorOutput(closed 实例零消费面——续跑仅取 open)。
|
|
487
|
+
if (r.reviewType === "code" && r.open) { r.open = false; r.priorOutput = null; closed = true }
|
|
478
488
|
}
|
|
479
489
|
}
|
|
480
490
|
return closed
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* ADVISOR-GUARDS.md §7)。记账本体 / 陈旧判定 / 轮次 / token 落盘零改。
|
|
22
22
|
*/
|
|
23
23
|
import { persistEngTokens } from "../token-ttl.mjs"
|
|
24
|
-
import { settleDesignReview,
|
|
24
|
+
import { settleDesignReview, stripDesignTokenEcho, stripApprovedSuffix } from "./design-token.mjs"
|
|
25
25
|
import { looksLikeReviewOutput, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
|
|
26
26
|
import { isCodePath, loadProjectDeclaration } from "../conventions.mjs"
|
|
27
27
|
import { logEvent } from "../log.mjs"
|
|
@@ -176,9 +176,7 @@ export function settleAdvisorRun(agent, entry) {
|
|
|
176
176
|
run.approvedSuffix = null
|
|
177
177
|
persistFailed = true
|
|
178
178
|
logEvent("advisor:error", { id: `advisor#${entry.id}`, err: "engDesignTokens slot persist failed — settle failed (re-review)" })
|
|
179
|
-
const stripped = String(result)
|
|
180
|
-
.replace(makeDesignTokenRegex(entry.designToken, "g"), "")
|
|
181
|
-
.trim()
|
|
179
|
+
const stripped = stripDesignTokenEcho(String(result), entry.designToken).trim()
|
|
182
180
|
report = `${stripped}\n\nD1: the design review passed but the token could NOT be durably written to the session ledger (slot persist failed) — re-run advisor(type='design') to re-issue; no eng-coder spawn is authorized for this review (评审通过但 token 未能持久化——需重评).`.trim()
|
|
183
181
|
}
|
|
184
182
|
} else {
|
|
@@ -210,17 +208,17 @@ export function settleAdvisorRun(agent, entry) {
|
|
|
210
208
|
}
|
|
211
209
|
} else if (run.reviewType === "design" && entry.designToken && result != null) {
|
|
212
210
|
// fix B(stale 分支):陈旧评审不签发——digest 不得展示未注册 token——先剥
|
|
213
|
-
//
|
|
214
|
-
const stripped = String(result)
|
|
215
|
-
.replace(makeDesignTokenRegex(entry.designToken, "g"), "")
|
|
216
|
-
.trim()
|
|
211
|
+
// token 回显(单源三形)+ 前置 "评审目标已变更——token 未签发"(不变式——两分支都清洗)。
|
|
212
|
+
const stripped = stripDesignTokenEcho(String(result), entry.designToken).trim()
|
|
217
213
|
report = `评审目标已变更——token 未签发 (review target changed after launch — this review judged a stale state; no design token was issued — re-run the review on the current state)\n\n${stripped}`.trim()
|
|
218
214
|
}
|
|
219
215
|
// Prior of round 2+ = the last REVIEW-LOOKING output (mirror of run.mjs's guard).
|
|
220
216
|
// F2e: strip the engine-approved suffix FIRST — the prior must never
|
|
221
217
|
// carry the raw token / designId (exact truncation — zero collateral).
|
|
222
218
|
// 结论块(下方)刻意晚于 prior 归一:prior 只承载评审正文,不携带机制文案。
|
|
223
|
-
|
|
219
|
+
// #863 关闭点轻量化(父侧批·五裁定——§2.3 表外追加):closed 实例零消费面(续跑仅取 open)——
|
|
220
|
+
// 关闭后不再写回 priorOutput(否则 settleDesignReview 关闭点的释放被覆盖)。
|
|
221
|
+
if (run.open && report && looksLikeReviewOutput(report)) {
|
|
224
222
|
run.priorOutput = stripApprovedSuffix(report, run.approvedSuffix)
|
|
225
223
|
}
|
|
226
224
|
// F28/F29 失败结论(2026-09-18 顾问面治理批——**两轨共用**,ADVISOR-GUARDS.md §7 契约二):
|
|
@@ -58,8 +58,9 @@ function assertMainAgentOnly(ctx, action, review) {
|
|
|
58
58
|
|
|
59
59
|
/** 递归收集目录下全部 `*.md`(#369 · KD-4:显式嵌套路径为合法 create 面 ⇒ 顶层平扫会欠计
|
|
60
60
|
* ⇒ 复数情形漏判、默认定位可落非预期档)。只认真目录(`dirent.isDirectory()`——symlink 不入,
|
|
61
|
-
*
|
|
62
|
-
|
|
61
|
+
* 防环);单目录读错按无候选处理(尽力面,同旧平扫)。**转导出**(read-data-interface 批——
|
|
62
|
+
* `batch/list` 读面文件集同源复用;零行为变)。 */
|
|
63
|
+
export function collectMarkdownFiles(dir, out = []) {
|
|
63
64
|
let entries = []
|
|
64
65
|
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
|
|
65
66
|
for (const e of entries) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/batch-read.mjs — 批次档只读读面(read-data-interface 批 · 设计档
|
|
3
|
+
* `docs/cli/design/READ-DATA-INTERFACE.md` §2.5 / §3.3 · 台账 #887)。
|
|
4
|
+
*
|
|
5
|
+
* `listBatchRecords({ cwd, bases })` —— ACP `batch/list` 与变更观察器共用的**文件集 + 段状态单源**:
|
|
6
|
+
* 文件集 = 基底根(`batchDocBases`——声明面单源)下全部 `*.md`(递归;收集器 `collectMarkdownFiles`
|
|
7
|
+
* 转导出复用);路径升序稳定;单档读错跳过(尽力面)。各段状态 = `readSectionStatusWord`(段头 /
|
|
8
|
+
* 状态行正则 / 词表全消费 batch-skeleton 单源——与冻结门字面零漂移;`§4` / `§6` = 字面「无状态词」)。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs"
|
|
11
|
+
import { basename } from "node:path"
|
|
12
|
+
|
|
13
|
+
import { collectMarkdownFiles } from "./batch-lifecycle.mjs"
|
|
14
|
+
import { batchDocBases } from "./batch-paths.mjs"
|
|
15
|
+
import { readSectionStatusWord } from "./batch-skeleton.mjs"
|
|
16
|
+
|
|
17
|
+
/** 段号表(1–6——输出映射键序固定;`readSectionStatusWord` 消费数形段号)。 */
|
|
18
|
+
const SEGMENTS = [1, 2, 3, 4, 5, 6]
|
|
19
|
+
|
|
20
|
+
/** 批次档列举(只读):`{ batches: [{ file, path, sections }] }`——`path` = 绝对路径(`/` 归一)、
|
|
21
|
+
* `file` = basename、`batches` 按路径升序;单档读错跳过(尽力面)。`bases` 缺省 = `batchDocBases(cwd)`。 */
|
|
22
|
+
export function listBatchRecords({ cwd, bases } = {}) {
|
|
23
|
+
const base = cwd ?? process.cwd()
|
|
24
|
+
const roots = bases ?? batchDocBases(base)
|
|
25
|
+
const files = new Set()
|
|
26
|
+
for (const root of roots) for (const abs of collectMarkdownFiles(root)) files.add(abs.replace(/\\/g, "/"))
|
|
27
|
+
const batches = []
|
|
28
|
+
for (const p of [...files].sort()) {
|
|
29
|
+
let src
|
|
30
|
+
try { src = readFileSync(p, "utf8") } catch { continue } // 单档读错跳过(尽力面)
|
|
31
|
+
const sections = {}
|
|
32
|
+
for (const seg of SEGMENTS) sections[`§${seg}`] = readSectionStatusWord(src, seg)
|
|
33
|
+
batches.push({ file: basename(p), path: p, sections })
|
|
34
|
+
}
|
|
35
|
+
return { batches }
|
|
36
|
+
}
|
|
@@ -125,6 +125,34 @@ export function readBatchStatusLine(src) {
|
|
|
125
125
|
return "unknown"
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* §N 段状态词解析(read-data-interface 批——`batch/list` 读面单源,设计档 §2.5):段头
|
|
130
|
+
* (sectionHeaderRe)/ 状态行正则(STATUS_LINE_RE)/ 词表(STATUS_WORDS)全消费既有单源常量——
|
|
131
|
+
* 与冻结门字面零漂移。`§1`–`§3` / `§5`:段内首个状态行的值按**终态词优先**(closed / done 先于
|
|
132
|
+
* open——镜像冻结门取值序)命中已识别关键词 ⇒ 返回该关键词;段头缺失 / 无状态行 / 无命中 ⇒ null。
|
|
133
|
+
* `§4` / `§6` 无词条 ⇒ 字面 `无状态词`(不造词)。
|
|
134
|
+
* @param {string} src — 档全文
|
|
135
|
+
* @param {number|string} seg — 段号(`2` / `"2"`——同 sectionHeaderRe 消费形)
|
|
136
|
+
* @returns {string|null} 已识别关键词 / null / 字面「无状态词」(§4 / §6)
|
|
137
|
+
*/
|
|
138
|
+
export function readSectionStatusWord(src, seg) {
|
|
139
|
+
const words = STATUS_WORDS[seg]
|
|
140
|
+
if (!words) return "无状态词"
|
|
141
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
142
|
+
if (!hdr) return null
|
|
143
|
+
const nextRe = /^## §\d/gm
|
|
144
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
145
|
+
const next = nextRe.exec(src)
|
|
146
|
+
const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
|
|
147
|
+
const m = body.split(/\r?\n/).map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)
|
|
148
|
+
if (!m) return null
|
|
149
|
+
const value = m[1].trim()
|
|
150
|
+
for (const w of [words.closed, words.done, words.open]) {
|
|
151
|
+
if (w !== undefined && value.includes(w)) return w
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
/**
|
|
129
157
|
* §N 段内「`**状态行**:` 行存在性」判据(#422③——close 对无状态行档自建机读位的**采集面**:
|
|
130
158
|
* 与 readBatchStatusLine 的「值可解析」正交——无行 ⇒ 可自建;有行而值不可解析 ⇒ 仍 fail-closed)。
|
package/agent-tools/batch.mjs
CHANGED
|
@@ -40,9 +40,11 @@ export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
|
|
|
40
40
|
export const MAX_TEXT_CHARS = 20000
|
|
41
41
|
/** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
|
|
42
42
|
const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
/** 凭证形态(`BATCH-RECORD.md` §4.1——三形):`[DESIGN-TOKEN:…]` 闭括号形 ∥ `designId: …` 键形 ∥ 无标两段
|
|
44
|
+
* token 值形 `⟨uuid⟩:⟨epoch⟩`(与单源 `design-token.mjs` 判据对表);裸 uuid 非凭证不剥(KD-7)。两字面勿
|
|
45
|
+
* 合并:`CRED_RE` 走 `.replace()` 须 `/g` ∥ `CRED_TEST_RE` 逐行 `.test()` 须非 `/g`(lastIndex 有状态会漏剥)。 */
|
|
46
|
+
const CRED_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:\d{10,16}/g
|
|
47
|
+
const CRED_TEST_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:\d{10,16}/
|
|
46
48
|
|
|
47
49
|
// ─── #84 记账面注入缝(「记账面按端注入」,形态参 §2.13.5 注入缝)───────────────
|
|
48
50
|
/**
|