@thincoder/core 0.9.4 → 0.10.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 (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -5,7 +5,18 @@
5
5
  * 摘要面(`extractSlotMeta` / `slotDigest`)· 清单读写(`loadManifest` / `saveManifest`)·
6
6
  * 属主面(`ownerPid` / `ownerPids` / `ownerStateOf` / `ownerArgs` / `cleanDeadOwners`)·
7
7
  * 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot` / **认领释放集
8
- * `staleClaims`**——F-CR1 · SESSION-CLAIM 批:判据单源,`saveManifest` 落盘 + 各落点共用)。
8
+ * `staleClaims`**——F-CR1 · SESSION-CLAIM 批:判据单源,`saveManifest` 落盘 + 各落点共用;
9
+ * **属主 ∕ 认领两族已随 2026-09-28 拆分迁出 `session-slot-claims.mjs`——见下**)。
10
+ *
11
+ * LEDGER-RELIABILITY 批(2026-09-28 · SESSION.md §6.25):补摘要谓词 `healNeeded` + 落点 A 顺手补写
12
+ * `healDigest` / `slotMtime`(判据句 3)· 账本面写安全(判据句 5)· 账本健康出口 `ledgerHealth`(判据句 4)。
13
+ *
14
+ * 2026-09-28 拆分批(R3——本档 421 > 300):**认领面族**(`staleClaims` ∕ owner 三件 ∕
15
+ * `cleanDeadOwners` ∕ `ensureActive` ∕ `allocateFresh` ∕ `claimSlot` ∕ `_releaseStats` ∕
16
+ * `releaseClaimsAll` ∕ `activeSlot`)外提 `./session-slot-claims.mjs`(verbatim——台账 #484
17
+ * 顺延项落形);本档保留摘要 / 清单读写 / 属主健康面,并 re-export 该档导出保既有 import 面
18
+ * (消费档路径与名面不动)。新增静态环:本档 ↔ `session-slot-claims.mjs`(`loadManifest` ∕
19
+ * `saveManifest` ↔ 认领面族——环安全同下)。
9
20
  *
10
21
  * 静态环(与既有的 session.mjs ↔ session-slots.mjs 同形):本档引 `session-slots.mjs` 的
11
22
  * 存储原语(`getSessionId` / `manifestPath` / `slotPath` / `writeSessionFile`),
@@ -18,14 +29,14 @@
18
29
  * 属主三态(`ownerState`)判据单源 = `process-probe.mjs`。`unknown`(探测失败 / 缺行)⇒
19
30
  * 不认领 / 不判死 / 不删(方向不对称见 process-probe.mjs 头注 · D-MI10)。
20
31
  */
21
-
22
- import { existsSync, readFileSync, renameSync } from "node:fs"
32
+ import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
23
33
  // 「真实用户消息」谓词单源(`history-window.mjs`——人读线窗口与槽摘要同判据)。
24
34
  import { isRealUserMsg } from "./history-window.mjs"
25
- // 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据面与探测面分离)。
26
- import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
27
35
  // 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
28
36
  import { getSessionId, manifestPath, slotPath, writeSessionFile } from "./session-slots.mjs"
37
+ // 认领面(2026-09-28 拆分外提——见头注):本档 import `staleClaims` 供 dropStaleClaims 取用,
38
+ // 并在下方 re-export 全族保既有 import 面。
39
+ import { staleClaims } from "./session-slot-claims.mjs"
29
40
 
30
41
  /** Extract slot metadata from history (shared by slotDigest and loadSlotMeta) */
31
42
  export function extractSlotMeta(history, activeProvider, updatedAt, title = "") {
@@ -42,11 +53,49 @@ export function extractSlotMeta(history, activeProvider, updatedAt, title = "")
42
53
  }
43
54
 
44
55
  /** Extract preview summary from session data for manifest storage (with current timestamp)
45
- * MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。 */
46
- export function slotDigest(data) {
56
+ * MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。
57
+ * SLOT-END-PARAM 批(SESSION.md §6.20 判据句 4 读面):创建端**有值才带**(老槽无键 ⇒
58
+ * 键缺席 = 未知——禁回填、禁以占用端冒充)。
59
+ * #503:`ts` 经打点单源 `digestTs(mtimeMs)`——`mtimeMs`(可选)= 该档保存面**写后**取的 mtime
60
+ * (地板);缺省 / 不可得 ⇒ 裸墙钟回溯(旧调用点零改)。 */
61
+ export function slotDigest(data, mtimeMs) {
47
62
  const meta = extractSlotMeta(data.history ?? [], data.activeProvider, data.updatedAt, data.title ?? "")
48
63
  if (data.activeModel) meta.activeModel = data.activeModel
49
- return { ts: Date.now(), ...meta }
64
+ if (data.createdBy) meta.createdBy = data.createdBy
65
+ return { ts: digestTs(mtimeMs), ...meta }
66
+ }
67
+
68
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v)
69
+
70
+ /** 摘要 `ts` 打点(**单源** · #503 地板):`max(墙钟, mtimeMs)`;`mtimeMs` 不可得(缺省 / 非数)⇒
71
+ * 裸墙钟回溯。沿先例 `session-slot-verify.mjs:99`(回写面地板):**平台文件时间戳可领先
72
+ * `Date.now()` 数 ms**——无地板则保存面刚写的摘要会被 `needsVerify`(`ts < mtime`)判「陈旧」
73
+ * 一次(有界自纠,非数据面);地板后 `ts ≥ mtime` 恒成立(同一 stat 值即比较对象)。 */
74
+ export function digestTs(mtimeMs) {
75
+ return isNum(mtimeMs) ? Math.max(Date.now(), mtimeMs) : Date.now()
76
+ }
77
+
78
+ /** 摘要补写谓词(§6.25 判据句 3 · **单源**):缺失 ∨ 陈旧(`ts < mtime`)⇒ 补写;新鲜 ⇒ 不写(幂等)。 */
79
+ export function healNeeded(entry, mtime) {
80
+ if (!entry || typeof entry !== "object") return true
81
+ if (!isNum(entry.ts)) return true
82
+ return isNum(mtime) ? entry.ts < mtime : false
83
+ }
84
+
85
+ /** 落点 A 顺手补写(§6.25 判据句 3 · 打开 / 切槽):数据**已在手** ⇒ 置入调用方 manifest 对象,随其
86
+ * 既有那次 `saveManifest` 落盘(**零额外槽文件读 + 零额外写**);`mtime` 不可得 ⇒ 不妄断陈旧。
87
+ * `ts` 携同一 `mtime` 地板(#503——否则 mtime 领先墙钟时刚补写的条目又判陈旧)。 */
88
+ export function healDigest(m, slot, data, mtime) {
89
+ if (!data || typeof data !== "object") return false
90
+ if (!healNeeded(m.slots?.[slot], mtime)) return false
91
+ m.slots ??= {}
92
+ m.slots[slot] = slotDigest(data, mtime)
93
+ return true
94
+ }
95
+
96
+ /** 槽文件 mtime(`healDigest` 新鲜度判据入参——读失败 ⇒ `null` = 不可得)。 */
97
+ export function slotMtime(cwd, slot) {
98
+ try { return statSync(slotPath(cwd, slot)).mtimeMs } catch { return null }
50
99
  }
51
100
 
52
101
  export function loadManifest(cwd) {
@@ -60,6 +109,62 @@ export function loadManifest(cwd) {
60
109
  } catch { return { slots: {}, sessionId: null } }
61
110
  }
62
111
 
112
+ /** 可信合并基座判据(§6.23 判据句 1 · ②):顶层非 null 非数组对象 ∧(`slots` / `slotSessions`
113
+ * 缺席或为对象)。**假值(null / `0` / `""` / `false`)按缺席归一**——判据线单源 = `loadManifest`
114
+ * 的 `!m.slots → {}`(同向:空基座可合并、不拒写;展开后自然落 `{}`);真值非对象(`42` / `"x"`)
115
+ * 与数组 ⇒ 不可信。形态非法 ⇒ 不写盘(判据句 2(c))。 */
116
+ function isTrustedBase(fresh) {
117
+ if (!fresh || typeof fresh !== "object" || Array.isArray(fresh)) return false
118
+ const mapField = (v) => !v || (typeof v === "object" && !Array.isArray(v))
119
+ return mapField(fresh.slots) && mapField(fresh.slotSessions)
120
+ }
121
+
122
+ /** (b)(c) 现场保全:原字节改名 `{manifest 路径}.corrupted`(与槽文件面同形)——同名已存在 ⇒
123
+ * 覆盖(单槽位 · last-wins);改名失败被吞、**不阻断拒写**(§6.23 判据句 2)。返回值为**改名目标
124
+ * 路径**(拒写 loud 行的 `preserved=` 即取此)——非存在性保证:改名失败面 = `.corrupted` 缺席自证。 */
125
+ function preserveScene(p) {
126
+ const dst = `${p}.corrupted`
127
+ try { renameSync(p, dst) } catch {}
128
+ return dst
129
+ }
130
+
131
+ /** 账本健康计数(§6.25 判据句 4——**本进程累计**):拒写(§6.23)+ 写后读回失败次数与最近一条。 */
132
+ const ledgerRefusals = { refused: 0, lastReason: null, lastPath: null, lastAt: null }
133
+ const noteRefusal = (reason, p) => Object.assign(ledgerRefusals, { refused: ledgerRefusals.refused + 1, lastReason: reason, lastPath: p, lastAt: Date.now() })
134
+
135
+ /** 账本健康出口(§6.25 判据句 4 · **判据单源**):`refused` = 本进程累计(§6.23 拒写 + 判据句 5 读回
136
+ * 失败)+ `lastReason` ∈ 四类(read-failed / parse-failed / shape-invalid / readback-failed);`scene` =
137
+ * 该 cwd 的 `{manifest}.corrupted` 在盘(损坏现场——随 30 天 GC / 手工清除而止)。 */
138
+ export function ledgerHealth(cwd) {
139
+ let scene = false
140
+ try { scene = existsSync(`${manifestPath(cwd)}.corrupted`) } catch { /* cwd 不可解析 ⇒ 无现场 */ }
141
+ return { ...ledgerRefusals, scene }
142
+ }
143
+
144
+ /** 拒写(§6.23 判据句 2/3):不调 `writeSessionFile`(本次调用对盘面零字节写),stderr 一行 loud,返回 `false`。 */
145
+ function refuseManifestWrite(p, reason, preserved = null) {
146
+ noteRefusal(reason, p)
147
+ console.error(`[session] saveManifest: write refused (reason=${reason}) path=${p}${preserved ? ` preserved=${preserved}` : ""}`)
148
+ return false
149
+ }
150
+
151
+ /** 写后钩子缝(§6.25 判据句 5:写后 / 读回前注入;生产零消费):`fn({ path, tmp })`。 */
152
+ let manifestWriteHook = null
153
+ export function _setManifestWriteHookForTest(fn) { manifestWriteHook = fn }
154
+
155
+ /** 账本面写(两路共用 · D-SE65):独占临时名 + **写后结构读回**(判据单源 = `isTrustedBase`);不过 ⇒
156
+ * loud 一行 + 现场改名 `.corrupted`(解封下一写)+ `false`。**非逐字节相等**(并发合法后写会假红——明裁)。 */
157
+ function writeManifestBase(p, m) {
158
+ const tmp = writeSessionFile(p, m, { tmpUnique: true })
159
+ manifestWriteHook?.({ path: p, tmp })
160
+ let trusted = false
161
+ try { trusted = isTrustedBase(JSON.parse(readFileSync(p, "utf8"))) } catch { trusted = false }
162
+ if (trusted) return true
163
+ noteRefusal("readback-failed", p)
164
+ console.error(`[session] saveManifest: readback-failed path=${p} preserved=${preserveScene(p)}`)
165
+ return false
166
+ }
167
+
63
168
  export function saveManifest(cwd, m, deletions = null, opts = {}) {
64
169
  // 2026-08-31 会诊 kimi/deepseek 🟡:写前重读并按"条目级"合并——原实现把"读时快照"
65
170
  // 整对象写回,另一进程在窗口内对 slots/slotSessions/active 的变更被覆盖抹除(被抹
@@ -74,49 +179,52 @@ export function saveManifest(cwd, m, deletions = null, opts = {}) {
74
179
  // (落点槽 ∪ 本进程其余活绑定槽;被占落点 ⇒ 空集)。③ 内存认领表先移除残留条目(否则下方
75
180
  // 条目级合并会把它从内存复活回写)——先于 try(fresh 不可读时亦须生效)。
76
181
  if (opts.release) dropStaleClaims(m, opts.release)
182
+ // MANIFEST-WRITE-GUARD 批(2026-09-28 · SESSION.md §6.23 判据句 1–3 / D-SE59):写前分类——只有
183
+ // 「① 合法创建(ENOENT)」与「② 可信合并基座」两路通向 writeSessionFile;文件在盘而基座不可信
184
+ // (读失败 / 解析失败 / 形态非法)⇒ **拒绝写**(不整档替换——#476 实证:读不可信窗口内一次保存把
185
+ // manifest 永久缩水且零告警)。拒写 = stderr 一行 + 返回 false(唯一消费点 = releaseClaimsAll 透传;
186
+ // 其余调用点忽略返回值——零行为变化);拒写不回滚调用方内存态(认领等内存变更归调用方所有,
187
+ // 下一次保存重试——与 F-XR1 同向的失败容忍形态)。
188
+ const p = manifestPath(cwd)
189
+ let fresh
77
190
  try {
78
- const fresh = JSON.parse(readFileSync(manifestPath(cwd), "utf8"))
79
- if (fresh && typeof fresh === "object" && fresh.slots && typeof fresh.slots === "object") {
80
- const merged = { ...fresh }
81
- if (opts.setActive) merged.active = m.active
82
- merged.slots = { ...fresh.slots, ...(m.slots ?? {}) }
83
- merged.slotSessions = { ...(fresh.slotSessions ?? {}), ...(m.slotSessions ?? {}) }
84
- if (m.sessionId) merged.sessionId = m.sessionId
85
- // ① 释放集按**本次 fresh 快照**取值(不得以陈旧内存 manifest 构 deletions);② **值条件删除**
86
- // (仅当 fresh 属主仍为本进程——窗口内他人的新认领不在集内、不被误删)⇒ 并入本次 deletions。
87
- if (opts.release) {
88
- const released = staleClaims(fresh, opts.release)
89
- if (released.length > 0) {
90
- deletions = { ...(deletions ?? {}), slotSessions: [...(deletions?.slotSessions ?? []), ...released] }
91
- }
92
- }
93
- if (deletions) {
94
- for (const [section, keys] of Object.entries(deletions)) {
95
- for (const k of keys) delete merged[section]?.[k]
96
- }
97
- }
98
- m = merged
191
+ fresh = JSON.parse(readFileSync(p, "utf8"))
192
+ } catch (err) {
193
+ // ① 合法创建:路径不存在(首建)⇒ 以调用方对象建新档(现行为保留)
194
+ if (err?.code === "ENOENT") {
195
+ m.sessionId = getSessionId()
196
+ return writeManifestBase(p, m)
99
197
  }
100
- } catch {
101
- // 首次创建或 manifest 不可读:用传入对象。2026-09-01 advisor 🟡:解析失败时先改名
102
- // 保留现场(与 loadSlotFile 对 slot 文件的 .corrupted 原则一致)——否则覆盖后全部
103
- // 槽位元数据(digest/title/updatedAt)永久丢失,/session 列表变空。文件不存在时
104
- // rename 抛错被吞,无害。
105
- try { renameSync(manifestPath(cwd), `${manifestPath(cwd)}.corrupted`) } catch {}
198
+ // (a) 读失败(非 ENOENT:EISDIR / EPERM / EBUSY 等)⇒ 拒写 · 不改名(读不到 ≠ 损坏——现场
199
+ // 原样保留;该窗口正是 #476 缩水损伤的成因窗口)
200
+ if (!(err instanceof SyntaxError)) return refuseManifestWrite(p, "read-failed")
201
+ // (b) 解析失败 ⇒ 拒写 · 保底改名 .corrupted(现场保全 + 解封下一写——原路径不重建)
202
+ return refuseManifestWrite(p, "parse-failed", preserveScene(p))
106
203
  }
204
+ // (c) 形态非法(顶层非 null 非数组对象 / `slots` 非对象 / `slotSessions` 非对象)⇒ 同 (b)
205
+ if (!isTrustedBase(fresh)) return refuseManifestWrite(p, "shape-invalid", preserveScene(p))
206
+ // ② 可信合并基座:读-合并-写(条目级合并 / deletions / setActive / opts.release 四判据零改)
207
+ const merged = { ...fresh }
208
+ if (opts.setActive) merged.active = m.active
209
+ merged.slots = { ...fresh.slots, ...(m.slots ?? {}) }
210
+ merged.slotSessions = { ...(fresh.slotSessions ?? {}), ...(m.slotSessions ?? {}) }
211
+ if (m.sessionId) merged.sessionId = m.sessionId
212
+ // ① 释放集按**本次 fresh 快照**取值(不得以陈旧内存 manifest 构 deletions);② **值条件删除**
213
+ // (仅当 fresh 属主仍为本进程——窗口内他人的新认领不在集内、不被误删)⇒ 并入本次 deletions。
214
+ if (opts.release) {
215
+ const released = staleClaims(fresh, opts.release)
216
+ if (released.length > 0) {
217
+ deletions = { ...(deletions ?? {}), slotSessions: [...(deletions?.slotSessions ?? []), ...released] }
218
+ }
219
+ }
220
+ if (deletions) {
221
+ for (const [section, keys] of Object.entries(deletions)) {
222
+ for (const k of keys) delete merged[section]?.[k]
223
+ }
224
+ }
225
+ m = merged
107
226
  m.sessionId = getSessionId()
108
- writeSessionFile(manifestPath(cwd), m)
109
- }
110
-
111
- /** 本进程**残留认领集**(F-CR1 释放谓词 —— **判据单源** · SESSION.md §6.2 / §6.16):
112
- * 「释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`」。`keep` = 保留集 = 落点槽 ∪ 本进程
113
- * 其余活绑定槽(单绑定落点 ⇒ `{落点槽}`;被占落点 ⇒ 空集)。返回槽号(**字符串键**形态)。 */
114
- export function staleClaims(src, keep = []) {
115
- const keepSet = new Set((keep ?? []).filter((s) => s != null).map(Number))
116
- const mySessionId = getSessionId()
117
- return Object.entries(src?.slotSessions ?? {})
118
- .filter(([slot, owner]) => owner === mySessionId && !keepSet.has(Number(slot)))
119
- .map(([slot]) => slot)
227
+ return writeManifestBase(p, m)
120
228
  }
121
229
 
122
230
  /** 从**内存**认领表移除本进程残留条目(判据③——与写盘同一时机;见 `saveManifest`)。 */
@@ -125,192 +233,11 @@ function dropStaleClaims(m, keep) {
125
233
  for (const slot of staleClaims(m, keep)) delete m.slotSessions[slot]
126
234
  }
127
235
 
128
- /** 属主串 → pid(`"pid-ts-rand"`——`owner.split("-")[0]` 格式判据单源:session-slots ↔
129
- * session-lifecycle 共用,防两处漂移)。不可解析 ⇒ NaN——判据层按 dead 处理
130
- * (见 process-probe `ownerState`)。 */
131
- export function ownerPid(owner) { return parseInt(owner.split("-")[0]) }
132
-
133
- /** manifest 属主 pid 全量清单(**非本进程**——束入参:一次探测覆盖所有待判 pid;
134
- * 空清单 ⇒ 探测面零 exec 早退)。`exclude` 缺省 = 本进程 sessionId。 */
135
- export function ownerPids(m, exclude = getSessionId()) {
136
- const pids = []
137
- for (const owner of Object.values(m.slotSessions ?? {})) {
138
- if (!owner || owner === exclude) continue
139
- const pid = ownerPid(owner)
140
- if (pid) pids.push(pid)
141
- }
142
- return pids
143
- }
144
-
145
- /** 束 → 属主三态(判据单源 = process-probe `ownerState`;束缺失 / 探测失败 ⇒ "unknown"——
146
- * 保守保留,**不得**当全死)。 */
147
- export function ownerStateOf(owner, bundle) {
148
- const pid = ownerPid(owner)
149
- return ownerState(pid, { aliveSet: bundle?.aliveSet ?? null, cmds: bundle?.cmds ?? null })
150
- }
151
-
152
- /** 束 → `filterDeadOwners` 入参(清理面 API 保面——三态 `alive`:无束 / 失败 ⇒ undefined = 未知)。 */
153
- function ownerArgs(owner, bundle) {
154
- const pid = ownerPid(owner)
155
- const aliveSet = bundle?.aliveSet ?? null
156
- return [pid, { alive: aliveSet ? aliveSet.has(pid) : undefined, cmdline: bundle?.cmds?.get?.(pid) }]
157
- }
158
-
159
- /** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
160
- * ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
161
- * 判据(不能以"文件缺失"短路——活进程在"认领 → 首次保存"窗口文件暂缺,误删会
162
- * 致双进程同槽),但**不自行探测**:判活 / 命令行来自调用面同一探测束(F-MI7——
163
- * 零逐 pid exec)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
164
- * 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
165
- * 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。
166
- *
167
- * 身份复核(批 1 · F-MI6 / D-MI11——判据单源 `filterDeadOwners`):pid 活 + 命令行**明确
168
- * 可得且非本产品** ⇒ 同判可删(pid 复用即此场景);探测失败 / 缺行 ⇒ 保守保留(D-MI10)。 */
169
- export function cleanDeadOwners(m, bundle) {
170
- const mySessionId = getSessionId()
171
- const entries = Object.entries(m.slotSessions).filter(([, o]) => o && o !== mySessionId)
172
- const deadSlots = []
173
- for (const [slot, owner] of entries) {
174
- const [pid, args] = ownerArgs(owner, bundle)
175
- if (filterDeadOwners(pid, args)) {
176
- delete m.slotSessions[slot]
177
- deadSlots.push(slot)
178
- }
179
- }
180
- return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
181
- }
182
-
183
- /**
184
- * Claim a slot for this process and set it as active. Idempotent.
185
- * Preference order:
186
- * 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
187
- * 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
188
- * 3. A brand-new slot when all are owned by live processes. → allocateFresh
189
- * The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
190
- * see which slots are taken and avoid them.
191
- *
192
- * F-MI7(同步例外收窄):粘性早退(已拥有 active)**零探测**(无束、无 exec);
193
- * 冷路径 = 一次有界同步束(`probeOwnersSync`)——清理 / 占用 / 空闲全部查表。
194
- */
195
- function ensureActive(cwd, m) {
196
- const mySessionId = getSessionId()
197
- if (!m.slotSessions) m.slotSessions = {}
198
-
199
- // Already own the active slot — nothing to do. 粘性早退:**零探测**(死项清理交给
200
- // 非粘性路径——resumeSlot 每次进程入口的异步束 / 冷路径同步束都会清并落盘)。
201
- if (m.active && m.slotSessions[m.active] === mySessionId) return
202
-
203
- // 冷路径:入口一次有界同步束(每 exec ≤ SYNC_PROBE_MS)覆盖全量属主 pid。
204
- const bundle = probeOwnersSync(ownerPids(m))
205
- const deadParam = cleanDeadOwners(m, bundle)
206
-
207
- const isFree = (slot) => {
208
- const owner = m.slotSessions[slot]
209
- if (!owner || owner === mySessionId) return true
210
- // 未知(探测失败 / 缺行)⇒ 不认为是空闲(不认领——D-MI10)
211
- return ownerStateOf(owner, bundle) === "dead"
212
- }
213
-
214
- // 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
215
- if (m.active && m.slots[m.active] && isFree(m.active)) {
216
- m.slotSessions[m.active] = mySessionId
217
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
218
- return
219
- }
220
-
221
- // 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §6.10 D-2——resumeSlot 全新分配复用)。
222
- allocateFresh(cwd, m, deadParam, bundle)
223
- }
224
-
225
- /**
226
- * Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §6.10 D-2 抽取——resumeSlot
227
- * 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
228
- * 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
229
- * 3. A brand-new slot when none is free / all numbers are taken.
230
- * 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
231
- * F-MI7:占用 / 空闲判据全部查**调用面束**(`bundle`——零自有探测);缺省 null ⇒
232
- * 全部属主按 unknown 论(只可能取全新号——保守,不认领陌生人)。
233
- */
234
- export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
235
- const mySessionId = getSessionId()
236
- if (!m.slotSessions) m.slotSessions = {}
237
- const isFree = (slot) => {
238
- const owner = m.slotSessions[slot]
239
- if (!owner || owner === mySessionId) return true
240
- return ownerStateOf(owner, bundle) === "dead" // 未知 ⇒ 非空闲(不认领——D-MI10)
241
- }
242
-
243
- // 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
244
- // 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
245
- // 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
246
- const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
247
- for (const slot of allSlots) {
248
- if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
249
- m.active = slot
250
- m.slotSessions[slot] = mySessionId
251
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
252
- return slot
253
- }
254
- }
255
-
256
- // 3. All slots owned by live processes — allocate a new one (no limit).
257
- // 2026-08-31 advisor round2 🟡:新号从 max+1 起逐号跳过"已被活进程认领但尚未落盘"
258
- // 的号(认领→首次保存窗口:slotSessions 有条目、m.slots 无条目、文件不存在——
259
- // 仅凭 m.slots/existsSync 查不到 → 双进程认领同一号 → 同槽双写/互旋)。
260
- // 2026-09-01 会诊 kimi 🟡:同时跳过文件仍存在的号(与 newSession 对齐)——manifest
261
- // 条目丢失/损坏时 max+1 会撞上孤儿槽文件 → F2 把真会话轮转成不可见的 .bak。
262
- // 另:allSlots 已升序,取 max 用 allSlots[allSlots.length-1](数万槽位时 Math.max
263
- // spread 有 RangeError 风险)。
264
- const liveClaimed = (n) => {
265
- const owner = m.slotSessions?.[n]
266
- if (!owner || owner === mySessionId) return false
267
- return ownerStateOf(owner, bundle) !== "dead" // 活 / 未知 ⇒ 视为已占(跳号——保守)
268
- }
269
- let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
270
- while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
271
- m.active = newSlot
272
- m.slotSessions[newSlot] = mySessionId
273
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
274
- return newSlot
275
- }
276
-
277
- /** 认领指定槽为本进程所有并置为 active(2026-09-05 §6.10 D-2 resumeSlot 认领路径——调用方
278
- * 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
279
- * 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
280
- * deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
281
- * deadParam 同型过滤)。
282
- * F-CR1(2026-09-21 · SESSION-CLAIM 批):认领落点释放本进程残留认领(保留集 = {认领槽}——
283
- * §6.2 公式代入);本函数 = 启动恢复 / 本端恢复落点(核 `resumeSlot` 与 VSC 端壳
284
- * `resumeSlot` 共用),落盘判据(fresh 快照 / 值条件删除 / 内存移除)见 `saveManifest`。 */
285
- export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
286
- m.slotSessions ??= {}
287
- m.slotSessions[slot] = getSessionId()
288
- m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
289
- saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true, release: [slot] })
290
- }
291
-
292
- /** 退出全释放(EXIT-CLAIM-RELEASE 批 · F-XR1 · SESSION.md §6.18 D-SE41):优雅退出前释放
293
- * 本进程**全部**槽认领(保留集空——`staleClaims` 释放谓词单源);落盘判据 = `saveManifest`
294
- * `opts.release` 三条自动继承(fresh 同次快照取释放集 / 值条件删除——他人窗口内新认领不误删 /
295
- * 内存认领表同步移除)。**零触碰 marker 面**(F-XR2 路标保留——下次启动经 `usableSlot`
296
- * 无属主短路零探测直达恢复);崩溃路径不经此(信号无 JS 钩子——F-XR3)。
297
- * 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
298
- * 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
299
- * (true = 有释放且落盘成功;false = 早退 / 失败——断言面);写失败盘面 = 认领残留 →
300
- * 恢复走既有探测面(现状形态,数据零险)。 */
301
- export function releaseClaimsAll(cwd) {
302
- try {
303
- if (!existsSync(manifestPath(cwd))) return false // 无 manifest ⇒ 零写(不造盘面)
304
- const m = loadManifest(cwd)
305
- if (staleClaims(m, []).length === 0) return false // 无本进程认领 ⇒ 零写(免白刷共享 sessionId)
306
- saveManifest(cwd, m, null, { release: [] }) // 保留集空 = 全释放;不 setActive(active 共享指针不动)
307
- return true
308
- } catch { return false } // F-XR1:退出恒达(写失败 = 认领残留 → 恢复走探测面)
309
- }
310
-
311
- /** Return the active slot number for this process, claiming one if necessary */
312
- export function activeSlot(cwd) {
313
- const m = loadManifest(cwd)
314
- ensureActive(cwd, m)
315
- return m.active
316
- }
236
+ // ========== 认领面(2026-09-28 拆分外提至 ./session-slot-claims.mjs——见头注)==========
237
+ // 认领面族(staleClaims / ownerPid / ownerPids / ownerStateOf / cleanDeadOwners / ensureActive /
238
+ // allocateFresh / claimSlot / _releaseStats / releaseClaimsAll / activeSlot)语义原样迁至
239
+ // `session-slot-claims.mjs`;本档上方 import 取用(dropStaleClaims)、re-export 保既有 import 面。
240
+ export {
241
+ staleClaims, ownerPid, ownerPids, ownerStateOf, cleanDeadOwners,
242
+ allocateFresh, claimSlot, _releaseStats, releaseClaimsAll, activeSlot,
243
+ } from "./session-slot-claims.mjs"