thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,100 @@
|
|
|
1
|
+
## [0.12.59] — 2026-09-05
|
|
2
|
+
|
|
3
|
+
> 0.12.58 → 0.12.59(§1.5 连续号——发布时定号)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **settings 工具(SETTINGS-TOOL.md——2026-09-05 用户三项裁定)**:agent 配置调整通道——list/get/set(全量 config.json 任意键点分路径);set = 写盘 + 热应用(运行中即生效);敏感键(apiKey/token/secret/password 段)回显永遮罩;已知键类型校验(DEFAULTS 自动派生);list/get 只读动作(planMode 放行);set 审批门。测试 T-S1.1-11(CLI 11 + VS Code 6)。
|
|
8
|
+
- **subagent-async 模块拆分(§20.9 Module Split Policy——2026-09-05 F-N1.5 两段式首批)**:CLI subagent-async.mjs 1020 → 405 行(subagent-scheduler.mjs 231——§20 调度组 + 文件域组 / subagent-actions.mjs 416——status/panel/escalate 动作执行器组)——纯迁移零行为变化(测试零改动 + 断言计数前后对拍一致)。
|
|
9
|
+
- **P-SL2 停滞机械检测(AGENT-LOOP §21.1 扩展注 P-SL2——2026-09-05)**:混合边环形等待停滞 → check/status 守卫明确报错列阻塞链(cancel 破环引导)——防御性(自然流不可达——人工注入可构造)。测试 T-SL2 ①-⑤(scheduler 22 → 27 用例)。
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **轨迹存档隐私默认翻转 + 启动清理(AGENT-LOOP.md §18.6 D-TR6/D-TR10——2026-09-05 用户"不希望用户那边也采集")**:`traces.enabled` 默认 **on → off**(发布后新用户零采集——本地分析可显式开);新增 `traces.retentionHours`(默认 24h——保留期可配置);CLI 启动时删除超过保留期的轨迹文件(`cleanupTraces`——D-TR10——fire-and-forget 不阻塞启动——空日期目录一并清除);`/config` 菜单新增 traces.enabled(开关)与 traces.retentionHours(保留期)两项。测试 T-TR15。
|
|
14
|
+
- **edit 空白差异自动落点(TOOLS.md §14.2——P15.11——2026-09-05 用户裁定)**:edit not-found 且文件存在**唯一内容相同仅空白不同**窗口 → 自动落点应用 + 结果附 note(`whitespace-only match`——双端同句);歧义(多窗口)/实质差异仍 not-found(不猜)。测试 P15.11a-d 双端。
|
|
15
|
+
- **编辑纪律三条固化进 discipline.md(AGENT-LOOP.md §21 扩展注 2——2026-09-05 记忆清空实验)**:① 新鲜读来源(never reconstruct from memory);② hash 来源(never invent one);③ 重试上限(never retry the identical input a third time)。测试 T-N1.8 双端。
|
|
16
|
+
- **普通模式两段式(AGENT-LOOP.md §21 F-N1.5——2026-09-05 用户"把 coder 用起来……解决自查问题")**:规模实现批次默认委托 coder 子代理(async——设计书为 task book)——执行/检查心智分离(隔离上下文破自查盲区);小改动/探索留内联;复核走 F-N1.4(偏差退回 coder ≤2 轮)。测试 T-N1.9 双端。
|
|
17
|
+
- **委托操作标准(AGENT-LOOP.md §21 扩展注 4——F-N1.6——2026-09-05 用户"spawn coder 干活现在并没有明确的标准是吗")**:规模判据可操作化(≥2 文件/单文件 >30 行逻辑/模块边界/双端镜像 → 委托;≤30 行/文档同步/探索 → 内联);任务书七字段标准(目标/已知事实/设计+禁止/约束/硬验收/报告格式/调度元数据——缺字段=委托缺陷);委托模式判据(async 默认/sync 仅依赖链/并行仅文件域互斥);通用验收基线。测试 T-N1.10 双端。
|
|
18
|
+
- **/config 交互修复(2026-09-05)**:改配置项保存后回主菜单(不再退到输入框)——每轮刷新 ac/tc 配置引用(reloadConfig 换对象后显示新值);view 浏览态同回菜单。
|
|
19
|
+
- **版本号连续性规则(RELEASE.md §1.5——2026-09-05 用户裁定"不要跳号")**:号在发布时定、开发期不预占(CHANGELOG 挂 [Unreleased]);待发号 = registry 最高 + 1;缺口不补。
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **§18.14 域拆分 import 残留两处(2026-09-05 发版 test:full 门禁抓出——slow 门控测试快层永不执行致漏网)**:session.test.mjs 缺 existsSync + advisor-review.test.mjs 缺 prepareAdvisorMessages。
|
|
24
|
+
- **压缩 fixture 阈值重校准 14000 → 15500(T3b——2026-09-05)**:提示词批(F-N1.4/1.5/1.6 main.md 委托句 + discipline 三条纪律 ≈550 token)抬升 systemPrompt 估算越刀锋——沿革惯例 +1500 档。
|
|
25
|
+
|
|
26
|
+
### Changed (追加——2026-09-04 后半批)
|
|
27
|
+
|
|
28
|
+
- **advisor 角色定位锚(AGENT-LOOP.md §12.1)**:四模板(advisor-design/round1/round2/round3)开头插入"Your role"段(独立评审者/证据纪律/边界/中立——Known behavior 记忆断言禁止)——修复"评审对象模糊/定级拉锯/行为验证捷径/出界犹豫"人格缺口。
|
|
29
|
+
- **子代理人格锚(AGENT-LOOP.md §7.3)**:coder.md(IMPLEMENTER——证据纪律/设计冲突停报/边界)+ consult-base.md(证据纪律/I-don't-know 合法/无权威)——审计发现"职责越重人格越薄"倒挂。
|
|
30
|
+
- **审计范围引导(AGENT-LOOP.md §18.13)**:eng-coder 内部审计 explore 改 quick 档 + 任务书加机械预算句(只读 touched 文件+点名节——10 轮上限——超时报 PROBLEM)——治理"审计 explore 跑非常久"。
|
|
31
|
+
- **工具输出预览保头保尾(TOOL-OUTPUT-LIMITS-TUNING.md §5)**:超 64K 落盘预览 = 头 16K + 中间省略注 + 尾(tail 优先——测试统计/错误在尾不再被截)——与压缩/蒸馏同口径。
|
|
32
|
+
- **bash 重定向护栏删除(TOOLS.md §13)**:`hasFileRedirection` 整条删除(用户裁定"拦截只误伤正常操作"——零文本拦截与 §5 安全剧场决策彻底对齐)——`2>&1` 误报、测试收窄被拦整链消除。
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **完整轨迹存档(AGENT-LOOP.md §18.6)**:`chat()` 出口统一收集——每次模型调用落 JSONL 到 `~/.thincoder/traces/YYYY-MM-DD/`(本地时区分日/seq=当日 max+1/脱敏复用 log.mjs 黑名单+SECRET_FORM/错误路径也落盘/`traces.enabled` 开关默认 on)——覆盖主/子代理/advisor/compress/distill/consult/auto-think 全部调用;续写标记 `isContinuation`。
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **子代理零 git(§18.5)**:explore/plan spawn 不再注入 `<untrusted_git_context>`(全角色零 git——审计证据=设计文档+磁盘状态+_touchedFiles 并集;审计任务书附零 git 范围权威声明;explore.md 删除 git 注入声明/命令承诺,描述改为“No git context injected”);顶层主 agent git 上下文不变(§3)。
|
|
41
|
+
- **审计任务书零 git 声明**(D-AG3):`_touchedFiles` 为审计范围——工作区未列改动不作超清单依据。
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **开发体验三项(用户需求批 2026-09-02,两端)**:
|
|
47
|
+
① **lint 零依赖化**(TOOLS.md §10.2):eslint 全套删除(devDependencies + eslint.config.mjs + 级联分支),lint = `node scripts/check-syntax.mjs`(node --check 遍历 src/test/bin/scripts 含自检,零依赖);CLI-LINT-REQUIREMENTS/TUNING.md 标记被取代;package-lock 再生(eslint 树移除)
|
|
48
|
+
② **工具作用域限制全部移除**(§10.1):resolveInCwd 去边界断言(信任模型 + 权限门禁为唯一防线,与 bash 一致);git workdir / execute scriptFile / file_ops 目录限制一并移除;工具描述与提示词 "confined to workspace" 措辞清理(两端 byte-identical)
|
|
49
|
+
③ **模型上下文长度可配置**(PROVIDER.md §15):`providers[].context`(K 单位,如 128 = 128K)覆盖 MODEL_SPECS——providerSpec 拷贝覆盖不污染共享 spec;压缩阈值(auto ×0.6)/ TOKEN 窗口 / 状态栏显示 / advisor 预算全链路跟随;数字字符串("128")两端统一归一;非法值忽略 + 警告一次;CLI /model 管理流 + VS Code settings.json 配置界面
|
|
50
|
+
|
|
51
|
+
## [0.12.57] — 2026-09-02
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **subagent 异步化:真后台并行**(AGENT-LOOP.md §15,两端):subagent 工具加 `async: true`——spawn 立即返回 `{id, status:"running"}`,主会话可继续自己的回合;新增 `subagent_check` 工具(arrival order 先完成先取 / 带 id 等待 / n 递增校验防循环 / readonly);**槽位队列**:并发上限 4,超限入队(position 可见),running 完成即腾槽补位(不拒绝、不分批);回合收尾自动等待全部完成并注入报告(XML 转义 + 超长预览落盘);Ctrl+C 清空不注入、ContinueError 状态保留;async 仅 depth-0、后台撞 turn-cap 自动拒绝继续
|
|
56
|
+
- **approval 批确认**(AGENT-LOOP.md §16.1,两端):同批多个非只读工具一次合并询问(approve all / one by one / deny;deny 全批拒绝无二次询问;无 handler 回退逐项;onPermissionRequest 签名不变)
|
|
57
|
+
- **批量形态引导**(§16.2,数据驱动——真实使用 94.6% 单条 edit / apply_patch 0 次 / 35 例手工批量):edit 描述强化 edits 数组原子批量、apply_patch 补多文件新建场景、system.md 并行条款扩展批量句(两端 byte-identical)
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
|
|
61
|
+
- 工程模式并发纪律上限 3 → 4(engineering.md + ENGINEERING-MODE.md FR8/决策③ 三处同步)
|
|
62
|
+
- VS Code 端同批对齐:escape v5 / UTF-16 安全截断 / 续写构造 / 压缩可见性(见 thincoder-vscode CHANGELOG 0.12.57)
|
|
63
|
+
|
|
64
|
+
## [0.12.56] — 2026-09-02
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- **上下文压缩面板 + 压缩失败可见性**(CONTEXT-COMPACTION.md §7):压缩开始即弹"Compressing…"面板区块(复用子 agent 面板机制:耗时 ticker + summarizing N messages)→ 完成态 `Compressed: N tokens freed → summary (Xs)` 可折叠冻结;失败态显示错误文本,连续 3 次失败后 compressFallback 截断兜底并显示降级说明;摘要正文永不进面板/会话流;headless 回调缺省 no-op
|
|
69
|
+
- **DeepSeek prefix 续写 400 止损**(PROVIDER.md §14):续写请求精简历史(过滤 tool/assistant(tool_calls) 消息,保留 system + 最近 ≤8 条文本)——真机矩阵实证:thinking 模式 prefix 续写 + 工具链消息必 400(补不补 reasoning_content 分别报 Function call / reasoning_content 错误),纯文本历史 200;续写失败注入 `_warnings` 不再静默飞出;partial 模式不受影响
|
|
70
|
+
- **会话恢复 provider/model 缺失 → 模型重选**(SESSION.md §8):CLI 启动校验 provider/model/baseURL 缺失 → 不再崩溃退出;TUI 首帧弹模型选择(复用 picker),Esc 仍进 TUI + 提示行;headless 可读错误 + 退出码 1;判据仅空缺失(MODEL_SPECS 未知不判无效——自定义模型保护)
|
|
71
|
+
- **MCP save&test 确认问句废除**(MCP.md §5 变更段):探活成功直接保存(删 `Save? (Y/n)`);探活失败报错回表单且无任何保存通道(save-anyway 整个废除);取消仅剩表单 Esc
|
|
72
|
+
- **搜索工具优先级条款**(PROMPT-DECOUPLING.md):discipline.md + engineering.md 行为规则——有 MCP 搜索工具优先用 MCP、websearch 仅备用;websearch 连续 2 次垃圾即切;被墙站点走镜像路径;动手抓页面前先扫工具表(两端 prompts byte-identical)
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **hex-escape 400 真凶根治(escape.mjs v5)**:2026-09-02 实锤——`unexpected end of hex escape` 400 的毒源**不是字面 hex 转义序列**,而是 **doc_search 预览 slice 按 UTF-16 码元截断切断了 emoji 代理对**(🔴 → 孤立高代理 D83D)→ deepseek 严格 UTF-16 解码 400。两层修复:① 发送前净化(sanitizeLoneSurrogates:孤立代理 → U+FFFD,全字段)+ hex 转义 odd-run 修复(v1-v4 的 double/替换方向全错,本版为对象层面正解);② 源头 UTF-16 安全截断(setup.mjs doc_search 预览 + helpers.mjs offloadToolResult 截断点落高代理时向前收一个码元)。验证:真实会话 953 条(含孤立代理)重放 400→200,带 thinking:enabled 6/6 全 200
|
|
77
|
+
- **MCP 磁盘无 mcp 段时 remove/edit 崩溃**(code review #1/#2):persistRaw 建段守卫(`raw.mcp ??=`)——磁盘 mcp 段被整体删除而连接保留(T23 场景)时,remove 不再 TypeError、edit 不再静默丢("updated" 提示与落盘一致)
|
|
78
|
+
|
|
79
|
+
## [0.12.55] — 2026-09-01
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **`/mcp` edit/add 统一字段 picker 表单(v2)+ agent 代配闭环**(docs/design/MCP.md §5,CLI-only):① **edit = 字段选择表单**——picker 列可编辑字段行(`HTTP URL https://…` / `Token d90c26bb…` 打码 / `Headers 2 items`;stdio `Command/Args/Env`;name 不可改无行)+ 末行 `✓ Save & test`;选中字段只输入该字段新值(提示 `(current: …)`——空=不变、`-`=删可选字段、`k=`=删 header/env 项、required 字段拒绝 `-`)→ 回 picker 循环连改多字段(中间 Esc 回 picker 不丢已改值);**废除逐字段预填重问**(改 token 不再被迫路过 URL/headers);② **add 复用同一表单**——空 entry 起、必填字段 `(required)` 标注、Save 校验必填非空(未满足提示并停留表单不落盘)、headers/env 不选即跳过、add 的 name 可编辑(重复名检查);③ **保存前预览 + 探活 + 字段级重试合一**——`showPreview`(token 遮蔽)→ `probeMcpServer` 零副作用探活(`✓ N tools, Xms` / `✗ 错误`)→ `Save? (Y/n)`;探活失败 → `Save anyway? (y/N)` 显式 y,否则**回同一字段表单**只重输失败字段复 probe(**独立 retry 路径废除**,不重启流程;save-anyway 显式 y 保留);④ **表单文件拆分**——新增 `src/tui/cmd-mcp-form.mjs`(fieldPicker 机制独立文件;`cmd-mcp.mjs` 499→382 行,脱离 500 硬限压线);⑤ **列表即菜单**——主菜单 = server 行(●/○ 连接态 + tool 数)+ `+ Add server` + `↻ Refresh` + 顶部 agent 代配提示行;选中行 → per-server 子菜单(Edit/Test/Reconnect/Remove),"先选操作再选 server"双弹层与 View list 废除;⑥ **磁盘重读**——`config.mjs` 新增 `reloadMcpFromDisk()`(菜单打开边界 + Refresh):磁盘→内存仅替换 mcp 段;畸形 config.json 回退内存态 + `⚠ disk config unreadable` 提示行;disk 删除/变更的已连接 server 连接不断 + 行尾 `⚠ disk changed` 对账标记(persistRaw 落盘后重读幂等防环);⑦ AI 生成降 transport picker 末位(生成的 entry 同走预览+探活确认环,失败回表单补齐/修正);`/mcp edit|test|remove|connect <name>` 直达参数路径零改动
|
|
84
|
+
- **MCP Streamable POST 误判修复 + `/mcp edit`/`/mcp test` + token 一等字段**(docs/design/MCP.md §4,两端落地):① `httpTransport` 增 postOnly 标记——GET SSE 405 降级后的纯 POST 模式 `isAlive()` 不再因 `eventSource == null` 误判死(glm-websearch "reconnect failed after 4 attempts" 根因:降级后 isAlive 恒 false → ensureAlive 触发无意义重连循环;legacy SSE 流断仍正常 fireDead 重连不回归);② `/mcp edit [name]`:逐字段预填重问(空输入保留 / `-` 删除可选字段 / `k=` 删除单个 header 项),persistRaw 原位替换保数组序,保存后自动重连(config 指纹含 token,变更自动关旧连接);③ `/mcp test [name]`:probeMcpServer 一次性探活(initialize + tools/list 计时 → `OK — N tools, Xms` / 错误透传),零副作用(不进 session 表、不动 agent.tools、探完即关);④ token 一等字段:config 增 `token` 字段,connect 链自动合成 `Authorization: Bearer <token>`(显式 headers 优先,不写回 config);⑤ parseHeaders 改逗号分隔(`Authorization=Bearer abc, X-Foo=bar`——修复空格截断把 Bearer token 截成 "Bearer" 的缺陷);⑥ VS Code 同构:http transport 同款修复 + probeMcpServer 镜像 + 面板 [Edit]/[Test] 按钮(同一表单编辑预填,token 字段 + 逗号分隔 headers 提示)+ **面板 [Reconnect] 死按钮修复**(webview 发的 `reconnectMcp` 消息在路由拆分时丢失 case,按钮此前无效)
|
|
85
|
+
- **memory_delete 工具**(跨端):三层记忆条目删除——personal 行级删(embedding/FTS 随行)、project/team 文件级删;scope 与 id 前缀匹配校验,非法 scope 明确报错
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- **multi-design 并行令牌(designId slots)**:eng-coder 子 agent 支持 `{designId, token}` 多槽并行 spawn——各设计独立令牌互不覆盖,复审不通过的设计不挤占既有槽(CLI 与 VS Code 镜像)
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- **edit 数组形态同文件串行**:同一文件多条 edit 的 raw 域快照随条目推进,第二条不再漂移(编辑器 CRLF 路径 + 磁盘路径双修复)
|
|
94
|
+
- **MCP tools/list 分页超时约束**(MCP.md §4 评审 #8):每页同受 INIT_TIMEOUT_MS 约束——probe 延迟统计有界
|
|
95
|
+
- **MCP 握手失败 transport 泄漏**(评审 #7):GET SSE 降级成功但 POST initialize 失败时关闭 transport,不留悬挂流
|
|
96
|
+
- **hex-escape 毒载荷 400 根治(escape.mjs v3)**:v2 的 lookbehind 单字符判定与 hex 窗口越界缺陷在长会话(讨论转义主题)下漏中和 → deepseek 等网关二次解析报 "unexpected end of hex escape";v3 数反斜杠 run 奇偶 + 窗口越界修复(`\\x/\\u` 相邻双写)+ 孤立代理对(`\\uD83D` 无配对 strict JSON 解析拒绝)预 double;真实会话 74 处毒点全量中和为 0,11 个 case 锁定(含 v1 行为兼容回归)
|
|
97
|
+
|
|
1
98
|
## [0.12.54] — 2026-09-01
|
|
2
99
|
|
|
3
100
|
### Added
|
|
@@ -10,6 +107,7 @@
|
|
|
10
107
|
|
|
11
108
|
- **跨端会话共享一致性(会诊 4 模型收敛)**:sessionStart 打点(跨端同槽不再 F2 互轮转);F2 写前磁盘校验(同会话并发追加 → 轮转 .bak 保留);legacy transient 双端过滤;contextHistory 机读线判定(length>0);activeModel 双向;cwd 先行校验;newSession 死主清理落盘(deletions)
|
|
12
109
|
- **checkpoint cwdHash 归一化**:`sha1(normalizeCwd(cwd)).slice(0,12)`——CLI/VS Code 快照跨端互通(存量旧路径孤儿化不迁移)
|
|
110
|
+
- **操作并行化纪律提示词条款**(2026-09-01 用户需求):system.md "How you work — while coding" 段在既有并行条款后追加 "Parallelize aggressively"——独立只读调用一次发起多个(执行器批并行)、多文件编辑用 `edits` 数组(原子一次往返)、独立子代理/独立子项目一次 spawn 多个(F7 触发条件:不共享待改文件 + 无交叉依赖 + 各自有独立测试);明确不并行边界(同一文件写入/依赖链/bash 审批命令 = 审批风暴/同仓库并发 git/有状态操作)与收益判断(大操作并行、<1s 微操作不并行)。两端 system.md byte-identical,测试同步断言
|
|
13
111
|
|
|
14
112
|
### Fixed
|
|
15
113
|
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Three layers, all "query if present, skip if absent", unified hybrid retrieval:
|
|
|
41
41
|
- **Hybrid retrieval**: FTS5 (BM25, per-character CJK indexing, bigrams matchable) + embedding vectors (brute-force cosine) + RRF(k=60) fusion ranking
|
|
42
42
|
- **Embeddings**: OpenAI-compatible `/v1/embeddings`, defaults to SiliconFlow `BAAI/bge-m3` (free tier, good CJK support); Ollama works as an offline option. Vectors generated lazily — not computed on write, backfilled and persisted on first search
|
|
43
43
|
- **Entry format**: Markdown + frontmatter (type/title/tags/author/created), readable and reviewable directly on GitHub; one file per entry, naturally avoiding merge conflicts; real conflicts produce honest errors, never auto-merged
|
|
44
|
-
- **Dual-track accumulation**: conventions written manually (`
|
|
44
|
+
- **Dual-track accumulation**: conventions written manually (the `memory` tool, action put), experience extracted from sessions via `/extract` — **the LLM proposes candidates, a human confirms each y/n** before anything is stored; never fully automatic
|
|
45
45
|
- **Retrieval isolation**: the Project layer is isolated by project path — project A's memories never leak into project B
|
|
46
46
|
|
|
47
47
|
- **Agent Client Protocol** ⭐: `thincoder acp` exposes the agent over [ACP](https://agentclientprotocol.com/) v1 on stdio — one terminal login drives sessions from **Zed**, **JetBrains** AI chat, or **Paseo**:
|
package/bin/thincoder.mjs
CHANGED
|
@@ -16,8 +16,9 @@ import { readFileSync } from "node:fs"
|
|
|
16
16
|
import { join } from "node:path"
|
|
17
17
|
import { runAgent } from "../src/agent.mjs"
|
|
18
18
|
import { loadConfig, configPath } from "../src/config.mjs"
|
|
19
|
+
import { cleanupTraces } from "../src/traces/trace-store.mjs"
|
|
19
20
|
import { createMemory, syncDir } from "../src/memory.mjs"
|
|
20
|
-
import { assembleAgent, teamConfig, gitAuthor } from "../src/cli/make-agent.mjs"
|
|
21
|
+
import { assembleAgent, teamConfig, gitAuthor, validateProvider } from "../src/cli/make-agent.mjs"
|
|
21
22
|
import { memoryCommand } from "../src/cli/memory-command.mjs"
|
|
22
23
|
import { setupWizard } from "../src/cli/setup-wizard.mjs"
|
|
23
24
|
import { summarize, askPermission } from "../src/cli/permission.mjs"
|
|
@@ -68,6 +69,13 @@ function exitSoon(code) {
|
|
|
68
69
|
setTimeout(() => process.exit(code), 100)
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// D-TR9(2026-09-05):启动轨迹清理——删除超过 traces.retentionHours(默认 24h)的
|
|
73
|
+
// 轨迹文件(fire-and-forget——不阻塞启动——失败静默——与轨迹写盘同纪律)。
|
|
74
|
+
try {
|
|
75
|
+
const startupCfg = loadConfig()
|
|
76
|
+
cleanupTraces({ retentionHours: startupCfg.traces?.retentionHours ?? 24 }).catch(() => {})
|
|
77
|
+
} catch { /* 配置缺失/损坏 → 跳过清理(零风险) */ }
|
|
78
|
+
|
|
71
79
|
switch (command) {
|
|
72
80
|
case "chat": {
|
|
73
81
|
const auto = args.includes("--auto")
|
|
@@ -79,6 +87,13 @@ switch (command) {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
const agent = await assembleAgent()
|
|
90
|
+
// SESSION.md §8 D-S4(F4):headless 无 TUI —— 可读错误 + 退出码 1,不弹 UI、不崩溃
|
|
91
|
+
if (agent._providerInvalid) {
|
|
92
|
+
const prov = agent.activeProvider || "(未设置)"
|
|
93
|
+
console.error(`[error] 未配置有效 provider(activeProvider "${prov}":${agent._providerInvalidReason})。请运行 thincoder 进入 TUI 重新选择,或编辑 ${configPath}`)
|
|
94
|
+
exitSoon(1)
|
|
95
|
+
break
|
|
96
|
+
}
|
|
82
97
|
if (!agent.provider.apiKey) {
|
|
83
98
|
if (!process.stdin.isTTY) {
|
|
84
99
|
console.error(noKeyMessage())
|
|
@@ -219,6 +234,9 @@ switch (command) {
|
|
|
219
234
|
case "tui":
|
|
220
235
|
case undefined: {
|
|
221
236
|
const agent = await assembleAgent()
|
|
237
|
+
// SESSION.md §8 D-S1:TUI 路径在 startTUI 前清空无效 provider——空 provider 不流入 runAgent
|
|
238
|
+
// (崩溃源:chat() 缺 model → 网关 400 或 fetch("undefined/...") TypeError)
|
|
239
|
+
if (agent._providerInvalid) agent.provider = null
|
|
222
240
|
const config = loadConfig()
|
|
223
241
|
// 恢复上次的会话(同一项目目录);provider 按保存的名字切回(用户上次可能换过模型)
|
|
224
242
|
const { loadSession, applySession } = await import("../src/session.mjs")
|
|
@@ -226,11 +244,15 @@ switch (command) {
|
|
|
226
244
|
if (restored) {
|
|
227
245
|
const switched = applySession(agent, restored)
|
|
228
246
|
if (switched && agent.config?.agent?.compactThresholdAuto) {
|
|
229
|
-
// 压缩阈值跟模型走(与 TUI 切换 provider
|
|
247
|
+
// 压缩阈值跟模型走(与 TUI 切换 provider 时的处理一致);传 provider 对象——
|
|
248
|
+
// providers[].context 覆盖生效(PROVIDER.md §15 T-C2)
|
|
230
249
|
const { resolveCompactThreshold } = await import("../src/config.mjs")
|
|
231
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider
|
|
250
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
232
251
|
}
|
|
233
252
|
}
|
|
253
|
+
// D-S3 优先级补全:applySession 可能已用会话中的有效 provider 修复(config 无效 + 会话有效)——
|
|
254
|
+
// 修复后复验清除标记,仅当两者都无效才弹重选(validateProvider 幂等)
|
|
255
|
+
if (agent._providerInvalid) validateProvider(agent)
|
|
234
256
|
// MCP 连接失败在 TUI alt-buffer 下 stderr 不可见,注入为下一条 user 消息后的提醒
|
|
235
257
|
if (agent._mcpWarnings?.length) {
|
|
236
258
|
agent._pendingReminders = agent._pendingReminders ?? []
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.59",
|
|
4
4
|
"description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -33,13 +33,9 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "node --test \"test/*.test.mjs\"",
|
|
35
35
|
"prepublishOnly": "npm run lint && node --test \"test/*.mjs\"",
|
|
36
|
-
"lint": "
|
|
36
|
+
"lint": "node scripts/check-syntax.mjs",
|
|
37
37
|
"test:full": "node test/run-full.mjs"
|
|
38
38
|
},
|
|
39
39
|
"author": "liwei <liwei@51marine.com> (上海新舶)",
|
|
40
|
-
"license": "MIT"
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"eslint": "^9.0.0",
|
|
43
|
-
"@eslint/js": "^9.0.0"
|
|
44
|
-
}
|
|
40
|
+
"license": "MIT"
|
|
45
41
|
}
|
package/src/acp/bridge.mjs
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* End-of-turn is NOT a notification: `session/prompt` resolves with
|
|
20
20
|
* `{ stopReason: "end_turn" }` (kimi session.ts parity).
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { detectDanger, normalizeEOL, joinWithEol } from "../tools/shared.mjs"
|
|
23
|
+
import { computeEditEntry, validateEditEntry, assertEditArgsExclusive } from "../tools/edit-diff.mjs"
|
|
24
24
|
|
|
25
25
|
/** ACP ToolKind inference (schema v1 enum) — best-effort, clients render by kind. */
|
|
26
26
|
function inferToolKind(name) {
|
|
@@ -56,25 +56,128 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
56
56
|
const update = (sessionUpdate, extra = {}) =>
|
|
57
57
|
notify("session/update", { sessionId, update: { sessionUpdate, ...extra } })
|
|
58
58
|
let toolSeq = 0
|
|
59
|
-
|
|
59
|
+
// D15.8(TOOLS.md §15.1):tool id FIFO 队列——并行同名工具按 call 序配对(dispatch B1
|
|
60
|
+
// 已测:并行结果回调顺序 = call 顺序——T-TS8/T-TS9)。取代旧 Map 按名覆盖(后写覆盖先写
|
|
61
|
+
// → tool_call_update 与 tool_call id 错配)。条目 { name, id, toolId }——toolId = 模型级
|
|
62
|
+
// toolCall.id(dispatch 在 onToolCall/onToolResult 均传第 3 参——同一 item 恒相同)。
|
|
63
|
+
// 拒绝/中断路径:dispatch 在 onToolCall 之前拒绝(被拒工具从未入队——无孤儿可滞);
|
|
64
|
+
// 中断/异常路径(onToolResult 永不回调——dispatch.mjs catch 分支——T-F5 契约)留下的
|
|
65
|
+
// 孤儿靠 onToolCall 的「同名同 toolId 先弹出」隔离(见 onToolCall)——模型级 id 跨轮
|
|
66
|
+
// 可重复(sse.mjs 每轮从 call_0 重置)——弹出保证精确配对恒命中最新条目。
|
|
67
|
+
const toolQueue = [] // FIFO of pending { name, id, toolId }
|
|
60
68
|
|
|
61
69
|
const toolCallId = () => `t${++toolSeq}`
|
|
70
|
+
/** D15.8:peek 同名最早项 id——权限面板展示用——不消费(result 仍要与自己的条目配对)。 */
|
|
71
|
+
const peekToolId = (name) => {
|
|
72
|
+
for (const e of toolQueue) if (e.name === name) return e.id
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
/** D15.8:消费——①模型级 toolId 精确配对(中断孤儿隔离)②无 id/未命中 → 名称 FIFO 回退
|
|
76
|
+
* (B1 保序)③均未命中 → null(调用方回退新 id——防御)。 */
|
|
77
|
+
const takeToolId = (name, toolId) => {
|
|
78
|
+
if (toolId != null) {
|
|
79
|
+
const i = toolQueue.findIndex((e) => e.name === name && e.toolId === toolId)
|
|
80
|
+
if (i >= 0) return toolQueue.splice(i, 1)[0].id
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < toolQueue.length; i++) {
|
|
83
|
+
if (toolQueue[i].name === name) return toolQueue.splice(i, 1)[0].id
|
|
84
|
+
}
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
const contentBlock = (text) => ({ type: "content", content: { type: "text", text } })
|
|
63
89
|
const pathOf = (args) => {
|
|
64
90
|
const p = args?.path ?? args?.filePath
|
|
65
91
|
return typeof p === "string" && p ? p : null
|
|
66
92
|
}
|
|
67
93
|
|
|
94
|
+
// §15.1(TOOLS.md)D15.7 委派:edit 判定/应用单一权威 = 本地 computeEditEntry
|
|
95
|
+
// (edit-diff.mjs——校验→判定序→应用:行级 LCS、零重叠→插入、replace_all 字面替换全部)。
|
|
96
|
+
// 桥只留「读 IDE 缓冲 → computeEditEntry → 写回 IDE 缓冲」——错误文本经抛错原样透传
|
|
97
|
+
// ——与本地通道逐字一致(NF15.6b / AC15.10:not found / occurrences / 空 old / 空 new)。
|
|
98
|
+
const EDIT_ABORT_PREFIX = "edit aborted (atomic — no files written): "
|
|
99
|
+
|
|
100
|
+
const readBuffer = async (p) => {
|
|
101
|
+
try {
|
|
102
|
+
const read = await request("fs/read_text_file", { sessionId, path: p }, { timeoutMs: 30000 })
|
|
103
|
+
return read?.text ?? read?.content ?? ""
|
|
104
|
+
} catch (e) {
|
|
105
|
+
throw new Error(`fs/read_text_file failed: ${e.message}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const writeBuffer = async (p, content) => {
|
|
109
|
+
try {
|
|
110
|
+
await request("fs/write_text_file", { sessionId, path: p, content }, { timeoutMs: 30000 })
|
|
111
|
+
} catch (e) {
|
|
112
|
+
throw new Error(`fs/write_text_file failed: ${e.message}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** 单形态:读 IDE 缓冲 → computeEditEntry(rich——无 abortPrefix——同本地 runSingleEdit)
|
|
116
|
+
* → 写回。EOL 权威(F1):判定/应用在 normalizeEOL 后的 LF 域;写回 joinWithEol 按原文
|
|
117
|
+
* 首换行恢复(LF 域判定——CRLF 域写回——与本地 edit 工具同判同恢复)。 */
|
|
118
|
+
const editSingle = async (p, args) => {
|
|
119
|
+
const raw = await readBuffer(p)
|
|
120
|
+
const content = normalizeEOL(raw)
|
|
121
|
+
const out = computeEditEntry(content, args, { path: p })
|
|
122
|
+
await writeBuffer(p, joinWithEol(normalizeEOL(out.updated).split("\n"), raw))
|
|
123
|
+
return `OK: edited ${p} via IDE (${out.occurrences} occurrence(s))${out.note ? ` — ${out.note}` : ""}`
|
|
124
|
+
}
|
|
125
|
+
/** 数组形态(D15.7):条目校验(path——顶层默认自 args.path ?? args.filePath(pathOf)——
|
|
126
|
+
* 2026-09-05 用户裁定 CLI parity——/validateEditEntry/互斥(只对顶层 old/new)——同本地 edit-batch 措辞)→
|
|
127
|
+
* 读全部涉及文件缓冲(同文件去重——一次读)→ 逐条 computeEditEntry(abortPrefix——批量
|
|
128
|
+
* 原子前缀;同文件条目按数组序串行累积——第二条基于第一条结果)→ 全部通过 → 逐文件写回
|
|
129
|
+
* 一次(判失败 → 零写;写失败 → 同本地 edit-batch 既有原子语义)。 */
|
|
130
|
+
const editBatch = async (args) => {
|
|
131
|
+
const edits = args.edits
|
|
132
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
133
|
+
throw new Error("edits must be a non-empty array of {path, old_string, new_string}")
|
|
134
|
+
}
|
|
135
|
+
assertEditArgsExclusive(args)
|
|
136
|
+
const groups = new Map() // path → { path, raw, content, edits }
|
|
137
|
+
for (const e of edits) {
|
|
138
|
+
// 2026-09-05 用户裁定(CLI parity——本地 edit-batch 同句):条目自带 path 优先;
|
|
139
|
+
// 缺省回退顶层 path(pathOf——path/filePath 别名同单形态)
|
|
140
|
+
const p = e.path ?? pathOf(args)
|
|
141
|
+
if (!p) throw new Error("each edit must have a path — give each entry its own path or pass a top-level path")
|
|
142
|
+
validateEditEntry(e, { label: `edit for ${p}: `, rich: false })
|
|
143
|
+
let g = groups.get(p)
|
|
144
|
+
if (!g) {
|
|
145
|
+
g = { path: p, raw: "", content: "", edits: [] }
|
|
146
|
+
groups.set(p, g)
|
|
147
|
+
}
|
|
148
|
+
g.edits.push(e)
|
|
149
|
+
}
|
|
150
|
+
for (const g of groups.values()) {
|
|
151
|
+
g.raw = await readBuffer(g.path)
|
|
152
|
+
g.content = normalizeEOL(g.raw)
|
|
153
|
+
}
|
|
154
|
+
const outcomes = []
|
|
155
|
+
for (const g of groups.values()) {
|
|
156
|
+
for (const e of g.edits) {
|
|
157
|
+
const out = computeEditEntry(g.content, e, { path: g.path, abortPrefix: EDIT_ABORT_PREFIX })
|
|
158
|
+
outcomes.push({ g, out })
|
|
159
|
+
g.content = out.updated // 同文件串行累积
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const g of groups.values()) {
|
|
163
|
+
await writeBuffer(g.path, joinWithEol(normalizeEOL(g.content).split("\n"), g.raw))
|
|
164
|
+
}
|
|
165
|
+
return outcomes.map((o) => `OK: edited ${o.g.path} via IDE (${o.out.occurrences} occurrence(s))${o.out.note ? ` — ${o.out.note}` : ""}`).join("\n")
|
|
166
|
+
}
|
|
167
|
+
|
|
68
168
|
const callbacks = {
|
|
69
169
|
onToken: (text) => {
|
|
70
170
|
// Strip the subagent `[model]` metadata token (role#id/[model]<name>) — it's a
|
|
71
171
|
// TUI/webview display signal, not conversation content, and must not reach ACP clients.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
172
|
+
// §19.5 D-M8 (round2 #6): nested prefixes recurse — eng-coder#2/explore#1/[model]…
|
|
173
|
+
if (/^(?:[\w-]+#\d+\/)*\[model\]/.test(text)) return
|
|
174
|
+
// D7 (AGENT-LOOP.md §7.2 + §19.5 round2 #6 + D-M7b): strip ⟦ev⟧ event tokens (bare or
|
|
175
|
+
// any-depth prefixed variants — turn/approval/done/settled/stopped/async — async
|
|
176
|
+
// = §19.5 D-M7b zero-field spawn marker) — they
|
|
177
|
+
// carry RS control characters and are a TUI display signal; structured ACP
|
|
75
178
|
// mapping (tool_call_update) is tracked separately in docs/TODO.md.
|
|
76
|
-
|
|
77
|
-
if (/^(?:[\w-]+#\d+\/)
|
|
179
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
180
|
+
if (/^(?:[\w-]+#\d+\/)*⟦ev⟧(?:turn|approval|done|settled|stopped|async)\x1e/.test(text)) return
|
|
78
181
|
update("agent_message_chunk", { content: { type: "text", text } })
|
|
79
182
|
},
|
|
80
183
|
onReasoning: (text) => update("agent_thought_chunk", { content: { type: "text", text } }),
|
|
@@ -82,9 +185,19 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
82
185
|
onWait: ({ phase, seconds }) => log(`[rate-limit] ${phase} waiting ~${seconds}s`),
|
|
83
186
|
onCompress: () => log("[context] auto-compacted"),
|
|
84
187
|
|
|
85
|
-
onToolCall: (name, args) => {
|
|
188
|
+
onToolCall: (name, args, toolId) => {
|
|
86
189
|
const id = toolCallId()
|
|
87
|
-
|
|
190
|
+
// D15.8(advisor 🔴#1 修复):模型级 id 每轮重置(sse.mjs finalizeToolCalls 内
|
|
191
|
+
// seq=0——call_0 call_1… 跨轮/跨消息可重复——设计自注「跨 turn 不保证唯一」)。
|
|
192
|
+
// 因此 push 前若队列已有同名同 toolId 条目,它必是结果永不回调的陈旧孤儿
|
|
193
|
+
// (dispatch 失败/中断路径不调 onToolResult——T-F5 契约)——先弹出再入队——
|
|
194
|
+
// 精确配对恒命中最新——"下个同名结果永不配到旧项"(设计目标,无需动 dispatch)。
|
|
195
|
+
if (toolId != null) {
|
|
196
|
+
for (let i = toolQueue.length - 1; i >= 0; i--) {
|
|
197
|
+
if (toolQueue[i].name === name && toolQueue[i].toolId === toolId) toolQueue.splice(i, 1)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
toolQueue.push({ name, id, toolId: toolId ?? null })
|
|
88
201
|
update("tool_call", {
|
|
89
202
|
toolCallId: id,
|
|
90
203
|
title: name,
|
|
@@ -95,9 +208,8 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
95
208
|
})
|
|
96
209
|
},
|
|
97
210
|
|
|
98
|
-
onToolResult: (name, result) => {
|
|
99
|
-
const id =
|
|
100
|
-
toolIds.delete(name)
|
|
211
|
+
onToolResult: (name, result, toolId) => {
|
|
212
|
+
const id = takeToolId(name, toolId) ?? toolCallId()
|
|
101
213
|
update("tool_call_update", {
|
|
102
214
|
toolCallId: id,
|
|
103
215
|
status: "completed",
|
|
@@ -117,7 +229,7 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
117
229
|
if (danger) content.push(contentBlock(`⚠️ Dangerous: ${danger}`))
|
|
118
230
|
content.push(contentBlock(JSON.stringify(args ?? {})))
|
|
119
231
|
const toolCall = {
|
|
120
|
-
toolCallId:
|
|
232
|
+
toolCallId: peekToolId(name) ?? toolCallId(), // D15.8:peek 不消费——result 仍要与自己的条目配对
|
|
121
233
|
title: name,
|
|
122
234
|
content,
|
|
123
235
|
}
|
|
@@ -137,7 +249,8 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
137
249
|
/**
|
|
138
250
|
* fs reverse-RPC router (dispatch.mjs toolRouter, M2):
|
|
139
251
|
* - write → fs/write_text_file (full content, no read-back)
|
|
140
|
-
* - edit → fs/read_text_file →
|
|
252
|
+
* - edit → fs/read_text_file → computeEditEntry(本地权威——单/数组形态)→ fs/write_text_file
|
|
253
|
+
* (§15.1 D15.7 委派——双通道同语义;数组=原子批量——逐条目串行累积)
|
|
141
254
|
* - apply_patch → local (unified-diff application is not routed in M2)
|
|
142
255
|
* - delete, reads → local
|
|
143
256
|
*/
|
|
@@ -156,19 +269,12 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {} }
|
|
|
156
269
|
return { handled: true, result: `Error: fs/write_text_file failed: ${e.message}` }
|
|
157
270
|
}
|
|
158
271
|
}
|
|
159
|
-
if (base === "edit" && path && typeof args?.old_string === "string" && typeof args?.new_string === "string") {
|
|
272
|
+
if (base === "edit" && (Array.isArray(args?.edits) || (path && typeof args?.old_string === "string" && typeof args?.new_string === "string"))) {
|
|
160
273
|
try {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
const idx = current.indexOf(args.old_string)
|
|
164
|
-
if (idx === -1) {
|
|
165
|
-
return { handled: true, result: `Error: old_string not found in ${path} (read via IDE buffer)` }
|
|
166
|
-
}
|
|
167
|
-
const next = current.slice(0, idx) + args.new_string + current.slice(idx + args.old_string.length)
|
|
168
|
-
await request("fs/write_text_file", { sessionId, path, content: next }, { timeoutMs: 30000 })
|
|
169
|
-
return { handled: true, result: `OK: edited ${path} via IDE (1 replacement)` }
|
|
274
|
+
const text = Array.isArray(args?.edits) ? await editBatch(args) : await editSingle(path, args)
|
|
275
|
+
return { handled: true, result: text }
|
|
170
276
|
} catch (e) {
|
|
171
|
-
return { handled: true, result: `Error:
|
|
277
|
+
return { handled: true, result: `Error: ${e.message}` }
|
|
172
278
|
}
|
|
173
279
|
}
|
|
174
280
|
return { handled: false } // read-only tools, delete, apply_patch stay local
|
package/src/advisor/messages.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readFileSync, existsSync } from "node:fs"
|
|
7
7
|
import { resolve, join, relative, dirname, sep } from "node:path"
|
|
8
|
-
import {
|
|
8
|
+
import { providerSpec } from "../config.mjs"
|
|
9
9
|
import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
|
|
10
10
|
import { buildConvergenceBody, buildConvergenceInstructions } from "./convergence.mjs"
|
|
11
11
|
import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable } from "./history.mjs"
|
|
@@ -90,7 +90,9 @@ function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
|
90
90
|
}
|
|
91
91
|
// readFileSync succeeded — compute the budget OUTSIDE the try so a spec
|
|
92
92
|
// lookup failure can never masquerade as "no AGENTS.md".
|
|
93
|
-
|
|
93
|
+
// providerSpec: the project-guide budget follows the provider-level context
|
|
94
|
+
// override (PROVIDER.md §15 — advisor messages budget is context-based).
|
|
95
|
+
const ctx = providerSpec(agent.provider).context
|
|
94
96
|
const cap = Math.max(PROJECT_GUIDE_MIN, Math.floor(ctx * PROJECT_GUIDE_FRACTION))
|
|
95
97
|
const shown = text.length <= cap
|
|
96
98
|
? text
|
|
@@ -103,6 +105,30 @@ function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
|
103
105
|
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
104
106
|
}
|
|
105
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Build the mechanical review-object declaration block (AGENT-LOOP.md §18.8
|
|
110
|
+
* D-OA2 — English anchored form). Injected at the START of the review user
|
|
111
|
+
* message every round: round 1 (design + code), the legacy convergence path,
|
|
112
|
+
* and the round-2+ follow-up (see buildAdvisorFollowUp) — the reviewer must
|
|
113
|
+
* not re-derive "who is being reviewed / why" from the documents (T-OA2:
|
|
114
|
+
* every round stays anchored). Absent object → "" (legacy calls degrade to
|
|
115
|
+
* the current behavior — T-OA3).
|
|
116
|
+
* @param {Object|null} [object] — { type, target, status, reason, exclude }
|
|
117
|
+
* (strings; `exclude` may also be a list — joined with ", ")
|
|
118
|
+
* @returns {string} the declaration block (empty when no object)
|
|
119
|
+
*/
|
|
120
|
+
export function buildObjectDeclarationBlock(object = null) {
|
|
121
|
+
if (!object || typeof object !== "object" || Array.isArray(object)) return ""
|
|
122
|
+
const field = (v) => (Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v))
|
|
123
|
+
return [
|
|
124
|
+
"## Review-object declaration (mechanical — do not infer)",
|
|
125
|
+
`Review type: ${field(object.type)} | Target: ${field(object.target)} | Object state: ${field(object.status)} | Trigger: ${field(object.reason)}`,
|
|
126
|
+
`Excluded (not in this review): ${field(object.exclude)}`,
|
|
127
|
+
"Follow this declaration — do not infer the review target from the documents.",
|
|
128
|
+
"",
|
|
129
|
+
].join("\n")
|
|
130
|
+
}
|
|
131
|
+
|
|
106
132
|
/**
|
|
107
133
|
* Build the user message for an advisor review session.
|
|
108
134
|
* @param {Object} agent — the parent agent
|
|
@@ -113,15 +139,24 @@ function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
|
113
139
|
* When set, the review input is built from this list ONLY — no git-diff change-set collection.
|
|
114
140
|
* When absent, the legacy git-diff-based scope is kept (backward compatible).
|
|
115
141
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review (deduped; shown under Review Scope)
|
|
142
|
+
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
|
|
143
|
+
* { type, target, status, reason, exclude } — mechanically injected at the
|
|
144
|
+
* start of the user message; absent → no injection (legacy calls unchanged).
|
|
116
145
|
* @returns {string} the user message
|
|
117
146
|
*/
|
|
118
|
-
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null) {
|
|
147
|
+
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null, object = null) {
|
|
119
148
|
// prior = the full prior review output (string) when a convergence round is
|
|
120
149
|
// being built (decision 2026-08-08 — verbatim injection, model understands it).
|
|
121
150
|
// Deterministic: only _advisorRound > 0 with stored output counts.
|
|
122
151
|
const p = prior ?? ((agent._advisorRound || 0) > 0 ? agent._lastAdvisorOutput : null)
|
|
123
152
|
|
|
124
153
|
const parts = []
|
|
154
|
+
// Review-object declaration FIRST — D-OA1: at the start of the user message
|
|
155
|
+
// (after the system prompt, before the review content). Covers round 1
|
|
156
|
+
// design/code and the legacy convergence path; the round-2+ normal path
|
|
157
|
+
// prepends it in buildAdvisorFollowUp (T-OA2 — every round stays anchored).
|
|
158
|
+
const declaration = buildObjectDeclarationBlock(object)
|
|
159
|
+
if (declaration) parts.push(declaration)
|
|
125
160
|
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
126
161
|
const pathList = Array.isArray(paths) ? [...new Set(paths.filter((p) => typeof p === "string" && p.trim()))] : []
|
|
127
162
|
|