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.
Files changed (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
package/src/session.mjs DELETED
@@ -1,475 +0,0 @@
1
- /**
2
- * session.mjs — session persistence (slot-based model)
3
- * Each project (keyed by cwd hash) keeps unlimited session slots.
4
- * Every session lives in a numbered slot; the manifest tracks the SHARED active pointer
5
- * (2026-09-05 §10 D-6 修订:active = 旧版端/ACP 的恢复依据 + 无记录端的一次性继承源 +
6
- * 列表回退高亮——本端恢复依据 = end marker `{manifest}.cli`,见 session-slots.mjs
7
- * resumeSlot/readEndMarker,SESSION.md §1/§10)。
8
- *
9
- * File layout: {hash}.json.N (slots), {hash}.json.manifest (slot metadata + active pointer).
10
- * Legacy {hash}.json is migrated to a slot on first access.
11
- *
12
- * 2026-08-31 advisor round1 🔴:slot/清单管理拆至 session-slots.mjs(本文件曾超 500 行
13
- * 硬限);本文件只保留核心读写 + re-export 全部 slot 导出(既有 import 路径不变)。
14
- */
15
-
16
- import { readFileSync, renameSync, existsSync, statSync } from "node:fs"
17
- import { basename } from "node:path"
18
- import { resolveCompactThreshold } from "./config.mjs"
19
- import {
20
- slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
21
- activeSlot, getSessionId, isProcessAlive,
22
- writeEndMarker, resumeSlot as slotsResumeSlot,
23
- } from "./session-slots.mjs"
24
- // F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
25
- import { guardForeignSlotFile } from "./session-guard.mjs"
26
- import { scheduleSessionGC } from "./session-gc.mjs"
27
- import { engTokenSlotFields, restoreEngTokens } from "./token-ttl.mjs"
28
-
29
- // re-export slot 管理(保持既有 import session.mjs 的调用点不变;resumeSlot 下方本地包装导出)
30
- export {
31
- getSessionId, normalizeCwd, sessionPath, slotPath, manifestPath, activePath,
32
- writeSessionFile, slotDigest, loadManifest, saveManifest, activeSlot, listSlots,
33
- deleteSlot, isProcessAlive, END, endMarkerPath,
34
- readEndMarker, writeEndMarker, claimSlot, allocateFresh,
35
- } from "./session-slots.mjs"
36
- // §12.2.5 契约改使 session-slots.mjs 超 500 行硬限 → renameSlot 拆至 session-rename.mjs(§12.3 授权)
37
- export { renameSlot } from "./session-rename.mjs"
38
-
39
- /** 恢复决策包装(SESSION.md §12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
40
- * scheduleSessionGC 内部 setImmediate 空闲执行 + 每进程每前缀去重,不阻塞启动路径(N4)。 */
41
- export function resumeSlot(cwd) {
42
- scheduleSessionGC(cwd)
43
- return slotsResumeSlot(cwd)
44
- }
45
-
46
- // ========== legacy transient prefix cleanup ==========
47
-
48
- const LEGACY_TRANSIENT_PREFIXES = [
49
- "[System reminder: working directory snapshot:",
50
- "[Relevant memories from previous sessions",
51
- ]
52
-
53
- function isLegacyTransient(m) {
54
- return (
55
- m.role === "user" &&
56
- typeof m.content === "string" &&
57
- LEGACY_TRANSIENT_PREFIXES.some((p) => m.content.startsWith(p))
58
- )
59
- }
60
-
61
- export { isLegacyTransient }
62
-
63
- // ========== core read/write ==========
64
-
65
- /** Slim the HUMAN line (history) for storage — the machine line (contextHistory)
66
- * keeps everything byte-identical for the provider. Deepseek-consult design
67
- * (2026-08-30): the human line is never compacted and carries the bulk of
68
- * session-file size (tool args JSON / full tool results / base64 images), while
69
- * nothing consumes its verbatim fidelity. Rules (copy-on-write ONLY — the two
70
- * lines share object references via pushReal; mutating in place would corrupt
71
- * the machine line and provider prefix cache):
72
- * - assistant.tool_calls[].function.arguments → trimmed to 300 chars (head + …)
73
- * - tool messages content → 500 chars (head + …)
74
- * - multimodal user content array → keep text parts, DROP image_url base64 parts
75
- * - plain string messages → untouched (not the size driver)
76
- */
77
- function slimForDisplay(m) {
78
- if (m && Array.isArray(m.content)) {
79
- // Multimodal user message: keep text parts, drop image parts.
80
- const textParts = m.content.filter((p) => p?.type !== "image_url")
81
- if (textParts.length === m.content.length) return m
82
- return { ...m, content: textParts }
83
- }
84
- if (m && m.role === "assistant" && Array.isArray(m.tool_calls)) {
85
- let changed = false
86
- const tool_calls = m.tool_calls.map((tc) => {
87
- const args = tc.function?.arguments
88
- if (typeof args === "string" && args.length > 300) {
89
- changed = true
90
- return { ...tc, function: { ...tc.function, arguments: args.slice(0, 300) + "…" } }
91
- }
92
- return tc
93
- })
94
- return changed ? { ...m, tool_calls } : m
95
- }
96
- if (m && m.role === "tool" && typeof m.content === "string" && m.content.length > 500) {
97
- return { ...m, content: m.content.slice(0, 500) + "\n… (truncated for storage)" }
98
- }
99
- return m
100
- }
101
-
102
- /** Save agent state to the active slot file (atomic write). `display` (the old
103
- * WYSIWYG render snapshot) is DEPRECATED — it drifted out of sync with history
104
- * whenever VS Code wrote the slot, and the TUI resumed from a stale snapshot.
105
- * Restore now always rebuilds from history (lazy, see startup.mjs).
106
- * 2026-08-31 会诊 F1:slot 粘性——首次认领后缓存 agent._slot,永不重跑 ensureActive
107
- * (原实现每次保存重推,manifest active 被并发方翻动时会话静默迁移)。
108
- * 返回轮转的 .bak 路径或 null。 */
109
- export function saveSession(agent) {
110
- // _fullHistory is written at the source via pushReal — no flush needed here.
111
- // history = FULL, never-compacted (human-readable; VS Code panel & CLI resume read this)
112
- // contextHistory = machine context (possibly compacted) so CLI resume keeps the token savings
113
- // Human line: transient machine injections never enter the readable record.
114
- const history = (agent._fullHistory ?? agent.history)
115
- .filter((m) => !m.transient && !isLegacyTransient(m))
116
- .map(slimForDisplay)
117
- // Machine line (contextHistory): KEEP transient messages — resume must rebuild the
118
- // machine line byte-identical to what the provider cache saw. Dropping them made every
119
- // process restart diverge at the first injection position (git/OS/time reminders are
120
- // re-injected with FRESH content) → whole-prefix cache miss on the CLI's very first
121
- // request of each session (2026-08-16 cache-hit report; Kimi review).
122
- const contextHistory = agent.history.filter((m) => !isLegacyTransient(m))
123
- const data = {
124
- version: 2,
125
- cwd: agent.cwd,
126
- title: agent.title ?? "",
127
- // MODEL-MERGE-SESSION 槽双字段恒非空(裁定 a):有 provider 即携带具体复合值——
128
- // 恢复不看 config(F-4)——无渠道默认可回落 ""/null(全新未配态——读侧容忍)
129
- activeProvider: agent.activeProvider ?? agent.provider?.name ?? "",
130
- activeModel: agent.activeModel ?? agent.provider?.model ?? null,
131
- updatedAt: Date.now(),
132
- history,
133
- contextHistory,
134
- tasks: agent.tasks ?? [],
135
- planMode: agent.planMode ?? false,
136
- autoApprove: agent.autoApprove ?? false,
137
- engineering: agent.config?.agent?.engineering ?? false,
138
- ...engTokenSlotFields(agent),
139
- goal: agent.goal ?? null,
140
- advisor: agent.config?.advisor ?? null,
141
- pendingReminders: agent._pendingReminders ?? [],
142
- sessionStart: agent._sessionStart ?? null,
143
- }
144
- // 2026-09-05 §10 D-4:首认领(_slot 为 null——如 /session 切换后首保存、"查看对方活槽 →
145
- // 保存 fork 新槽"的落盘槽跟随)写本端记录——marker = 端内最后认领者;粘性 _slot 期间
146
- // 的日常保存不写(F1:保存永不参与竞争)
147
- const claimedNow = agent._slot == null
148
- const slot = agent._slot ??= activeSlot(agent.cwd)
149
- if (claimedNow) writeEndMarker(agent.cwd, slot)
150
- const p = slotPath(agent.cwd, slot)
151
- // 2026-08-31 会诊 F2 🔴:写前校验磁盘文件的 sessionStart——与本进程会话不符(另一
152
- // 进程/会话的现场)→ 先轮转 .bak 保留再写(11311 条历史被新进程覆盖的实锤场景)。
153
- // 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-guard.mjs——DESIGN-TOKEN-
154
- // SETTLEMENT D1 二次拆分:先入 session-slots、再因 500 行硬限迁 session-guard)——
155
- // saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
156
- // mtime 缓存 _slotMtime:自写未变跳过全量解析)。
157
- const rotated = guardForeignSlotFile(agent, p, slot)
158
- writeSessionFile(p, data)
159
- // 记录我们刚写的 mtime——下次保存跳过重复解析
160
- try { agent._slotMtime = statSync(p).mtimeMs } catch {}
161
- // Update slot metadata in manifest
162
- try {
163
- const m = loadManifest(agent.cwd)
164
- m.slots[slot] = slotDigest(data)
165
- saveManifest(agent.cwd, m)
166
- } catch (e) {
167
- // Manifest update failure is non-fatal — data is safe, metadata will lazy-recover on next listSlots
168
- console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
169
- }
170
- return rotated
171
- }
172
-
173
- /** Load slot file with shared validation — 2026-08-31 会诊 deepseek 🟡 抽取:
174
- * loadSession(active 槽)/switchToSlot/ACP session/load 共享。无认领副作用。
175
- * version 1/2 + history 数组 + cwd 匹配(2026-09-01 会诊 kimi 🔵:cwd 先行——"别人的
176
- * 文件不动"优先于结构校验,异 cwd + 坏 version 不得改名);结构不符改名 .unreadable、
177
- * 解析失败 .tmp 回退成功后提升为正主(损坏主文件改名 .corrupted 保留)、主文件缺失
178
- * 时恢复孤儿 .tmp(rename 前崩溃现场)。 */
179
- export function loadSlotFile(cwd, slot) {
180
- const p = slotPath(cwd, slot)
181
- const tryLoad = (path) => {
182
- try {
183
- if (!existsSync(path)) return null
184
- const data = JSON.parse(readFileSync(path, "utf8"))
185
- if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null // 别人的文件,不动
186
- if (data?.version !== 1 && data?.version !== 2) {
187
- if (typeof data?.version === "number" && data.version > 2) return null // 新版 CLI 的文件,不动(F2 轮转兜底)
188
- try { renameSync(path, `${path}.unreadable`) } catch {}
189
- console.error(`[session] unsupported version ${data?.version} at ${path} — preserved as ${basename(path)}.unreadable`)
190
- return null
191
- }
192
- if (!Array.isArray(data.history)) {
193
- try { renameSync(path, `${path}.unreadable`) } catch {}
194
- console.error(`[session] slot file ${path}: history is not an array — preserved as ${basename(path)}.unreadable`)
195
- return null
196
- }
197
- data.history = data.history.filter((m) => !isLegacyTransient(m))
198
- return data
199
- } catch (e) {
200
- return { _error: e }
201
- }
202
- }
203
-
204
- let result = tryLoad(p)
205
- if (result && !result._error) return result
206
- if (result?._error) {
207
- console.error(`[session] failed to load slot ${slot}: ${result._error.message}. Trying .tmp fallback...`)
208
- const tmpResult = tryLoad(`${p}.tmp`)
209
- if (tmpResult && !tmpResult._error) {
210
- console.error(`[session] recovered from .tmp fallback`)
211
- // 2026-09-01 会诊 🟢:.tmp 提升为正主(损坏主文件改名 .corrupted 保留现场)——
212
- // 否则主文件留在原地,每次加载都重复"恢复",且保存侧 F2 会持续轮转它。
213
- try {
214
- renameSync(p, `${p}.corrupted`)
215
- renameSync(`${p}.tmp`, p)
216
- } catch {}
217
- return tmpResult
218
- }
219
- console.error(`[session] .tmp fallback also failed — session lost.`)
220
- try { renameSync(p, `${p}.corrupted`) } catch {}
221
- } else if (!result) {
222
- // 2026-09-01 advisor 🔵:主文件缺失但孤儿 .tmp 存在(原子写 rename 前崩溃)——
223
- // 恢复并提升为正主(SESSION.md §4 的 .tmp 回退语义应覆盖此场景)。
224
- const orphan = tryLoad(`${p}.tmp`)
225
- if (orphan && !orphan._error) {
226
- console.error(`[session] slot ${slot} main file missing — recovered orphan .tmp`)
227
- try { renameSync(`${p}.tmp`, p) } catch {}
228
- return orphan
229
- }
230
- }
231
- return null
232
- }
233
-
234
- /** Load session data for this cwd — 2026-09-05 §10 D-2:平移为 resumeSlot 的数据包装
235
- * (签名不变——测试兼容;读槽校验/.tmp 回退/legacy 兜底语义原样保留在 resumeSlot 的
236
- * data 层)。恢复目标选择按本端记录(end marker)——manifest active 仅作无记录端的
237
- * 一次性继承源(D-6),不再作本端恢复第一依据。 */
238
- export function loadSession(cwd) {
239
- return resumeSlot(cwd).data
240
- }
241
-
242
- /** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
243
- * v1 老文件回退播种机器线时置为 {}(合法空参数),防止半截 \\uXXXX 毒化发送载荷(会诊 F6)。 */
244
- function stripTruncatedToolArgs(m) {
245
- if (m?.role !== "assistant" || !Array.isArray(m.tool_calls)) return m
246
- let changed = false
247
- const tool_calls = m.tool_calls.map((tc) => {
248
- const args = tc?.function?.arguments
249
- if (typeof args === "string" && args.endsWith("…")) {
250
- changed = true
251
- return { ...tc, function: { ...tc.function, arguments: "{}" } }
252
- }
253
- return tc
254
- })
255
- return changed ? { ...m, tool_calls } : m
256
- }
257
-
258
- /** Apply loaded session data onto an agent object; returns true if the runtime composite
259
- * was switched. 2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
260
- *
261
- * MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
262
- * "三支"误读):
263
- * ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel,
264
- * legacy 槽 null/空串回该渠道默认模型 `providers[].model`(单值——2026-09-10 MODEL-SELECTION
265
- * v2;老 models[] 首候选形态已退场))+ 重算
266
- * compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
267
- * ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
268
- * 复验 validateProvider 弹重选)。
269
- * 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
270
- * 三旧层删除消失——双字段恒非空故不可能触发(评审 #7)。 */
271
- export function applySession(agent, data) {
272
- // 机读线语义(历史注释保留):data.history = FULL 未压缩记录(人读线);data.contextHistory =
273
- // 压缩后机器线——恢复各从其源(保留压缩收益——从完整 history 重建会塞回中间过程——实测
274
- // prompt 膨胀 283%)。v1 老文件(无 contextHistory)回退播种,剥离被 slimForDisplay 截断的
275
- // tool_calls.arguments(… 结尾 → 置 {}——截断可劈断 \\uXXXX 产生 400 毒载荷)。length > 0 才
276
- // 当机读线(contextHistory: [] 是"无机读线"而非空机器线——空机器线静默丢全部上下文)。
277
- agent.config ??= {} // ACP test mocks may omit config; be defensive like the ??= below
278
- const full = Array.isArray(data.history) ? data.history : []
279
- // SESSION.md §11.2(2026-09-08——F2/F3):恢复事件按当前 data 重定——载入历史非空 = 会话
280
- // 恢复 → 下个 depth-0 回合 env-state resumed:yes 一次(pushEnvStateReminder 读即清——每次恢复
281
- // 一次)。/new 与空历史槽切换不武装(显式清 false)——无恢复事件恒 no(F3 伪触发消除——不再靠
282
- // _sessionStart != null 推断;已武装 agent 切到空历史槽也不残留上次恢复事件——评审 round2 🟡)。
283
- // _processRestartPending 不在此清——由启动路径(bin/thincoder.mjs)设、prepareRun 发句清
284
- // (N6——双信号独立:切槽不报进程重启)。
285
- // 跨端异名互指(结构债批 5 N7——双端同语义各自独立实现、命名不统一是刻意——SESSION.md
286
- // §11.2):VSC 端同机制载体异名 = agent._resumedPending(src/agent/setup.mjs hydrateRun——
287
- // restore:true 工厂 + fullHistory 非空时武装——每槽恢复一次)+ 模块级 restartDetectionDone
288
- // 一次性闸(src/agent/setup-reminders.mjs——process restarted 句的进程级信号)。
289
- agent._envResumed = full.length > 0
290
- const ch = data.contextHistory
291
- const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
292
- agent._fullHistory = [...full]
293
- agent.history = [...machine]
294
- agent.title = data.title ?? ""
295
- agent.tasks = data.tasks ?? []
296
- agent.planMode = data.planMode ?? false
297
- agent.autoApprove = data.autoApprove ?? false
298
- agent.goal = data.goal ?? null
299
- agent._pendingReminders = data.pendingReminders ?? []
300
- agent._sessionStart = data.sessionStart ?? null
301
- // R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens)
302
- restoreEngTokens(agent, data)
303
- // engineering is session-level (2026-08-29): the slot value is the CLI session's authority
304
- // — config.json is only the initial default / cross-end mirror. A legacy slot without the
305
- // field keeps whatever config.json seeded (unchanged behavior).
306
- if (data.engineering !== undefined) {
307
- agent.config.agent ??= {}
308
- agent.config.agent.engineering = data.engineering === true
309
- }
310
- if (data.advisor) {
311
- agent.config.advisor = { ...data.advisor }
312
- }
313
- // Reset stall/compaction state on session switch
314
- agent._compressFailures = 0
315
- agent._verifyRetries = 0
316
- agent._verifyPassed = false
317
- agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
318
- agent._slotMtime = null
319
- // ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
320
- const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
321
- if (slotProvider) {
322
- // ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
323
- // null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
324
- // F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
325
- // 空槽 model 恒有值,provider.model 键不缺失)
326
- const prevName = agent.activeProvider
327
- const prevModel = agent.activeModel ?? null
328
- const slotModel = data.activeModel || slotProvider.model
329
- const switched = prevName !== slotProvider.name || prevModel !== slotModel
330
- agent.activeProvider = slotProvider.name
331
- agent.activeModel = slotModel
332
- agent.provider = { ...slotProvider }
333
- if (slotModel) agent.provider.model = slotModel
334
- // 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
335
- if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
336
- agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
337
- }
338
- return switched
339
- }
340
- // ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
341
- // config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
342
- return false
343
- }
344
-
345
- /**
346
- * Create a new session slot: allocate a free slot number,
347
- * write an empty session, and mark it as the active slot.
348
- * No limit on the number of sessions.
349
- */
350
- export function newSession(cwd) {
351
- const m = loadManifest(cwd)
352
-
353
- // 2026-09-01 advisor 🔵:先清理死主条目(与 ensureActive 分支2 语义一致)——否则
354
- // "死主 + 文件缺失"的空槽(m.slots 有条目、无文件、属主已死)永不复用,槽号持续
355
- // 增长。死主且无文件 = 该会话从未落盘(进程死了没保存),条目可安全删除回收。
356
- // 2026-09-01 会诊 deepseek/glm 🟡:清理必须经 deletions 显式落盘——saveManifest 条目级
357
- // 合并会把磁盘死条目从 fresh 复活回写,仅传 m 等于没删(VS Code newSlot 已修,CLI 对称)。
358
- const mySessionId = getSessionId()
359
- const deadSlots = []
360
- for (const [s, owner] of Object.entries(m.slotSessions ?? {})) {
361
- if (owner && owner !== mySessionId) {
362
- const pid = parseInt(owner.split("-")[0])
363
- if (!pid || !isProcessAlive(pid)) {
364
- delete m.slotSessions[s]
365
- if (!existsSync(slotPath(cwd, Number(s)))) delete m.slots[s]
366
- deadSlots.push(s)
367
- }
368
- }
369
- }
370
-
371
- // Find next available slot number — 2026-08-31 会诊 deepseek 🟡:不能只看 manifest
372
- // 条目(丢失更新可能让条目消失而文件仍在)——复用该号会直接覆写真实会话
373
- // (F2 防护不覆盖 newSession 的空数据直写)。
374
- // 2026-08-31 advisor round2 🟡:同时跳过"已被另一活进程认领但尚未落盘"的号
375
- // (slotSessions 有条目、slots 无条目、文件不存在)——否则双进程会认领同一号。
376
- const liveClaimed = (n) => {
377
- const owner = m.slotSessions?.[n]
378
- return !!(owner && owner !== mySessionId && isProcessAlive(parseInt(owner.split("-")[0])))
379
- }
380
- let slot = 1
381
- while (m.slots[slot] || existsSync(slotPath(cwd, slot)) || liveClaimed(slot)) slot++
382
-
383
- // Write empty session — 2026-09-01 advisor 🔵:补 contextHistory/planMode 字段与
384
- // VS Code newSlot 对齐(SESSION.md §3 v2 格式双端一致;两端读侧均有兜底,功能等价)
385
- const data = { version: 2, cwd, title: "", updatedAt: Date.now(), history: [], contextHistory: [], tasks: [], planMode: false, goal: null, autoApprove: false, advisor: null, pendingReminders: [], sessionStart: null }
386
- writeSessionFile(slotPath(cwd, slot), data)
387
- m.slots[slot] = slotDigest(data)
388
- m.active = slot
389
- // 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
390
- // 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
391
- // 认领 → 双进程写同一槽(F2 轮转互旋)。
392
- m.slotSessions ??= {}
393
- m.slotSessions[slot] = mySessionId
394
- // 2026-09-01 会诊三家 🟡:显式翻 active 的调用点传 setActive(saveManifest 默认保留
395
- // 磁盘 fresh.active,避免把并发方刚翻的指针回滚)
396
- // 2026-09-01 会诊 deepseek/glm 🟡:deletions 过滤掉本调用刚重新认领的槽(防删掉自己的
397
- // 新属主)——与 ensureActive deadParam / VS Code newSlot 同型。
398
- const deletions = deadSlots.length
399
- ? {
400
- slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId),
401
- slots: deadSlots.filter((s) => !m.slots[s]),
402
- }
403
- : null
404
- saveManifest(cwd, m, deletions, { setActive: true })
405
- // 2026-09-05 §10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
406
- writeEndMarker(cwd, slot)
407
- return slot
408
- }
409
-
410
- /** 清空会话运行态(/new 用,2026-08-31 会诊 F3):_fullHistory/title/_sessionStart 等
411
- * 全部会话级状态 + 一次性注入标志必须全清——否则新会话首次落盘把旧会话完整人类线 +
412
- * 旧标题写进新槽(实锤 .19/.3 双副本);注入标志不清则 /new 后新会话永不注入
413
- * OS/cwd reminder(2026-09-01 会诊 glm 🟡)。autoApprove 是用户偏好,跨会话保留(有意)。 */
414
- export function resetSessionState(agent) {
415
- agent._fullHistory = []
416
- agent.history = []
417
- agent.title = ""
418
- agent.tasks = []
419
- agent._sessionStart = null
420
- // DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——不再维护
421
- agent._engDesignTokens = new Map() // multi-design slots die with the session (2026-09-01 fix #2)
422
- agent._compressFailures = 0
423
- agent._verifyRetries = 0
424
- agent._verifyPassed = undefined
425
- agent._runStartHistoryLen = 0
426
- agent._lastPromptTokens = null
427
- agent._usageAtLen = null
428
- agent.planMode = false
429
- agent.goal = null
430
- agent._pendingReminders = []
431
- agent._slot = null
432
- agent._slotMtime = null
433
- agent._osReminderInjected = false
434
- // §11.2(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
435
- // _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
436
- agent._envResumed = false
437
- agent._processRestartPending = false
438
- agent._lastEngState = false
439
- }
440
-
441
- /** Switch the manifest active pointer to a slot. Returns the slot's session data
442
- * (null if the slot doesn't exist / can't be read). 2026-08-31 会诊 deepseek 🔴:
443
- * 原实现经 loadSession → activeSlot 有认领副作用——目标槽被活进程占用时 ensureActive
444
- * 分支 3 会把 active 拨到新空槽并读回 null + 劫持对方指针。现改为 loadSlotFile 直接读
445
- * (无认领副作用);目标槽空闲则一并认领、被另一活进程占用则不认领(slotOccupancy——
446
- * 下次保存经 activeSlot 自然 fork 到新槽);只改 manifest 指针(setActive 意图)。
447
- * 2026-09-01 会诊三家 🟡:saveManifest 条目级合并 + setActive(不把并发方刚翻的指针回滚)。 */
448
- export function switchToSlot(cwd, slot) {
449
- const m = loadManifest(cwd)
450
- if (!m.slots[slot]) return null
451
- const data = loadSlotFile(cwd, slot)
452
- if (!data) return null
453
- m.active = slot
454
- const occ = slotOccupancy(cwd, slot)
455
- if (!occ.occupied) {
456
- m.slotSessions ??= {}
457
- m.slotSessions[slot] = getSessionId()
458
- }
459
- saveManifest(cwd, m, null, { setActive: true })
460
- // 2026-09-05 §10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
461
- writeEndMarker(cwd, slot)
462
- return data
463
- }
464
-
465
- /** 目标槽是否被另一活进程占用(2026-09-01 会诊/advisor 🟡):排除本进程属主——
466
- * /session 重选当前槽不误报;同进程双会话防护由 ACP sameProcessPinned 承担。 */
467
- export function slotOccupancy(cwd, slot) {
468
- const m = loadManifest(cwd)
469
- const owner = m.slotSessions?.[slot]
470
- if (!owner) return { occupied: false }
471
- if (owner === getSessionId()) return { occupied: false }
472
- const pid = parseInt(owner.split("-")[0])
473
- if (!pid || !isProcessAlive(pid)) return { occupied: false }
474
- return { occupied: true, owner }
475
- }
package/src/skills.mjs DELETED
@@ -1,153 +0,0 @@
1
- /**
2
- * skills.mjs — skill system
3
- * Discovers .md skill files AND subdirectory SKILL.md from .thincoder/skills/ directory,
4
- * injects into system prompt for agent to load on demand.
5
- * Use the skill tool to activate a specific skill; content is written into conversation history wrapped in <skill-loaded>.
6
- *
7
- * Supported formats:
8
- * .thincoder/skills/my-skill.md (flat, name = "my-skill")
9
- * .thincoder/skills/my-skill/SKILL.md (subdirectory, name = "my-skill")
10
- */
11
-
12
- import { readFile, readdir, stat } from "node:fs/promises"
13
- import { join } from "node:path"
14
- import { homedir } from "node:os"
15
-
16
- /** Valid skill name pattern (alphanumeric + hyphens/underscores) */
17
- const NAME_RE = /^[a-zA-Z0-9_-]+$/
18
-
19
- /**
20
- * Try to read a skill from a path, return { name, path, description } or null.
21
- */
22
- async function tryReadSkill(dir, name, filePath) {
23
- try {
24
- const s = await stat(filePath)
25
- if (!s.isFile()) return null
26
- const head = await readFile(filePath, "utf8")
27
- const body = head.slice(0, 400).split("\n")
28
- let desc = ""
29
- let inFrontmatter = false
30
- for (const line of body) {
31
- const t = line.trim()
32
- if (t === "---") { inFrontmatter = !inFrontmatter; continue }
33
- if (inFrontmatter) continue
34
- if (t && !t.startsWith("#")) {
35
- desc = t.slice(0, 120)
36
- break
37
- }
38
- }
39
- return { name, path: filePath, description: desc || "(no description)" }
40
- } catch {
41
- return null
42
- }
43
- }
44
-
45
- /**
46
- * Scan a single skills directory, return skill list.
47
- * Supports flat .md files and subdirectories with SKILL.md inside.
48
- * Each skill: { name, path, description } — name derived from filename or directory.
49
- * Returns empty array if directory is missing or empty.
50
- */
51
- async function loadSkillsFromDir(dir) {
52
- let entries
53
- try {
54
- entries = await readdir(dir, { withFileTypes: true })
55
- // Deterministic order: readdir order is filesystem-dependent; an unsorted scan would
56
- // reshuffle the skills listing → system prompt byte change with zero content change
57
- // (2026-08-16 cache audit — that silently misses the provider prefix cache).
58
- entries.sort((a, b) => a.name.localeCompare(b.name))
59
- } catch {
60
- return []
61
- }
62
- const skills = []
63
- const added = new Set()
64
-
65
- // Pass 1: subdirectories (higher priority — standard convention)
66
- for (const entry of entries) {
67
- if (!entry.isDirectory()) continue
68
- if (!NAME_RE.test(entry.name)) continue
69
- const skill = await tryReadSkill(dir, entry.name, join(dir, entry.name, "SKILL.md"))
70
- if (skill) { skills.push(skill); added.add(entry.name) }
71
- }
72
-
73
- // Pass 2: flat .md files (backward compat; skipped if subdirectory with same name exists)
74
- for (const entry of entries) {
75
- if (!entry.isFile()) continue
76
- const m = entry.name.match(/^([a-zA-Z0-9_-]+)\.md$/)
77
- if (!m) continue
78
- const name = m[1]
79
- if (added.has(name)) continue
80
- const skill = await tryReadSkill(dir, name, join(dir, entry.name))
81
- if (skill) { skills.push(skill); added.add(name) }
82
- }
83
- return skills
84
- }
85
-
86
- /**
87
- * Scan both project-level and user-level skills directories.
88
- * Project-level skills (cwd/.thincoder/skills/) take priority over user-level (~/.thincoder/skills/).
89
- * Returns merged skill list with project-level skills first.
90
- */
91
- export async function loadSkills(cwd) {
92
- const projectDir = join(cwd, ".thincoder", "skills")
93
- const userDir = join(homedir(), ".thincoder", "skills")
94
-
95
- // Load project-level skills first (higher priority)
96
- const projectSkills = await loadSkillsFromDir(projectDir)
97
- const added = new Set(projectSkills.map(s => s.name))
98
-
99
- // Load user-level skills, skipping duplicates
100
- const userSkills = await loadSkillsFromDir(userDir)
101
- for (const skill of userSkills) {
102
- if (!added.has(skill.name)) {
103
- projectSkills.push(skill)
104
- added.add(skill.name)
105
- }
106
- }
107
-
108
- return projectSkills
109
- }
110
-
111
- /**
112
- * Generate skill listing text for system prompt injection.
113
- * At most 3 (small footprint); overflow marked "... and N more".
114
- * Prefixed with DISREGARD: when listing refreshes (skills added/removed), old listings are auto-invalidated without needing to delete history (inspired by kimi-code).
115
- */
116
- export function formatSkillListing(skills) {
117
- if (skills.length === 0) return ""
118
- const listed = skills.slice(0, 3)
119
- const lines = listed.map((s) => `- **${s.name}**: ${s.description}`)
120
- if (skills.length > 3) lines.push(` ... and ${skills.length - 3} more`)
121
- return "DISREGARD any earlier skill listings. Current available skills (use the skill tool to load one):\n" + lines.join("\n")
122
- }
123
-
124
- /**
125
- * Read the full content of a specific skill file.
126
- * Tries project-level directory first, then user-level directory.
127
- * For each directory, tries subdirectory format (name/SKILL.md) first, then flat format (name.md).
128
- * Returns text, or null if not found.
129
- */
130
- export async function readSkill(cwd, name) {
131
- if (!NAME_RE.test(name)) return null
132
-
133
- const dirs = [
134
- join(cwd, ".thincoder", "skills"),
135
- join(homedir(), ".thincoder", "skills")
136
- ]
137
-
138
- for (const baseDir of dirs) {
139
- // Try subdirectory format: name/SKILL.md
140
- try {
141
- const p = join(baseDir, name, "SKILL.md")
142
- return await readFile(p, "utf8")
143
- } catch { /* not found, try flat */ }
144
-
145
- // Fallback to flat format: name.md
146
- try {
147
- const p = join(baseDir, `${name}.md`)
148
- return await readFile(p, "utf8")
149
- } catch { /* not found, try next directory */ }
150
- }
151
-
152
- return null
153
- }