thincoder 0.12.66 → 0.12.68

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +23 -5
  3. package/bin/thincoder.mjs +43 -331
  4. package/package.json +2 -2
  5. package/src/acp/bridge.mjs +13 -12
  6. package/src/acp/handlers-session.mjs +12 -1
  7. package/src/cli/make-agent.mjs +44 -119
  8. package/src/cli/memory-command.mjs +45 -3
  9. package/src/cli/permission.mjs +1 -1
  10. package/src/command-interactive.mjs +186 -0
  11. package/src/command-table.mjs +185 -0
  12. package/src/completions.mjs +36 -12
  13. package/src/crash-reports.mjs +42 -20
  14. package/src/distill.mjs +3 -3
  15. package/src/heap-watch.mjs +5 -14
  16. package/src/prompt-injections.mjs +1 -1
  17. package/src/tui/agent-turn.mjs +114 -69
  18. package/src/tui/clipboard.mjs +74 -30
  19. package/src/tui/cmd-advisor.mjs +54 -13
  20. package/src/tui/cmd-clear.mjs +2 -2
  21. package/src/tui/cmd-config.mjs +26 -2
  22. package/src/tui/cmd-eng.mjs +1 -1
  23. package/src/tui/cmd-mcp.mjs +1 -1
  24. package/src/tui/cmd-new.mjs +2 -2
  25. package/src/tui/cmd-reindex.mjs +1 -1
  26. package/src/tui/cmd-session.mjs +13 -5
  27. package/src/tui/cmd-think.mjs +35 -17
  28. package/src/tui/cmd-timers.mjs +33 -0
  29. package/src/tui/conversation-writer.mjs +2 -2
  30. package/src/tui/display-budget.mjs +9 -9
  31. package/src/tui/fold-block.mjs +1 -1
  32. package/src/tui/index.mjs +23 -5
  33. package/src/tui/interaction.mjs +5 -3
  34. package/src/tui/key-handler-busy.mjs +17 -12
  35. package/src/tui/key-handler-ctrlc.mjs +5 -5
  36. package/src/tui/key-handler-edit.mjs +12 -8
  37. package/src/tui/key-handler-search.mjs +1 -1
  38. package/src/tui/key-handler.mjs +2 -2
  39. package/src/tui/key-modes.mjs +1 -1
  40. package/src/tui/layout.mjs +5 -5
  41. package/src/tui/ledger-surface.mjs +26 -5
  42. package/src/tui/lifecycle-records.mjs +199 -0
  43. package/src/tui/model-picker.mjs +16 -199
  44. package/src/tui/mouse.mjs +5 -5
  45. package/src/tui/pickers.mjs +6 -2
  46. package/src/tui/provider-admin.mjs +213 -0
  47. package/src/tui/queued-merge.mjs +12 -0
  48. package/src/tui/queued-pickup.mjs +34 -0
  49. package/src/tui/render-conversation.mjs +49 -4
  50. package/src/tui/render-frame.mjs +54 -11
  51. package/src/tui/render-loop.mjs +2 -3
  52. package/src/tui/render-segments.mjs +4 -4
  53. package/src/tui/render.mjs +1 -1
  54. package/src/tui/slash-commands.mjs +3 -0
  55. package/src/tui/startup.mjs +28 -3
  56. package/src/tui/subagent-blocks.mjs +37 -29
  57. package/src/tui/subagent-children.mjs +5 -5
  58. package/src/tui/subagent-freeze.mjs +43 -41
  59. package/src/tui/subagent-panel.mjs +11 -11
  60. package/src/tui/suspension-drive.mjs +173 -253
  61. package/src/tui/timer-watch.mjs +64 -0
  62. package/src/tui/tool-args.mjs +10 -71
  63. package/src/tui/tool-display.mjs +7 -7
  64. package/src/tui/tool-events.mjs +40 -35
  65. package/src/tui/tui-lifecycle.mjs +13 -4
  66. package/src/tui/tui-state.mjs +3 -2
  67. package/src/tui/turn-face.mjs +6 -3
  68. package/src/tui/wizard.mjs +7 -2
  69. package/src/tui/wrapped-spawn.mjs +3 -3
@@ -0,0 +1,185 @@
1
+ /**
2
+ * command-table.mjs — CLI 命令分发表(`bin/thincoder.mjs` 外提——2026-09-28 拆档批 R4 · 纯移动 ·
3
+ * 命令行为 ∕ USAGE 文本 ∕ 旗标语义零改):分发骨架 + 八薄命令族(memory ∕ sync ∕ distill ∕
4
+ * reindex ∕ completion ∕ upgrade ∕ session ∕ ledger)+ help ∕ version;交互长驻三命令
5
+ * (chat ∕ tui ∕ acp)经 `command-interactive.mjs` 承接。依赖经 ctx 注入(`usage` ∕ `version` ∕
6
+ * `exitSoon`——bin 装配面注入;先例 = `src/tui/turn-face.mjs` 出档)。
7
+ */
8
+ import { readFileSync } from "node:fs"
9
+ import { isAbsolute, join } from "node:path"
10
+ import { loadConfig } from "@thincoder/core/config.mjs"
11
+ import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
12
+ import { teamConfig } from "./cli/make-agent.mjs"
13
+ import { memoryCommand } from "./cli/memory-command.mjs"
14
+ import { distillCommand } from "./cli/distill-command.mjs"
15
+ import { chatCommand, tuiCommand, acpCommand } from "./command-interactive.mjs"
16
+
17
+ /** 命令分发(原 `bin/thincoder.mjs` 顶层 switch 骨架逐字搬移——命令行为零改)。 */
18
+ export async function runCommandTable(command, args, ctx) {
19
+ const { usage, version, exitSoon } = ctx
20
+ switch (command) {
21
+ case "chat": {
22
+ await chatCommand(args, ctx)
23
+ break
24
+ }
25
+
26
+ case "memory": {
27
+ const config = loadConfig()
28
+ const memory = createMemory({ dbPath: config.memory.dbPath })
29
+ if (config.embedding?.apiKey) {
30
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
31
+ memory.embedder = createEmbedder(config.embedding)
32
+ }
33
+ if (config.memory.projectDir) {
34
+ memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
35
+ }
36
+ const code = await memoryCommand(memory, args)
37
+ if (code) exitSoon(code)
38
+ break
39
+ }
40
+
41
+ case "sync": {
42
+ const config = loadConfig()
43
+ const team = teamConfig(config)
44
+ if (!team) {
45
+ console.error("Team memory not configured. Set memory.team in ~/.thincoder/config.json:")
46
+ console.error(' "team": { "name": "myteam", "repo": "git@github.com:org/team-memory.git" }')
47
+ exitSoon(1)
48
+ break
49
+ }
50
+ const memory = createMemory({ dbPath: config.memory.dbPath })
51
+ const { ensureClone, pullTeam } = await import("@thincoder/core/git/gitmem.mjs")
52
+ try {
53
+ const cloned = await ensureClone(team)
54
+ if (cloned) console.log(`Cloned team repo to ${team.dir}`)
55
+ await pullTeam(team.dir)
56
+ const stats = await syncDir(memory, { layer: "team", dir: team.dir })
57
+ console.log(`Synced. Index: +${stats.added} ~${stats.updated} -${stats.removed}`)
58
+ } catch (error) {
59
+ console.error(`[error] ${error.message}`)
60
+ exitSoon(1)
61
+ }
62
+ break
63
+ }
64
+
65
+ case "distill": {
66
+ const code = await distillCommand(args, exitSoon)
67
+ if (code) exitSoon(code)
68
+ break
69
+ }
70
+
71
+ case "reindex": {
72
+ const config = loadConfig()
73
+ const memory = createMemory({ dbPath: config.memory.dbPath })
74
+ // files 层(project/team 的 markdown 索引)全量重建;索引是易失品,真相在 markdown
75
+ memory.db.prepare(`DELETE FROM files`).run()
76
+ const cwd = process.cwd()
77
+ let total = { added: 0, removed: 0 }
78
+ if (config.memory.projectDir) {
79
+ const s = await syncDir(memory, { layer: "project", dir: isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir) })
80
+ total.added += s.added
81
+ }
82
+ const team = teamConfig(config)
83
+ if (team) {
84
+ const { ensureClone } = await import("@thincoder/core/git/gitmem.mjs")
85
+ await ensureClone(team)
86
+ const s = await syncDir(memory, { layer: "team", dir: team.dir })
87
+ total.added += s.added
88
+ }
89
+ console.log(`Reindexed ${total.added} markdown entries (project${team ? " + team" : ""}). Vectors will be lazily regenerated on next search.`)
90
+ break
91
+ }
92
+
93
+ case "tui":
94
+ case undefined: {
95
+ await tuiCommand(ctx)
96
+ break
97
+ }
98
+
99
+ case "completion": {
100
+ const shell = args[0]
101
+ if (!["bash", "zsh", "fish"].includes(shell)) {
102
+ console.error(`Usage: thincoder completion <bash|zsh|fish>`)
103
+ exitSoon(1)
104
+ break
105
+ }
106
+ // §11.2 交付行数债(2026-09-08——500 行硬限触碰执行):直写段逐字迁出
107
+ // src/completions.mjs(脚本字节不变——printCompletion 发射)
108
+ const { printCompletion } = await import("./completions.mjs")
109
+ printCompletion(shell)
110
+ break
111
+ }
112
+
113
+ case "upgrade": {
114
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
115
+ const local = pkg.version
116
+ const { checkForUpdate } = await import("./upgrade.mjs")
117
+ const result = await checkForUpdate(local)
118
+ if (!result) {
119
+ console.error("[upgrade] Unable to query npm registry — check your network connection and that npm is installed")
120
+ exitSoon(1)
121
+ break
122
+ }
123
+ if (!result.newer) {
124
+ console.log(`ThinCoder ${local} is already the latest.`)
125
+ } else {
126
+ console.log(`Upgrading: ${local} → ${result.latest}`)
127
+ const { execSync } = await import("node:child_process")
128
+ execSync("npm install -g thincoder@latest", { stdio: "inherit" })
129
+ console.log(`Upgraded to ${result.latest}`)
130
+ }
131
+ break
132
+ }
133
+
134
+ case "acp": {
135
+ await acpCommand(args, ctx)
136
+ break
137
+ }
138
+
139
+ case "session": {
140
+ // SESSION.md §6.12 / §6.17:会话目录 GC 显式面(冷 cwd 90 天面 + 三合取存量组全量面)——
141
+ // 双端同面(2026-09-21 端差注销:VS Code 端命令 `thincoder.sessionGc` 走同一核数据面)。
142
+ // SESSION.md §6.19 D-SE46(2026-09-22 会话索引批):`session index` = 派生索引库显式面
143
+ // (--status 读数 / --rebuild 全量重扫)——同一 `session` 分发族(壳侧分发,实现在核)。
144
+ if (args[0] === "index") {
145
+ const { runSessionIndex } = await import("@thincoder/core/session-index-cmd.mjs")
146
+ process.exitCode = await runSessionIndex(args)
147
+ break
148
+ }
149
+ const { runSessionGc } = await import("@thincoder/core/session-gc.mjs")
150
+ process.exitCode = await runSessionGc(args)
151
+ break
152
+ }
153
+
154
+ case "ledger": {
155
+ // LEDGER.md §2.2(2026-09-25 批):台账存量收正面——变体键合并迁移(migrate)/ 残档审计(audit);
156
+ // 核内实现(ledger-migrate.mjs),壳侧只分发(先例 = `session gc` 双档:--dry-run 只读 / --confirm 执行)。
157
+ if (args[0] === "migrate" || args[0] === "audit") {
158
+ const m = await import("@thincoder/core/ledger-migrate.mjs")
159
+ process.exitCode = await (args[0] === "migrate" ? m.runLedgerMigrate(args) : m.runLedgerAudit(args))
160
+ break
161
+ }
162
+ console.error("Usage: thincoder ledger migrate --dry-run | --confirm [--from <key>] | thincoder ledger audit [--root <dir>]")
163
+ exitSoon(1)
164
+ break
165
+ }
166
+
167
+ case "--help":
168
+ case "-h": {
169
+ process.stdout.write(usage)
170
+ break
171
+ }
172
+
173
+ case "--version":
174
+ case "-v": {
175
+ console.log(version)
176
+ break
177
+ }
178
+
179
+ default: {
180
+ console.error(`Unknown command: ${command}\n`)
181
+ process.stdout.write(usage)
182
+ exitSoon(1)
183
+ }
184
+ }
185
+ }
@@ -2,6 +2,10 @@
2
2
  * completions.mjs — `thincoder completion <shell>` shell 补全脚本发射(bash/zsh/fish)。
3
3
  * bin/thincoder.mjs case "completion" 直写段拆分(2026-09-08——500 行硬限触碰执行:
4
4
  * 段逐字迁出,脚本字节与既有输出一致)。
5
+ * 发射契约(CLI-ENTRY.md §3 —— #704 收正 2026-09-30):**源档形 ≡ 发射字节形**——bash 直写形零反斜杠
6
+ * `\$(...)` 非命令替换 ⇒ `bash -n` 语法错;zsh 分派行同排直写(双引号内 `\$` = 字面 `$` ⇒ 分派永不匹配);
7
+ * 余反斜杠仅续行 `\\`(行尾接续,发射 `\`)——保留。
8
+ * 例外(JS 插值险位):须发射字面 `${…}` 的段在源档保留 `\${` 转义。
5
9
  */
6
10
  export function printCompletion(shell) {
7
11
  if (shell === "bash") {
@@ -11,17 +15,23 @@ export function printCompletion(shell) {
11
15
  cur="\${COMP_WORDS[COMP_CWORD]}"
12
16
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
13
17
  case "\${COMP_WORDS[1]}" in
14
- chat) COMPREPLY=( \\$(compgen -W "--auto" -- "\\$cur") ) ;;
18
+ chat) COMPREPLY=( $(compgen -W "--auto" -- "$cur") ) ;;
15
19
  memory)
16
- case "\\$prev" in
17
- memory) COMPREPLY=( \\$(compgen -W "list search put remove" -- "\\$cur") ) ;;
18
- list) COMPREPLY=( \\$(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "\\$cur") ) ;;
19
- put) COMPREPLY=( \\$(compgen -W "--type= --title= --content= --tags=" -- "\\$cur") ) ;;
20
+ case "$prev" in
21
+ memory) COMPREPLY=( $(compgen -W "list search put remove sweep" -- "$cur") ) ;;
22
+ list) COMPREPLY=( $(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "$cur") ) ;;
23
+ put) COMPREPLY=( $(compgen -W "--type= --title= --content= --tags=" -- "$cur") ) ;;
24
+ sweep) COMPREPLY=( $(compgen -W "--origin= --dry-run --confirm" -- "$cur") ) ;;
20
25
  esac ;;
21
- distill) COMPREPLY=( \\$(compgen -W "--yes --layer=" -- "\\$cur") ) ;;
22
- completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
26
+ ledger) case "$prev" in
27
+ ledger) COMPREPLY=( $(compgen -W "migrate audit" -- "$cur") ) ;;
28
+ migrate) COMPREPLY=( $(compgen -W "--dry-run --confirm --from" -- "$cur") ) ;;
29
+ audit) COMPREPLY=( $(compgen -W "--root" -- "$cur") ) ;;
30
+ esac ;;
31
+ distill) COMPREPLY=( $(compgen -W "--yes --layer=" -- "$cur") ) ;;
32
+ completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ) ;;
23
33
  *)
24
- COMPREPLY=( \\$(compgen -W "tui chat acp memory sync reindex distill upgrade completion session -v --version -h --help" -- "\\$cur") ) ;;
34
+ COMPREPLY=( $(compgen -W "tui chat acp memory sync reindex distill upgrade completion session ledger -v --version -h --help" -- "$cur") ) ;;
25
35
  esac
26
36
  }
27
37
  complete -F _thincoder thincoder
@@ -36,7 +46,7 @@ _thincoder() {
36
46
  '1: :->cmd' \\
37
47
  '*:: :->args'
38
48
 
39
- case "\\$state" in
49
+ case "$state" in
40
50
  cmd)
41
51
  _values 'command' \\
42
52
  'tui[Launch the interactive TUI]' \\
@@ -48,16 +58,21 @@ _thincoder() {
48
58
  'distill[Extract knowledge from session transcript]' \\
49
59
  'upgrade[Update to latest version from npm]' \\
50
60
  'completion[Generate shell completion script]' \\
51
- 'session[Session dir GC: session gc --dry-run|--confirm]'
61
+ 'session[Session dir GC: session gc --dry-run|--confirm]' \\
62
+ 'ledger[Ledger variants: migrate / audit]'
52
63
  ;;
53
64
  args)
54
- case "\\$words[1]" in
65
+ case "$words[1]" in
55
66
  chat) _arguments '--auto[Auto-approve all tool calls]' ;;
56
67
  memory)
57
- case "\\$words[2]" in
68
+ case "$words[2]" in
58
69
  list) _arguments '--type=[Filter by type]' ;;
59
70
  put) _arguments '--type=[Entry type]' '--title=[Title]' '--content=[Content]' '--tags=[Space-separated tags]' ;;
71
+ sweep) _arguments '--origin=[Origin path]' '--dry-run[Report only]' '--confirm[Apply writes]' ;;
60
72
  esac ;;
73
+ ledger) case "$words[2]" in
74
+ migrate) _arguments '--dry-run[Report only]' '--confirm[Apply migration]' '--from:Source key:' ;;
75
+ audit) _arguments '--root:Scan root dir:' ;; esac ;;
61
76
  distill) _arguments '--yes[Skip confirmation]' '--layer=[Layer filter]' ;;
62
77
  completion) _values 'shell' 'bash' 'zsh' 'fish' ;;
63
78
  esac ;;
@@ -79,6 +94,7 @@ complete -c thincoder -a distill -d 'Extract knowledge from session'
79
94
  complete -c thincoder -a upgrade -d 'Update to latest version'
80
95
  complete -c thincoder -a completion -d 'Shell completion'
81
96
  complete -c thincoder -a session -d 'Session dir GC (session gc --dry-run|--confirm)'
97
+ complete -c thincoder -a ledger -d 'Ledger variants: migrate / audit'
82
98
  complete -c thincoder -a acp -d 'Agent Client Protocol server for IDEs'
83
99
 
84
100
  # Flags
@@ -93,6 +109,7 @@ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a list -d 'List
93
109
  complete -c thincoder -n '__fish_seen_subcommand_from memory' -a search -d 'Search memory'
94
110
  complete -c thincoder -n '__fish_seen_subcommand_from memory' -a put -d 'Add entry'
95
111
  complete -c thincoder -n '__fish_seen_subcommand_from memory' -a remove -d 'Remove entry'
112
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a sweep -d 'Sweep dead origins'
96
113
 
97
114
  # memory list flags
98
115
  complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from list' -l type -d 'Filter by type' -xa 'rule knowledge decision pattern'
@@ -103,6 +120,11 @@ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_su
103
120
  complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l content -d 'Content'
104
121
  complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l tags -d 'Space-separated tags'
105
122
 
123
+ # memory sweep flags
124
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from sweep' -l origin -d 'Origin path'
125
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from sweep' -l dry-run -d 'Report only'
126
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from sweep' -l confirm -d 'Apply writes'
127
+
106
128
  # distill flags
107
129
  complete -c thincoder -n '__fish_seen_subcommand_from distill' -l yes -d 'Skip confirmation'
108
130
  complete -c thincoder -n '__fish_seen_subcommand_from distill' -l layer -d 'Layer filter'
@@ -111,6 +133,8 @@ complete -c thincoder -n '__fish_seen_subcommand_from distill' -l layer -d 'Laye
111
133
  complete -c thincoder -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completions'
112
134
  complete -c thincoder -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completions'
113
135
  complete -c thincoder -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completions'
136
+ complete -c thincoder -n '__fish_seen_subcommand_from ledger' -a 'migrate audit' -d 'Ledger variants: migrate / audit'
137
+ complete -c thincoder -n '__fish_seen_subcommand_from ledger' -l dry-run -l confirm -l from -l root -d 'Ledger flags'
114
138
  `)
115
139
  }
116
140
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * crash-reports.mjs — 崩溃捕获与取证(docs/design/CRASH-REPORTS.md——F1/F2 为 R25 与
2
+ * crash-reports.mjs — 崩溃捕获与取证(docs/cli/design/CRASH-REPORTS.md——F1/F2 为 R25 与
3
3
  * TUI-STDERR-CAPTURE 存量迁移;F3 近堆上限堆快照 = TUI-OOM-FORENSICS 批新增)。
4
4
  *
5
5
  * 能力(与设计逐项对应):
@@ -8,7 +8,8 @@
8
8
  * 自动写 report.*.json。实现批实测(2026-09-07):目录缺失时 Node 对 fatal 静默不写
9
9
  * 报告——预建是必要动作而非"零成本保险"。
10
10
  * F3①:同点武装近堆上限堆快照(v8.setHeapSnapshotNearHeapLimit——对象级证据,回答
11
- * "谁在持内存";全路径单源,默认开、THINCODER_HEAP_SNAPSHOT 可关——F3②)。
11
+ * "谁在持内存";全路径单源,默认关、配置键 `diagnostics.heapSnapshot` 取 `true` 才武装
12
+ * (fail-closed;bin 入口读键后经 `heapSnapshot` 参数显式传入——F3②)。
12
13
  * - writeCrashRecord()(F-R25a):JS 异常钩子同步落盘 crash-{ts}-{pid}.json(ts = epoch ms
13
14
  * UTC + pid——跨进程同 ms 防覆盖——复审 #3)——权限 0600(config.json 先例)。
14
15
  * - recentCrashHint()(F-R25c):启动扫描 24h 窗内记录(两类文件模式定死——评审 #8:
@@ -23,15 +24,12 @@
23
24
  * 纪律:全部同步 API(崩溃路径无异步);调用方按执行序列各步独立 try/catch(复审 #2——
24
25
  * 写失败/恢复失败不阻断后续步——exit 非 0 恒达)。
25
26
  */
26
- import { chmodSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"
27
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs"
27
28
  import { join } from "node:path"
28
29
  // F3① 命名空间 import:API 缺失(旧 Node)降级为调用期异常并被武装 try 吞掉——不做 import 期硬失败
29
30
  import * as v8 from "node:v8"
30
31
  import { configDir } from "@thincoder/core/config.mjs"
31
32
 
32
- /** F3② 关值集合(trim + 大小写不敏感);其余取值(未设 / 空串 / 未知串)默认开——fail-open 向取证。 */
33
- const HEAP_SNAPSHOT_OFF_VALUES = new Set(["0", "false", "off", "no"])
34
-
35
33
  /** crash-reports 运行时目录(~/.thincoder/crash-reports——非仓内——写时自清理)。 */
36
34
  export function crashReportsDir() {
37
35
  return join(configDir, "crash-reports")
@@ -43,8 +41,14 @@ export function crashReportsDir() {
43
41
  * Heap.*.heapsnapshot(CRASH-REPORTS F3③——快照 GB 级、TUI 落点须自动兜底)——但
44
42
  * recentCrashHint 两类均不计(N3):tui-stderr 每次 TUI 启动都生成(正常退出也留档)、
45
43
  * 快照非「异常终止」证据类——计入即正常会话误报。 */
44
+ /** 记录类判定单源(`isCrashRecordName` 与 `recordFacts` 共用——防两处正则漂移):自写 / Node fatal / 其余。 */
45
+ function recordClassOf(name) {
46
+ if (/^crash-.+\.json$/.test(name)) return "self"
47
+ if (/^report\..+\.json$/.test(name)) return "native"
48
+ return null
49
+ }
46
50
  function isCrashRecordName(name) {
47
- return /^crash-.+\.json$/.test(name) || /^report\..+\.json$/.test(name)
51
+ return recordClassOf(name) !== null
48
52
  }
49
53
  function isPurgeRecordName(name) {
50
54
  return isCrashRecordName(name) || /^tui-stderr-.+\.log$/.test(name) || /^Heap\..+\.heapsnapshot$/.test(name)
@@ -63,21 +67,16 @@ function purgeOldCrashReports(dir) {
63
67
  }
64
68
  }
65
69
 
66
- /** F3② 武装判定(单点——包装器不判 env,防两处判定漂移):关值集合命中 → 不武装;其余 → 武装。 */
67
- function heapSnapshotEnabled(env) {
68
- return !HEAP_SNAPSHOT_OFF_VALUES.has(String(env?.THINCODER_HEAP_SNAPSHOT ?? "").trim().toLowerCase())
69
- }
70
-
71
70
  /**
72
71
  * F-R25b:入口最前调用(一切重活前——缩编程期窗口)——预建目录 + process.report 启用。
73
72
  * 任何失败不阻断启动(尽力面——record 路径自带降级)。返回目录路径。
74
- * F3①:同点武装近堆上限堆快照(默认开——全路径单源;环境开关见 heapSnapshotEnabled)。
75
- * @param {object} [opts] 注入缝(默认参数保 bin 入口调用点零改)
73
+ * F3①:同点武装近堆上限堆快照(默认关——fail-closed,显式 `true` 才武装;全路径单源;开关 = bin 入口读配置键后经 `heapSnapshot` 传入)。
74
+ * @param {object} [opts] 注入缝(默认参数 = 生产常态)
76
75
  * @param {string} [opts.dir] 目录注入(测试用——同时服务 mkdir / report / purge / 返回)
77
- * @param {object} [opts.env] env 注入(测试用)——仅服务 F3② 判定
76
+ * @param {boolean} [opts.heapSnapshot] 快照武装开关(bin 入口经配置键判定后显式传入;配置默认关、显式 `true` 才武装——fail-closed;形参缺省 = 内部约定,生产调用点恒显式传值)
78
77
  * @param {(n: number) => void} [opts.armHeapSnapshot] 武装实现注入(测试用)——默认真实 node:v8 API
79
78
  */
80
- export function prepareCrashReporting({ dir = crashReportsDir(), env = process.env, armHeapSnapshot = v8.setHeapSnapshotNearHeapLimit } = {}) {
79
+ export function prepareCrashReporting({ dir = crashReportsDir(), heapSnapshot = true, armHeapSnapshot = v8.setHeapSnapshotNearHeapLimit } = {}) {
81
80
  try {
82
81
  mkdirSync(dir, { recursive: true })
83
82
  // 代码内启用:shebang 入口无法携带启动参数(env 单参数限制 + execArgv 仅子进程——评审 #1)
@@ -85,7 +84,7 @@ export function prepareCrashReporting({ dir = crashReportsDir(), env = process.e
85
84
  process.report.reportOnFatalError = true
86
85
  } catch { /* mkdir/启用失败不阻断启动——尽力面 */ }
87
86
  // F3① 武装(独立 try——失败不阻断启动、不影响上方 F1 既有步骤;尽力面静默)
88
- if (heapSnapshotEnabled(env)) {
87
+ if (heapSnapshot) {
89
88
  try { armHeapSnapshot(1) } catch { /* 武装失败不阻断——N1 */ }
90
89
  }
91
90
  purgeOldCrashReports(dir) // F-R25b 入口 mkdir 搭车清理(复审 #4——纯 fatal 序列也触发)
@@ -124,10 +123,33 @@ export function writeCrashRecord({ type, error }) {
124
123
  }
125
124
  }
126
125
 
126
+ /**
127
+ * 提示行事实段(台账 #212 · 用户裁 B):从选中记录读回 `uptime` / `cwd` 两段——段序固定 =
128
+ * uptime 段(` · 运行 <u>s`,一位小数)在前 / cwd 段(` · cwd <c>`)在后。
129
+ * 字段源按记录类分:自写记录(`crash-*.json`)取顶部 `uptime` / `cwd`;Node fatal 报告
130
+ * (`report.*.json`)取 `header.cwd`——Node 报告无 uptime 字段(2026-09-25 实测),该段略去。
131
+ * 退化(尽力面 · N1):读档 / 解析失败、记录类不明、字段缺 / 型不符(`uptime` 非有限非负数 ·
132
+ * `cwd` 非非空字符串)⇒ 对应段略去;两段皆缺 ⇒ 空串(基础形)。不抛、不阻断启动。
133
+ */
134
+ function recordFacts(file, name) {
135
+ const cls = recordClassOf(name)
136
+ if (cls === null) return "" // 记录类不明 ⇒ 零事实
137
+ let rec
138
+ try { rec = JSON.parse(readFileSync(file, "utf8")) } catch { return "" }
139
+ if (rec === null || typeof rec !== "object") return ""
140
+ let facts = ""
141
+ if (cls === "self" && typeof rec.uptime === "number" && Number.isFinite(rec.uptime) && rec.uptime >= 0) {
142
+ facts += ` · 运行 ${rec.uptime.toFixed(1)}s`
143
+ }
144
+ const cwd = cls === "native" ? rec.header?.cwd : rec.cwd
145
+ if (typeof cwd === "string" && cwd !== "") facts += ` · cwd ${cwd}`
146
+ return facts
147
+ }
148
+
127
149
  /**
128
150
  * F-R25c:启动扫描——crash-reports 24h 窗内是否有记录(两类模式——mtime 判定)。
129
151
  * @param {object} [opts] 可选 { dir }——目录注入(测试用);默认 crashReportsDir()
130
- * @returns {string|null} 提示文本(含最新记录完整路径);无匹配 → null(负例——不提示)
152
+ * @returns {string|null} 提示文本(含最新记录完整路径 + 事实段——#212);无匹配 → null(负例——不提示)
131
153
  */
132
154
  export function recentCrashHint({ dir = crashReportsDir() } = {}) {
133
155
  purgeOldCrashReports(dir) // F-R25c 启动扫描搭车清理(复审 #4)
@@ -141,10 +163,10 @@ export function recentCrashHint({ dir = crashReportsDir() } = {}) {
141
163
  try {
142
164
  const st = statSync(join(dir, name))
143
165
  if (st.mtimeMs >= cutoff && (!best || st.mtimeMs > best.mtimeMs)) {
144
- best = { path: join(dir, name), mtimeMs: st.mtimeMs }
166
+ best = { path: join(dir, name), name, mtimeMs: st.mtimeMs }
145
167
  }
146
168
  } catch { /* 单文件 stat 失败跳过 */ }
147
169
  }
148
170
  if (!best) return null
149
- return `上次运行异常终止(记录:${best.path})`
171
+ return `上次运行异常终止(记录:${best.path}${recordFacts(best.path, best.name)})`
150
172
  }
package/src/distill.mjs CHANGED
@@ -40,7 +40,7 @@ If the session is long, prioritize conclusions that appeared last and are still
40
40
  Session log:
41
41
  `
42
42
 
43
- /** §18.6 D-TR6(2026-09-04 fix round1):distill 调用点无 agent 作用域——traces 开关
43
+ /** TRACES.md §6.1 D-TR6(2026-09-04 fix round1):distill 调用点无 agent 作用域——traces 开关
44
44
  * 缺省回退磁盘配置(loadConfig——与 agent.config 同源:traces.enabled 缺省 on);
45
45
  * 配置不可读时按缺省 on(注:CLI 启动早已 loadConfig——此处仅是兜底防御)。 */
46
46
  function tracesEnabledFromConfig() {
@@ -54,14 +54,14 @@ function tracesEnabledFromConfig() {
54
54
  /**
55
55
  * Extract candidates from a session transcript. transcript: plain-text session record.
56
56
  * Returns [{ type, title, content, tags, layer }], or [] on parse failure.
57
- * opts.traces(可选):§18.6 D-TR6 开关显式透传(测试隔离/未来调用方)——缺省回退
57
+ * opts.traces(可选):TRACES.md §6.1 D-TR6 开关显式透传(测试隔离/未来调用方)——缺省回退
58
58
  * 磁盘配置(tracesEnabledFromConfig)——关 = chat() 出口不落盘。
59
59
  */
60
60
  export async function extractCandidates(provider, transcript, opts = {}) {
61
61
  const traces = opts.traces ?? tracesEnabledFromConfig()
62
62
  const res = await chat(provider, {
63
63
  messages: [{ role: "user", content: DISTILL_PROMPT + transcript }],
64
- // §18.6 D-TR4/D-TR6(fix round1):distill 调用经 chat() 唯一采集点——补轨迹
64
+ // TRACES.md §6.1 D-TR4/D-TR6(fix round1):distill 调用经 chat() 唯一采集点——补轨迹
65
65
  // 元数据 + traces 开关透传(关=不落盘必须全覆盖——不再静默越过开关)
66
66
  logCtx: { stage: "distill", kind: "distill", traces },
67
67
  })
@@ -9,8 +9,8 @@
9
9
  * 无绝对 MB 常量);双档边缘触发(每档一次/进程——不重复刷屏);零常态开销
10
10
  * (60s 一次 memoryUsage(),无输出——D-HW5)。
11
11
  *
12
- * 开关单点:`THINCODER_HEAP_WATCH`——关值集合 {`0`,`false`,`off`,`no`}(trim + 大小写
13
- * 不敏感)→ 不启动;未设/空串/其他值 → 启动(默认开——与 F3② 同约定)。
12
+ * 开关:配置键 `diagnostics.heapWatch`(默认开——判定形 `!== false`,与 F3 快照面默认相反〔快照默认关〕)——bin 入口读键后经
13
+ * `enabled` 参数显式传入;关 ⇒ 不注册定时器(返回惰性句柄)。
14
14
  *
15
15
  * 注入缝(N5 可测):`sample` / `heapLimit` / `timer`——测试以假实现 + `checkNow()`
16
16
  * 直驱(零等待);失败面全吞(采样抛错不阻断——样本失败静默跳过本次)。
@@ -18,15 +18,6 @@
18
18
  import { getHeapStatistics } from "node:v8"
19
19
  import { logEvent } from "@thincoder/core/log.mjs"
20
20
 
21
- /** 关值集合(单点判定——§3.2 同约定复用)。 */
22
- export const HEAP_WATCH_OFF = new Set(["0", "false", "off", "no"])
23
-
24
- /** 开关判定(单点——grep 面:仅本模块读 env.THINCODER_HEAP_WATCH)。 */
25
- export function heapWatchEnabled(env = process.env) {
26
- const v = String(env.THINCODER_HEAP_WATCH ?? "").trim().toLowerCase()
27
- return !HEAP_WATCH_OFF.has(v)
28
- }
29
-
30
21
  // 订阅表(TUI 行接线——§8.3 订阅制;模块级:武装在 bin、订阅在 startTUI)
31
22
  const _subscribers = new Set()
32
23
 
@@ -45,14 +36,14 @@ export function heapWarnLine(used, limit, ratio) {
45
36
 
46
37
  /**
47
38
  * 启动看门狗(返回 `{ stop(), checkNow() }`——checkNow 直驱一次采样并返回本轮新发预警行)。
48
- * 关值(§8.3)→ 不注册定时器(返回惰性句柄)。
39
+ * 开关关(`enabled: false`)→ 不注册定时器(返回惰性句柄)。
49
40
  */
50
41
  export function startHeapWatch({
51
42
  intervalMs = 60_000,
52
43
  ratios = [0.7, 0.85],
53
44
  sample = process.memoryUsage,
54
45
  heapLimit = () => getHeapStatistics().heap_size_limit,
55
- env = process.env,
46
+ enabled = true,
56
47
  timer = setInterval,
57
48
  } = {}) {
58
49
  const warned = new Set()
@@ -76,7 +67,7 @@ export function startHeapWatch({
76
67
  } catch { /* 失败面全吞(N5——采样/上限抛错不阻断、状态保持) */ }
77
68
  return out
78
69
  }
79
- if (!heapWatchEnabled(env)) return { stop() {}, checkNow }
70
+ if (!enabled) return { stop() {}, checkNow }
80
71
  const handle = timer(checkNow, intervalMs)
81
72
  try { handle?.unref?.() } catch { /* unref 失败不阻断(一次性命令自然退出) */ }
82
73
  return {
@@ -16,5 +16,5 @@ export const CLI_PROMPT_INJECTIONS = {
16
16
  // bash 宿主终端面——CLI 无 terminal 参数(工具面 review 再统一)
17
17
  "bash-terminal-face": "",
18
18
  // question 工具可用性行(核内正文该行已移出正文、锚原位替换——本端填回 CLI 措辞)
19
- "question-ui-face": "- Availability: this tool needs an interactive UI — in contexts without one (headless runs, subagent children) it returns an error instead of asking; put the question in your reply text instead.",
19
+ "question-ui-face": "- Availability: this tool needs an interactive UI — in contexts without one (headless runs) it returns an error instead of asking; subagent children (depth>0) never get it (excluded from their tool tables); put the question in your reply text instead.",
20
20
  }