thincoder 0.12.61 → 0.12.63
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 +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
package/src/token-ttl.mjs
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* token-ttl.mjs — design-token TTL 语义 + 槽位清理 + 会话槽权威台账 I/O(R16——2026-09-06;
|
|
3
|
-
* DESIGN-TOKEN-SETTLEMENT D1-D3——2026-09-08)。
|
|
4
|
-
*
|
|
5
|
-
* 共享目标(D-R16b):过期判定语义在此定义一次,validateDesignToken
|
|
6
|
-
* (agent-tools/design-token.mjs)、恢复过滤(session.mjs applySession)、开模式清理
|
|
7
|
-
* (agent-tools/eng.mjs + tui/cmd-eng.mjs ON 路径)、spawn 门禁过期拒删槽
|
|
8
|
-
* (subagent-spawn.mjs)共引。
|
|
9
|
-
*
|
|
10
|
-
* token 格式 = `uuid:expiresAt`(无签名流程凭证——HMAC 防伪层已删——见
|
|
11
|
-
* ENGINEERING-MODE.md 2026-09-06 段)。过期判定只对**格式合法**的 token 判
|
|
12
|
-
* 过期:格式/畸形串不在此清理(恢复时读回由门禁格式拒、门禁拒时也不删槽——
|
|
13
|
-
* 防误删有效槽——F-R16b ①③)。
|
|
14
|
-
*
|
|
15
|
-
* DESIGN-TOKEN-SETTLEMENT(2026-09-08):
|
|
16
|
-
* - **D1**:persistEngTokens = settle 当场落盘的可复用函数(engTokenSlotFields 序列化 +
|
|
17
|
-
* session 安全写/轮转——guardForeignSlotFile 与 saveSession 共用同一份守卫)。
|
|
18
|
-
* - **D2/D3**:readEngTokensFromSlot / reconcileEngTokensFromSlot = 门禁 miss 回读权威
|
|
19
|
-
* 槽(spawn 门 D2 与 dispatch 写门 D3 同源 reconcile)。
|
|
20
|
-
* - **D3**:**单值镜像 `_engDesignToken` 退役**——AC3 零写 + 仅一次性迁移读。本模块
|
|
21
|
-
* 不再写镜像(engTokenSlotFields 只产多槽表);restoreEngTokens 是唯一迁移读点(旧
|
|
22
|
-
* slot 残留 engDesignToken → 迁入 Map,此后不写不读镜像)。
|
|
23
|
-
*
|
|
24
|
-
* 依赖方向:本模块只 import 底层槽 I/O(session-slots.mjs 原语 + session-guard.mjs 轮转
|
|
25
|
-
* 守卫)——session.mjs 同时 import 本模块(engTokenSlotFields/restoreEngTokens),形成
|
|
26
|
-
* 静态环,与既有的 session ↔ session-slots 环同构(函数声明实例化期已初始化、只函数体
|
|
27
|
-
* 内运行时使用——环安全)。
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
31
|
-
import {
|
|
32
|
-
slotPath, writeSessionFile, activeSlot, loadManifest, saveManifest, slotDigest,
|
|
33
|
-
writeEndMarker,
|
|
34
|
-
} from "./session-slots.mjs"
|
|
35
|
-
// F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
|
|
36
|
-
import { guardForeignSlotFile } from "./session-guard.mjs"
|
|
37
|
-
|
|
38
|
-
const DESIGN_TOKEN_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
39
|
-
|
|
40
|
-
/** 格式校验 + 数值过期时刻:格式合法 → expiresAt 数值;格式/畸形 → null
|
|
41
|
-
* (与 validateDesignToken 的 fail-closed 判定同源——单一权威)。 */
|
|
42
|
-
export function tokenExpiryMs(token) {
|
|
43
|
-
if (!token || typeof token !== "string") return null
|
|
44
|
-
const parts = token.split(":")
|
|
45
|
-
if (parts.length !== 2) return null
|
|
46
|
-
const [uuid, expiresAt] = parts
|
|
47
|
-
if (!DESIGN_TOKEN_UUID_RE.test(uuid)) return null
|
|
48
|
-
if (!/^\d+$/.test(expiresAt)) return null
|
|
49
|
-
const expiry = parseInt(expiresAt, 10)
|
|
50
|
-
if (isNaN(expiry)) return null
|
|
51
|
-
return expiry
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 过期判定——仅格式合法的 token 可判过期(畸形 → false,由门禁格式拒)。 */
|
|
55
|
-
export function tokenExpired(token, now = Date.now()) {
|
|
56
|
-
const expiry = tokenExpiryMs(token)
|
|
57
|
-
return expiry !== null && now > expiry
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 删一个 designId 槽(R16 D-R16c ③——spawn 门禁过期拒的清理面)。
|
|
61
|
-
* 条件由调用方保证(槽在位且槽值 === token 且已过期):Map 无该 id / 值不符 →
|
|
62
|
-
* 不删(防误删有效槽);designId 缺省(单槽省略 designId 路径)→ 按 token 扫删。
|
|
63
|
-
* DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像已退役——不再同步清镜像。
|
|
64
|
-
* 返回是否删了槽。 */
|
|
65
|
-
export function removeDesignTokenSlot(agent, designId, token) {
|
|
66
|
-
const map = agent?._engDesignTokens
|
|
67
|
-
let slotRemoved = false
|
|
68
|
-
if (map instanceof Map && token) {
|
|
69
|
-
if (designId) {
|
|
70
|
-
if (map.get(designId) === token) {
|
|
71
|
-
map.delete(designId)
|
|
72
|
-
slotRemoved = true
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
for (const [id, t] of map) {
|
|
76
|
-
if (t === token) {
|
|
77
|
-
map.delete(id)
|
|
78
|
-
slotRemoved = true
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return slotRemoved
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** 遍历删过期槽(R16 F-R16b ②——eng enter / cmd-eng ON 真转换路径):Map 只留
|
|
88
|
-
* 有效 token。DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像已退役——无
|
|
89
|
-
* legacy 镜像分支。返回清理个数。 */
|
|
90
|
-
export function purgeExpiredDesignTokens(agent) {
|
|
91
|
-
const map = agent?._engDesignTokens
|
|
92
|
-
let cleared = 0
|
|
93
|
-
if (map instanceof Map) {
|
|
94
|
-
for (const [id, t] of [...map]) {
|
|
95
|
-
if (typeof t === "string" && tokenExpired(t)) {
|
|
96
|
-
map.delete(id)
|
|
97
|
-
cleared++
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return cleared
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── 会话槽序列化/恢复面(session.mjs saveSession/applySession 的 token 字段——
|
|
105
|
-
// 2026-09-06 R16:slot 持久化 = 跨重启/跨模式恢复的有意载体) ──
|
|
106
|
-
|
|
107
|
-
/** saveSession 数据面:token 的序列化形态。多槽 Map → {designId: token}
|
|
108
|
-
* (JSON-safe);空/缺 Map → undefined → JSON.stringify 丢 key——清过的会话写
|
|
109
|
-
* NO field,不从上次 save 复活槽。
|
|
110
|
-
* DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `engDesignToken` 退役——
|
|
111
|
-
* saveSession 不再写镜像字段(AC3 零写);只产多槽表 engDesignTokens。 */
|
|
112
|
-
export function engTokenSlotFields(agent) {
|
|
113
|
-
return {
|
|
114
|
-
engDesignTokens: agent._engDesignTokens instanceof Map && agent._engDesignTokens.size > 0
|
|
115
|
-
? Object.fromEntries(agent._engDesignTokens)
|
|
116
|
-
: undefined,
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** applySession 恢复面(F-R16b ①——恢复 TTL 过滤):EXPIRED token 不读回(丢弃——
|
|
121
|
-
* 下次 save 自然清字段,清盘闭环);格式/畸形串读回(门禁拒——不主动删——恢复不
|
|
122
|
-
* 得销毁它无法判定的槽数据)。
|
|
123
|
-
* DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
|
|
124
|
-
* 本函数是**唯一镜像迁移读点**(AC3):旧 slot 文件可能残留 engDesignToken 单值
|
|
125
|
-
* (D3 前 saveSession 写的 legacy 字段)。当多槽表缺失/为空(Map 空)且残留镜像为
|
|
126
|
-
* **格式合法且未过期** token 时 → 一次性迁入 Map(标 legacy——以 token 自身 uuid
|
|
127
|
-
* 为 designId——确定性、可复现、不依赖曾回显的原 designId)。此后该字段随下次
|
|
128
|
-
* saveSession/persistEngTokens(不再写镜像)自然从槽文件消失,本函数不再读到
|
|
129
|
-
* 残留——一次性语义。镜像若已过期/畸形 → 不迁移(drop——与 Map 过期项同处理,
|
|
130
|
-
* 清盘闭环)。永不写 agent._engDesignToken。 */
|
|
131
|
-
export function restoreEngTokens(agent, data) {
|
|
132
|
-
const map = new Map()
|
|
133
|
-
if (data.engDesignTokens && typeof data.engDesignTokens === "object" && !Array.isArray(data.engDesignTokens)) {
|
|
134
|
-
for (const [id, t] of Object.entries(data.engDesignTokens)) {
|
|
135
|
-
if (!(typeof t === "string" && tokenExpired(t))) map.set(id, t)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// legacy 单值镜像一次性迁移读(唯一镜像读点——AC3):Map 空且残留为格式合法未过期 token
|
|
139
|
-
if (map.size === 0) {
|
|
140
|
-
const storedMirror = data.engDesignToken
|
|
141
|
-
if (typeof storedMirror === "string" && tokenExpiryMs(storedMirror) !== null && !tokenExpired(storedMirror)) {
|
|
142
|
-
map.set(storedMirror.split(":")[0], storedMirror)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (map.size > 0) agent._engDesignTokens = map
|
|
146
|
-
else delete agent._engDesignTokens
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ── 会话槽权威台账 I/O(DESIGN-TOKEN-SETTLEMENT D1/D2/D3——槽文件 = 权威结算台账) ──
|
|
150
|
-
|
|
151
|
-
/** 当前会话槽号(读侧——无认领副作用):粘性 _slot 优先;未钉槽时读 manifest active
|
|
152
|
-
* (loadManifest 只读——不在门禁路径触发认领写)。无槽 → null。 */
|
|
153
|
-
function currentSlotNoReadOnly(agent) {
|
|
154
|
-
const slot = agent?._slot
|
|
155
|
-
if (Number.isInteger(slot)) return slot
|
|
156
|
-
const active = loadManifest(agent?.cwd)?.active
|
|
157
|
-
return Number.isInteger(active) ? active : null
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** 读当前会话槽文件的权威台账(engDesignTokens 多槽表——不做 TTL 过滤,判定由调用方)。
|
|
161
|
-
* 门禁侧轻读:JSON.parse 只读不改文件(.tmp 回退/改名等 loadSlotFile 副作用不引入)。
|
|
162
|
-
* 单值镜像 engDesignToken 字段已退役——此处不读(AC3——运行时零镜像读;唯一镜像
|
|
163
|
-
* 迁移读在 restoreEngTokens)。返回 {designId: token} 对象或 null(无槽/无文件/不可读)。 */
|
|
164
|
-
export function readEngTokensFromSlot(agent) {
|
|
165
|
-
try {
|
|
166
|
-
const slot = currentSlotNoReadOnly(agent)
|
|
167
|
-
if (slot == null) return null
|
|
168
|
-
const p = slotPath(agent.cwd, slot)
|
|
169
|
-
if (!existsSync(p)) return null
|
|
170
|
-
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
171
|
-
return (data && typeof data === "object" && !Array.isArray(data.engDesignTokens)
|
|
172
|
-
&& data.engDesignTokens && typeof data.engDesignTokens === "object")
|
|
173
|
-
? data.engDesignTokens
|
|
174
|
-
: null
|
|
175
|
-
} catch { return null }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** 内存 miss 时从权威槽 reconcile(D2 spawn 门 + D3 dispatch 写门同源):把槽文件中
|
|
179
|
-
* **未过期**项并进内存 Map(TTL 过滤保留——expired 不并入,fail-closed;格式/畸形串
|
|
180
|
-
* 与 restoreEngTokens 同语义读回——门禁格式拒);槽无活项 → 内存原样。返回判定用 Map。 */
|
|
181
|
-
export function reconcileEngTokensFromSlot(agent) {
|
|
182
|
-
const slots = agent?._engDesignTokens
|
|
183
|
-
const obj = readEngTokensFromSlot(agent)
|
|
184
|
-
if (!obj) return slots instanceof Map ? slots : new Map()
|
|
185
|
-
const live = new Map()
|
|
186
|
-
for (const [id, t] of Object.entries(obj)) {
|
|
187
|
-
if (typeof t === "string" && !tokenExpired(t)) live.set(id, t)
|
|
188
|
-
}
|
|
189
|
-
if (live.size === 0) return slots instanceof Map ? slots : new Map()
|
|
190
|
-
const merged = slots instanceof Map ? slots : new Map()
|
|
191
|
-
for (const [id, t] of live) merged.set(id, t)
|
|
192
|
-
agent._engDesignTokens = merged
|
|
193
|
-
return merged
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** 是否存在任一活槽(DESIGN-TOKEN-SETTLEMENT D3——dispatch 写门判定"任一活槽存在";
|
|
197
|
-
* AC4)。先问内存 Map(当前进程活缓存);miss 回读权威槽文件 reconcile(与 spawn 门
|
|
198
|
-
* D2 同源)。判定 fail-closed:格式合法且未过期才算活槽(畸形/过期不构成活槽——不
|
|
199
|
-
* 授权产品代码写)。 */
|
|
200
|
-
export function anyLiveDesignSlot(agent) {
|
|
201
|
-
const live = (m) => {
|
|
202
|
-
for (const t of m.values()) {
|
|
203
|
-
if (typeof t === "string" && tokenExpiryMs(t) !== null && !tokenExpired(t)) return true
|
|
204
|
-
}
|
|
205
|
-
return false
|
|
206
|
-
}
|
|
207
|
-
const m = agent?._engDesignTokens
|
|
208
|
-
if (m instanceof Map && live(m)) return true
|
|
209
|
-
const rec = reconcileEngTokensFromSlot(agent)
|
|
210
|
-
return rec instanceof Map && live(rec)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* D1(DESIGN-TOKEN-SETTLEMENT,2026-09-08):settle 当场同步落盘——把当前内存多槽
|
|
215
|
-
* Map 写入会话槽文件的权威台账,消除 settle→回合尾 saveSession 间的重启丢 token 窗口。
|
|
216
|
-
* 复用 engTokenSlotFields 序列化 + session 安全写/轮转(guardForeignSlotFile =
|
|
217
|
-
* saveSession 同一份守卫——勿裸写文件)。槽文件缺失(本会话首次落盘)→ 写最小全新
|
|
218
|
-
* 记录(字段形状同 saveSession——loadSlotFile 可读);既有文件 → 读-改-写(只动 token
|
|
219
|
-
* 字段,历史等其他字段原样保留)。写失败(writeSessionFile/parse 抛错)→ 抛出/返回
|
|
220
|
-
* false——调用方(settle)失败即 settle 失败(token 不注册、可重评——评审 #1 语义)。
|
|
221
|
-
*/
|
|
222
|
-
export function persistEngTokens(agent) {
|
|
223
|
-
const claimedNow = agent._slot == null
|
|
224
|
-
const slot = agent._slot ??= activeSlot(agent.cwd)
|
|
225
|
-
if (claimedNow) writeEndMarker(agent.cwd, slot)
|
|
226
|
-
const p = slotPath(agent.cwd, slot)
|
|
227
|
-
guardForeignSlotFile(agent, p, slot)
|
|
228
|
-
let data = null
|
|
229
|
-
if (existsSync(p)) {
|
|
230
|
-
data = JSON.parse(readFileSync(p, "utf8"))
|
|
231
|
-
} else {
|
|
232
|
-
// 全新槽(claim 先行、首保存落盘前)——最小记录(VSC newSlotData 同构——字段形状
|
|
233
|
-
// 同 saveSession 数据面,loadSlotFile 校验可读;历史留空——本会话首保存尚未发生,
|
|
234
|
-
// 此前历史本就未落盘,token 台账先行落盘不制造任何额外损失)。
|
|
235
|
-
data = {
|
|
236
|
-
version: 2,
|
|
237
|
-
cwd: agent.cwd,
|
|
238
|
-
title: agent.title ?? "",
|
|
239
|
-
activeProvider: agent.activeProvider ?? agent.provider?.name,
|
|
240
|
-
// MODEL-MERGE-SESSION 恒非空形态:随 saveSession 同款回落链(无渠道仍容忍 null)
|
|
241
|
-
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
242
|
-
history: [],
|
|
243
|
-
contextHistory: [],
|
|
244
|
-
tasks: agent.tasks ?? [],
|
|
245
|
-
planMode: agent.planMode ?? false,
|
|
246
|
-
autoApprove: agent.autoApprove ?? false,
|
|
247
|
-
engineering: agent.config?.agent?.engineering ?? false,
|
|
248
|
-
goal: agent.goal ?? null,
|
|
249
|
-
advisor: agent.config?.advisor ?? null,
|
|
250
|
-
pendingReminders: agent._pendingReminders ?? [],
|
|
251
|
-
sessionStart: agent._sessionStart ?? null,
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
const fields = engTokenSlotFields(agent)
|
|
255
|
-
if (fields.engDesignTokens !== undefined) data.engDesignTokens = fields.engDesignTokens
|
|
256
|
-
else delete data.engDesignTokens
|
|
257
|
-
// 单值镜像字段永不复写(D3 零写)——残留 engDesignToken 随本次写一并清(更快退役,
|
|
258
|
-
// 杜绝 stale 镜像 + 空 Map 组合在下次 restore 二次迁移复活死 token 的边角)。
|
|
259
|
-
delete data.engDesignToken
|
|
260
|
-
data.updatedAt = Date.now()
|
|
261
|
-
writeSessionFile(p, data)
|
|
262
|
-
// 记录我们刚写的 mtime——下次保存/守卫跳过重复解析(saveSession 同款)
|
|
263
|
-
try { agent._slotMtime = statSync(p).mtimeMs } catch {}
|
|
264
|
-
// Update slot metadata in manifest(saveSession 同款尾——非致命:数据已安全,
|
|
265
|
-
// metadata 下次 listSlots 惰性恢复)
|
|
266
|
-
try {
|
|
267
|
-
const m = loadManifest(agent.cwd)
|
|
268
|
-
m.slots[slot] = slotDigest(data)
|
|
269
|
-
saveManifest(agent.cwd, m)
|
|
270
|
-
} catch (e) {
|
|
271
|
-
console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
|
|
272
|
-
}
|
|
273
|
-
return true
|
|
274
|
-
}
|
package/src/tools/apply_patch.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Apply a unified diff to one or more files, atomically: if any hunk fails to apply, nothing is written.
|
|
2
|
-
|
|
3
|
-
**Use it for whole-file and multi-file changes:** creating MULTIPLE new files at once (`--- /dev/null` header per file), whole-file replacement, and cross-file refactors — one unified-diff call covers the whole change. A batched call is one permission ask and one turn instead of N separate calls.
|
|
4
|
-
|
|
5
|
-
Parameters:
|
|
6
|
-
- patch (required): Unified diff text. One `--- a/path` / `+++ b/path` header pair per file, then `@@ -old,count +new,count @@` hunks. Use `--- /dev/null` to create a new file. The `+++ b/path` pair may be omitted for existing files — a lone `--- a/path` (or `--- b/path`) header followed directly by hunks applies to that path (new files still need `--- /dev/null` + `+++ b/path`).
|
|
7
|
-
|
|
8
|
-
Notes:
|
|
9
|
-
- Hunk header "@@" without coordinates is accepted. Coordinate-less hunks are located by their anchor lines: context lines plus the removed (-) lines, matched as a contiguous sequence — a unique match applies. The anchor-free forms require context: a hunk with no removed (-) lines (pure additions) needs at least 2 context lines for a unique match; a zero/one-context hunk with at least one removed (-) line is located by its anchor sequence (context + removed lines, in order) and applies on a unique match.
|
|
10
|
-
- Returns the applied-file summary (`Applied patch to N file(s)` plus per-file change lines and syntax-check notes for changed .mjs files)
|
|
11
|
-
- Use this for multi-file changes (e.g. rename an interface + update all callers) — one call, all-or-nothing
|
|
12
|
-
- Hunks are located by their context/removed lines, not line numbers — but the context must match the file EXACTLY. Read the files first and generate the patch from actual content
|
|
13
|
-
- If a hunk's context matches multiple locations it is rejected — add more surrounding context lines
|
|
14
|
-
- Deleting files is not supported — use the delete tool
|
|
15
|
-
- For single-file edits, edit is simpler; for full rewrites, write is simpler
|
package/src/tools/bash.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
Execute a shell command and return stdout+stderr. Use for running commands, builds, tests.
|
|
2
|
-
|
|
3
|
-
**Route to a dedicated tool instead of bash:**
|
|
4
|
-
- `cat file` / `head` / `tail` → `read`
|
|
5
|
-
- `ls` / `dir` → `ls`
|
|
6
|
-
- `find` / glob search → `glob`
|
|
7
|
-
- `grep` / `rg` → `grep`
|
|
8
|
-
- `echo >` / `sed` / `printf >` / `cat << EOF` → `write` / `edit` / `hashline_edit` / `apply_patch` (preferred: write tools handle encoding/EOL correctly)
|
|
9
|
-
- `git diff` / `git status` / `git log` → `git` tool
|
|
10
|
-
|
|
11
|
-
Parameters:
|
|
12
|
-
- command (required): Shell command to execute
|
|
13
|
-
- timeout: Timeout in milliseconds (default 120000, max ~300000)
|
|
14
|
-
- filter: Optional — a regex; only output lines matching it are returned (case-insensitive). Use instead of hand-writing a pipe into `findstr`/`grep`.
|
|
15
|
-
|
|
16
|
-
Output format:
|
|
17
|
-
```
|
|
18
|
-
[stdout]:
|
|
19
|
-
<standard output, or "(empty)">
|
|
20
|
-
|
|
21
|
-
[stderr]:
|
|
22
|
-
<standard error, only present if non-empty>
|
|
23
|
-
|
|
24
|
-
(exit code N)
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Notes:
|
|
28
|
-
- There is NO TTY — editors, pagers (vim, less), and interactive prompts WILL hang. Always pass non-interactive flags: `git commit -m`, `git --no-pager`, `-y`/`--yes` where applicable
|
|
29
|
-
- The environment sets GIT_PAGER=cat, PAGER=cat, EDITOR=true, TERM=dumb — but still always use non-interactive flags
|
|
30
|
-
- Output is capped at ~200K chars; if you need more, redirect to a file and read it. Truncated output ends with a `[... truncated: N chars omitted]` marker — the missing tail may contain errors, so read the saved log file's tail before trusting success. Use `filter` to narrow instead of hand-piping.
|
|
31
|
-
- Check `[stderr]` for error messages, warnings, and diagnostic output — it is separated from `[stdout]` so you can quickly identify problems.
|
|
32
|
-
- On Windows the shell is **cmd.exe** (NOT Git Bash, NOT PowerShell): `&&`/`||` chaining works, use cmd built-ins (`del`, `dir`, `type`, `findstr`, `tasklist`) and `/dev/null`→`NUL`, `2>/dev/null`→`>nul 2>&1`. Bash-isms (`rm -rf`, `cp -r`, `head`, `$(...)`, `${VAR}`, single quotes, `;` separators) FAIL — the tool prepends a hint when it detects POSIX-only syntax, and still executes. For complex logic prefer the execute tool (node) over shell gymnastics.
|
|
33
|
-
- Never use bash to read, copy, or transmit secret files (.env, keys, tokens)
|
|
34
|
-
- Do NOT run destructive commands (rm -rf, force-push, drop table) without explicit user confirmation
|
|
35
|
-
- After commands that change files (git checkout, npm install, etc.), repo_outline and code_search may be stale — re-run them to get current results.
|
|
36
|
-
- Prefer read/glob/grep/ls for file operations inside the project — file tools and bash reach the same paths (no directory restriction).
|
|
37
|
-
- NEVER use bash to write or modify files (echo/sed/printf > file, cat << EOF, etc.). Use write/edit/insert_after/apply_patch instead — they handle encoding, escaping, and EOL conventions correctly.
|
package/src/tools/bash.mjs
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DESC,
|
|
3
|
-
sanitizeOutput,
|
|
4
|
-
truncate,
|
|
5
|
-
makeDecoder,
|
|
6
|
-
BASH_TIMEOUT_MS,
|
|
7
|
-
} from "./shared.mjs";
|
|
8
|
-
import { spawn, execFileSync } from "node:child_process";
|
|
9
|
-
|
|
10
|
-
/** Maximum buffer size per stream (stdout / stderr) before truncation */
|
|
11
|
-
const MAX_STREAM_BUF = 2_000_000
|
|
12
|
-
|
|
13
|
-
/** Keep only output lines matching a regex (bash filter, case-insensitive). */
|
|
14
|
-
function applyLineFilter(output, filter) {
|
|
15
|
-
let re
|
|
16
|
-
try { re = new RegExp(filter, "i") } catch (e) { return `Error: filter regex invalid: ${e.message}` }
|
|
17
|
-
const lines = output.split("\n").filter((l) => re.test(l))
|
|
18
|
-
if (lines.length === 0) return `(no output lines matched filter "${filter}")`
|
|
19
|
-
return truncate(lines.join("\n"))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** POSIX-only constructs that cmd.exe reads literally (and thus breaks). Detected
|
|
23
|
-
* so the agent is told IMMEDIATELY instead of chasing a confusing failure — warning
|
|
24
|
-
* only, never a block (the approval layer is the real gate, same as destructive-command policy). */
|
|
25
|
-
function posixSyntaxHint(command) {
|
|
26
|
-
const hits = []
|
|
27
|
-
if (/\$\([^)]*\)/.test(command)) hits.push("$(...)")
|
|
28
|
-
if (command.includes("`")) hits.push("backtick")
|
|
29
|
-
if (/;\s+/.test(command)) hits.push("';' separators (cmd.exe needs && or newline)")
|
|
30
|
-
if (/2>\s*\/dev\/null|>\s*\/dev\/null|&>\s*\/dev\/null/.test(command)) hits.push("/dev/null (use NUL)")
|
|
31
|
-
if (/'.*'/.test(command)) hits.push("single quotes (cmd.exe doesn't group)")
|
|
32
|
-
if (/\$\{[A-Za-z_]/.test(command)) hits.push("${VAR} (use %VAR%)")
|
|
33
|
-
if (!hits.length) return ""
|
|
34
|
-
return "[hint: POSIX-only construct(s) detected — " + hits.join(", ") + ". Current shell is cmd.exe; these will NOT work. Use && / NUL / %VAR%, or use the execute tool (node) for complex logic]"
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// ====================================================================
|
|
39
|
-
// bash — command execution with safety gates
|
|
40
|
-
// ====================================================================
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Build environment for child process.
|
|
44
|
-
* Passes through all parent env vars, with non-interactive overrides (EDITOR/PAGER/TERM).
|
|
45
|
-
* Sets PYTHONIOENCODING on Windows to override GBK default for Python scripts.
|
|
46
|
-
*/
|
|
47
|
-
function buildBashEnv() {
|
|
48
|
-
const isWindows = process.platform === "win32"
|
|
49
|
-
return {
|
|
50
|
-
...process.env,
|
|
51
|
-
GIT_EDITOR: "true",
|
|
52
|
-
EDITOR: "true",
|
|
53
|
-
VISUAL: "true",
|
|
54
|
-
GIT_PAGER: "cat",
|
|
55
|
-
PAGER: "cat",
|
|
56
|
-
TERM: "dumb",
|
|
57
|
-
...(isWindows ? { PYTHONIOENCODING: "utf-8" } : {}),
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Platform-aware process tree kill.
|
|
63
|
-
* POSIX: kill process group (spawned with detached=true).
|
|
64
|
-
* Windows: taskkill /T to reach grandchildren (npm test's subprocesses, etc.).
|
|
65
|
-
*/
|
|
66
|
-
function killProcessTree(child) {
|
|
67
|
-
if (process.platform === "win32") {
|
|
68
|
-
try { execFileSync("taskkill", ["/PID", String(child.pid), "/T", "/F"], { stdio: "ignore" }) } catch {}
|
|
69
|
-
} else {
|
|
70
|
-
try { process.kill(-child.pid, "SIGKILL") } catch {}
|
|
71
|
-
try { child.kill("SIGKILL") } catch {} // fallback: kill directly if group kill fails
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Detect destructive git commands and auto-snapshot BEFORE execution.
|
|
77
|
-
* The scenario: the model writes uncommitted code, then (after breaking things)
|
|
78
|
-
* runs `git checkout -- .` / `git restore` / `git reset --hard` / `git clean -f` to
|
|
79
|
-
* roll back — which silently DESTROYS all uncommitted work. The pre-task checkpoint
|
|
80
|
-
* cannot help (it was taken before the code was written); only a snapshot taken
|
|
81
|
-
* immediately before the destructive command can.
|
|
82
|
-
*
|
|
83
|
-
* This is the defense-in-depth guard: a WIDE match that snapshots before the command
|
|
84
|
-
* runs — the command itself is NEVER blocked (a determined model bypasses text
|
|
85
|
-
* matching anyway; the real gate is the approval layer). A snapshot alone is not
|
|
86
|
-
* enough: the model may retry a variant that slips through the exact matcher
|
|
87
|
-
* (e.g. `git checkout HEAD -- .`), or run git outside the bash tool. The snapshot
|
|
88
|
-
* taken here survives all of those paths.
|
|
89
|
-
*
|
|
90
|
-
* Matching is intentionally WIDE (false positives are harmless — one extra snapshot;
|
|
91
|
-
* a missed match is a data-loss disaster).
|
|
92
|
-
*/
|
|
93
|
-
const GIT_DESTRUCTIVE_RE = /\bgit\s+(?:checkout\s+(?:[\w./-]+\s+)?--(?!\w)|checkout\s+\.|restore\s+(?!--help\b)(?!--staged\b(?!.*--worktree))|reset\s+--hard|clean\s+-(?=\S*f)(?!\S*n))/i
|
|
94
|
-
|
|
95
|
-
/** Returns { id, notice } when a snapshot was taken, else null. Never throws. */
|
|
96
|
-
async function gitGuardSnapshot(command, cwd) {
|
|
97
|
-
if (!GIT_DESTRUCTIVE_RE.test(command)) return null
|
|
98
|
-
try {
|
|
99
|
-
const { isGitRepo, createCheckpoint } = await import("../git/checkpoint.mjs")
|
|
100
|
-
if (!isGitRepo(cwd)) return null
|
|
101
|
-
const cp = await createCheckpoint(cwd)
|
|
102
|
-
if (!cp) return null
|
|
103
|
-
return {
|
|
104
|
-
id: cp.id,
|
|
105
|
-
notice: `[auto-protection] Destructive git command detected — snapshot ${cp.id} created BEFORE execution (${cp.files} file(s): ${cp.tracked.length} tracked, ${cp.untracked.length} untracked). If this command destroyed uncommitted work, restore it: checkpoint action=checkpoint checkpointAction=rewind checkpointId=${cp.id}`,
|
|
106
|
-
}
|
|
107
|
-
} catch {
|
|
108
|
-
return null // protection is best-effort — never block the command
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Run a bash command: spawn, stream stdout/stderr with buffering and decoding,
|
|
114
|
-
* enforce timeout and signal abort, return formatted result.
|
|
115
|
-
*
|
|
116
|
-
* Returns a promise that resolves to the formatted output string (stdout + stderr + status + truncation note).
|
|
117
|
-
*/
|
|
118
|
-
function runBash(command, cwd, { timeout, signal, onOutput, shell }) {
|
|
119
|
-
return new Promise((resolve) => {
|
|
120
|
-
// Windows + default cmd: force UTF-8 code page for this child process (each spawn
|
|
121
|
-
// is an independent cmd, so chcp has no side effects on other shells) — otherwise
|
|
122
|
-
// cmd emits GBK bytes that the UTF-8 decoder turns into mojibake, and the model
|
|
123
|
-
// fights encoding errors instead of the actual command (reported UX on win11).
|
|
124
|
-
const effectiveCommand = process.platform === "win32" && !shell
|
|
125
|
-
? `chcp 65001 >nul && ${command}`
|
|
126
|
-
: command
|
|
127
|
-
// args MUST be an explicit [] — the two-arg spawn(cmd, options) form is
|
|
128
|
-
// DEP0190-deprecated (Node 24): the options object would be misread as args.
|
|
129
|
-
const child = spawn(effectiveCommand, [], {
|
|
130
|
-
cwd,
|
|
131
|
-
shell: shell ?? true,
|
|
132
|
-
windowsHide: true,
|
|
133
|
-
detached: process.platform !== "win32",
|
|
134
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
135
|
-
env: buildBashEnv(),
|
|
136
|
-
// 双保险(2026-09-05——裸 spawn 无 signal 教训:abort 只靠 killTree 手动杀——taskkill
|
|
137
|
-
// best-effort 可能失败/竞态)——Node signal option = 第一道(abort 时自动杀**直接**子
|
|
138
|
-
// 进程——cmd——语义保证);killTree 仍是必需兜底(孙进程握管道使 close 不触发——见
|
|
139
|
-
// L191 注释——spawn 级 kill 不达孙进程)
|
|
140
|
-
...(signal ? { signal } : {}),
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const killTree = () => killProcessTree(child)
|
|
144
|
-
|
|
145
|
-
// Separate decoders for stdout / stderr (same encoding in practice, but separate
|
|
146
|
-
// collection is cleaner and lets the model locate errors via stderr quickly)
|
|
147
|
-
const outDecoder = makeDecoder()
|
|
148
|
-
const errDecoder = makeDecoder()
|
|
149
|
-
let outBuf = ""
|
|
150
|
-
let errBuf = ""
|
|
151
|
-
let truncatedNote = ""
|
|
152
|
-
|
|
153
|
-
child.stdout.on("data", (d) => {
|
|
154
|
-
const s = sanitizeOutput(outDecoder(d))
|
|
155
|
-
if (s) onOutput?.(s)
|
|
156
|
-
if (outBuf.length < MAX_STREAM_BUF) outBuf += s
|
|
157
|
-
else if (!truncatedNote) truncatedNote =
|
|
158
|
-
"\n[... output exceeded 2MB, remainder discarded — redirect to a file if you need the full output]"
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
child.stderr.on("data", (d) => {
|
|
162
|
-
const s = sanitizeOutput(errDecoder(d))
|
|
163
|
-
if (s) onOutput?.(s)
|
|
164
|
-
if (errBuf.length < MAX_STREAM_BUF) errBuf += s
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const timer = setTimeout(killTree, timeout)
|
|
168
|
-
if (signal) signal.addEventListener("abort", killTree, { once: true })
|
|
169
|
-
|
|
170
|
-
let settled = false
|
|
171
|
-
let graceTimer = null
|
|
172
|
-
const finish = (result) => {
|
|
173
|
-
if (settled) return
|
|
174
|
-
settled = true
|
|
175
|
-
clearTimeout(timer)
|
|
176
|
-
clearTimeout(graceTimer)
|
|
177
|
-
// 2026-09-05(advisor 🟡#2):收尾移除 abort 监听器——ctx.signal 为长生命周期对象,
|
|
178
|
-
// 残留 once 监听器每次 bash 调用累积(闭包持有 child/输出缓冲直到 abort 才释放)
|
|
179
|
-
if (signal) signal.removeEventListener("abort", killTree)
|
|
180
|
-
resolve(result)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
child.on("error", (error) => {
|
|
184
|
-
// signal 双保险(2026-09-05):abort 时 Node signal option 杀直接子进程 → 本事件
|
|
185
|
-
// 以 AbortError 先触发——**跳过 finish**(不 settled)——exit 事件随后必到(实证
|
|
186
|
-
// 2ms 内)走 L195 分支带已收集输出收尾(user interrupted——不丢 partial);
|
|
187
|
-
// 非 abort 的真 spawn 错误(command not found 等)保持原分支
|
|
188
|
-
if (error.name === "AbortError") return
|
|
189
|
-
finish(truncate(`Command failed: ${error.message}\n[stdout]:\n${outBuf || "(empty)"}`))
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
// Shell exited. Normally 'close' follows within milliseconds — but a BACKGROUND
|
|
193
|
-
// child (start /b, &, nohup …) inherits the stdio pipes, so the pipe stays open
|
|
194
|
-
// and 'close' never fires: the tool would hang until the timeout. Resolve after
|
|
195
|
-
// a short grace period instead, returning whatever output was collected.
|
|
196
|
-
child.on("exit", (code, exitSignal) => {
|
|
197
|
-
graceTimer = setTimeout(() => {
|
|
198
|
-
const outFlush = sanitizeOutput(outDecoder(Buffer.alloc(0), true))
|
|
199
|
-
const errFlush = sanitizeOutput(errDecoder(Buffer.alloc(0), true))
|
|
200
|
-
if (outFlush) onOutput?.(outFlush)
|
|
201
|
-
if (errFlush) onOutput?.(errFlush)
|
|
202
|
-
const status = exitSignal ? `killed: ${exitSignal}` : `exit code ${code}`
|
|
203
|
-
const parts = [`[stdout]:\n${(outBuf + outFlush).trim() || "(empty)"}`]
|
|
204
|
-
if ((errBuf + errFlush).trim()) parts.push(`[stderr]:\n${(errBuf + errFlush).trim()}`)
|
|
205
|
-
parts.push(`(${status})`)
|
|
206
|
-
parts.push("[background] the shell exited but a child process still holds the output pipe — output may be incomplete; the process may still be running")
|
|
207
|
-
finish(truncate(parts.join("\n\n") + truncatedNote))
|
|
208
|
-
}, 1500)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
child.on("close", (code, exitSignal) => {
|
|
212
|
-
// Flush decoder tails — also push final bytes to panel
|
|
213
|
-
const outFlush = sanitizeOutput(outDecoder(Buffer.alloc(0), true))
|
|
214
|
-
const errFlush = sanitizeOutput(errDecoder(Buffer.alloc(0), true))
|
|
215
|
-
outBuf += outFlush
|
|
216
|
-
errBuf += errFlush
|
|
217
|
-
if (outFlush) onOutput?.(outFlush)
|
|
218
|
-
if (errFlush) onOutput?.(errFlush)
|
|
219
|
-
|
|
220
|
-
// Windows has no POSIX signals — check signal.aborted for user interrupts
|
|
221
|
-
const status = (exitSignal || signal?.aborted)
|
|
222
|
-
? `killed: ${signal?.aborted ? "user interrupted" : "timeout"}`
|
|
223
|
-
: `exit code ${code}`
|
|
224
|
-
|
|
225
|
-
const parts = [`[stdout]:\n${outBuf.trim() || "(empty)"}`]
|
|
226
|
-
if (errBuf.trim()) parts.push(`[stderr]:\n${errBuf.trim()}`)
|
|
227
|
-
parts.push(`(${status})`)
|
|
228
|
-
finish(truncate(parts.join("\n\n") + truncatedNote))
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ====================================================================
|
|
234
|
-
// tool definitions
|
|
235
|
-
// ====================================================================
|
|
236
|
-
|
|
237
|
-
export const bashTool = {
|
|
238
|
-
name: "bash",
|
|
239
|
-
description: DESC("bash"),
|
|
240
|
-
parameters: {
|
|
241
|
-
type: "object",
|
|
242
|
-
properties: {
|
|
243
|
-
command: { type: "string", description: "Shell command to execute" },
|
|
244
|
-
timeout: { type: "number", description: `Timeout in ms (default ${BASH_TIMEOUT_MS})` },
|
|
245
|
-
filter: { type: "string", description: "Optional: only return output lines matching this regex (case-insensitive)" },
|
|
246
|
-
},
|
|
247
|
-
required: ["command"],
|
|
248
|
-
},
|
|
249
|
-
readonly: false,
|
|
250
|
-
outputPanel: true, // stream stdout/stderr to panel during execution, collapse to summary on completion
|
|
251
|
-
async execute(args, ctx) {
|
|
252
|
-
// Git destructive commands are NEVER rejected — the model would bypass the
|
|
253
|
-
// guard anyway. Instead: snapshot every uncommitted file first, then ALLOW
|
|
254
|
-
// the command. The snapshot makes the rollback reversible (defense in depth:
|
|
255
|
-
// the wide matcher also covers variants like `git checkout HEAD -- .`).
|
|
256
|
-
const guard = await gitGuardSnapshot(args.command, ctx.cwd)
|
|
257
|
-
const result = await runBash(args.command, ctx.cwd, {
|
|
258
|
-
timeout: args.timeout ?? BASH_TIMEOUT_MS,
|
|
259
|
-
signal: ctx.signal,
|
|
260
|
-
onOutput: ctx.onOutput,
|
|
261
|
-
shell: ctx.agent?.config?.shell ?? null,
|
|
262
|
-
})
|
|
263
|
-
const filtered = args.filter ? applyLineFilter(result, args.filter) : result
|
|
264
|
-
const hint = posixSyntaxHint(args.command)
|
|
265
|
-
const body = guard ? `${guard.notice}\n\n${filtered}` : filtered
|
|
266
|
-
return hint ? `${hint}\n${body}` : body
|
|
267
|
-
},
|
|
268
|
-
}
|