thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- 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 +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/tools/web.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { proxyFetch } from "../proxy.mjs";
|
|
|
4
4
|
|
|
5
5
|
export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
6
6
|
const FETCH_TIMEOUT = 15_000
|
|
7
|
+
// §14 D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
|
|
8
|
+
const PROXY_HINT = "network failure — retry with proxy: 'http://host:port' if the target is blocked"
|
|
7
9
|
|
|
8
10
|
// ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
|
|
9
11
|
|
|
@@ -204,19 +206,19 @@ export const fetchTool = {
|
|
|
204
206
|
const r = resolveRedirectTarget(loc, args.url)
|
|
205
207
|
if (r.error) throw new Error(`fetch failed: ${r.error}`)
|
|
206
208
|
const r2 = await proxyFetch(r.target, { headers: { "User-Agent": UA } }, proxyUri)
|
|
207
|
-
if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}`)
|
|
209
|
+
if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}\n${PROXY_HINT}`)
|
|
208
210
|
const ct2 = headerOf(r2, "content-type") ?? ""
|
|
209
211
|
const b2 = await r2.text()
|
|
210
212
|
if (ct2.includes("text/html")) { const t = htmlToText(b2); return truncate(t + detectSparseHtml(b2, t)) }
|
|
211
213
|
return truncate(b2)
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
|
-
throw new Error(`fetch failed: HTTP ${response.status}`)
|
|
216
|
+
throw new Error(`fetch failed: HTTP ${response.status}\n${PROXY_HINT}`)
|
|
215
217
|
}
|
|
216
218
|
const ct = headerOf(response, "content-type") ?? ""
|
|
217
219
|
const body = await response.text()
|
|
218
220
|
if (ct.includes("text/html")) { const t = htmlToText(body); return truncate(t + detectSparseHtml(body, t)) }
|
|
219
221
|
return truncate(body)
|
|
220
|
-
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}`, { cause: e }) }
|
|
222
|
+
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}\n${PROXY_HINT}`, { cause: e }) }
|
|
221
223
|
},
|
|
222
224
|
}
|
package/src/tools/websearch.md
CHANGED
|
@@ -8,7 +8,8 @@ Parameters:
|
|
|
8
8
|
- proxy: http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling); Bing/foreign sites usually need a proxy, domestic targets don't
|
|
9
9
|
|
|
10
10
|
Notes:
|
|
11
|
-
- Before searching the web, call
|
|
11
|
+
- Before searching the web, call the memory tool (action: search) first — you may already know the answer from a previous session. Only reach for websearch if memory comes up empty.
|
|
12
|
+
- Runs synchronously — returns when the search completes (all selected engines run in parallel).
|
|
12
13
|
- Use this for information that is NOT in the local codebase — current docs, error messages, API references
|
|
13
14
|
- Follow up with `fetch` to read full pages from the results
|
|
14
15
|
- **Weak engine warning**: Bing's index is noisy for technical queries — if a first websearch returns irrelevant/townhall-grade results, DO NOT retry the same query. Configure a search MCP tool (e.g. `glm-websearch` via the MCP config) for technical lookups; websearch is the fallback.
|
package/src/tools/write.md
CHANGED
|
@@ -5,5 +5,7 @@ Parameters:
|
|
|
5
5
|
- content (required): Full content to write
|
|
6
6
|
|
|
7
7
|
Notes:
|
|
8
|
+
- write replaces the WHOLE file — read it first and confirm you intend to rewrite it entirely; for a small change use edit / insert_after.
|
|
9
|
+
- Returns `Wrote N chars to <path>` + git diff + syntax-check note.
|
|
8
10
|
- This overwrites the entire file — use `edit` for targeted changes
|
|
9
11
|
- The file is atomic: it either writes completely or fails
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trace-store.mjs — §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 D-TR1..TR8 权威规格)。
|
|
3
|
+
*
|
|
4
|
+
* 完整请求-响应轨迹落盘:每次 chat() 调用一个 JSONL 文件到
|
|
5
|
+
* ~/.thincoder/traces/YYYY-MM-DD/<sessionKey>-<seq>.jsonl。采集点唯一 = core.mjs
|
|
6
|
+
* chat() 导出出口(所有模型调用——主回合/消化轮/compress/distill/advisor/子代理/
|
|
7
|
+
* auto-think/consult——都经该函数)——续写/重试在出口已合并(reasoning 全量)。
|
|
8
|
+
*
|
|
9
|
+
* 纪律(与 log.mjs 同源惯例):
|
|
10
|
+
* - 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 同步预留(原子号位),
|
|
11
|
+
* 写盘异步(node:fs/promises——不 await);落盘失败静默降级(不抛错、不阻塞
|
|
12
|
+
* chat() 返回)。recordChatTrace 返回落盘 promise——仅供测试/显式消费方 await。
|
|
13
|
+
* - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
|
|
14
|
+
* 轨迹目录(既有测试跑真实 agent 管线会产生数百次 chat 调用);显式设置
|
|
15
|
+
* THINCODER_TRACES_DIR 强制写入该目录(traces.test.mjs 用它隔离临时目录——
|
|
16
|
+
* 与 log.mjs 的 THINCODER_LOG_DIR 同惯例)。
|
|
17
|
+
* - 脱敏(D-TR2):复用 log.mjs 字段名黑名单(apikey/designtoken/password/secret/
|
|
18
|
+
* token/authorization/proxyuri/proxy)+ SECRET_FORM 形态扫描(redactSecret)——对
|
|
19
|
+
* messages/content/reasoning/toolCalls/error 全字段递归应用;不发明新遮蔽模式。
|
|
20
|
+
* - 容量有界(N-TR1):单条轨迹一个文件,大小不限("成本不是问题"——不截断、不
|
|
21
|
+
* 脱漏行);目录按日组织(YYYY-MM-DD),可按日/会话过滤;不自动清理(D-TR8——
|
|
22
|
+
* 清理机制记 docs/TODO.md 技术组)。
|
|
23
|
+
* - seq = 当日目录内最大已有 seq + 1(D-TR3——跨会话/进程重启不覆写既有旧轨迹——
|
|
24
|
+
* 18.6.1 评审 #3)。
|
|
25
|
+
* - 日期分日按本地时区(D-TR3——2026-09-04 fix round1:初版 toISOString()=UTC——
|
|
26
|
+
* 本地 00:00-08:00 的调用会落进前一日目录——改本地日期字符串 YYYY-MM-DD——T-TR12)。
|
|
27
|
+
* - isContinuation(D-TR1——2026-09-04 fix round1):续写/重试链标记——true = 该调用
|
|
28
|
+
* 是续写链的一环(core.mjs 续写递归传出 logCtx.isContinuation);新调用 false。
|
|
29
|
+
* 记录不输出 round 字段(全设计无 round 定义——删——不发明无来源字段)。
|
|
30
|
+
*/
|
|
31
|
+
import { readdirSync, existsSync } from "node:fs"
|
|
32
|
+
import { appendFile, mkdir, readdir, stat, unlink, rmdir } from "node:fs/promises"
|
|
33
|
+
import { join } from "node:path"
|
|
34
|
+
import { createHash } from "node:crypto"
|
|
35
|
+
import { configDir } from "../config.mjs"
|
|
36
|
+
import { redactSecret, errText, classifyErr } from "../log.mjs"
|
|
37
|
+
import { normalizeCwd } from "../session-slots.mjs"
|
|
38
|
+
|
|
39
|
+
/** 轨迹根目录:THINCODER_TRACES_DIR(测试隔离/override——同 THINCODER_LOG_DIR
|
|
40
|
+
* 惯例)> ~/.thincoder/traces(configDir——D-TR3——与 sessions/ 同域)。 */
|
|
41
|
+
export function tracesRoot() {
|
|
42
|
+
return process.env.THINCODER_TRACES_DIR ?? join(configDir, "traces")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
|
|
46
|
+
* THINCODER_TRACES_DIR override(traces.test.mjs 隔离临时目录)。 */
|
|
47
|
+
function writeEnabled() {
|
|
48
|
+
if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_TRACES_DIR) return false
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** sessionKey = sha1(normalizeCwd(cwd))[:12](D-TR3——与 session-slots sessionPath
|
|
53
|
+
* 同算法同 cwd 归一——两端 hash 一致)。 */
|
|
54
|
+
export function traceSessionKey(cwd) {
|
|
55
|
+
return createHash("sha1").update(normalizeCwd(cwd)).digest("hex").slice(0, 12)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 本地时区日期字符串 YYYY-MM-DD(D-TR3——2026-09-04 fix round1:初版
|
|
59
|
+
* toISOString()=UTC 日期——本地 00:00-08:00 落前一日目录;本地日期才是用户视角的
|
|
60
|
+
* "今天"——T-TR12)。recordChatTrace 与测试读取 helpers 共用同一实现——不双写。 */
|
|
61
|
+
export function localDateStr(date = new Date()) {
|
|
62
|
+
const y = date.getFullYear()
|
|
63
|
+
const m = String(date.getMonth() + 1).padStart(2, "0")
|
|
64
|
+
const d = String(date.getDate()).padStart(2, "0")
|
|
65
|
+
return `${y}-${m}-${d}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Test hooks(_rateHooks 同惯例——rate.mjs:测试可替换 now——T-TR12 注入本地日 ≠
|
|
69
|
+
* UTC 日的时刻验证分日;生产永远走真实时钟)。 */
|
|
70
|
+
export const _traceHooks = {
|
|
71
|
+
now: () => new Date(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 当日轨迹目录(D-TR3:traces/YYYY-MM-DD——分日组织——N-TR1)。 */
|
|
75
|
+
export function tracesDirFor(dateStr) {
|
|
76
|
+
return join(tracesRoot(), dateStr)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 进程内 seq 预留表:同步预留(原子——异步写盘在途时并发调用不撞号);键 = 目录。
|
|
80
|
+
// 跨进程/重启由磁盘 max 兜底(同键无预留时磁盘值即事实——T-TR10)。
|
|
81
|
+
const _reservedSeq = new Map()
|
|
82
|
+
|
|
83
|
+
/** seq = max(当日目录最大已有 seq, 进程内已预留) + 1(D-TR3——跨会话/进程重启
|
|
84
|
+
* 不覆写——T-TR8/T-TR10;同步预留 = 异步写盘启动前的原子号位分配)。 */
|
|
85
|
+
export function nextTraceSeq(dateStr) {
|
|
86
|
+
const dir = tracesDirFor(dateStr)
|
|
87
|
+
let max = 0
|
|
88
|
+
if (existsSync(dir)) {
|
|
89
|
+
let names
|
|
90
|
+
try {
|
|
91
|
+
names = readdirSync(dir)
|
|
92
|
+
} catch {
|
|
93
|
+
// 目录不可读——按预留表续号(不静默撞号)
|
|
94
|
+
max = _reservedSeq.get(dir) ?? 0
|
|
95
|
+
_reservedSeq.set(dir, max + 1)
|
|
96
|
+
return max + 1
|
|
97
|
+
}
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
const m = name.match(/-(\d+)\.jsonl$/)
|
|
100
|
+
if (m) max = Math.max(max, Number(m[1]))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const seq = Math.max(max, _reservedSeq.get(dir) ?? 0) + 1
|
|
104
|
+
_reservedSeq.set(dir, seq)
|
|
105
|
+
return seq
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 开关(D-TR6):traces.enabled 缺省 on(默认全采集);logCtx.traces === false
|
|
109
|
+
* 显式关闭(调用点读取 agent.config.traces.enabled——D-TR6)。 */
|
|
110
|
+
export function tracesEnabled(logCtx) {
|
|
111
|
+
return logCtx?.traces !== false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 递归脱敏(D-TR2):对 messages/content/reasoning/toolCalls/error 全字段应用——
|
|
115
|
+
* 字段名命中黑名单 → 遮蔽;字符串内容命中密钥形态(sk-/Bearer/-key=)→ 截断到
|
|
116
|
+
* 形态前 + 标记。数组/对象递归(消息 content 可为 parts 数组、toolCalls 嵌套)。 */
|
|
117
|
+
function redactValue(fieldKey, value) {
|
|
118
|
+
if (typeof value === "string") return redactSecret(fieldKey, value)
|
|
119
|
+
if (Array.isArray(value)) return value.map((v) => redactValue(fieldKey, v))
|
|
120
|
+
if (value && typeof value === "object") {
|
|
121
|
+
const out = {}
|
|
122
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactValue(k, v)
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 收集一次 chat 调用轨迹(D-TR1 字段集)——fire-and-forget(F-TR3)。
|
|
130
|
+
* 由 core.mjs chat() 导出出口调用(唯一采集点——N-TR2);签名零参数膨胀——
|
|
131
|
+
* 数据全部来自调用方已传入的 opts(logCtx 元数据——D-TR4)+ provider + result/error。
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} provider chat() 的 provider 参数(provider/model 字段来源)
|
|
134
|
+
* @param {Object} opts chat() 原始 opts —— messages(输入)与 logCtx(元数据:
|
|
135
|
+
* role/depth/kind/session/cwd/stage/turn/traces——调用点增补,D-TR4)
|
|
136
|
+
* @param {Object|null} result chatImpl 返回值(成功路径——content/reasoning/
|
|
137
|
+
* toolCalls/usage/finishReason;N-TR3:出口汇总——续写/重试后全量)
|
|
138
|
+
* @param {Error|null} error chat() 抛出的错误(失败路径——D-TR5:error(errText
|
|
139
|
+
* 截断 + 类别)+ finishReason:null——失败轨迹恰是分析纠结点最有效的材料)
|
|
140
|
+
*/
|
|
141
|
+
export function recordChatTrace(provider, opts = {}, result = null, error = null) {
|
|
142
|
+
if (!writeEnabled()) return
|
|
143
|
+
const logCtx = opts.logCtx ?? {}
|
|
144
|
+
if (!tracesEnabled(logCtx)) return
|
|
145
|
+
// cwd/session 经 logCtx 增补(调用点传 agent.cwd / agent._sessionStart——
|
|
146
|
+
// D-TR3 的 cwdHash/命名与 D-TR1 的 session 字段所需);无 agent 作用域的调用点
|
|
147
|
+
// 回退 process.cwd()——CLI 会话即工作区。该类点开关状态:distill 已含
|
|
148
|
+
// traces 开关(D-TR6 fix round1);auto-think 已闭环(D-TS12——chat 调用点
|
|
149
|
+
// logCtx 全字段补传:traces/session/cwd/role/depth/kind——无残留点)。
|
|
150
|
+
const cwd = logCtx.cwd ?? process.cwd()
|
|
151
|
+
// D-TR3(fix round1):分日按本地时区(_traceHooks.now——T-TR12 注入点);
|
|
152
|
+
// ts 与 dateStr 同源(同一时刻)——记录时间戳与目录日不撕裂。
|
|
153
|
+
const now = _traceHooks.now()
|
|
154
|
+
const dateStr = localDateStr(now)
|
|
155
|
+
const seq = nextTraceSeq(dateStr)
|
|
156
|
+
const sessionKey = traceSessionKey(cwd)
|
|
157
|
+
const record = {
|
|
158
|
+
ts: now.toISOString(),
|
|
159
|
+
session: logCtx.session ?? null,
|
|
160
|
+
cwdHash: createHash("sha1").update(normalizeCwd(cwd)).digest("hex"),
|
|
161
|
+
role: logCtx.role ?? null,
|
|
162
|
+
depth: logCtx.depth ?? null,
|
|
163
|
+
turn: logCtx.turn ?? null,
|
|
164
|
+
provider: provider?.name ?? provider?.model ?? "unknown",
|
|
165
|
+
model: provider?.model ?? "",
|
|
166
|
+
stage: logCtx.stage ?? null,
|
|
167
|
+
kind: logCtx.kind ?? null,
|
|
168
|
+
// D-TR1(fix round1):续写/重试链标记——true = 该调用是续写链的一环
|
|
169
|
+
// (core.mjs 续写递归传出);新调用缺省 false——round 字段已删(无来源)。
|
|
170
|
+
isContinuation: logCtx.isContinuation === true,
|
|
171
|
+
messages: redactValue("messages", opts.messages ?? []),
|
|
172
|
+
content: redactValue("content", result?.content ?? null),
|
|
173
|
+
reasoning: redactValue("reasoning", result?.reasoning ?? null),
|
|
174
|
+
toolCalls: redactValue("toolCalls", result?.toolCalls ?? null),
|
|
175
|
+
usage: result?.usage ?? null,
|
|
176
|
+
finishReason: result?.finishReason ?? null,
|
|
177
|
+
}
|
|
178
|
+
if (error != null) {
|
|
179
|
+
// D-TR5:错误路径轨迹——error(errText 截断 + 类别)+ finishReason:null
|
|
180
|
+
record.error = {
|
|
181
|
+
err: redactValue("err", errText(error, 500)),
|
|
182
|
+
kind: classifyErr(error, opts.signal),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 已在上面同步预留
|
|
186
|
+
// (原子号位——写盘在途并发不撞号);写盘异步(不 await——chat() 出口立即返回)。
|
|
187
|
+
// 返回值 = 落盘 promise(测试 await 用;chat() 不消费——fire-and-forget 语义)。
|
|
188
|
+
return (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const dir = tracesDirFor(dateStr)
|
|
191
|
+
await mkdir(dir, { recursive: true }) // D-TR3:写前建目录(与 sessions/tool-results 同惯例)
|
|
192
|
+
await appendFile(join(dir, `${sessionKey}-${seq}.jsonl`), JSON.stringify(record) + "\n", "utf8")
|
|
193
|
+
} catch {
|
|
194
|
+
// F-TR3:落盘失败静默降级——不抛错、不阻塞 chat() 返回
|
|
195
|
+
}
|
|
196
|
+
})()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* D-TR10(2026-09-05 用户裁定——发布隐私 + 磁盘卫生):启动清理——删除 traces 根下
|
|
201
|
+
* mtime 超过保留期的轨迹文件(保留期 = config.traces.retentionHours,默认 24h);
|
|
202
|
+
* 删空的日期目录(YYYY-MM-DD)。目录里非 .jsonl 文件不碰。CLI 启动点 fire-and-forget
|
|
203
|
+
* 调用(不 await——不阻塞启动——失败静默——与轨迹写盘同纪律)。返回删除文件数。
|
|
204
|
+
*/
|
|
205
|
+
export async function cleanupTraces({ dir = tracesRoot(), retentionHours = 24 } = {}) {
|
|
206
|
+
const cutoff = Date.now() - retentionHours * 3_600_000
|
|
207
|
+
let days
|
|
208
|
+
try { days = await readdir(dir) } catch { return 0 } // 目录不存在/不可读 → 无事可做
|
|
209
|
+
let removed = 0
|
|
210
|
+
for (const day of days) {
|
|
211
|
+
const dayDir = join(dir, day)
|
|
212
|
+
try { if (!(await stat(dayDir)).isDirectory()) continue } catch { continue }
|
|
213
|
+
let names
|
|
214
|
+
try { names = await readdir(dayDir) } catch { continue }
|
|
215
|
+
for (const n of names) {
|
|
216
|
+
if (!n.endsWith(".jsonl")) continue
|
|
217
|
+
try {
|
|
218
|
+
if ((await stat(join(dayDir, n))).mtimeMs < cutoff) { await unlink(join(dayDir, n)); removed++ }
|
|
219
|
+
} catch { /* 单个文件失败不影响其余 */ }
|
|
220
|
+
}
|
|
221
|
+
try { if ((await readdir(dayDir)).length === 0) await rmdir(dayDir) } catch {}
|
|
222
|
+
}
|
|
223
|
+
return removed
|
|
224
|
+
}
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -7,35 +7,79 @@
|
|
|
7
7
|
* + flushStream)在 tool-events.mjs buildToolCallbacks;子agent 区块缓冲与完成冻结
|
|
8
8
|
* (routeSub* / finishSubTask / freeze*SubTasks)在 subagent-blocks.mjs;标题生成
|
|
9
9
|
* 在 generate-title.mjs ensureSessionTitle。
|
|
10
|
+
*
|
|
11
|
+
* 2026-09-05 module-split:§17 挂起会话段(suspensionSession/digestTurn/poolLive/
|
|
12
|
+
* poolCounts 等——agent-turn.mjs 535 > 500 硬限)verbatim 迁至 suspension-drive.mjs——
|
|
13
|
+
* 本文件回合尾经 suspensionSession 进入驱动器;驱动器内 digest/用户回合经 runAgentTurn
|
|
14
|
+
* 递归回本文件(函数级静态环——模块求值期无顶层调用,环安全——session-slots ↔
|
|
15
|
+
* session.mjs 同款先例)。
|
|
10
16
|
*/
|
|
11
17
|
import { runAgent, ContinueError } from "../agent.mjs"
|
|
12
18
|
import { saveSession } from "../session.mjs"
|
|
13
19
|
import { ansi, C } from "./ansi.mjs"
|
|
14
20
|
import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
|
|
15
|
-
import { freezeAllSubTasks } from "./subagent-blocks.mjs"
|
|
21
|
+
import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 §17 段迁 suspension-drive.mjs
|
|
16
22
|
import { ensureSessionTitle } from "../generate-title.mjs"
|
|
23
|
+
import { logEvent, errText } from "../log.mjs"
|
|
24
|
+
import { suspensionSession, poolLive, planQueuedInput } from "./suspension-drive.mjs"
|
|
17
25
|
|
|
18
26
|
/** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
|
|
19
27
|
* wait at most this long for the in-flight distill before the final session save —
|
|
20
28
|
* never let shutdown hang on the background summary call. */
|
|
21
29
|
const DISTILL_FLUSH_TIMEOUT_MS = 5000
|
|
22
30
|
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
31
|
+
/**
|
|
32
|
+
* LOGGING(docs/design/LOGGING.md)包装:回合骨架事件(turn:start/turn:end——kind
|
|
33
|
+
* user/auto;result ok/stopped/error)。内层经 _logOutcome 载具回传终止原因(中止/
|
|
34
|
+
* turn-cap 拒绝/错误 vs 正常完成)——嵌套回合(队列递归/挂起会话内 digest 轮)各自
|
|
35
|
+
* 新开载具(每次包装调用独立),互不串扰。err:internal = 逃出内层的未分类异常。
|
|
36
|
+
*/
|
|
37
|
+
export async function runAgentTurn(ctx, text, opts = {}) {
|
|
38
|
+
const kind = opts?.autoTurn ? "auto" : "user"
|
|
39
|
+
const runOpts = { ...(opts ?? {}) }
|
|
40
|
+
delete runOpts._logOutcome
|
|
41
|
+
const carrier = {}
|
|
42
|
+
runOpts._logOutcome = carrier
|
|
43
|
+
const t0 = Date.now()
|
|
44
|
+
logEvent("turn:start", { kind })
|
|
45
|
+
try {
|
|
46
|
+
const result = await runAgentTurnInner(ctx, text, runOpts)
|
|
47
|
+
return result
|
|
48
|
+
} catch (e) {
|
|
49
|
+
carrier.result = "error"
|
|
50
|
+
logEvent("err:internal", { msg: errText(e, 200), where: "runAgentTurn" })
|
|
51
|
+
throw e
|
|
52
|
+
} finally {
|
|
53
|
+
logEvent("turn:end", { kind, ms: Date.now() - t0, result: carrier.result ?? "ok" })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** runAgentTurn 本体(LOGGING 包装之外——见上方包装器)。 */
|
|
58
|
+
async function runAgentTurnInner(ctx, text, opts) {
|
|
59
|
+
const { autoTurn = false, skipSession = false } = opts ?? {}
|
|
29
60
|
const { agent, state, pushLine, pushLabel, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, handleSlash } = ctx
|
|
30
61
|
// 可注入覆盖(测试用);默认走真实实现
|
|
31
62
|
const runAgentImpl = ctx.runAgent ?? runAgent
|
|
32
63
|
const saveSessionImpl = ctx.saveSession ?? saveSession
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
// autoTurn(消化轮):无用户输入——不画 "❯ You:"(系统驱动回合,§17 D-S6)
|
|
65
|
+
if (!autoTurn) {
|
|
66
|
+
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
67
|
+
pushLine(text, C.text)
|
|
68
|
+
}
|
|
35
69
|
|
|
36
70
|
ctx.assistantLabeled = false
|
|
37
71
|
state.processing = true
|
|
38
72
|
state.status = "Processing..."
|
|
73
|
+
// §17.6(advisor round2 🟡):回合启动即解除空闲退出武装——exitArmed 只属于空闲态
|
|
74
|
+
// 双确认,跨回合残留会让"停回合后的 armed 落空穿透 + 陈旧 exitArmed"组合把一次
|
|
75
|
+
// 意图为全停/退出的按下变成无二次确认的即时退出(key-handler 落空分支落回空闲
|
|
76
|
+
// 分支时 `!state.exitArmed` 判假即 exit);计时一并清(key-handler 空闲分支按
|
|
77
|
+
// ctx.exitArmTimer 有无决定是否重建——陈旧 timer 残留会在回合中途复位新武装)。
|
|
78
|
+
state.exitArmed = false
|
|
79
|
+
if (ctx.exitArmTimer) {
|
|
80
|
+
clearTimeout(ctx.exitArmTimer)
|
|
81
|
+
ctx.exitArmTimer = null
|
|
82
|
+
}
|
|
39
83
|
state.streaming = ""
|
|
40
84
|
state.reasoning = ""
|
|
41
85
|
state._advisorBlocks = []
|
|
@@ -46,7 +90,27 @@ export async function runAgentTurn(ctx, text) {
|
|
|
46
90
|
// bounded by the N2 per-child 500-line ring buffer.
|
|
47
91
|
state.currentTool = null
|
|
48
92
|
state.processingStarted = Date.now()
|
|
49
|
-
|
|
93
|
+
// §17 偏差修复 #3(会话 abort 全覆盖):回合链 controller 登记。链头 = 非挂起会话内
|
|
94
|
+
// 且非释放窗口期(suspended/_suspPending 均 false)开启的回合——登记表清零;队列
|
|
95
|
+
// 递归回合(_suspPending 置位期)与会话内回合(suspended=true)继续累积。链条内每次
|
|
96
|
+
// 重建(Ctrl+I 续跑 / ContinueError 续跑 / AUTO 续跑)都登记——挂起会话的 abort 集合
|
|
97
|
+
// (_sessionAbortAll)必须在会话建立时覆盖进入会话以来的全部 controller:只 abort
|
|
98
|
+
// 最后一个会让旧 controller 下 spawn 的 async children 逃逸中止(偏差 #3)。
|
|
99
|
+
// 链头同时清掉上一链条残留的会话句柄(上一链 pool 先 live 后耗尽、未进入会话即结束
|
|
100
|
+
// 时 _sessionAbort 会滞留——不清理则下一链的会话 signal 指向旧 controller,Ctrl+C
|
|
101
|
+
// 中止集合漏掉会话期 spawn 的 children)。
|
|
102
|
+
if (!state.suspended && !state._suspPending) {
|
|
103
|
+
state._turnControllers = []
|
|
104
|
+
agent._sessionAbort = null
|
|
105
|
+
agent._sessionAbortAll = null
|
|
106
|
+
}
|
|
107
|
+
state._turnControllers ??= []
|
|
108
|
+
const makeController = () => {
|
|
109
|
+
const c = new AbortController()
|
|
110
|
+
state._turnControllers.push(c)
|
|
111
|
+
return c
|
|
112
|
+
}
|
|
113
|
+
state.controller = makeController()
|
|
50
114
|
state.interruptPrompt = null
|
|
51
115
|
// Refresh status bar every second during processing; also refresh when any
|
|
52
116
|
// subagent block is still running so its header elapsed ticks (§7.2 D4 —
|
|
@@ -68,24 +132,55 @@ export async function runAgentTurn(ctx, text) {
|
|
|
68
132
|
try {
|
|
69
133
|
for (let resume = false; ; resume = true) {
|
|
70
134
|
try {
|
|
71
|
-
await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume })
|
|
135
|
+
await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume, autoTurn, suspDriven: true })
|
|
72
136
|
flushStream()
|
|
73
137
|
break // Normal completion, exit loop
|
|
74
138
|
} catch (error) {
|
|
75
139
|
flushStream()
|
|
76
140
|
if (error.name === "AbortError" || state.controller?.signal.aborted) {
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
141
|
+
const reason = state.controller?.signal?.reason
|
|
142
|
+
// §17.6 D-C1(round1 #1 区分机制——2026-09-03):interrupt 两种语义——
|
|
143
|
+
// 有 message(Ctrl+I 注入——key-modes handleInterruptMode)= 重建 controller
|
|
144
|
+
// 续跑(既有语义——agent loop 已把消息注入 history,中止的 signal 不能重试);
|
|
145
|
+
// 无 message(Ctrl+C 首按停回合——key-handler abort({ interrupt: true })
|
|
146
|
+
// 不带 message——非挂起 processing 态 / 挂起态 digest·会话内回合)
|
|
147
|
+
// = 停回合不续跑——池保留由 agent.mjs 回合收尾的 !interrupt 清池条件排除
|
|
148
|
+
// 实现(D-C2——agent.mjs 零改动)。
|
|
149
|
+
if (reason?.interrupt && reason?.message) {
|
|
150
|
+
state.controller = makeController()
|
|
82
151
|
resume = true
|
|
83
152
|
continue
|
|
84
153
|
}
|
|
154
|
+
if (reason?.interrupt) {
|
|
155
|
+
// 无 message interrupt 的注入副作用回滚:agent.mjs 中断三段(chat catch /
|
|
156
|
+
// response.interrupted / 工具执行中断)无条件把 "[User interrupt: <msg>]"
|
|
157
|
+
// 落 history,对 message 存在性无守卫——无 message 时成为 "[User interrupt:
|
|
158
|
+
// undefined]" 垃圾上下文(消息注入语义只属于 Ctrl+I 续跑——停回合无注入
|
|
159
|
+
// 消息)。D-C2 agent.mjs 零改动约束下在回合层回滚尾部垃圾(确定性:中断
|
|
160
|
+
// 注入恒为最后一条——break 前 history 不再追加)。partial 部分输出不回滚
|
|
161
|
+
// ——interrupt 家族语义(§2 Ctrl+I 同款"提交部分输出")——advisor round1
|
|
162
|
+
// 🟡 裁定:回滚需区分工具/子代理路径的既有完整消息(history 层不可靠)。
|
|
163
|
+
const h = agent.history
|
|
164
|
+
while (h?.length > 0 && String(h.at(-1)?.content ?? "") === "[User interrupt: undefined]") h.pop()
|
|
165
|
+
}
|
|
85
166
|
pushLine("[stopped]", C.warn)
|
|
167
|
+
if (opts?._logOutcome) opts._logOutcome.result = "stopped"
|
|
86
168
|
break
|
|
87
169
|
}
|
|
88
170
|
if (error instanceof ContinueError) {
|
|
171
|
+
if (autoTurn) {
|
|
172
|
+
// §17 digest turn-cap 规则(D-S9 ContinueError 行):无面板——AUTO 档按
|
|
173
|
+
// §2 统一规则自动 resume(无人值守授权);手动档静默拒绝(部分消化留在
|
|
174
|
+
// 历史,会话回挂起——结果不丢,只是不再烧轮次)。
|
|
175
|
+
if (agent.autoApprove) {
|
|
176
|
+
pushLine("[auto-turn: continuing past turn cap…]", C.dim)
|
|
177
|
+
state.controller = makeController()
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
pushLine(`[auto-turn stopped at ${error.turn} turns — partial digest; finished reports stay in history]`, C.warn)
|
|
181
|
+
if (opts?._logOutcome) opts._logOutcome.result = "stopped"
|
|
182
|
+
break
|
|
183
|
+
}
|
|
89
184
|
pushLabel(`❯ Continue`, ansi.bold + C.warn)
|
|
90
185
|
pushLine(`Ran ${error.turn} turns (limit ${error.turn}). Continue?`, C.warn)
|
|
91
186
|
// Pause to ask: reuse permission mechanism
|
|
@@ -101,14 +196,16 @@ export async function runAgentTurn(ctx, text) {
|
|
|
101
196
|
state.permission = null
|
|
102
197
|
if (!willContinue) {
|
|
103
198
|
pushLine("[continue cancelled]", C.warn)
|
|
199
|
+
if (opts?._logOutcome) opts._logOutcome.result = "stopped"
|
|
104
200
|
break
|
|
105
201
|
}
|
|
106
202
|
pushLine("[continuing…]", C.tool)
|
|
107
203
|
// Recreate AbortController: once aborted, resume immediately fails (defensive; current path unreachable but tightly coupled)
|
|
108
|
-
state.controller =
|
|
204
|
+
state.controller = makeController()
|
|
109
205
|
continue
|
|
110
206
|
}
|
|
111
207
|
pushLine(`[error] ${error.message}`, C.error)
|
|
208
|
+
if (opts?._logOutcome) opts._logOutcome.result = "error"
|
|
112
209
|
break
|
|
113
210
|
}
|
|
114
211
|
}
|
|
@@ -116,10 +213,40 @@ export async function runAgentTurn(ctx, text) {
|
|
|
116
213
|
clearInterval(ticker)
|
|
117
214
|
state.processing = false
|
|
118
215
|
state._advisorBlocks = []
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
216
|
+
// §17 D-S1/D-S8:回合正常结束且后台池仍 live → 挂起会话:子agent 区块保持 live
|
|
217
|
+
// (不冻结——各 settle 事件自行处理),本次回合 controller 交会话层作 abort 句柄
|
|
218
|
+
// (挂起期 Ctrl+C 中止全部后台子代理);池空 / 中断 / 错误 → 现状 freezeAllSubTasks
|
|
219
|
+
// (中断态块标 interrupted;正常态块已在 settle 时各自冻结)。
|
|
220
|
+
const willSuspend = poolLive(agent)
|
|
221
|
+
// §17.5.5:挂起会话内回合(digest/会话内用户回合——skipSession 且 suspended)的
|
|
222
|
+
// 收尾**不冻结驻留块**——已消化(pinned 且条目已注入)块由 suspensionSession 在
|
|
223
|
+
// run 返回后 freezeReclaimDigestedBlocks 逐条回收(settle 锚点 splice——digest 总览文本
|
|
224
|
+
// 之前——round1 #1 裁定,不等池空);
|
|
225
|
+
// 未消化残项由会话退出 freezeAllSubTasks 兜底。若无此例外,池空的 digest 回合会在
|
|
226
|
+
// finally 抢先按 settle 锚点冻结(旧池空补发语义)——回收时序被抢占(T-S6)。
|
|
227
|
+
const inSessionTurn = skipSession && state.suspended
|
|
228
|
+
if (!willSuspend && !inSessionTurn) {
|
|
229
|
+
// Interrupted runs (Ctrl+C abort / error mid-turn): still-running child blocks
|
|
230
|
+
// would linger as pinned ghosts above the input box — freeze them like normal
|
|
231
|
+
// completions so the trace scrolls away with the conversation (2026-08-30).
|
|
232
|
+
freezeAllSubTasks(state)
|
|
233
|
+
} else if (!agent._sessionAbort) {
|
|
234
|
+
agent._sessionAbort = state.controller // 会话 abort 句柄(children 共享此 signal)
|
|
235
|
+
}
|
|
236
|
+
// §17 偏差修复 #1(回合释放窗口守卫):willSuspend 判定后、进入任何 await 之前置位
|
|
237
|
+
// 挂起待定标志。suspensionSession 真正启动前还有真实 await(ensureSessionTitle /
|
|
238
|
+
// distill flush ≤5s / saveSession),其间 processing=false 且 suspended 尚未置位——
|
|
239
|
+
// 无此标志 key-handler Enter 会走 submit 并发开第二个 runAgentTurn(双驱动器竞态:
|
|
240
|
+
// 两个 runAgent 循环同时 pushReal 同一 agent.history,工具配对/上下文交错)。标志
|
|
241
|
+
// 有效区间 = 释放窗口(suspensionSession 启动即清除);会话内回合(skipSession)
|
|
242
|
+
// 由 state.suspended 覆盖;_suspAborted 时挂起会话不会启动(下方会话入口同条件)。
|
|
243
|
+
state._suspPending = willSuspend && !skipSession && !state._suspAborted
|
|
244
|
+
// §17 偏差修复 #3(会话 abort 全覆盖):abort 集合快照 = 本链条内全部 controller
|
|
245
|
+
// (含 Ctrl+I/ContinueError 重建的旧 controller——其下 spawn 的 async children 持旧
|
|
246
|
+
// signal,Ctrl+C 只 abort 最后一个会让它们跑完整个 turn 预算,正常完成后仍
|
|
247
|
+
// mergeChildMutations 写父 guard 标记,绕过 advisor/verify 门)。会话内回合的 finally
|
|
248
|
+
// 会重复快照(链条累积),幂等。
|
|
249
|
+
if (willSuspend) agent._sessionAbortAll = [...(state._turnControllers ?? [])]
|
|
123
250
|
// Tool-block carriers get the same sweep (P0-2, 2026-08-30 consult): without
|
|
124
251
|
// an onToolResult their header would say "running" forever; ticks are cleared
|
|
125
252
|
// so no stale start time leaks into the next turn.
|
|
@@ -156,17 +283,42 @@ export async function runAgentTurn(ctx, text) {
|
|
|
156
283
|
render()
|
|
157
284
|
}
|
|
158
285
|
|
|
159
|
-
//
|
|
286
|
+
// §17 偏差 #1 兜底:释放窗口期 Enter 已入 pendingInput——链条走到此处若池已空
|
|
287
|
+
//(挂起会话不会启动,下方 while 是最后一个消费点)则转正队列照常续发,消息不滞留
|
|
288
|
+
// 不并发。池非空时挂起会话先消费 pendingInput(D-S5 输入优先),无需此处处理。
|
|
289
|
+
if (!skipSession && !state._suspAborted && (state.pendingInput?.length ?? 0) > 0 && !poolLive(agent)) {
|
|
290
|
+
state.queue.push(...state.pendingInput.splice(0).map((t) => ({ text: String(t) })))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Queued messages: auto-process next one(§24 D-24c/R15:攒批合并——合并仅限
|
|
294
|
+
// 连续文本——/cmd 逐条保序即时——单条超长直发;每条 runAgentTurn 回合后余项
|
|
295
|
+
// 由递归层的本循环续取——边界幂等不丢)
|
|
160
296
|
while (state.queue.length > 0 && !state.processing) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
297
|
+
const head = planQueuedInput(state.queue.map((q) => q.text))[0]
|
|
298
|
+
state.queue.splice(0, head.count)
|
|
299
|
+
// Queued slash commands execute directly — order-preserved, never merged
|
|
300
|
+
if (head.kind === "slash") {
|
|
301
|
+
await handleSlash(head.text)
|
|
165
302
|
render()
|
|
166
303
|
continue
|
|
167
304
|
}
|
|
168
305
|
pushLabel(`❯ You: (from queue)`, ansi.bold + C.user)
|
|
169
|
-
await runAgentTurn(ctx,
|
|
306
|
+
await runAgentTurn(ctx, head.text)
|
|
170
307
|
return
|
|
171
308
|
}
|
|
309
|
+
|
|
310
|
+
// §17 D-S2: 回合尾后台池非空 → 挂起会话(D-S9 状态机——输入放开、settle 驱动
|
|
311
|
+
// auto-turn 消化、池空自然退出回 idle)。skipSession:会话内回合(消化轮/会话内
|
|
312
|
+
// 用户回合)由 suspensionSession 统一调度,不再递归进入新会话。
|
|
313
|
+
if (!skipSession && poolLive(agent) && !state._suspAborted) {
|
|
314
|
+
await suspensionSession(ctx)
|
|
315
|
+
// §17 round2 偏差 #1(_suspAborted 粘滞,code review round2 #1):挂起会话退出
|
|
316
|
+
// (自然耗尽或 Ctrl+C 中止)即复位中止标志——中止 unwind 已完成、池已清空/耗尽,
|
|
317
|
+
// 复位不会误触发重入。不清则 _suspPending 守卫与下方会话入口被同一标志永久门控:
|
|
318
|
+
// 中止后用户再 spawn async、回合尾池再 live 时永不再次进入挂起态(suspensionSession
|
|
319
|
+
// 首行复位成死代码)——子代理结果退化到"下个回合尾才注入"、状态行/自动消化/区块
|
|
320
|
+
// 驻留全消失;释放窗口守卫同步失效(Enter 并发开第二个 runAgentTurn——双驱动器
|
|
321
|
+
// 竞态复现)。
|
|
322
|
+
state._suspAborted = false
|
|
323
|
+
}
|
|
172
324
|
}
|
package/src/tui/clipboard.mjs
CHANGED
|
@@ -71,17 +71,28 @@ function spawnWait(spawn, cmd, args, stdinText) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Insert pasted text into the active text target.
|
|
74
|
-
* Free-text question active →
|
|
74
|
+
* Free-text question active → splice into its answer at cursor (codepoint array; \r\n runs
|
|
75
|
+
* collapse to spaces — single-line field invariant, text preserved).
|
|
75
76
|
* Options question active → ignore (no text field; must not leak into the input box).
|
|
76
77
|
* Otherwise → splice into the main input box at cursor (newlines kept, tabs → 2 spaces).
|
|
77
78
|
* Shared by bracketed paste (stdin data handler) and Ctrl+V clipboard read,
|
|
78
|
-
* so pasted content lands in the same place regardless of how the terminal delivered it.
|
|
79
|
-
|
|
79
|
+
* so pasted content lands in the same place regardless of how the terminal delivered it.
|
|
80
|
+
* activeQuestion(异步 Ctrl+V 路径传发起时的 q):目标 question 已关闭/被换(Esc 中止后
|
|
81
|
+
* readClipboardText 才 resolve)→ 整段丢弃——不得落主输入框(审计 F1 竞态守卫)。 */
|
|
82
|
+
export function insertPastedText(state, rawText, activeQuestion = null) {
|
|
80
83
|
if (!rawText) return
|
|
84
|
+
if (activeQuestion !== null && state.question !== activeQuestion) return // stale async paste(Esc/Enter 后到达)
|
|
81
85
|
const q = state.question
|
|
82
86
|
if (q) {
|
|
83
87
|
if (q.options.length > 0) return
|
|
84
|
-
|
|
88
|
+
// 自由文本态(TUI-INPUT-BOX.md §7.2 round2 #1/#2/#9):answer 是 codepoint 数组——
|
|
89
|
+
// 粘贴落 cursor 位置;\r\n/\r 折叠为空格、\t → 2 空格(单行不变式硬守卫:不劈行、
|
|
90
|
+
// 不吞文本——粘贴文本保字)。options 态无文本字段——不落输入框。
|
|
91
|
+
if (!Array.isArray(q.answer)) q.answer = q.answer ? [...q.answer] : []
|
|
92
|
+
const cur = Math.max(0, Math.min(q.cursor ?? q.answer.length, q.answer.length))
|
|
93
|
+
const chars = [...rawText.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n+/g, " ").replace(/\t/g, " ")]
|
|
94
|
+
q.answer.splice(cur, 0, ...chars)
|
|
95
|
+
q.cursor = cur + chars.length
|
|
85
96
|
return
|
|
86
97
|
}
|
|
87
98
|
// IKBU3J (2026-08-28):Ctrl+I 注入框激活时,粘贴进注入文本(与按键路径同语义:去换行保单行),
|