@thincoder/core 0.9.1

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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * session-rename.mjs — 槽位重命名(标题写路径)。2026-09-06 自 session-slots.mjs 拆出:
3
+ * §6.12 标题写契约(boolean → { ok, reason })使 session-slots.mjs 超 500 行硬限
4
+ *(§6.12 模块与实现约束——超 500 行硬限即拆文件,renameSlot 一并拆出)。
5
+ * 原语(slotPath/manifest 读写)自 session-slots.mjs import;session.mjs re-export
6
+ * renameSlot 保持既有调用点(cmd-session.mjs / 测试经 session.mjs)不变。
7
+ */
8
+
9
+ import { readFileSync, existsSync, statSync } from "node:fs"
10
+ import { slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile } from "./session-slots.mjs"
11
+
12
+ /** Rename a slot: update the slot file's title + the manifest metadata (shared with VS Code).
13
+ * 2026-09-01 会诊 glm 🟡:写回前按 mtime 门控重读——原实现读全量→改 title→整文件写回,
14
+ * 窗口内并发方的最新保存会被旧数据覆盖(丢消息);mtime 变了即放弃本次重命名。
15
+ * 2026-09-06 §6.12 标题写契约:契约 boolean → { ok, reason? }(file-missing/parse-failure/mtime-conflict/invalid-slot,F3 失败可见);_stat = mtime-conflict 测试缝(默认 statSync)。 */
16
+ export function renameSlot(cwd, slot, title, _stat = statSync) {
17
+ const n = Number(slot)
18
+ if (!Number.isInteger(n) || n < 1) return { ok: false, reason: "invalid-slot" }
19
+ const p = slotPath(cwd, n)
20
+ if (!existsSync(p)) return { ok: false, reason: "file-missing" }
21
+ let data
22
+ try {
23
+ data = JSON.parse(readFileSync(p, "utf8"))
24
+ } catch {
25
+ return { ok: false, reason: "parse-failure" }
26
+ }
27
+ const t0 = _stat(p).mtimeMs
28
+ data.title = title
29
+ // 读与写之间文件被并发方改过 → 放弃(保留并发内容,重命名下次重试)
30
+ if (_stat(p).mtimeMs !== t0) return { ok: false, reason: "mtime-conflict" }
31
+ writeSessionFile(p, data)
32
+ const m = loadManifest(cwd)
33
+ if (m.slots[n]) {
34
+ m.slots[n] = slotDigest(data)
35
+ saveManifest(cwd, m)
36
+ }
37
+ return { ok: true }
38
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * session-segments.mjs — 记录存储的段文件原语 + 人读线条目形态(TUI-OOM-ROOTCAUSE 批)。
3
+ *
4
+ * 机制契约全文 = docs/core/design/SESSION.md §6.14(D2 单一权威源);本文件 = 「段 IO + 条目
5
+ * 形态」叶子层(`session-store.mjs` 拆分产物——503+ 行越 500 硬限,§6.14 表列模块按职责
6
+ * 拆分;store 继续 re-export 全部公开名,调用面零改)。零项目内依赖(仅 `node:`)。
7
+ */
8
+
9
+ import { readdirSync, readFileSync } from "node:fs"
10
+
11
+ /** 常量单源(§6.14):每段消息数 / sidecar 目录后缀。 */
12
+ export const RECORD_SEG_MESSAGES = 100
13
+ export const RECORD_DIR_SUFFIX = ".d"
14
+
15
+ /** 观测计数(测试缝——AC-RS9「恢复只读末段」以段读次数断言)。 */
16
+ export const _storeStats = { segmentReads: 0, segmentWrites: 0, appendedEntries: 0, catchUpRetries: 0 }
17
+
18
+ const LEGACY_TRANSIENT_PREFIXES = [
19
+ "[System reminder: working directory snapshot:",
20
+ "[Relevant memories from previous sessions",
21
+ ]
22
+
23
+ /** legacy transient 判定(旧进程写进人读线的机器注入——落盘/展示均跳过)。 */
24
+ export function isLegacyTransient(m) {
25
+ return (
26
+ m?.role === "user" &&
27
+ typeof m.content === "string" &&
28
+ LEGACY_TRANSIENT_PREFIXES.some((p) => m.content.startsWith(p))
29
+ )
30
+ }
31
+
32
+ /** Slim the HUMAN line for storage(自 session.mjs 迁出——§6.14):机器线(contextHistory)
33
+ * 逐字节保持。Copy-on-write ONLY(两线经 pushReal 共享对象引用):tool args → 300 字符头
34
+ * 截断 / tool content → 500 / 多模态 user → 保留 text、丢 image_url。 */
35
+ export function slimForDisplay(m) {
36
+ if (m && Array.isArray(m.content)) {
37
+ const textParts = m.content.filter((p) => p?.type !== "image_url")
38
+ if (textParts.length === m.content.length) return m
39
+ return { ...m, content: textParts }
40
+ }
41
+ if (m && m.role === "assistant" && Array.isArray(m.tool_calls)) {
42
+ let changed = false
43
+ const tool_calls = m.tool_calls.map((tc) => {
44
+ const args = tc.function?.arguments
45
+ if (typeof args === "string" && args.length > 300) {
46
+ changed = true
47
+ return { ...tc, function: { ...tc.function, arguments: args.slice(0, 300) + "…" } }
48
+ }
49
+ return tc
50
+ })
51
+ return changed ? { ...m, tool_calls } : m
52
+ }
53
+ if (m && m.role === "tool" && typeof m.content === "string" && m.content.length > 500) {
54
+ return { ...m, content: m.content.slice(0, 500) + "\n… (truncated for storage)" }
55
+ }
56
+ return m
57
+ }
58
+
59
+ /** 记录条目过滤(与 saveSession 同源):transient 机器注入与 legacy-transient 前缀不进记录。 */
60
+ export function shouldAppend(m) {
61
+ return !!m && typeof m === "object" && !m.transient && !isLegacyTransient(m)
62
+ }
63
+
64
+ /** 真实 user 消息(turnCount/firstMessage 口径——同 session-slots.extractSlotMeta)。
65
+ * 单源 = `history-window.mjs`(本档只 re-export 保既有消费面:session-store 等)。 */
66
+ export { isRealUserMsg } from "./history-window.mjs"
67
+
68
+ /** sidecar 目录路径(`{slot 文件路径}.d`——同目录同前缀)。 */
69
+ export function recordDirOf(slotFile) { return slotFile + RECORD_DIR_SUFFIX }
70
+
71
+ /** 段文件名(六位零填充)。 */
72
+ export function segName(n) { return `seg-${String(n).padStart(6, "0")}.jsonl` }
73
+
74
+ /** 段号升序表(readdir——不读内容)。 */
75
+ export function listSegments(dir) {
76
+ let names
77
+ try { names = readdirSync(dir) } catch { return [] }
78
+ const out = []
79
+ for (const n of names) {
80
+ const m = /^seg-(\d+)\.jsonl$/.exec(n)
81
+ if (m) out.push(Number(m[1]))
82
+ }
83
+ return out.sort((a, b) => a - b)
84
+ }
85
+
86
+ /** 读段(原始行——去尾空行);读失败按空段容忍(读路径尽力面)。 */
87
+ export function readSegmentLines(path) {
88
+ _storeStats.segmentReads++
89
+ let text
90
+ try { text = readFileSync(path, "utf8") } catch { return [] }
91
+ const lines = text.split("\n")
92
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop()
93
+ return lines
94
+ }
95
+
96
+ /** 逐行容错解析:半行/损坏行 → undefined(调用方跳过——不抛、不自动修复)。 */
97
+ export function tryParse(line) {
98
+ try { return JSON.parse(line) } catch { return undefined }
99
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * session-slot-write.mjs — 会话槽「元数据开关」写面(Parnas 边界:开关写决策组——
3
+ * autoApprove / planMode / engineering / advisor guard,外加全新槽默认记录与落盘单点)。
4
+ *
5
+ * 归属:会话数据面(同上位 `session.mjs` / `session-slots.mjs` / `session-store.mjs`)。
6
+ * 本档 = **(cwd, slot)形态**的槽写入面(读-改-写);另一形态 = `session.mjs` 的
7
+ * `saveSession(agent)` 全量面。两者共用同一批原语(`loadSlotFile` / `writeSessionFile` /
8
+ * `slotDigest`)与同一 F2 轮转判据(见下),差别只在入参来源。
9
+ *
10
+ * 轮转判据(与 `session-guard.mjs` 的 agent 面守卫**同判据集**):磁盘槽文件
11
+ * version>2 / 异 cwd / 会话不符(sessionStart 不一致)→ 先轮转 `.bak` 保留现场再写;
12
+ * 解析失败(损坏 / 半写)→ 改名 `.corrupted` 保留。本面额外收**同会话并发追加**
13
+ * (磁盘 history 比本次待写长 = 窗口内有外部写入 ⇒ 对方现场先轮转 `.bak` 保下,
14
+ * **本快照随后落位**——不静默覆盖,但档位以本次快照为准,较新内容只在 `.bak` 里)。
15
+ * 检查按 mtime 缓存:
16
+ * 文件自上次检查/自写未变就跳过全量解析。
17
+ *
18
+ * token 台账字段(`engDesignTokens`)的**槽 I/O 面**不在本档——单源 = `token-ttl.mjs`
19
+ * (settle 落盘 / 门禁回读 / 链终清理三面齐备);本档只承载其**保存面合并规则**
20
+ * (`mergeEngTokensForSave`——空态保留 + 并集 + 新铸者胜,纯函数)。槽标题写面同样不在
21
+ * 本档——单源 = `session-rename.mjs` 的 `renameSlot`(同 `{ok, reason}` 契约 + mtime 门控)。
22
+ */
23
+
24
+ import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
25
+
26
+ import { getSessionId, loadManifest, saveManifest, slotDigest, slotPath, writeSessionFile } from "./session-slots.mjs"
27
+ import { loadSlotFile } from "./session.mjs"
28
+ // token 形态单源(格式校验 + 到期时刻)——本档的保存面合并规则复用同一判定。
29
+ import { tokenExpiryMs } from "./token-ttl.mjs"
30
+
31
+ /** mtime 缓存(键 = 槽文件路径 → 上次所见 mtimeMs):同一槽连写时跳过重复解析。
32
+ * 测试复位缝(与 `_setSessionsDirForTest` 同款):缓存为进程级单例,用例间需可复位
33
+ * ——否则同一路径在同一 mtime tick 内被直接重写时,轮转判定会因「mtime 未变」早退。 */
34
+ const slotMtimeCache = new Map()
35
+ export function _resetSlotMtimeCacheForTest() { slotMtimeCache.clear() }
36
+
37
+ /** Fresh slot data default:全新空槽的规范结构(`newSession` 写盘面同形)。 */
38
+ export function newSlotData(cwd) {
39
+ return {
40
+ version: 2, cwd, title: "", updatedAt: Date.now(),
41
+ history: [], contextHistory: [], tasks: [],
42
+ planMode: false, goal: null, autoApprove: false, advisor: null,
43
+ pendingReminders: [], sessionStart: null,
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 轮转判定(写前):命中即把现场改名 `.bak-<ts>` 保留,返回该路径或 null。
49
+ * 判据四项——version>2(新版文件,非本版可写)/ 异 cwd(别的项目文件误落本路径)/
50
+ * sessionStart 不符(另一会话现场)/ 磁盘 history 更长(同会话并发追加——对方现场先轮转保下,
51
+ * 本快照随后落位)。
52
+ */
53
+ function rotateIfForeign(p, slot, data) {
54
+ try {
55
+ if (!existsSync(p)) return null
56
+ const st = statSync(p)
57
+ if (st.mtimeMs === slotMtimeCache.get(p)) return null // 自上次检查/自写未变——磁盘态已知
58
+ const disk = JSON.parse(readFileSync(p, "utf8"))
59
+ slotMtimeCache.set(p, st.mtimeMs)
60
+ const diskIsNewer = typeof disk?.version === "number" && disk.version > 2
61
+ const diskForeign = typeof disk?.cwd === "string" && typeof data?.cwd === "string" &&
62
+ disk.cwd.toLowerCase() !== data.cwd.toLowerCase()
63
+ const diskStart = disk?.sessionStart ?? null
64
+ const myStart = data?.sessionStart ?? null
65
+ const diskLonger = Array.isArray(disk?.history) && Array.isArray(data?.history) &&
66
+ disk.history.length > data.history.length
67
+ if (!(diskIsNewer || diskForeign || (diskStart && diskStart !== myStart) || diskLonger)) return null
68
+ const bak = `${p}.bak-${Date.now()}`
69
+ renameSync(p, bak)
70
+ console.error(`[session] slot ${slot} holds ${diskIsNewer ? `a newer-version file (v${disk.version})` : diskForeign ? `a foreign-cwd file (${disk.cwd})` : diskLonger ? `a longer record (${disk.history.length} > ${data.history.length} messages — concurrent append)` : `another session (start ${diskStart}, ours ${myStart})`} — preserved as ${bak}`)
71
+ return bak
72
+ } catch (e) {
73
+ // 文件存在但不可读(损坏 / 半写):改名保留现场,不覆盖
74
+ let preserved = false
75
+ if (existsSync(p)) {
76
+ try { renameSync(p, `${p}.corrupted`); preserved = true } catch {}
77
+ }
78
+ // 排障面:区分「现场不可读」与「轮转失败」——静默会让半写/占用现场无从定位。
79
+ console.error(`[session] slot ${slot}: pre-write rotation check failed (${e?.message ?? e}) —${preserved ? " preserved as .corrupted" : " the scene could NOT be preserved"}`)
80
+ return null
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 槽落盘单点(读-改-写面共用):轮转判定 → 写槽文件 → 回写 manifest 摘要。
86
+ * 返回轮转出的 `.bak` 路径或 null(调用方按需透出)。
87
+ */
88
+ export function saveSlotData(cwd, slot, data) {
89
+ data.updatedAt = Date.now()
90
+ const p = slotPath(cwd, slot)
91
+ const rotated = rotateIfForeign(p, slot, data)
92
+ writeSessionFile(p, data)
93
+ slotMtimeCache.set(p, (() => { try { return statSync(p).mtimeMs } catch { return 0 } })())
94
+ try {
95
+ const m = loadManifest(cwd)
96
+ m.slots[slot] = slotDigest(data)
97
+ saveManifest(cwd, m)
98
+ } catch { /* 摘要失败非致命——数据已安全,元数据下次 listSlots 惰性恢复 */ }
99
+ return rotated
100
+ }
101
+
102
+ /**
103
+ * 读槽供开关写入。返回已解析槽数据;当槽**尚无数据文件**但本进程已认领该槽
104
+ * (认领先行、首保存落盘前)→ 返回全新默认记录。其余失败(version>2 / 异 cwd /
105
+ * 损坏 / 未知槽)返回 null——保持「未识别的槽 ⇒ 开关写返回 false」的契约。
106
+ */
107
+ function loadSlotForWrite(cwd, slot) {
108
+ const data = loadSlotFile(cwd, slot)
109
+ if (data || !slot) return data
110
+ const m = loadManifest(cwd)
111
+ if (m.slotSessions?.[slot] !== getSessionId()) return null
112
+ if (existsSync(slotPath(cwd, slot))) return null // 文件已在但不可读 → 不覆盖
113
+ return newSlotData(cwd)
114
+ }
115
+
116
+ /** 开关写单点:读 → 改一枚开关 → 落盘;槽不可读 ⇒ false(写未发生)。 */
117
+ function writeFlag(cwd, slot, mutate) {
118
+ const data = loadSlotForWrite(cwd, slot)
119
+ if (!data) return false
120
+ mutate(data)
121
+ saveSlotData(cwd, slot, data)
122
+ return true
123
+ }
124
+
125
+ /** 会话级 autoApprove 开关(槽字段,非配置文件)。槽不可读 ⇒ false。 */
126
+ export function setSlotAutoApprove(cwd, slot, value) {
127
+ return writeFlag(cwd, slot, (d) => { d.autoApprove = value })
128
+ }
129
+
130
+ /** 会话级 planMode 开关(与 autoApprove 同层)。 */
131
+ export function setSlotPlanMode(cwd, slot, value) {
132
+ return writeFlag(cwd, slot, (d) => { d.planMode = value })
133
+ }
134
+
135
+ /** 会话级 engineering 开关——槽是会话真值来源(配置文件只作初始默认 / 跨端镜像)。 */
136
+ export function setSlotEngineering(cwd, slot, value) {
137
+ return writeFlag(cwd, slot, (d) => { d.engineering = value })
138
+ }
139
+
140
+ /** advisor 守卫开关(`advisor: { guard }`——空值升格为对象)。 */
141
+ export function setSlotAdvisorGuard(cwd, slot, value) {
142
+ return writeFlag(cwd, slot, (d) => {
143
+ d.advisor = { ...(typeof d.advisor === "object" && d.advisor !== null ? d.advisor : {}), guard: value }
144
+ })
145
+ }
146
+
147
+ /**
148
+ * 保存面 token 台账合并规则(纯函数——槽 = 权威台账,保存时不得把内存快照的缺席
149
+ * 当成删除):
150
+ * - `incoming === undefined`(调用方未携带该字段)⇒ 保留槽值(空态不触发清理);
151
+ * - incoming 空值 ⇒ 槽有值则保留槽、槽也空 ⇒ null(写 null = 清字段);
152
+ * - 两端都有 ⇒ 并集:槽独有项保留(consume 已对称删盘,不复活);同 key 以**新铸者胜**
153
+ * (token 尾 `:expiresAt` 大者 = 后铸);平手 / 无法解析 ⇒ 保留槽值(fail-safe)。
154
+ * 槽 I/O 面(读/写/清)在 `token-ttl.mjs`——本函数只产出「应写什么」。
155
+ */
156
+ export function mergeEngTokensForSave(incoming, existing) {
157
+ if (incoming === undefined) return existing ?? null
158
+ const hasNew = incoming && typeof incoming === "object" && !Array.isArray(incoming) && Object.keys(incoming).length > 0
159
+ const slotHas = existing && typeof existing === "object" && !Array.isArray(existing) && Object.keys(existing).length > 0
160
+ if (!hasNew) return slotHas ? existing : null
161
+ if (!slotHas) return incoming
162
+ const expiry = (t) => tokenExpiryMs(t) ?? -Infinity
163
+ const out = { ...existing }
164
+ for (const [key, tok] of Object.entries(incoming)) {
165
+ if (!(key in out) || expiry(tok) > expiry(out[key])) out[key] = tok
166
+ }
167
+ return out
168
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * session-slots-manifest.mjs — 清单 / 认领 / 属主面(init-block 批 · F-MI7 拆分:
3
+ * `session-slots.mjs` 探测束接线后实读 517 行,破 490 判定线并越 500 硬限 ⇒ 按
4
+ * `CORE-UNIFICATION.md` §2.8.1 表第 3 行随批执行外提;各函数语义**原样**迁移):
5
+ * 摘要面(`extractSlotMeta` / `slotDigest`)· 清单读写(`loadManifest` / `saveManifest`)·
6
+ * 属主面(`ownerPid` / `ownerPids` / `ownerStateOf` / `ownerArgs` / `cleanDeadOwners`)·
7
+ * 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot`)。
8
+ *
9
+ * 静态环(与既有的 session.mjs ↔ session-slots.mjs 同形):本档引 `session-slots.mjs` 的
10
+ * 存储原语(`getSessionId` / `manifestPath` / `slotPath` / `writeSessionFile`),
11
+ * `session-slots.mjs` 反向 re-export 本档导出保持既有 import 面——函数声明在实例化期已
12
+ * 初始化、双方只在函数体内运行时使用(模块求值期零顶层调用 ⇒ 环安全;同 session-slots.mjs
13
+ * 头注)。存储契约(路径 / 原子写 / 属主串格式)单源仍在 `session-slots.mjs`,本档零副本。
14
+ *
15
+ * 探测纪律(init-block 批 · F-MI7 / SESSION.md §6.2):本档**零自有 exec、零逐 pid 探测**——
16
+ * 只消费调用面传入的**探测束**(`bundle` = `probeOwnersSync` / `probeOwnersAsync` 输出);
17
+ * 属主三态(`ownerState`)判据单源 = `process-probe.mjs`。`unknown`(探测失败 / 缺行)⇒
18
+ * 不认领 / 不判死 / 不删(方向不对称见 process-probe.mjs 头注 · D-MI10)。
19
+ */
20
+
21
+ import { existsSync, readFileSync, renameSync } from "node:fs"
22
+ // 「真实用户消息」谓词单源(`history-window.mjs`——人读线窗口与槽摘要同判据)。
23
+ import { isRealUserMsg } from "./history-window.mjs"
24
+ // 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据面与探测面分离)。
25
+ import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
26
+ // 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
27
+ import { getSessionId, manifestPath, slotPath, writeSessionFile } from "./session-slots.mjs"
28
+
29
+ /** Extract slot metadata from history (shared by slotDigest and loadSlotMeta) */
30
+ export function extractSlotMeta(history, activeProvider, updatedAt, title = "") {
31
+ const userMsgs = history.filter(isRealUserMsg)
32
+ const first = userMsgs[0]?.content ?? ""
33
+ return {
34
+ messageCount: history.length,
35
+ turnCount: userMsgs.length,
36
+ firstMessage: first.slice(0, 80),
37
+ activeProvider: activeProvider ?? "",
38
+ updatedAt: updatedAt ?? Date.now(),
39
+ title,
40
+ }
41
+ }
42
+
43
+ /** Extract preview summary from session data for manifest storage (with current timestamp)
44
+ * MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。 */
45
+ export function slotDigest(data) {
46
+ const meta = extractSlotMeta(data.history ?? [], data.activeProvider, data.updatedAt, data.title ?? "")
47
+ if (data.activeModel) meta.activeModel = data.activeModel
48
+ return { ts: Date.now(), ...meta }
49
+ }
50
+
51
+ export function loadManifest(cwd) {
52
+ try {
53
+ const p = manifestPath(cwd)
54
+ if (!existsSync(p)) return { slots: {}, sessionId: null }
55
+ const m = JSON.parse(readFileSync(p, "utf8"))
56
+ if (!m.slots) m.slots = {} // 2026-09-01 会诊 deepseek 🔵:损坏的 {} manifest 不再让调用方抛 TypeError
57
+ if (!m.sessionId) m.sessionId = null
58
+ return m
59
+ } catch { return { slots: {}, sessionId: null } }
60
+ }
61
+
62
+ export function saveManifest(cwd, m, deletions = null, opts = {}) {
63
+ // 2026-08-31 会诊 kimi/deepseek 🟡:写前重读并按"条目级"合并——原实现把"读时快照"
64
+ // 整对象写回,另一进程在窗口内对 slots/slotSessions/active 的变更被覆盖抹除(被抹
65
+ // 认领的槽变"文件在、无属主"→ 第三方可认领 → 双属主 → F2 互旋)。无锁文件无法
66
+ // 完全原子,重读合并把丢失更新窗口缩到最小;删除意图经 deletions 参数显式表达
67
+ // (deleteSlot:{ slots: [n], slotSessions: [n] })。
68
+ // 2026-09-01 会诊 deepseek/kimi/glm 🟡:active 是单值——只有显式翻指针的调用方
69
+ // (ensureActive 分支、newSession、switchToSlot、deleteSlot 删到 active 时)传
70
+ // opts.setActive;其余调用方(saveSession/ACP 认领/死项清理)默认保留磁盘 fresh 的
71
+ // active,否则毫秒窗口内会把并发方刚翻的 active 回滚(F1 防漂移的反向变体)。
72
+ try {
73
+ const fresh = JSON.parse(readFileSync(manifestPath(cwd), "utf8"))
74
+ if (fresh && typeof fresh === "object" && fresh.slots && typeof fresh.slots === "object") {
75
+ const merged = { ...fresh }
76
+ if (opts.setActive) merged.active = m.active
77
+ merged.slots = { ...fresh.slots, ...(m.slots ?? {}) }
78
+ merged.slotSessions = { ...(fresh.slotSessions ?? {}), ...(m.slotSessions ?? {}) }
79
+ if (m.sessionId) merged.sessionId = m.sessionId
80
+ if (deletions) {
81
+ for (const [section, keys] of Object.entries(deletions)) {
82
+ for (const k of keys) delete merged[section]?.[k]
83
+ }
84
+ }
85
+ m = merged
86
+ }
87
+ } catch {
88
+ // 首次创建或 manifest 不可读:用传入对象。2026-09-01 advisor 🟡:解析失败时先改名
89
+ // 保留现场(与 loadSlotFile 对 slot 文件的 .corrupted 原则一致)——否则覆盖后全部
90
+ // 槽位元数据(digest/title/updatedAt)永久丢失,/session 列表变空。文件不存在时
91
+ // rename 抛错被吞,无害。
92
+ try { renameSync(manifestPath(cwd), `${manifestPath(cwd)}.corrupted`) } catch {}
93
+ }
94
+ m.sessionId = getSessionId()
95
+ writeSessionFile(manifestPath(cwd), m)
96
+ }
97
+
98
+ /** 属主串 → pid(`"pid-ts-rand"`——`owner.split("-")[0]` 格式判据单源:session-slots ↔
99
+ * session-lifecycle 共用,防两处漂移)。不可解析 ⇒ NaN——判据层按 dead 处理
100
+ * (见 process-probe `ownerState`)。 */
101
+ export function ownerPid(owner) { return parseInt(owner.split("-")[0]) }
102
+
103
+ /** manifest 属主 pid 全量清单(**非本进程**——束入参:一次探测覆盖所有待判 pid;
104
+ * 空清单 ⇒ 探测面零 exec 早退)。`exclude` 缺省 = 本进程 sessionId。 */
105
+ export function ownerPids(m, exclude = getSessionId()) {
106
+ const pids = []
107
+ for (const owner of Object.values(m.slotSessions ?? {})) {
108
+ if (!owner || owner === exclude) continue
109
+ const pid = ownerPid(owner)
110
+ if (pid) pids.push(pid)
111
+ }
112
+ return pids
113
+ }
114
+
115
+ /** 束 → 属主三态(判据单源 = process-probe `ownerState`;束缺失 / 探测失败 ⇒ "unknown"——
116
+ * 保守保留,**不得**当全死)。 */
117
+ export function ownerStateOf(owner, bundle) {
118
+ const pid = ownerPid(owner)
119
+ return ownerState(pid, { aliveSet: bundle?.aliveSet ?? null, cmds: bundle?.cmds ?? null })
120
+ }
121
+
122
+ /** 束 → `filterDeadOwners` 入参(清理面 API 保面——三态 `alive`:无束 / 失败 ⇒ undefined = 未知)。 */
123
+ function ownerArgs(owner, bundle) {
124
+ const pid = ownerPid(owner)
125
+ const aliveSet = bundle?.aliveSet ?? null
126
+ return [pid, { alive: aliveSet ? aliveSet.has(pid) : undefined, cmdline: bundle?.cmds?.get?.(pid) }]
127
+ }
128
+
129
+ /** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
130
+ * ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
131
+ * 判据(不能以"文件缺失"短路——活进程在"认领 → 首次保存"窗口文件暂缺,误删会
132
+ * 致双进程同槽),但**不自行探测**:判活 / 命令行来自调用面同一探测束(F-MI7——
133
+ * 零逐 pid exec)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
134
+ * 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
135
+ * 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。
136
+ *
137
+ * 身份复核(批 1 · F-MI6 / D-MI11——判据单源 `filterDeadOwners`):pid 活 + 命令行**明确
138
+ * 可得且非本产品** ⇒ 同判可删(pid 复用即此场景);探测失败 / 缺行 ⇒ 保守保留(D-MI10)。 */
139
+ export function cleanDeadOwners(m, bundle) {
140
+ const mySessionId = getSessionId()
141
+ const entries = Object.entries(m.slotSessions).filter(([, o]) => o && o !== mySessionId)
142
+ const deadSlots = []
143
+ for (const [slot, owner] of entries) {
144
+ const [pid, args] = ownerArgs(owner, bundle)
145
+ if (filterDeadOwners(pid, args)) {
146
+ delete m.slotSessions[slot]
147
+ deadSlots.push(slot)
148
+ }
149
+ }
150
+ return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
151
+ }
152
+
153
+ /**
154
+ * Claim a slot for this process and set it as active. Idempotent.
155
+ * Preference order:
156
+ * 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
157
+ * 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
158
+ * 3. A brand-new slot when all are owned by live processes. → allocateFresh
159
+ * The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
160
+ * see which slots are taken and avoid them.
161
+ *
162
+ * F-MI7(同步例外收窄):粘性早退(已拥有 active)**零探测**(无束、无 exec);
163
+ * 冷路径 = 一次有界同步束(`probeOwnersSync`)——清理 / 占用 / 空闲全部查表。
164
+ */
165
+ function ensureActive(cwd, m) {
166
+ const mySessionId = getSessionId()
167
+ if (!m.slotSessions) m.slotSessions = {}
168
+
169
+ // Already own the active slot — nothing to do. 粘性早退:**零探测**(死项清理交给
170
+ // 非粘性路径——resumeSlot 每次进程入口的异步束 / 冷路径同步束都会清并落盘)。
171
+ if (m.active && m.slotSessions[m.active] === mySessionId) return
172
+
173
+ // 冷路径:入口一次有界同步束(每 exec ≤ SYNC_PROBE_MS)覆盖全量属主 pid。
174
+ const bundle = probeOwnersSync(ownerPids(m))
175
+ const deadParam = cleanDeadOwners(m, bundle)
176
+
177
+ const isFree = (slot) => {
178
+ const owner = m.slotSessions[slot]
179
+ if (!owner || owner === mySessionId) return true
180
+ // 未知(探测失败 / 缺行)⇒ 不认为是空闲(不认领——D-MI10)
181
+ return ownerStateOf(owner, bundle) === "dead"
182
+ }
183
+
184
+ // 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
185
+ if (m.active && m.slots[m.active] && isFree(m.active)) {
186
+ m.slotSessions[m.active] = mySessionId
187
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
188
+ return
189
+ }
190
+
191
+ // 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §6.10 D-2——resumeSlot 全新分配复用)。
192
+ allocateFresh(cwd, m, deadParam, bundle)
193
+ }
194
+
195
+ /**
196
+ * Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §6.10 D-2 抽取——resumeSlot
197
+ * 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
198
+ * 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
199
+ * 3. A brand-new slot when none is free / all numbers are taken.
200
+ * 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
201
+ * F-MI7:占用 / 空闲判据全部查**调用面束**(`bundle`——零自有探测);缺省 null ⇒
202
+ * 全部属主按 unknown 论(只可能取全新号——保守,不认领陌生人)。
203
+ */
204
+ export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
205
+ const mySessionId = getSessionId()
206
+ if (!m.slotSessions) m.slotSessions = {}
207
+ const isFree = (slot) => {
208
+ const owner = m.slotSessions[slot]
209
+ if (!owner || owner === mySessionId) return true
210
+ return ownerStateOf(owner, bundle) === "dead" // 未知 ⇒ 非空闲(不认领——D-MI10)
211
+ }
212
+
213
+ // 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
214
+ // 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
215
+ // 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
216
+ const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
217
+ for (const slot of allSlots) {
218
+ if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
219
+ m.active = slot
220
+ m.slotSessions[slot] = mySessionId
221
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
222
+ return slot
223
+ }
224
+ }
225
+
226
+ // 3. All slots owned by live processes — allocate a new one (no limit).
227
+ // 2026-08-31 advisor round2 🟡:新号从 max+1 起逐号跳过"已被活进程认领但尚未落盘"
228
+ // 的号(认领→首次保存窗口:slotSessions 有条目、m.slots 无条目、文件不存在——
229
+ // 仅凭 m.slots/existsSync 查不到 → 双进程认领同一号 → 同槽双写/互旋)。
230
+ // 2026-09-01 会诊 kimi 🟡:同时跳过文件仍存在的号(与 newSession 对齐)——manifest
231
+ // 条目丢失/损坏时 max+1 会撞上孤儿槽文件 → F2 把真会话轮转成不可见的 .bak。
232
+ // 另:allSlots 已升序,取 max 用 allSlots[allSlots.length-1](数万槽位时 Math.max
233
+ // spread 有 RangeError 风险)。
234
+ const liveClaimed = (n) => {
235
+ const owner = m.slotSessions?.[n]
236
+ if (!owner || owner === mySessionId) return false
237
+ return ownerStateOf(owner, bundle) !== "dead" // 活 / 未知 ⇒ 视为已占(跳号——保守)
238
+ }
239
+ let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
240
+ while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
241
+ m.active = newSlot
242
+ m.slotSessions[newSlot] = mySessionId
243
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
244
+ return newSlot
245
+ }
246
+
247
+ /** 认领指定槽为本进程所有并置为 active(2026-09-05 §6.10 D-2 resumeSlot 认领路径——调用方
248
+ * 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
249
+ * 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
250
+ * deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
251
+ * deadParam 同型过滤)。 */
252
+ export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
253
+ m.slotSessions ??= {}
254
+ m.slotSessions[slot] = getSessionId()
255
+ m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
256
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
257
+ }
258
+
259
+ /** Return the active slot number for this process, claiming one if necessary */
260
+ export function activeSlot(cwd) {
261
+ const m = loadManifest(cwd)
262
+ ensureActive(cwd, m)
263
+ return m.active
264
+ }