thincoder 0.12.62 → 0.12.64

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 (231) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +46 -49
  3. package/bin/thincoder.mjs +68 -34
  4. package/package.json +7 -6
  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 +257 -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 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +60 -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 +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  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-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +35 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-plan.mjs +9 -0
  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 +24 -1
  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 +33 -11
  38. package/src/tui/index.mjs +18 -10
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-modes.mjs +9 -4
  41. package/src/tui/ledger-surface.mjs +22 -59
  42. package/src/tui/model-catalog.mjs +4 -4
  43. package/src/tui/model-picker.mjs +8 -7
  44. package/src/tui/mouse.mjs +11 -6
  45. package/src/tui/pickers.mjs +15 -2
  46. package/src/tui/render-conversation.mjs +1 -1
  47. package/src/tui/render-frame.mjs +15 -6
  48. package/src/tui/render-loop.mjs +1 -1
  49. package/src/tui/render-segments.mjs +3 -1
  50. package/src/tui/slash-commands.mjs +1 -1
  51. package/src/tui/startup.mjs +14 -14
  52. package/src/tui/subagent-blocks.mjs +20 -3
  53. package/src/tui/subagent-freeze.mjs +73 -2
  54. package/src/tui/suspension-drive.mjs +57 -23
  55. package/src/tui/tool-events.mjs +11 -8
  56. package/src/tui/tui-lifecycle.mjs +9 -0
  57. package/src/tui/wizard.mjs +3 -3
  58. package/src/tui/wrapped-spawn.mjs +6 -2
  59. package/src/abort-provenance.mjs +0 -116
  60. package/src/advisor/citations.mjs +0 -139
  61. package/src/advisor/compaction.mjs +0 -174
  62. package/src/advisor/convergence.mjs +0 -80
  63. package/src/advisor/history.mjs +0 -77
  64. package/src/advisor/loop.mjs +0 -293
  65. package/src/advisor/messages.mjs +0 -299
  66. package/src/advisor/project-context.mjs +0 -194
  67. package/src/advisor/repos.mjs +0 -150
  68. package/src/advisor/run.mjs +0 -293
  69. package/src/advisor/truncate.mjs +0 -57
  70. package/src/advisor.mjs +0 -290
  71. package/src/agent/completion.mjs +0 -146
  72. package/src/agent/dispatch.mjs +0 -489
  73. package/src/agent/helpers.mjs +0 -384
  74. package/src/agent/post-turn.mjs +0 -70
  75. package/src/agent/record-results.mjs +0 -174
  76. package/src/agent/relay-prefix.mjs +0 -39
  77. package/src/agent/run-stages.mjs +0 -244
  78. package/src/agent/setup-reminders.mjs +0 -69
  79. package/src/agent/setup.mjs +0 -354
  80. package/src/agent/spawn-child.mjs +0 -243
  81. package/src/agent-tools/advisor-async.mjs +0 -346
  82. package/src/agent-tools/advisor-settle.mjs +0 -231
  83. package/src/agent-tools/advisor.mjs +0 -260
  84. package/src/agent-tools/async-settle.mjs +0 -204
  85. package/src/agent-tools/batch-segment.mjs +0 -195
  86. package/src/agent-tools/consult.mjs +0 -473
  87. package/src/agent-tools/design-token.mjs +0 -117
  88. package/src/agent-tools/digest-budget.mjs +0 -76
  89. package/src/agent-tools/eng.mjs +0 -67
  90. package/src/agent-tools/escalate-async.mjs +0 -295
  91. package/src/agent-tools/goal.mjs +0 -119
  92. package/src/agent-tools/plan.mjs +0 -81
  93. package/src/agent-tools/read-history.mjs +0 -309
  94. package/src/agent-tools/recent-changes.mjs +0 -24
  95. package/src/agent-tools/review-streak.mjs +0 -93
  96. package/src/agent-tools/settings.mjs +0 -265
  97. package/src/agent-tools/skill.mjs +0 -47
  98. package/src/agent-tools/subagent-actions.mjs +0 -482
  99. package/src/agent-tools/subagent-async.mjs +0 -434
  100. package/src/agent-tools/subagent-panel.mjs +0 -160
  101. package/src/agent-tools/subagent-run.mjs +0 -205
  102. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  103. package/src/agent-tools/subagent-spawn.mjs +0 -459
  104. package/src/agent-tools/subagent.mjs +0 -404
  105. package/src/agent-tools/task.mjs +0 -87
  106. package/src/agent-tools/timer.mjs +0 -46
  107. package/src/agent-tools/verify.mjs +0 -271
  108. package/src/agent-tools.mjs +0 -17
  109. package/src/agent.mjs +0 -417
  110. package/src/auto-think.mjs +0 -115
  111. package/src/config-migrate.mjs +0 -70
  112. package/src/config.mjs +0 -496
  113. package/src/context.mjs +0 -392
  114. package/src/conventions.mjs +0 -223
  115. package/src/embedding.mjs +0 -120
  116. package/src/escape.mjs +0 -152
  117. package/src/expand-home.mjs +0 -16
  118. package/src/explore-distill.mjs +0 -155
  119. package/src/generate-title.mjs +0 -88
  120. package/src/git/checkpoint.mjs +0 -448
  121. package/src/git/gitmem.mjs +0 -100
  122. package/src/hooks.mjs +0 -97
  123. package/src/ledger.mjs +0 -227
  124. package/src/log.mjs +0 -195
  125. package/src/markdown.mjs +0 -106
  126. package/src/mcp/helpers.mjs +0 -51
  127. package/src/mcp/transport-http.mjs +0 -248
  128. package/src/mcp/transport-stdio.mjs +0 -140
  129. package/src/mcp/transport-ws.mjs +0 -122
  130. package/src/mcp.mjs +0 -295
  131. package/src/memory/code-index.mjs +0 -219
  132. package/src/memory/code-sync.mjs +0 -415
  133. package/src/memory/core.mjs +0 -299
  134. package/src/memory/delete.mjs +0 -236
  135. package/src/memory/docs.mjs +0 -419
  136. package/src/memory/file-walk.mjs +0 -109
  137. package/src/memory/scan.mjs +0 -95
  138. package/src/memory/schema.mjs +0 -452
  139. package/src/memory.mjs +0 -21
  140. package/src/model-ref.mjs +0 -66
  141. package/src/model-specs.mjs +0 -179
  142. package/src/peer-domains.mjs +0 -265
  143. package/src/peer-instances.mjs +0 -231
  144. package/src/prompt-overlays.mjs +0 -82
  145. package/src/prompts/advisor-design.md +0 -41
  146. package/src/prompts/advisor-round1.md +0 -41
  147. package/src/prompts/advisor-round2.md +0 -46
  148. package/src/prompts/advisor-round3.md +0 -42
  149. package/src/prompts/common.md +0 -115
  150. package/src/prompts/consult-base.md +0 -19
  151. package/src/prompts/discipline-engineering.md +0 -258
  152. package/src/prompts/discipline-normal.md +0 -185
  153. package/src/prompts/persona-coder.md +0 -21
  154. package/src/prompts/persona-eng-coder.md +0 -37
  155. package/src/prompts/persona-eng-designer.md +0 -60
  156. package/src/prompts/persona-engineering.md +0 -55
  157. package/src/prompts/persona-explore.md +0 -15
  158. package/src/prompts/persona-normal.md +0 -27
  159. package/src/prompts/persona-plan.md +0 -26
  160. package/src/provider/anthropic.mjs +0 -225
  161. package/src/provider/core.mjs +0 -476
  162. package/src/provider/errors.mjs +0 -101
  163. package/src/provider/google.mjs +0 -257
  164. package/src/provider/index.mjs +0 -7
  165. package/src/provider/list-models.mjs +0 -93
  166. package/src/provider/normalize.mjs +0 -81
  167. package/src/provider/rate.mjs +0 -108
  168. package/src/provider/responses.mjs +0 -495
  169. package/src/provider/retry.mjs +0 -88
  170. package/src/provider/sse.mjs +0 -264
  171. package/src/proxy.mjs +0 -261
  172. package/src/rules.mjs +0 -53
  173. package/src/session-gc.mjs +0 -221
  174. package/src/session-guard.mjs +0 -59
  175. package/src/session-migrate.mjs +0 -48
  176. package/src/session-rename.mjs +0 -38
  177. package/src/session-segments.mjs +0 -100
  178. package/src/session-slots.mjs +0 -492
  179. package/src/session-store.mjs +0 -441
  180. package/src/session.mjs +0 -492
  181. package/src/skills.mjs +0 -153
  182. package/src/text-budget.mjs +0 -46
  183. package/src/token-ttl.mjs +0 -274
  184. package/src/tools/apply_patch.md +0 -15
  185. package/src/tools/bash.md +0 -37
  186. package/src/tools/bash.mjs +0 -268
  187. package/src/tools/checklist-sync.mjs +0 -181
  188. package/src/tools/checklist.md +0 -13
  189. package/src/tools/checklist.mjs +0 -299
  190. package/src/tools/delete.md +0 -13
  191. package/src/tools/edit-batch.mjs +0 -191
  192. package/src/tools/edit-diff.mjs +0 -348
  193. package/src/tools/edit.md +0 -30
  194. package/src/tools/execute.md +0 -21
  195. package/src/tools/execute.mjs +0 -228
  196. package/src/tools/fetch.md +0 -12
  197. package/src/tools/file.mjs +0 -469
  198. package/src/tools/file_ops.md +0 -17
  199. package/src/tools/get_current_time.md +0 -8
  200. package/src/tools/git-checkpoint.mjs +0 -143
  201. package/src/tools/git-ext.mjs +0 -173
  202. package/src/tools/git.md +0 -54
  203. package/src/tools/git.mjs +0 -356
  204. package/src/tools/glob-dialect.mjs +0 -130
  205. package/src/tools/glob.md +0 -11
  206. package/src/tools/grep.md +0 -19
  207. package/src/tools/hashline_edit.md +0 -14
  208. package/src/tools/index.mjs +0 -36
  209. package/src/tools/insert_after.md +0 -15
  210. package/src/tools/lint.md +0 -10
  211. package/src/tools/linter.mjs +0 -128
  212. package/src/tools/ls.md +0 -12
  213. package/src/tools/lsp.md +0 -10
  214. package/src/tools/lsp.mjs +0 -316
  215. package/src/tools/ops.mjs +0 -299
  216. package/src/tools/patch.mjs +0 -282
  217. package/src/tools/process.md +0 -10
  218. package/src/tools/question.md +0 -16
  219. package/src/tools/question.mjs +0 -26
  220. package/src/tools/read.md +0 -20
  221. package/src/tools/read_image.md +0 -8
  222. package/src/tools/repomap.mjs +0 -314
  223. package/src/tools/search.mjs +0 -236
  224. package/src/tools/shared.mjs +0 -446
  225. package/src/tools/tree.md +0 -14
  226. package/src/tools/tree.mjs +0 -66
  227. package/src/tools/wait_for.md +0 -22
  228. package/src/tools/web.mjs +0 -224
  229. package/src/tools/websearch.md +0 -16
  230. package/src/tools/write.md +0 -11
  231. package/src/traces/trace-store.mjs +0 -355
@@ -1,492 +0,0 @@
1
- /**
2
- * session-slots.mjs — slot / manifest 管理(2026-08-31 advisor round1 🔴 拆分:
3
- * session.mjs 曾超 500 行硬限;slot 所有权、认领、清单与核心读写分离,session.mjs
4
- * re-export 全部导出以保持既有 import 兼容)。
5
- *
6
- * 模型:每个项目(cwd hash)拥有无限编号 slot;manifest 记录 active 指针 + 每个
7
- * slot 的属主进程(slotSessions: slot → "pid-timestamp-random",CLI ↔ VS Code
8
- * 共享 manifest 以互斥认领)。属主判定用 PID 存活探测(isProcessAlive)。
9
- */
10
-
11
- import { createHash } from "node:crypto"
12
- import { mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, existsSync } from "node:fs"
13
- import { join, dirname, basename } from "node:path"
14
- import { execSync } from "node:child_process"
15
- import { configDir } from "./config.mjs"
16
- import { migrateHashLength } from "./session-migrate.mjs"
17
- // §10 D-2(2026-09-05):resumeSlot 决策归本文件(slot/claim 层);data 层读(loadSlotFile/
18
- // legacy 过滤)属 session.mjs(500 行内不迁移)——静态环仅此一处:函数声明实例化期已初始化、
19
- // 只函数体内运行时使用(环安全);VS Code 端 session-slots ↔ session-io 同构镜像。
20
- import { loadSlotFile, isLegacyTransient } from "./session.mjs"
21
- // TUI-OOM-ROOTCAUSE 批(SESSION.md §14.3.8):删槽联动记录存储(store 零项目内依赖——无环)。
22
- import { unlinkRecordStore } from "./session-store.mjs"
23
-
24
- let currentSessionId = null
25
-
26
- /** Generate unique session ID for this process */
27
- export function getSessionId() {
28
- if (!currentSessionId) {
29
- currentSessionId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
30
- }
31
- return currentSessionId
32
- }
33
-
34
- /** Normalize cwd for hashing: uppercase Windows drive letter so both ends
35
- * (CLI's process.cwd() vs VS Code's uri.fsPath, which lowercases it) agree. */
36
- export function normalizeCwd(cwd) {
37
- return cwd.replace(/^([a-z]):/, (_, d) => d.toUpperCase() + ":")
38
- }
39
-
40
- /** Full sha1 hex (40 chars), not truncated. Shared contract with the VS Code extension. */
41
- function cwdHash(cwd) {
42
- return createHash("sha1").update(normalizeCwd(cwd)).digest("hex")
43
- }
44
-
45
- /** sessions 根目录隔离缝(2026-09-08 DESIGN-TOKEN-SETTLEMENT 测试——VSC 端
46
- * _setSessionsDirForTest 同构):默认 null 走真实 configDir;测试指向临时目录
47
- * (beforeEach 设、afterEach 复位)。migrateHashLength 的 legacy 迁移仍查真实
48
- * 目录(temp 哈希无 legacy 文件——no-op 无害)。 */
49
- let sessionsDirOverride = null
50
- export function _setSessionsDirForTest(dir) { sessionsDirOverride = dir }
51
- export function _resetSessionsDirForTest() { sessionsDirOverride = null }
52
- function sessionsBase() { return sessionsDirOverride ?? join(configDir, "sessions") }
53
-
54
- /** Derive base session path from cwd hash. Migrates legacy short-hash files on first access. */
55
- export function sessionPath(cwd) {
56
- const hash = cwdHash(cwd)
57
- migrateHashLength(cwd, hash)
58
- return join(sessionsBase(), `${hash}.json`)
59
- }
60
-
61
- export function slotPath(cwd, n) { return sessionPath(cwd) + "." + n }
62
- export function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" }
63
-
64
- // ========== end marker(SESSION.md §10 端分离恢复——本端"最后使用槽位"记录)==========
65
-
66
- /** 端常量:本端记录文件后缀——CLI 写 .cli、VS Code 写 .vscode,互不触碰
67
- * (NF1:本端记录是本端单写者文件——不新增跨端共享可变字段)。 */
68
- export const END = "cli"
69
-
70
- /** 本端记录路径:{manifest}.{END}(manifest 旁的独立小文件,非内嵌字段——NF1)。 */
71
- export function endMarkerPath(cwd) { return `${manifestPath(cwd)}.${END}` }
72
-
73
- /** 读本端记录:返回 { slot: <number|null>, updatedAt } 或 null。三态语义(D-1):
74
- * 文件缺失 = 从未记录(升级/首用迁移窗口——可能触发一次性继承);
75
- * JSON 解析失败/结构非法(损坏)= 按"缺失"降级——不 rename 不 unlink(幂等、不误伤,
76
- * T-M13);`slot: null` = 显式置空(删过本端记录槽——绝不触发继承,T-M4)。 */
77
- export function readEndMarker(cwd) {
78
- try {
79
- const p = endMarkerPath(cwd)
80
- if (!existsSync(p)) return null
81
- const m = JSON.parse(readFileSync(p, "utf8"))
82
- if (m && m.slot === null) return { slot: null, updatedAt: m.updatedAt ?? null }
83
- if (m && Number.isInteger(m.slot) && m.slot >= 1) return { slot: m.slot, updatedAt: m.updatedAt ?? null }
84
- return null
85
- } catch { return null }
86
- }
87
-
88
- /** 写本端记录(原子 .tmp+rename,复用 writeSessionFile):slot = 目标槽号或 null(显式置空)。
89
- * 失败容忍(NF2):写失败按无记录路径降级——不抛错,不影响会话数据(T-M14)。 */
90
- export function writeEndMarker(cwd, slot) {
91
- try {
92
- writeSessionFile(endMarkerPath(cwd), { slot, updatedAt: Date.now() })
93
- } catch { /* NF2:失败容忍 */ }
94
- }
95
-
96
- /** Path to the active slot's file */
97
- export function activePath(cwd) {
98
- return slotPath(cwd, activeSlot(cwd))
99
- }
100
-
101
- /** Atomic write: write to temp file then rename to replace, preventing truncated JSON from mid-write crash. */
102
- export function writeSessionFile(p, data) {
103
- mkdirSync(dirname(p), { recursive: true })
104
- const tmp = `${p}.tmp`
105
- writeFileSync(tmp, JSON.stringify(data), "utf8")
106
- try {
107
- renameSync(tmp, p)
108
- } catch {
109
- try { unlinkSync(p) } catch {}
110
- try {
111
- renameSync(tmp, p)
112
- try { unlinkSync(tmp) } catch {}
113
- } catch {
114
- writeFileSync(p, readFileSync(tmp, "utf8"), "utf8")
115
- }
116
- }
117
- }
118
-
119
- // ========== slot management ==========
120
-
121
- /** Detect a genuine user message (excludes system-reminder injected messages) */
122
- function isRealUserMsg(m) {
123
- return m.role === "user" && typeof m.content === "string" && !m.content.startsWith("[System reminder:")
124
- }
125
-
126
- /** Extract slot metadata from history (shared by slotDigest and loadSlotMeta) */
127
- function extractSlotMeta(history, activeProvider, updatedAt, title = "") {
128
- const userMsgs = history.filter(isRealUserMsg)
129
- const first = userMsgs[0]?.content ?? ""
130
- return {
131
- messageCount: history.length,
132
- turnCount: userMsgs.length,
133
- firstMessage: first.slice(0, 80),
134
- activeProvider: activeProvider ?? "",
135
- updatedAt: updatedAt ?? Date.now(),
136
- title,
137
- }
138
- }
139
-
140
- /** Extract preview summary from session data for manifest storage (with current timestamp)
141
- * MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。 */
142
- export function slotDigest(data) {
143
- const meta = extractSlotMeta(data.history ?? [], data.activeProvider, data.updatedAt, data.title ?? "")
144
- if (data.activeModel) meta.activeModel = data.activeModel
145
- return { ts: Date.now(), ...meta }
146
- }
147
-
148
- export function loadManifest(cwd) {
149
- try {
150
- const p = manifestPath(cwd)
151
- if (!existsSync(p)) return { slots: {}, sessionId: null }
152
- const m = JSON.parse(readFileSync(p, "utf8"))
153
- if (!m.slots) m.slots = {} // 2026-09-01 会诊 deepseek 🔵:损坏的 {} manifest 不再让调用方抛 TypeError
154
- if (!m.sessionId) m.sessionId = null
155
- return m
156
- } catch { return { slots: {}, sessionId: null } }
157
- }
158
-
159
- export function saveManifest(cwd, m, deletions = null, opts = {}) {
160
- // 2026-08-31 会诊 kimi/deepseek 🟡:写前重读并按"条目级"合并——原实现把"读时快照"
161
- // 整对象写回,另一进程在窗口内对 slots/slotSessions/active 的变更被覆盖抹除(被抹
162
- // 认领的槽变"文件在、无属主"→ 第三方可认领 → 双属主 → F2 互旋)。无锁文件无法
163
- // 完全原子,重读合并把丢失更新窗口缩到最小;删除意图经 deletions 参数显式表达
164
- // (deleteSlot:{ slots: [n], slotSessions: [n] })。
165
- // 2026-09-01 会诊 deepseek/kimi/glm 🟡:active 是单值——只有显式翻指针的调用方
166
- // (ensureActive 分支、newSession、switchToSlot、deleteSlot 删到 active 时)传
167
- // opts.setActive;其余调用方(saveSession/ACP 认领/死项清理)默认保留磁盘 fresh 的
168
- // active,否则毫秒窗口内会把并发方刚翻的 active 回滚(F1 防漂移的反向变体)。
169
- try {
170
- const fresh = JSON.parse(readFileSync(manifestPath(cwd), "utf8"))
171
- if (fresh && typeof fresh === "object" && fresh.slots && typeof fresh.slots === "object") {
172
- const merged = { ...fresh }
173
- if (opts.setActive) merged.active = m.active
174
- merged.slots = { ...fresh.slots, ...(m.slots ?? {}) }
175
- merged.slotSessions = { ...(fresh.slotSessions ?? {}), ...(m.slotSessions ?? {}) }
176
- if (m.sessionId) merged.sessionId = m.sessionId
177
- if (deletions) {
178
- for (const [section, keys] of Object.entries(deletions)) {
179
- for (const k of keys) delete merged[section]?.[k]
180
- }
181
- }
182
- m = merged
183
- }
184
- } catch {
185
- // 首次创建或 manifest 不可读:用传入对象。2026-09-01 advisor 🟡:解析失败时先改名
186
- // 保留现场(与 loadSlotFile 对 slot 文件的 .corrupted 原则一致)——否则覆盖后全部
187
- // 槽位元数据(digest/title/updatedAt)永久丢失,/session 列表变空。文件不存在时
188
- // rename 抛错被吞,无害。
189
- try { renameSync(manifestPath(cwd), `${manifestPath(cwd)}.corrupted`) } catch {}
190
- }
191
- m.sessionId = getSessionId()
192
- writeSessionFile(manifestPath(cwd), m)
193
- }
194
-
195
- /** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
196
- * ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
197
- * isProcessAlive——不能以"文件缺失"短路(活进程在"认领 → 首次保存"窗口文件暂缺,误删会
198
- * 致双进程同槽)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
199
- * 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
200
- * 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。 */
201
- function cleanDeadOwners(m) {
202
- const mySessionId = getSessionId()
203
- const deadSlots = []
204
- for (const [slot, owner] of Object.entries(m.slotSessions)) {
205
- if (owner && owner !== mySessionId) {
206
- const pid = parseInt(owner.split("-")[0])
207
- if (!pid || !isProcessAlive(pid)) {
208
- delete m.slotSessions[slot]
209
- deadSlots.push(slot)
210
- }
211
- }
212
- }
213
- return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
214
- }
215
-
216
- /**
217
- * Claim a slot for this process and set it as active. Idempotent.
218
- * Preference order:
219
- * 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
220
- * 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
221
- * 3. A brand-new slot when all are owned by live processes. → allocateFresh
222
- * The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
223
- * see which slots are taken and avoid them.
224
- */
225
- function ensureActive(cwd, m) {
226
- const mySessionId = getSessionId()
227
- if (!m.slotSessions) m.slotSessions = {}
228
- // 顺手清理死主条目(见 cleanDeadOwners)。ensureActive 因 F1 粘性每次进程只跑几次,
229
- // 全量 tasklist 成本可接受;清理结果必须落盘——早退 + 分支 1/2/3 全部传 deletions。
230
- const deadParam = cleanDeadOwners(m)
231
-
232
- // Already own the active slot — nothing to do.
233
- // 2026-08-31 advisor round2 🔵:清理结果此时落盘(否则死项清理只在内存生效,早退
234
- // 路径永不持久化——死条目一直滞留到其他路径保存才消失)。
235
- if (m.active && m.slotSessions[m.active] === mySessionId) {
236
- if (deadParam) saveManifest(cwd, m, deadParam())
237
- return
238
- }
239
-
240
- const isFree = (slot) => {
241
- const owner = m.slotSessions[slot]
242
- if (!owner || owner === mySessionId) return true
243
- return !isProcessAlive(parseInt(owner.split("-")[0]))
244
- }
245
-
246
- // 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
247
- if (m.active && m.slots[m.active] && isFree(m.active)) {
248
- m.slotSessions[m.active] = mySessionId
249
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
250
- return
251
- }
252
-
253
- // 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §10 D-2——resumeSlot 全新分配复用)。
254
- allocateFresh(cwd, m, deadParam)
255
- }
256
-
257
- /**
258
- * Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §10 D-2 抽取——resumeSlot
259
- * 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
260
- * 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
261
- * 3. A brand-new slot when none is free / all numbers are taken.
262
- * 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
263
- */
264
- export function allocateFresh(cwd, m, deadParam = null) {
265
- const mySessionId = getSessionId()
266
- if (!m.slotSessions) m.slotSessions = {}
267
- const isFree = (slot) => {
268
- const owner = m.slotSessions[slot]
269
- if (!owner || owner === mySessionId) return true
270
- return !isProcessAlive(parseInt(owner.split("-")[0]))
271
- }
272
-
273
- // 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
274
- // 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
275
- // 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
276
- const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
277
- for (const slot of allSlots) {
278
- if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
279
- m.active = slot
280
- m.slotSessions[slot] = mySessionId
281
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
282
- return slot
283
- }
284
- }
285
-
286
- // 3. All slots owned by live processes — allocate a new one (no limit).
287
- // 2026-08-31 advisor round2 🟡:新号从 max+1 起逐号跳过"已被活进程认领但尚未落盘"
288
- // 的号(认领→首次保存窗口:slotSessions 有条目、m.slots 无条目、文件不存在——
289
- // 仅凭 m.slots/existsSync 查不到 → 双进程认领同一号 → 同槽双写/互旋)。
290
- // 2026-09-01 会诊 kimi 🟡:同时跳过文件仍存在的号(与 newSession 对齐)——manifest
291
- // 条目丢失/损坏时 max+1 会撞上孤儿槽文件 → F2 把真会话轮转成不可见的 .bak。
292
- // 另:allSlots 已升序,取 max 用 allSlots[allSlots.length-1](数万槽位时 Math.max
293
- // spread 有 RangeError 风险)。
294
- const liveClaimed = (n) => {
295
- const owner = m.slotSessions?.[n]
296
- return !!(owner && owner !== mySessionId && isProcessAlive(parseInt(owner.split("-")[0])))
297
- }
298
- let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
299
- while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
300
- m.active = newSlot
301
- m.slotSessions[newSlot] = mySessionId
302
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
303
- return newSlot
304
- }
305
-
306
- /** 认领指定槽为本进程所有并置为 active(2026-09-05 §10 D-2 resumeSlot 认领路径——调用方
307
- * 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
308
- * 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
309
- * deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
310
- * deadParam 同型过滤)。 */
311
- export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
312
- m.slotSessions ??= {}
313
- m.slotSessions[slot] = getSessionId()
314
- m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
315
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
316
- }
317
-
318
- /**
319
- * Check if a process with given PID is still alive.
320
- * Returns false if process doesn't exist or we can't determine.
321
- */
322
- export function isProcessAlive(pid) {
323
- if (!pid || isNaN(pid)) return false
324
- try {
325
- // On Windows: tasklist /FI "PID eq <pid>" /NH
326
- // On Unix: kill(pid, 0) or check /proc/<pid>
327
- if (process.platform === 'win32') {
328
- // 2026-08-31 会诊 F4 + advisor round1 🔵:/FI 已按 PID 过滤;用 CSV 格式解析 PID
329
- // 列(第 2 列),避免旧 includes() 误报活、新行解析在罕见镜像名(含"数字+空格")
330
- // 下误报死。
331
- const output = execSync(`tasklist /FO CSV /FI "PID eq ${pid}" /NH`, { encoding: 'utf8', stdio: 'pipe' })
332
- return output.split(/\r?\n/).some((line) => {
333
- const m = line.match(/^"([^"]*)","(\d+)"/)
334
- return m && m[2] === String(pid)
335
- })
336
- } else {
337
- // Unix: try to send signal 0 (doesn't kill, just checks)
338
- process.kill(pid, 0)
339
- return true
340
- }
341
- } catch {
342
- return false
343
- }
344
- }
345
-
346
- /** Return the active slot number for this process, claiming one if necessary */
347
- export function activeSlot(cwd) {
348
- const m = loadManifest(cwd)
349
- ensureActive(cwd, m)
350
- return m.active
351
- }
352
-
353
- /** Lazy-load slot metadata from slot file (for old-format manifest entries that lack metadata) */
354
- function loadSlotMeta(cwd, slot, v) {
355
- if (typeof v === "object" && v !== null && "ts" in v) return v
356
- const ts = typeof v === "number" ? v : 0
357
- try {
358
- const p = slotPath(cwd, slot)
359
- if (!existsSync(p)) return { ts }
360
- const data = JSON.parse(readFileSync(p, "utf8"))
361
- const history = data.history ?? []
362
- const meta = extractSlotMeta(history, data.activeProvider, data.updatedAt ?? ts, data.title ?? "")
363
- if (data.activeModel) meta.activeModel = data.activeModel
364
- return { ts, ...meta }
365
- } catch {
366
- return { ts }
367
- }
368
- }
369
-
370
- /** List all slots, newest first. Includes isActive flag.
371
- * 2026-09-01 会诊 🟢:只读操作不认领——原实现 active 缺失时调 activeSlot(写 manifest
372
- * 副作用,ACP session/list 可触发)。m.active 缺失时全部 isActive=false,由下一次
373
- * activeSlot 正常认领。 */
374
- export function listSlots(cwd) {
375
- const m = loadManifest(cwd)
376
- const active = m.active ?? null
377
- return Object.entries(m.slots)
378
- .filter(([n]) => /^\d+$/.test(n))
379
- .map(([n, v]) => {
380
- const meta = loadSlotMeta(cwd, Number(n), v)
381
- return {
382
- slot: Number(n),
383
- isActive: Number(n) === active,
384
- timestamp: meta.ts,
385
- date: new Date(meta.ts).toLocaleString(),
386
- messageCount: meta.messageCount ?? 0,
387
- turnCount: meta.turnCount ?? 0,
388
- firstMessage: meta.firstMessage ?? "",
389
- // MODEL-MERGE-SESSION 摘要 "p:m":activeProvider 保持裸渠道名——列表消费面显复合
390
- // (旧摘要无 activeModel → 回退裸渠道名——新老兼容;cmd-session/VSC sessions 行免改)
391
- activeProvider: meta.activeProvider ? (meta.activeModel ? `${meta.activeProvider}:${meta.activeModel}` : meta.activeProvider) : "",
392
- updatedAt: meta.updatedAt ?? meta.ts,
393
- updatedDate: new Date(meta.updatedAt ?? meta.ts).toLocaleString(),
394
- title: meta.title ?? "",
395
- }
396
- })
397
- .sort((a, b) => b.updatedAt - a.updatedAt)
398
- }
399
-
400
- /** Delete a slot: remove its file and manifest entry. Deleting the active slot
401
- * resets the manifest active pointer (the next claim re-creates one). */
402
- export function deleteSlot(cwd, slot) {
403
- const n = Number(slot)
404
- if (!Number.isInteger(n) || n < 1) return false
405
- const m = loadManifest(cwd)
406
- if (!m.slots[n]) return false
407
- delete m.slots[n]
408
- delete m.slotSessions?.[n] // orphan session-id entries bloat the manifest forever
409
- try { unlinkSync(slotPath(cwd, n)) } catch { /* missing file is fine */ }
410
- unlinkRecordStore(slotPath(cwd, n)) // §14.3.8:删槽连带删除记录存储(sidecar)
411
- if (m.active === n) delete m.active
412
- // setActive: true —— 显式表达"删到 active 时 active 置空"的意图(saveManifest 默认
413
- // 保留 fresh.active,2026-09-01 会诊三家 🟡)
414
- saveManifest(cwd, m, { slots: [n], slotSessions: [n] }, { setActive: true })
415
- // 2026-09-05 §10 D-4/F4:删到本端记录槽 → 记录显式置空(文件保留 + slot:null——下次全新起步,不复活——T-M4/T-M8)
416
- if (readEndMarker(cwd)?.slot === n) writeEndMarker(cwd, null)
417
- return true
418
- }
419
-
420
- // renameSlot 已拆至 session-rename.mjs(2026-09-06 §12.2.5 契约改使本文件超 500 行硬限,
421
- // §12.3 授权拆分);session.mjs re-export 保持调用点不变。
422
-
423
- // ========== resumeSlot(SESSION.md §10 D-2——端分离恢复决策)==========
424
-
425
- /** 本端记录槽可用判据(D-2):slot ∈ m.slots + 槽文件在盘 + 属主 空/死/本进程。 */
426
- function usableSlot(cwd, m, slot) {
427
- if (!m.slots[slot] || !existsSync(slotPath(cwd, slot))) return false
428
- const owner = m.slotSessions?.[slot]
429
- if (!owner || owner === getSessionId()) return true
430
- const pid = parseInt(owner.split("-")[0])
431
- return !pid || !isProcessAlive(pid)
432
- }
433
-
434
- /** legacy 单文件兜底(v1/v2 单会话 {hash}.json——迁移前残留;仅 data 层——2026-09-05 §10
435
- * D-2:恢复数据兜底不改变 claim 落点,T-M10)。纪律与 loadSlotFile 一致:cwd 不匹配(别人的文件)
436
- * 直接 null 不改名;坏结构改名 .unreadable 保留(version>2 不动);解析失败 .corrupted 保留。 */
437
- function loadLegacyFile(cwd) {
438
- const legacy = sessionPath(cwd)
439
- try {
440
- if (existsSync(legacy)) {
441
- const data = JSON.parse(readFileSync(legacy, "utf8"))
442
- // cwd 不匹配是别人的文件——与 loadSlotFile 一致直接 return null 不改名
443
- // ("别人的文件不动"原则,2026-08-31 advisor round2 🟡)
444
- if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null
445
- if ((data?.version === 1 || data?.version === 2) && Array.isArray(data.history)) {
446
- data.history = data.history.filter((m) => !isLegacyTransient(m))
447
- return data
448
- }
449
- // 结构不匹配:保留现场(version>2 的新版文件不动)
450
- if (!(typeof data?.version === "number" && data.version > 2)) {
451
- try { renameSync(legacy, `${legacy}.unreadable`) } catch {}
452
- console.error(`[session] legacy file ${legacy}: invalid structure — preserved as ${basename(legacy)}.unreadable`)
453
- }
454
- }
455
- } catch (e) {
456
- console.error(`[session] failed to load legacy ${legacy}: ${e.message}`)
457
- try { renameSync(legacy, `${legacy}.corrupted`) } catch {}
458
- }
459
- return null
460
- }
461
-
462
- /**
463
- * 恢复决策(SESSION.md §10 D-2)——TUI 启动的本端恢复入口(VS Code 面板同构镜像)。
464
- * 返回 { slot, data }(data 可为 null——全新起步或读槽失败)。判据:
465
- * ① 本端记录可用(slot ≠ null 且 ∈ m.slots 且槽文件在盘 且属主 空/死/本进程)→ claimSlot;
466
- * ② 记录缺失(从未记录 = 升级/首用迁移窗口):②a active 属主 = 本进程(同进程重入——
467
- * ensureActive 早退语义镜像:认领后尚未写 slots 条目/文件的窗口)→ 直接沿用;
468
- * ②b 否则一次性继承 manifest.active(同判据;活属主绝不继承——全新槽起步,T-M3);
469
- * ③ 其余一切(slot:null 显式置空 / 槽被删 / 属主为活外人 / 继承失败)→ allocateFresh。
470
- * 每次落点都写本端记录;claim 后读槽失败(.corrupted/.unreadable——loadSlotFile 既有改名
471
- * 保全语义)→ 保持已 claim 槽 + data:null——不改 marker——下次保存原地重建(T-M15)。
472
- */
473
- export function resumeSlot(cwd) {
474
- const m = loadManifest(cwd)
475
- m.slotSessions ??= {}
476
- // 与 ensureActive 同型的死主清理(认领路径持久化——F5a 纪律:仅传 m 等于没删)
477
- const deadParam = cleanDeadOwners(m)
478
- const rec = readEndMarker(cwd) // null = 缺失/损坏;{slot:null|N} = 文件在(D-1)
479
- let slot = null
480
- if (rec?.slot != null && usableSlot(cwd, m, rec.slot)) slot = rec.slot // ① 本端记录可用
481
- else if (rec === null) {
482
- if (m.active && m.slotSessions?.[m.active] === getSessionId()) slot = m.active // ②a 同进程重入
483
- else if (m.active && usableSlot(cwd, m, m.active)) slot = m.active // ②b 一次性继承
484
- }
485
- if (slot !== null) claimSlot(cwd, slot, m, deadParam)
486
- else slot = allocateFresh(cwd, m, deadParam) // ③ 全新分配(slot:null 绝不继承——T-M4)
487
- // data 层:读已认领槽(loadSlotFile 自 session.mjs——环 import 见文件头);读失败/槽文件不在 → legacy 单文件兜底(仅 data)
488
- let data = loadSlotFile(cwd, slot)
489
- if (!data) data = loadLegacyFile(cwd)
490
- writeEndMarker(cwd, slot)
491
- return { slot, data }
492
- }