thincoder 0.12.60 → 0.12.61
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 +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
package/src/agent/dispatch.mjs
CHANGED
|
@@ -5,18 +5,18 @@ import { logEvent, errText, headText } from "../log.mjs"
|
|
|
5
5
|
import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
6
6
|
import { runHooks } from "../hooks.mjs"
|
|
7
7
|
import { snapshotForUndo } from "../tui/cmd-undo.mjs"
|
|
8
|
-
import {
|
|
8
|
+
import { isCodePath, loadConventions } from "../conventions.mjs"
|
|
9
9
|
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):写工具钩子——peerCollabNote(执行前冲突
|
|
10
10
|
// 检测——软提示不阻止)+ recordPeerWrites(成功后累积本回合写足迹——回合末 flush)。
|
|
11
11
|
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites } from "../peer-domains.mjs"
|
|
12
12
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
13
|
-
import { join, resolve } from "node:path"
|
|
13
|
+
import { join, resolve, relative } from "node:path"
|
|
14
14
|
import { homedir } from "node:os"
|
|
15
15
|
// §29 fix A(AGENT-LOOP.md §29——2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
|
|
16
16
|
// 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
|
|
17
17
|
// + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
|
|
18
18
|
// 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(§29 症状根因)。
|
|
19
|
-
import { noteMutations } from "../agent-tools/advisor-async.mjs"
|
|
19
|
+
import { noteMutations, inflightDesignReviewConflict } from "../agent-tools/advisor-async.mjs"
|
|
20
20
|
import { anyLiveDesignSlot } from "../token-ttl.mjs"
|
|
21
21
|
|
|
22
22
|
const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
|
|
@@ -182,11 +182,13 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
182
182
|
// Engineering mode PARENT gate: the parent agent must not touch code files
|
|
183
183
|
// before the design review passed. Signaled by a live design slot (design-review
|
|
184
184
|
// approval — persists in the session slot, survives across turns; _engDesignReviewed
|
|
185
|
-
// is eng-coder-only and reset per run). Exemptions
|
|
186
|
-
// (
|
|
187
|
-
//
|
|
188
|
-
// src
|
|
189
|
-
//
|
|
185
|
+
// is eng-coder-only and reset per run). Exemptions = the non-code classes of
|
|
186
|
+
// src/conventions.mjs (documentation and temp scratch files) — writing a design
|
|
187
|
+
// document IS the design step. Anything inside a declared code segment (default:
|
|
188
|
+
// src — incl. src/prompts/*.md) is product code, not documentation, and needs a
|
|
189
|
+
// design token. The project can declare its own code paths (.thincoder/
|
|
190
|
+
// conventions.json) so a non-src layout is not silently exempted. Mechanically
|
|
191
|
+
// blocks "talk then code".
|
|
190
192
|
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):资格判据 = 权威槽"任一活槽存在"
|
|
191
193
|
// (anyLiveDesignSlot——查内存 Map,miss 回读槽文件——单值镜像 `_engDesignToken`
|
|
192
194
|
// 已退役,门禁不再读镜像——AC4)。
|
|
@@ -194,14 +196,45 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
194
196
|
&& !anyLiveDesignSlot(agent)
|
|
195
197
|
&& FILE_MUTATORS.has(toolCall.name)) {
|
|
196
198
|
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
199
|
+
const conv = loadConventions(agent.cwd)
|
|
197
200
|
// Unknown/missing paths (non-string, e.g. no path argument) are treated
|
|
198
|
-
// as code — cannot tell what they touch, so block conservatively.
|
|
199
|
-
|
|
201
|
+
// as code — cannot tell what they touch, so block conservatively. Known
|
|
202
|
+
// paths go through the single shared classifier: a declared code segment
|
|
203
|
+
// (default: "src") at ANY depth, else anything that is not documentation.
|
|
204
|
+
const touchesCode = paths.some((p) => typeof p !== "string" || isCodePath(p, conv))
|
|
200
205
|
if (touchesCode) {
|
|
206
|
+
// Undeclared project → point at the declaration file (§4.3 降级可见契约).
|
|
207
|
+
const convNote = conv.declared
|
|
208
|
+
? ""
|
|
209
|
+
: ` — this path was classified as product code by the default conventions (code paths: ${conv.codePaths.join(", ")}); declare project conventions in .thincoder/conventions.json to adjust.`
|
|
201
210
|
prepared.push({
|
|
202
211
|
toolCall, tool, denied: true,
|
|
203
212
|
reason: "engineering design gate",
|
|
204
|
-
hint:
|
|
213
|
+
hint: `Engineering mode: write the design document first(location per your project's document conventions), then call advisor with type='design' to review it, and wait for user approval. Implementation is done by eng-coder subagents.${convNote}`,
|
|
214
|
+
})
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// E(第 11 批·F17/§14.14 E-3d):D5 冻结窗口写前拦截——设计评审在途(点火 → 结算)期间,
|
|
220
|
+
// 父侧对被审文件集(声明文档集 + 批次档)的写入会被拒绝:在途写使本轮结算 stale——pass 轮
|
|
221
|
+
// = token 直接丢失(实证:第 10 批 id=20 整轮作废)。工具面 = FILE_MUTATORS(与变更记账
|
|
222
|
+
// 同集——不记入日志的写面既不判 stale 也不拦);判据与 reviewIsStale 同源
|
|
223
|
+
// (inflightDesignReviewConflict——同 docAbs / 同 normAbs;仅扫 running 未取消的设计条目)。
|
|
224
|
+
// 位置:只读 / autoApprove 短路之前——审批不得绕过冻结;拒绝 = 可见 denied + 逃生门
|
|
225
|
+
// (先 cancel → 改 → 重发)。
|
|
226
|
+
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
227
|
+
let touched = []
|
|
228
|
+
try { touched = tool.touchedPaths ? tool.touchedPaths(args) : [args.path] } catch { touched = [] }
|
|
229
|
+
const absPaths = (touched ?? [])
|
|
230
|
+
.filter((p) => typeof p === "string" && p)
|
|
231
|
+
.map((p) => resolve(agent.cwd, p))
|
|
232
|
+
const conflict = inflightDesignReviewConflict(agent, absPaths)
|
|
233
|
+
if (conflict) {
|
|
234
|
+
prepared.push({
|
|
235
|
+
toolCall, tool, denied: true,
|
|
236
|
+
reason: "d5 freeze window",
|
|
237
|
+
hint: `write refused — design review #${conflict.id} is in flight over ${relative(agent.cwd, conflict.path)} (D5 freeze window). A write now would settle it stale — no token for a pass (the round is lost). Wait for the report, or cancel the review first (subagent action:'cancel' id:'${conflict.id}') and re-launch after the change.`,
|
|
205
238
|
})
|
|
206
239
|
continue
|
|
207
240
|
}
|
|
@@ -293,6 +326,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
293
326
|
? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
|
|
294
327
|
: item.reason === "engineering design gate"
|
|
295
328
|
? `Error: design review required before any file modification. ${item.hint}`
|
|
329
|
+
: item.reason === "d5 freeze window"
|
|
330
|
+
? `Error: ${item.hint}`
|
|
296
331
|
: item.reason === "denied by user"
|
|
297
332
|
? "Error: permission denied by user"
|
|
298
333
|
: item.reason === "blocked by PreToolUse hook"
|
|
@@ -352,7 +387,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
352
387
|
depth,
|
|
353
388
|
signal,
|
|
354
389
|
callbacks,
|
|
355
|
-
// §
|
|
390
|
+
// §11.2 D-24b: per-call id — the advisor tool marker-keys its launch so
|
|
356
391
|
// recordToolResults can split async-ack accounting from sync settles.
|
|
357
392
|
_toolCallId: item.toolCall.id,
|
|
358
393
|
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import { readFileSync, readdirSync } from "node:fs"
|
|
|
6
6
|
import { homedir } from "node:os"
|
|
7
7
|
import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
|
|
8
8
|
import { join } from "node:path"
|
|
9
|
-
import {
|
|
9
|
+
import { execFile } from "node:child_process"
|
|
10
|
+
import { promisify } from "node:util"
|
|
10
11
|
|
|
11
12
|
/** Single source for the AUTO-mode reminder (was duplicated in agent.mjs +
|
|
12
13
|
* setup.mjs — consult P2, 2026-08-30). */
|
|
@@ -138,20 +139,65 @@ export async function offloadToolResult(text, callId, dir = join(configDir, "too
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// 失败冷却 v1(用户裁——GIT-ASYNC L21):真失败/超时后 30s 内跳过该 cwd 的收集——
|
|
143
|
+
// 病态 repo 周期拖慢变一次性。VSC setup-reminders.mjs 同构镜像。
|
|
144
|
+
const GIT_FAILURE_COOLDOWN_MS = 30_000
|
|
145
|
+
/** cwd → 最近一次 git 收集失败的 ts。评审 #2:访问时惰性清 >30s 旧条目(防无界累积)。 */
|
|
146
|
+
const gitFailureCooldowns = new Map()
|
|
147
|
+
// maxBuffer 沿用 execSync 默认现值 1 MiB(评审 #1——防大输出仓 ENOBUFS 翻转 → "" 破字节 parity)
|
|
148
|
+
const GIT_MAX_BUFFER = 1024 * 1024
|
|
149
|
+
|
|
150
|
+
const execFileAsync = promisify(execFile)
|
|
151
|
+
|
|
152
|
+
/** 冷却检查 + 惰性清扫:冷却期内的 cwd 返回 true(调用方直接 "" 跳过)。 */
|
|
153
|
+
function gitCooldownActive(cwd) {
|
|
154
|
+
const now = Date.now()
|
|
155
|
+
const lastFailure = gitFailureCooldowns.get(cwd)
|
|
156
|
+
if (lastFailure === undefined) return false
|
|
157
|
+
if (now - lastFailure >= GIT_FAILURE_COOLDOWN_MS) {
|
|
158
|
+
gitFailureCooldowns.delete(cwd) // 惰性清过期条目
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Test seams — cooldown Map 状态控制(生产从不调用):
|
|
165
|
+
* `_gitFailureCooldownForTests(cwd, ts)` 读(ts 省略)或写 ts;`_clear…` 删条目。 */
|
|
166
|
+
export function _gitFailureCooldownForTests(cwd, ts) {
|
|
167
|
+
if (ts !== undefined) gitFailureCooldowns.set(cwd, ts)
|
|
168
|
+
return gitFailureCooldowns.get(cwd)
|
|
169
|
+
}
|
|
170
|
+
export function _clearGitFailureCooldownForTests(cwd) {
|
|
171
|
+
gitFailureCooldowns.delete(cwd)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Git context 纯格式化(branch/log/status = trim 后原串)——独立导出供单测锁字节
|
|
175
|
+
* parity(现拼装逐字节保留——detached/dirty>20 截断/clean 三形态——VSC 镜像)。 */
|
|
176
|
+
export function composeGitContext({ branch, log, status }) {
|
|
177
|
+
const dirty = status ? status.split("\n").length : 0
|
|
178
|
+
return [
|
|
179
|
+
`Git context: on branch \`${branch || "(detached)"}\`${dirty ? `, ${dirty} uncommitted change(s)` : ", working tree clean"}.`,
|
|
180
|
+
log ? `Recent commits:\n${log}` : "",
|
|
181
|
+
status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
|
|
182
|
+
].filter(Boolean).join("\n")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Collect git branch, recent commits, and working tree status as context text.
|
|
186
|
+
* GIT-ASYNC L21:3×execSync 串行(最坏 15s 阻塞)→ 3×execFile 并行(Promise.all——
|
|
187
|
+
* 最坏 = 单次 5s 超时——事件循环不冻结)。单 catch → "" 保持 all-or-nothing(任一
|
|
188
|
+
* 失败/超时 → 整段不注入——非 git 快失败同路径);catch 记冷却 ts——30s 内跳过。 */
|
|
189
|
+
export async function collectGitContext(cwd) {
|
|
190
|
+
if (gitCooldownActive(cwd)) return ""
|
|
143
191
|
try {
|
|
144
|
-
const opts = { cwd, encoding: "utf8",
|
|
145
|
-
const branch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
log ? `Recent commits:\n${log}` : "",
|
|
152
|
-
status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
|
|
153
|
-
].filter(Boolean).join("\n")
|
|
192
|
+
const opts = { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS, windowsHide: true, maxBuffer: GIT_MAX_BUFFER }
|
|
193
|
+
const [branch, log, status] = await Promise.all([
|
|
194
|
+
execFileAsync("git", ["branch", "--show-current"], opts).then((r) => r.stdout.trim()),
|
|
195
|
+
execFileAsync("git", ["--no-pager", "log", "--oneline", "-5"], opts).then((r) => r.stdout.trim()),
|
|
196
|
+
execFileAsync("git", ["status", "--short"], opts).then((r) => r.stdout.trim()),
|
|
197
|
+
])
|
|
198
|
+
return composeGitContext({ branch, log, status })
|
|
154
199
|
} catch {
|
|
200
|
+
gitFailureCooldowns.set(cwd, Date.now()) // 真失败/超时——冷却 30s
|
|
155
201
|
return ""
|
|
156
202
|
}
|
|
157
203
|
}
|
|
@@ -164,6 +210,18 @@ export class ContinueError extends Error {
|
|
|
164
210
|
}
|
|
165
211
|
}
|
|
166
212
|
|
|
213
|
+
/** 跨段累计编号帧(TURN-CAP-CONTINUE.md §19.3——第 19 批 TURN-ACROSS-SEGMENTS):
|
|
214
|
+
* 唯一计算点(纯函数)——把链内累计序数换算成面向消费面的编号载荷。
|
|
215
|
+
* - seq = 该轮链内累计序数(1 起——`agent._turnSeq` 每轮 +1,续跑不重置)
|
|
216
|
+
* - turn = 段内轮号(0 起——段内帽判定的循环变量)
|
|
217
|
+
* - maxTurns = 本段预算(段内帽判定值——原样传入)
|
|
218
|
+
* 返回 { turn, maxTurns }:turn = seq(累计已跑轮数);maxTurns = 段前累计 + 本段预算
|
|
219
|
+
* = seq - turn - 1 + maxTurns(差额项 = 本段开始前的链内累计)。
|
|
220
|
+
* 段内帽判定不读本帧(只读段内 turn / maxTurns——N6 零机制改动)。 */
|
|
221
|
+
export function turnFrame(seq, turn, maxTurns) {
|
|
222
|
+
return { turn: seq, maxTurns: seq - turn - 1 + maxTurns }
|
|
223
|
+
}
|
|
224
|
+
|
|
167
225
|
/** Repair malformed conversation history: remove orphan tool messages and fill missing tool results */
|
|
168
226
|
export function repairHistory(history) {
|
|
169
227
|
const out = []
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* invalidate verify only (user decision 2026-08-08: reviews trigger on
|
|
17
17
|
* code mutations, not environment changes).
|
|
18
18
|
* - advisor calls always advance _advisorRound (convergence budget counts
|
|
19
|
-
* attempts, not successes).
|
|
19
|
+
* attempts, not successes). §15 F16 (2026-09-11 第 13 批): the SYNC
|
|
20
|
+
* called-mark is gated by the single incomplete-tail predicate
|
|
21
|
+
* (advisorIncompleteMarker) — a truncated-then-settled CODE review must not
|
|
22
|
+
* count as covered (the guard re-pushes); design reviews keep the mark
|
|
23
|
+
* (no code face — settle-side `failureVerdict` parity).
|
|
20
24
|
* - Mutations feed _touchedFiles and fire-and-forget memory reindex.
|
|
21
25
|
*/
|
|
22
26
|
import { pushReal } from "../context.mjs"
|
|
@@ -24,7 +28,7 @@ import { specForModel } from "../config.mjs"
|
|
|
24
28
|
import { FILE_MUTATORS } from "./helpers.mjs"
|
|
25
29
|
import { resolve } from "node:path"
|
|
26
30
|
import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
|
|
27
|
-
import { looksLikeReviewOutput } from "../advisor/run.mjs"
|
|
31
|
+
import { looksLikeReviewOutput, advisorIncompleteMarker } from "../advisor/run.mjs"
|
|
28
32
|
|
|
29
33
|
let _reindexFile = null
|
|
30
34
|
|
|
@@ -96,7 +100,7 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
96
100
|
}
|
|
97
101
|
if (toolCall.name === "verify") agent._verifiedThisRun = true
|
|
98
102
|
if (toolCall.name === "advisor") {
|
|
99
|
-
// §
|
|
103
|
+
// §11.2 D-24b (settle accounting split — fix #2): an ASYNC launch returns an
|
|
100
104
|
// ack and settles later — the settle callback owns its called/round/token
|
|
101
105
|
// accounting. Only the SYNC path (depth>0 / explicit async:false) accounts
|
|
102
106
|
// here — per-review instance round++ (marker-keyed by tool call id) + the
|
|
@@ -111,10 +115,14 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
111
115
|
} else if (asyncAck) {
|
|
112
116
|
agent._advisorAsyncAcks.delete(toolCall.id)
|
|
113
117
|
} else {
|
|
114
|
-
agent._calledAdvisorThisRun = true
|
|
115
118
|
const reviewId = agent._advisorSyncCalls?.get(toolCall.id)
|
|
119
|
+
const run = reviewId !== undefined ? advisorRuns(agent).get(reviewId) : undefined
|
|
120
|
+
// F16 同步面(第 13 批 §15.2——ADVISOR-CONVERGENCE.md §15):未完成尾 + 非设计面
|
|
121
|
+
// (含类型不可判——run 缺失的 legacy 直调)⇒ 不置「已覆盖」(与 settle 面
|
|
122
|
+
// 逐条 parity;设计评审保持计入——无代码面)。
|
|
123
|
+
const incomplete = advisorIncompleteMarker(String(result))
|
|
124
|
+
if (!(incomplete && run?.reviewType !== "design")) agent._calledAdvisorThisRun = true
|
|
116
125
|
if (reviewId !== undefined) {
|
|
117
|
-
const run = advisorRuns(agent).get(reviewId)
|
|
118
126
|
if (run) {
|
|
119
127
|
run.round++
|
|
120
128
|
agent._advisorRound = run.round
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* relay-prefix.mjs — relay 前缀(`role#id/`)文法**单一权威**(第 27 批 §12.3①——R-A2.3)。
|
|
3
|
+
*
|
|
4
|
+
* 零依赖(不 import 任何模块)——TUI(`src/tui/`)与 ACP(`src/acp/`)双向可导入,无环;
|
|
5
|
+
* 生成侧 `src/agent/spawn-child.mjs` 再导出(枢纽)。文法 = 自
|
|
6
|
+
* `src/tui/subagent-blocks.mjs` 逐字迁入(第 27 批 §12.3②——零语义改动)。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** `role#id/` prefix router — hyphen included since the eng-coder fix (2026-08-21). */
|
|
10
|
+
export const RELAY_PREFIX_RE = /^([\w-]+)#(\d+)\//
|
|
11
|
+
|
|
12
|
+
/** 嵌套 relay 前缀通用解析(循环解析任意深度——显示契约 docs/design/TUI.md §6):
|
|
13
|
+
* `eng-coder#2/explore#1/read` → { head(块路由), inner[], label(inner 链——R23:
|
|
14
|
+
* 子块折叠键/外层 currentTool 全路径用), rest }。
|
|
15
|
+
* 单层 = inner[]/label ""——与既有单段匹配语义零改;无前缀 → null。 */
|
|
16
|
+
export function parseRelayPath(text) {
|
|
17
|
+
const segments = []
|
|
18
|
+
let rest = String(text)
|
|
19
|
+
for (;;) {
|
|
20
|
+
const m = rest.match(RELAY_PREFIX_RE)
|
|
21
|
+
if (!m) break
|
|
22
|
+
segments.push(`${m[1]}#${m[2]}`)
|
|
23
|
+
rest = rest.slice(m[0].length)
|
|
24
|
+
}
|
|
25
|
+
if (segments.length === 0) return null
|
|
26
|
+
return {
|
|
27
|
+
head: segments[0],
|
|
28
|
+
inner: segments.slice(1),
|
|
29
|
+
label: segments.slice(1).join("/"),
|
|
30
|
+
rest,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 构造向单一实现(本批统一的两处构造点——`makeRelay` 与 async 取号分支共用;
|
|
35
|
+
* 另两处字面构造(escalate/advisor async)不在本批声明面——批次档 §2 修正轮登记,如需收敛另批):
|
|
36
|
+
* `relayPrefixOf("coder", 3)` → `"coder#3/"`。 */
|
|
37
|
+
export function relayPrefixOf(label, id) {
|
|
38
|
+
return `${label}#${id}/`
|
|
39
|
+
}
|
package/src/agent/run-stages.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { getAsyncPool } from "../agent-tools/async-settle.mjs"
|
|
|
16
16
|
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末域登记 flush(写工具钩子累积 →
|
|
17
17
|
// 整写一次本实例 peers 文件——无写入跳过;失败容忍不抛)
|
|
18
18
|
import { flushPeerDomains } from "../peer-domains.mjs"
|
|
19
|
+
// 第 30 批 D1(Stop 钩子——AGENT-LOOP.md §21):主会话 run 终止事件(触发块见 finalizeAgentTurn)
|
|
20
|
+
import { runHooks } from "../hooks.mjs"
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* 响应后置提醒注入(2026-09-05 实践轮——自 runAgent 响应处理链提取,verbatim):
|
|
@@ -113,16 +115,29 @@ export async function injectTurnReminders(agent, ctx) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
|
-
* 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim +
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
118
|
+
* 回合收尾(2026-09-05 实践轮——自 runAgent finally 提取,verbatim + 签名化):写足迹
|
|
119
|
+
* flush → Stop 钩子(第 30 批 D1——ctx.depth === 0 的主会话 run 终止事件,
|
|
120
|
+
* fire-and-forget)→ consult 清理(无孤儿烧 token 的 consult children)→ async 池回合尾
|
|
121
|
+
* 分流(Ctrl+C 清池不注入 / ContinueError 留池续跑 / 其余 collectSettledAsync)→ auto-turn
|
|
122
|
+
* guard 标记继承(对象字段清单——正常结束才继承;中止丢弃;ContinueError 由续跑快照)。
|
|
120
123
|
*/
|
|
121
124
|
export async function finalizeAgentTurn(agent, ctx) {
|
|
122
|
-
const { signal, autoTurn, suspDriven, thrownError } = ctx
|
|
125
|
+
const { signal, autoTurn, suspDriven, thrownError, depth } = ctx
|
|
123
126
|
// R10 L3(MULTI-INSTANCE-COLLAB §2a.5 D-L3a):回合末登记 flush——本回合写足迹整写
|
|
124
127
|
// 一次(首行执行:收尾链后续任何异常都不吞登记);无写入 → 跳过(hot 窗口自然衰减)。
|
|
125
128
|
flushPeerDomains(agent)
|
|
129
|
+
// 第 30 批 D1(Stop 钩子):主会话 run 终止 → fire-and-forget(非阻塞;失败静默——与 PostToolUse 同语义)。
|
|
130
|
+
// 排除用户中止(Ctrl+C / Ctrl+I——中止后 TUI 重建 controller 续跑或已显式停回合)与 AbortError 展开。
|
|
131
|
+
if (depth === 0 && !signal?.aborted && thrownError?.name !== "AbortError") {
|
|
132
|
+
runHooks("Stop", {
|
|
133
|
+
agent,
|
|
134
|
+
error: thrownError && !(thrownError instanceof ContinueError) ? thrownError : undefined,
|
|
135
|
+
extra: {
|
|
136
|
+
turn: agent._currentTurn ?? 0,
|
|
137
|
+
reason: thrownError instanceof ContinueError ? "maxTurns" : thrownError ? "error" : "done",
|
|
138
|
+
},
|
|
139
|
+
}).catch(() => {})
|
|
140
|
+
}
|
|
126
141
|
// R17(AGENT-LOOP.md §25 D-R17a): consultation sessions are cross-turn
|
|
127
142
|
// background work now — NO unconditional turn-end cleanup (the old rule aborted
|
|
128
143
|
// leftover consult children at every turn end because check-loop consumption was
|
|
@@ -163,7 +178,7 @@ export async function finalizeAgentTurn(agent, ctx) {
|
|
|
163
178
|
// keep _asyncSubagents/_asyncAdvisors/_consultSessions — the resumed run continues them
|
|
164
179
|
} else {
|
|
165
180
|
const injectedAdvisor = await collectSettledAsync(agent, { suspDriven })
|
|
166
|
-
// §
|
|
181
|
+
// §11.2 D-24b: a normally-ended USER run with no review/child activity closes
|
|
167
182
|
// the OPEN code instances — the converged/abandoned thread must not burn the
|
|
168
183
|
// 5-round cap of a later task (the next code review starts a fresh round 1).
|
|
169
184
|
// Digest auto-turns are exempt (their disposition precedes the fix round).
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* One unified transient user reminder per turn covers the whole self-awareness
|
|
6
6
|
* family: env identity (R8 — §10 D-1 END 常量先例:静态常量,不做 cmdline 判别),
|
|
7
|
-
* engineering mode (R9), active model (R11),
|
|
7
|
+
* engineering mode (R9), active model (R11), session slot (F1), restart awareness
|
|
8
|
+
* (R5 resumed). §11.2(2026-09-08):resumed 按会话跟踪——恢复事件由 session.mjs
|
|
9
|
+
* applySession 武装(agent._envResumed——载入历史非空 = 恢复);进程重启句独立走
|
|
10
|
+
* agent._processRestartPending(bin 启动 resume 路径设)——双信号互不绑门(N6)。
|
|
8
11
|
* Change awareness needs no dedicated injection — every turn carries the
|
|
9
12
|
* CURRENT state, so a mode/model flip shows up in the next turn's line.
|
|
10
13
|
* git is NOT a field here (§11.1: CLI 不重复注入 clean|dirty 摘要) — the rich
|
|
@@ -16,26 +19,30 @@
|
|
|
16
19
|
import { END } from "../session-slots.mjs"
|
|
17
20
|
import { peerInstances } from "../peer-instances.mjs"
|
|
18
21
|
|
|
19
|
-
/** env-state line builder — pure, unit-testable.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
/** env-state line builder — pure, unit-testable.
|
|
23
|
+
* §11.2(F1):slot 字段入行——位置在 model 后 resumed 前(N3:无绑定 → 显式 null——
|
|
24
|
+
* 不读 manifest active 共享指针——粘性 _slot 才是"本 agent 之槽")。 */
|
|
25
|
+
export function envStateLine({ mode, model, slot, resumed }) {
|
|
26
|
+
return `[System reminder: env: ${END}, mode: ${mode}, model: ${model}, slot: ${slot}, resumed: ${resumed ? "yes" : "no"}.]`
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Push the per-turn env-state reminder (setup.mjs calls it for depth-0 runs —
|
|
26
|
-
* the line describes the MAIN agent's host/mode/model identity). resumed=yes
|
|
27
|
-
* exactly once
|
|
28
|
-
* agent._envResumed
|
|
29
|
-
*
|
|
31
|
+
* the line describes the MAIN agent's host/mode/model/slot identity). resumed=yes
|
|
32
|
+
* exactly once per session restore: applySession (session.mjs) arms
|
|
33
|
+
* agent._envResumed when restored history is non-empty; consumed here so every
|
|
34
|
+
* later turn reads resumed=no(§11.2 F2——每次恢复一次;/new 与空历史槽切换不武装——
|
|
35
|
+
* F3 伪触发消除)。slot = 粘性 agent._slot(§11.2 N2/N3——无绑定显式 null)。
|
|
30
36
|
* Degrades safely: activeModel null → provider.model → "unknown" (T-E12);
|
|
31
37
|
* a missing config/history never throws (T-E13).
|
|
32
38
|
*/
|
|
33
39
|
export function pushEnvStateReminder(agent) {
|
|
34
40
|
const mode = agent.config?.agent?.engineering ? "eng" : "normal"
|
|
35
41
|
const model = agent.activeModel ?? agent.provider?.model ?? "unknown"
|
|
42
|
+
const slot = agent._slot ?? null
|
|
36
43
|
const resumed = agent._envResumed === true
|
|
37
44
|
agent._envResumed = false
|
|
38
|
-
agent.history.push({ role: "user", content: envStateLine({ mode, model, resumed }), transient: true })
|
|
45
|
+
agent.history.push({ role: "user", content: envStateLine({ mode, model, slot, resumed }), transient: true })
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/**
|