@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
@@ -0,0 +1,230 @@
1
+ /**
2
+ * session-slot-claims.mjs — 认领面族(2026-09-28 拆分批 · R3——自 `session-slots-manifest.mjs`
3
+ * (421 > 300)外提认领面:`staleClaims` ∕ owner 三件(`ownerPid` / `ownerPids` /
4
+ * `ownerStateOf`)∕ `cleanDeadOwners` ∕ `ensureActive` ∕ `allocateFresh` ∕ `claimSlot` ∕
5
+ * `_releaseStats` ∕ `releaseClaimsAll` ∕ `activeSlot`;**逐字迁移**、零行为改——台账 #484
6
+ * 「认领面族拆分」顺延项落形)。
7
+ * 原档(`session-slots-manifest.mjs`)保留摘要 / 清单读写 / 属主健康面,并 re-export 本档
8
+ * 导出保既有 import 面(消费档路径与名面不动)。
9
+ *
10
+ * 静态环(与既有的 session.mjs ↔ session-slots.mjs 同形):本档引 `session-slots.mjs` 的存储
11
+ * 原语(`getSessionId` / `manifestPath` / `slotPath`)与 `session-slots-manifest.mjs` 的清单
12
+ * 读写(`loadManifest` / `saveManifest`),后者反向 re-export 本档导出——函数声明在实例化期
13
+ * 已初始化、双方只在函数体内运行时使用(模块求值期零顶层调用 ⇒ 环安全)。
14
+ * 探测纪律(init-block 批 · F-MI7 / SESSION.md §6.2):本档**零自有 exec、零逐 pid 探测**——
15
+ * 只消费调用面传入的**探测束**(`bundle`);属主三态判据单源 = `process-probe.mjs`,
16
+ * `unknown`(探测失败 / 缺行)⇒ 不认领 / 不判死 / 不删(D-MI10)。
17
+ */
18
+ import { existsSync } from "node:fs"
19
+ // 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据面与探测面分离)。
20
+ import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
21
+ // 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
22
+ import { getSessionId, manifestPath, slotPath } from "./session-slots.mjs"
23
+ // 清单读写(落盘单源住原档——静态环见头注;反向 re-export 保 import 面)。
24
+ import { loadManifest, saveManifest } from "./session-slots-manifest.mjs"
25
+
26
+ /** 本进程**残留认领集**(F-CR1 释放谓词 —— **判据单源** · SESSION.md §6.2 / §6.16):
27
+ * 「释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`」。`keep` = 保留集 = 落点槽 ∪ 本进程
28
+ * 其余活绑定槽(单绑定落点 ⇒ `{落点槽}`;被占落点 ⇒ 空集)。返回槽号(**字符串键**形态)。 */
29
+ export function staleClaims(src, keep = []) {
30
+ const keepSet = new Set((keep ?? []).filter((s) => s != null).map(Number))
31
+ const mySessionId = getSessionId()
32
+ return Object.entries(src?.slotSessions ?? {})
33
+ .filter(([slot, owner]) => owner === mySessionId && !keepSet.has(Number(slot)))
34
+ .map(([slot]) => slot)
35
+ }
36
+
37
+ /** 属主串 → pid(`"pid-ts-rand"`——`owner.split("-")[0]` 格式判据单源:session-slots ↔
38
+ * session-lifecycle 共用,防两处漂移)。不可解析 ⇒ NaN——判据层按 dead 处理
39
+ * (见 process-probe `ownerState`)。 */
40
+ export function ownerPid(owner) { return parseInt(owner.split("-")[0]) }
41
+
42
+ /** manifest 属主 pid 全量清单(**非本进程**——束入参:一次探测覆盖所有待判 pid;
43
+ * 空清单 ⇒ 探测面零 exec 早退)。`exclude` 缺省 = 本进程 sessionId。 */
44
+ export function ownerPids(m, exclude = getSessionId()) {
45
+ const pids = []
46
+ for (const owner of Object.values(m.slotSessions ?? {})) {
47
+ if (!owner || owner === exclude) continue
48
+ const pid = ownerPid(owner)
49
+ if (pid) pids.push(pid)
50
+ }
51
+ return pids
52
+ }
53
+
54
+ /** 束 → 属主三态(判据单源 = process-probe `ownerState`;束缺失 / 探测失败 ⇒ "unknown"——
55
+ * 保守保留,**不得**当全死)。 */
56
+ export function ownerStateOf(owner, bundle) {
57
+ const pid = ownerPid(owner)
58
+ return ownerState(pid, { aliveSet: bundle?.aliveSet ?? null, cmds: bundle?.cmds ?? null })
59
+ }
60
+
61
+ /** 束 → `filterDeadOwners` 入参(清理面 API 保面——三态 `alive`:无束 / 失败 ⇒ undefined = 未知)。 */
62
+ function ownerArgs(owner, bundle) {
63
+ const pid = ownerPid(owner)
64
+ const aliveSet = bundle?.aliveSet ?? null
65
+ return [pid, { alive: aliveSet ? aliveSet.has(pid) : undefined, cmdline: bundle?.cmds?.get?.(pid) }]
66
+ }
67
+
68
+ /** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
69
+ * ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
70
+ * 判据(不能以"文件缺失"短路——活进程在"认领 → 首次保存"窗口文件暂缺,误删会
71
+ * 致双进程同槽),但**不自行探测**:判活 / 命令行来自调用面同一探测束(F-MI7——
72
+ * 零逐 pid exec)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
73
+ * 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
74
+ * 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。
75
+ *
76
+ * 身份复核(批 1 · F-MI6 / D-MI11——判据单源 `filterDeadOwners`):pid 活 + 命令行**明确
77
+ * 可得且非本产品** ⇒ 同判可删(pid 复用即此场景);探测失败 / 缺行 ⇒ 保守保留(D-MI10)。 */
78
+ export function cleanDeadOwners(m, bundle) {
79
+ const mySessionId = getSessionId()
80
+ const entries = Object.entries(m.slotSessions).filter(([, o]) => o && o !== mySessionId)
81
+ const deadSlots = []
82
+ for (const [slot, owner] of entries) {
83
+ const [pid, args] = ownerArgs(owner, bundle)
84
+ if (filterDeadOwners(pid, args)) {
85
+ delete m.slotSessions[slot]
86
+ deadSlots.push(slot)
87
+ }
88
+ }
89
+ return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
90
+ }
91
+
92
+ /**
93
+ * Claim a slot for this process and set it as active. Idempotent.
94
+ * Preference order:
95
+ * 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
96
+ * 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
97
+ * 3. A brand-new slot when all are owned by live processes. → allocateFresh
98
+ * The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
99
+ * see which slots are taken and avoid them.
100
+ *
101
+ * F-MI7(同步例外收窄):粘性早退(已拥有 active)**零探测**(无束、无 exec);
102
+ * 冷路径 = 一次有界同步束(`probeOwnersSync`)——清理 / 占用 / 空闲全部查表。
103
+ */
104
+ function ensureActive(cwd, m) {
105
+ const mySessionId = getSessionId()
106
+ if (!m.slotSessions) m.slotSessions = {}
107
+
108
+ // Already own the active slot — nothing to do. 粘性早退:**零探测**(死项清理交给
109
+ // 非粘性路径——resumeSlot 每次进程入口的异步束 / 冷路径同步束都会清并落盘)。
110
+ if (m.active && m.slotSessions[m.active] === mySessionId) return
111
+
112
+ // 冷路径:入口一次有界同步束(每 exec ≤ SYNC_PROBE_MS)覆盖全量属主 pid。
113
+ const bundle = probeOwnersSync(ownerPids(m))
114
+ const deadParam = cleanDeadOwners(m, bundle)
115
+
116
+ const isFree = (slot) => {
117
+ const owner = m.slotSessions[slot]
118
+ if (!owner || owner === mySessionId) return true
119
+ // 未知(探测失败 / 缺行)⇒ 不认为是空闲(不认领——D-MI10)
120
+ return ownerStateOf(owner, bundle) === "dead"
121
+ }
122
+
123
+ // 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
124
+ if (m.active && m.slots[m.active] && isFree(m.active)) {
125
+ m.slotSessions[m.active] = mySessionId
126
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
127
+ return
128
+ }
129
+
130
+ // 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §6.10 D-2——resumeSlot 全新分配复用)。
131
+ allocateFresh(cwd, m, deadParam, bundle)
132
+ }
133
+
134
+ /**
135
+ * Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §6.10 D-2 抽取——resumeSlot
136
+ * 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
137
+ * 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
138
+ * 3. A brand-new slot when none is free / all numbers are taken.
139
+ * 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
140
+ * F-MI7:占用 / 空闲判据全部查**调用面束**(`bundle`——零自有探测);缺省 null ⇒
141
+ * 全部属主按 unknown 论(只可能取全新号——保守,不认领陌生人)。
142
+ */
143
+ export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
144
+ const mySessionId = getSessionId()
145
+ if (!m.slotSessions) m.slotSessions = {}
146
+ const isFree = (slot) => {
147
+ const owner = m.slotSessions[slot]
148
+ if (!owner || owner === mySessionId) return true
149
+ return ownerStateOf(owner, bundle) === "dead" // 未知 ⇒ 非空闲(不认领——D-MI10)
150
+ }
151
+
152
+ // 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
153
+ // 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
154
+ // 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
155
+ const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
156
+ for (const slot of allSlots) {
157
+ if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
158
+ m.active = slot
159
+ m.slotSessions[slot] = mySessionId
160
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
161
+ return slot
162
+ }
163
+ }
164
+
165
+ // 3. All slots owned by live processes — allocate a new one (no limit).
166
+ // 2026-08-31 advisor round2 🟡:新号从 max+1 起逐号跳过"已被活进程认领但尚未落盘"
167
+ // 的号(认领→首次保存窗口:slotSessions 有条目、m.slots 无条目、文件不存在——
168
+ // 仅凭 m.slots/existsSync 查不到 → 双进程认领同一号 → 同槽双写/互旋)。
169
+ // 2026-09-01 会诊 kimi 🟡:同时跳过文件仍存在的号(与 newSession 对齐)——manifest
170
+ // 条目丢失/损坏时 max+1 会撞上孤儿槽文件 → F2 把真会话轮转成不可见的 .bak。
171
+ // 另:allSlots 已升序,取 max 用 allSlots[allSlots.length-1](数万槽位时 Math.max
172
+ // spread 有 RangeError 风险)。
173
+ const liveClaimed = (n) => {
174
+ const owner = m.slotSessions?.[n]
175
+ if (!owner || owner === mySessionId) return false
176
+ return ownerStateOf(owner, bundle) !== "dead" // 活 / 未知 ⇒ 视为已占(跳号——保守)
177
+ }
178
+ let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
179
+ while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
180
+ m.active = newSlot
181
+ m.slotSessions[newSlot] = mySessionId
182
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
183
+ return newSlot
184
+ }
185
+
186
+ /** 认领指定槽为本进程所有并置为 active(2026-09-05 §6.10 D-2 resumeSlot 认领路径——调用方
187
+ * 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
188
+ * 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
189
+ * deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
190
+ * deadParam 同型过滤)。
191
+ * F-CR1(2026-09-21 · SESSION-CLAIM 批):认领落点释放本进程残留认领(保留集 = {认领槽}——
192
+ * §6.2 公式代入);本函数 = 启动恢复 / 本端恢复落点(核 `resumeSlot` 与 VSC 端壳
193
+ * `resumeSlot` 共用),落盘判据(fresh 快照 / 值条件删除 / 内存移除)见 `saveManifest`。 */
194
+ export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
195
+ m.slotSessions ??= {}
196
+ m.slotSessions[slot] = getSessionId()
197
+ m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
198
+ saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true, release: [slot] })
199
+ }
200
+
201
+ /** 观测计数(测试缝——`_storeStats` / `_staleHooks` 同惯例):`releaseClaimsAll` 调用计数。
202
+ * 端壳「工作区转空 ⇒ 旧 cwd 释放恰一次」类断言以此为桩计数(生产零消费)。 */
203
+ export const _releaseStats = { calls: 0 }
204
+
205
+ /** 退出全释放(EXIT-CLAIM-RELEASE 批 · F-XR1 · SESSION.md §6.18 D-SE41):优雅退出前释放
206
+ * 本进程**全部**槽认领(保留集空——`staleClaims` 释放谓词单源);落盘判据 = `saveManifest`
207
+ * `opts.release` 三条自动继承(fresh 同次快照取释放集 / 值条件删除——他人窗口内新认领不误删 /
208
+ * 内存认领表同步移除)。**零触碰 marker 面**(F-XR2 路标保留——下次启动经 `usableSlot`
209
+ * 无属主短路零探测直达恢复);崩溃路径不经此(信号无 JS 钩子——F-XR3)。
210
+ * 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
211
+ * 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
212
+ * (true = 有释放且落盘成功;false = 早退 / 失败 / **写面拒写**——`saveManifest` 返回值透传,
213
+ * §6.23 判据句 3——断言面);写失败盘面 = 认领残留 →
214
+ * 恢复走既有探测面(现状形态,数据零险)。 */
215
+ export function releaseClaimsAll(cwd) {
216
+ _releaseStats.calls += 1
217
+ try {
218
+ if (!existsSync(manifestPath(cwd))) return false // 无 manifest ⇒ 零写(不造盘面)
219
+ const m = loadManifest(cwd)
220
+ if (staleClaims(m, []).length === 0) return false // 无本进程认领 ⇒ 零写(免白刷共享 sessionId)
221
+ return saveManifest(cwd, m, null, { release: [] }) !== false // 保留集空 = 全释放;不 setActive;返回值透传(§6.23 判据句 3——拒写 ⇒ false)
222
+ } catch { return false } // F-XR1:退出恒达(写失败 = 认领残留 → 恢复走探测面)
223
+ }
224
+
225
+ /** Return the active slot number for this process, claiming one if necessary */
226
+ export function activeSlot(cwd) {
227
+ const m = loadManifest(cwd)
228
+ ensureActive(cwd, m)
229
+ return m.active
230
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * session-slot-scan.mjs — 会话槽盘面扫描(SESSION.md §6.22 · SESSION-LIST-DISK 批 · 2026-09-28)。
3
+ *
4
+ * 条目集单源 = 盘面实读(判据句 1):sessions 根下 `^<hash>\.json\.(\d+)$` 的**普通文件**——
5
+ * 逐条出槽号 N;manifest `m.slots` 不参与条目集(既不筛也不补),只作判据句 2 取数链的
6
+ * **摘要缓存**(叠加源)。后缀闭集(`.manifest` / `.manifest.<端名>` / `.tmp` / `.corrupted` /
7
+ * `.unreadable` / `.bak-<ts>` / `.d` 目录 / `.stale-<ts>` / 遗留单档 `<hash>.json`)由
8
+ * 「数字槽号全锚 + 普通文件」两个判据一并排除——零伪槽号、零目录误收。
9
+ *
10
+ * 取数链(判据句 2 · 免费先行):① 摘要快路(摘要在场且 **`ts`(摘要落盘时刻)≥ 文件 mtime** ⇒
11
+ * 整条由摘要供给、**槽文件零字节读**——设计档 §6.22 判据句 2 快路比较口径**已收正为 `ts` 单源**;
12
+ * `updatedAt`(会话逻辑时,取于写槽文件之前)恒早于 mtime ⇒ 按之快路生产永不命中)② 小档全读
13
+ * (`size ≤ SCAN_FULL_MAX` = 256 KiB ⇒ JSON 全解析、全字段)③ 大档早键截读(读头 `min(size,
14
+ * SCAN_HEAD_BYTES)` = 64 KiB——**结构感知扫描**:键值边界由结构定界、小值解码走 `JSON.parse`
15
+ * 切片,零正则、零整档物化;遇顶层键 `history` 即停,其后的键不读)④ stat 兜底(`updatedAt` =
16
+ * mtime)。字段优先级 = **盘面实读值 > 摘要值(任意新鲜度)> 缺省**(`""` / `null` / mtime);单次
17
+ * 调用槽文件读 ≤ `SCAN_BUDGET_BYTES`(4 MiB)——预算按 mtime 降序耗用(最上面 = 用户最可能看
18
+ * 的行),越预算条目退 ④。计数两字段**不可得 = `null`**(真 0 与未知可分——§6.25 判据句 4)。
19
+ *
20
+ * 降级阶梯(判据句 3 · 入列不筛):坏 JSON / 半写 / `version > 2` / 异 cwd 内容档一律入列
21
+ * (存在性 ≠ 可读性)——内容面不可用 ⇒ 该档退摘要 ∪ 缺省、日期退 mtime;点开走既有失败面。
22
+ *
23
+ * 静态环(与 session-slots.mjs ↔ session-slots-manifest.mjs 同形):本档引 `sessionPath`
24
+ * (前缀 / 目录单源——先短哈希迁移、后枚举)与 `extractSlotMeta`(② 全读面摘要单源);两侧只在
25
+ * 函数体内运行时使用(模块求值期零顶层调用 ⇒ 环安全)。`SCAN_*` 三常量单源住本档。
26
+ */
27
+ import { closeSync, openSync, readSync, readFileSync, readdirSync, statSync } from "node:fs"
28
+ import { basename, dirname, join } from "node:path"
29
+ // 环 import:见头注(前缀 = `sessionPath` 的 basename——与 `slotPath` 同源)。
30
+ import { sessionPath } from "./session-slots.mjs"
31
+ // ② 全读面摘要单源(计数 / 首条真实用户消息 / 字段缺省口径——与槽摘要写面同函数)。
32
+ import { extractSlotMeta } from "./session-slots-manifest.mjs"
33
+ // 「真实用户消息」谓词单源(人读线窗口 / 槽摘要 / ③ 早键截读三面同判据)。
34
+ import { isRealUserMsg } from "./history-window.mjs"
35
+
36
+ /** ② 小档全读上界(判据句 2)。 */
37
+ export const SCAN_FULL_MAX = 256 * 1024
38
+ /** ③ 大档早键截读窗口(判据句 2)。 */
39
+ export const SCAN_HEAD_BYTES = 64 * 1024
40
+ /** 单次扫描的槽文件读预算(判据句 2——按 mtime 降序耗用,越预算退 ④)。 */
41
+ export const SCAN_BUDGET_BYTES = 4 * 1024 * 1024
42
+
43
+ /** 观测计数(测试缝——`_storeStats` / `_releaseStats` 同惯例):读放大上界的断言语料。
44
+ * `bytes` = 本进程槽文件读累计字节;`reads` = 逐次读 `{ slot, bytes, window }`(window =
45
+ * `"full"` ② / `"head"` ③);`fastPath` = ① 命中次数。生产零消费。 */
46
+ export const _scanStats = { calls: 0, bytes: 0, fastPath: 0, reads: [] }
47
+ /** 测试缝复位(跨用例隔离——`_resetSessionsDirForTest` 同惯例)。 */
48
+ export function _resetScanStats() {
49
+ _scanStats.calls = 0; _scanStats.bytes = 0; _scanStats.fastPath = 0; _scanStats.reads.length = 0
50
+ }
51
+
52
+ const SLOT_SUFFIX = /^\d+$/
53
+ const HEAD_KEYS = new Set(["title", "activeProvider", "activeModel", "createdBy", "cwd", "updatedAt", "version"])
54
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v)
55
+ const isStr = (v) => typeof v === "string"
56
+ const isWs = (c) => c === " " || c === "\n" || c === "\r" || c === "\t"
57
+
58
+ /** JSON 切片解码(结构定界的**小值**:键 / 短串 / 数字);不可解析 ⇒ `undefined`。 */
59
+ function parseSlice(text, start, end) { try { return JSON.parse(text.slice(start, end)) } catch { return undefined } }
60
+
61
+ /** JSON 字符串跳过(不建值——大串零分配)。返回闭引号后一位置;窗截断 ⇒ `null`。 */
62
+ function skipJsonString(text, i) {
63
+ const n = text.length
64
+ let j = i + 1
65
+ while (j < n) {
66
+ const c = text[j]
67
+ if (c === "\\") { j += 2; continue }
68
+ if (c === '"') return j + 1
69
+ j++
70
+ }
71
+ return null
72
+ }
73
+
74
+ /** 结构化跳过任意 JSON 值(对象 / 数组按深度配对,字符串走 `skipJsonString`)。窗截断 ⇒ `null`。 */
75
+ function skipJsonValue(text, i) {
76
+ const n = text.length
77
+ const c = text[i]
78
+ if (c === '"') return skipJsonString(text, i)
79
+ if (c === "{" || c === "[") {
80
+ let depth = 0
81
+ let j = i
82
+ while (j < n) {
83
+ const ch = text[j]
84
+ if (ch === '"') {
85
+ const end = skipJsonString(text, j)
86
+ if (end === null) return null
87
+ j = end
88
+ continue
89
+ }
90
+ if (ch === "{" || ch === "[") depth++
91
+ else if (ch === "}" || ch === "]") {
92
+ depth--
93
+ if (depth === 0) return j + 1
94
+ }
95
+ j++
96
+ }
97
+ return null
98
+ }
99
+ let j = i
100
+ while (j < n && !isWs(text[j]) && text[j] !== "," && text[j] !== "}" && text[j] !== "]") j++
101
+ return j === i ? null : j
102
+ }
103
+
104
+ /** ③ 供给面:窗内首个真实用户消息(谓词单源 = history-window.mjs)。`i` = `history` 数组起点。
105
+ * 逐条结构定界 + `JSON.parse` 切片(谓词拿整条对象);窗截断 ⇒ 停(已得部分照给)。 */
106
+ function firstUserMessage(text, i) {
107
+ const n = text.length
108
+ if (text[i] !== "[") return null
109
+ let j = i + 1
110
+ while (j < n) {
111
+ const c = text[j]
112
+ if (isWs(c) || c === ",") { j++; continue }
113
+ if (c !== "{") return null // 数组尾 / 非对象条 ⇒ 不再走(保守)
114
+ const end = skipJsonValue(text, j)
115
+ if (end === null) return null
116
+ const m = parseSlice(text, j, end)
117
+ if (isRealUserMsg(m)) return m.content.slice(0, 80)
118
+ j = end
119
+ }
120
+ return null
121
+ }
122
+
123
+ /** ③ 早键截读(判据句 2):自首字节走 JSON **顶层键**——供给面键取值、遇 `history` 置
124
+ * `historySeen` 并取首条真实用户消息后**即停**(其后的键不读)。窗口截断 / 结构不符 ⇒ 已得
125
+ * 部分照给(缺键由摘要补位);值跳过一律结构定界(非正则)。 */
126
+ function scanHeadKeys(text) {
127
+ const out = {}
128
+ const n = text.length
129
+ let i = 0
130
+ while (i < n && isWs(text[i])) i++
131
+ if (text[i] !== "{") return out
132
+ i++
133
+ while (i < n) {
134
+ while (i < n && isWs(text[i])) i++
135
+ const c = text[i]
136
+ if (c === "}" || c === undefined) break
137
+ if (c === ",") { i++; continue }
138
+ if (c !== '"') break
139
+ const keyEnd = skipJsonString(text, i)
140
+ if (keyEnd === null) break
141
+ const key = parseSlice(text, i, keyEnd)
142
+ if (!isStr(key)) break
143
+ i = keyEnd
144
+ while (i < n && isWs(text[i])) i++
145
+ if (text[i] !== ":") break
146
+ i++
147
+ while (i < n && isWs(text[i])) i++
148
+ if (key === "history") {
149
+ out.historySeen = text[i] === "[" // 数组起点 = ③ 面「history 数组」判据(② 面 = Array.isArray)
150
+ const first = firstUserMessage(text, i)
151
+ if (first !== null) out.firstMessage = first
152
+ break // 判据:遇 history 即停(其后的键不读——大数组不跳跃、不物化)
153
+ }
154
+ const valEnd = skipJsonValue(text, i)
155
+ if (valEnd === null) break // 窗截断 ⇒ 停(已得键照给)
156
+ if (HEAD_KEYS.has(key)) {
157
+ const v = parseSlice(text, i, valEnd)
158
+ if (isStr(v) || isNum(v)) out[key] = v
159
+ }
160
+ i = valEnd
161
+ }
162
+ return out
163
+ }
164
+
165
+ /** 内容面可用性(判据句 3 降级阶梯):`history` **数组**已见(② = `Array.isArray` · ③ = 值以 `[`
166
+ * 起)∧ version ∈ {1,2} ∧ cwd 一致(缺键 = 本档)。坏 JSON / 半写 / `version > 2` / 异 cwd ⇒
167
+ * 内容面不可用(该档退摘要 ∪ 缺省、日期退 mtime)。 */
168
+ function contentUsable(version, fileCwd, hasHistory, cwd) {
169
+ if (!hasHistory) return false
170
+ if (version !== 1 && version !== 2) return false
171
+ return !(isStr(fileCwd) && fileCwd.toLowerCase() !== cwd.toLowerCase())
172
+ }
173
+
174
+ /** ② 小档全读:JSON 全解析 ⇒ 全字段(② 供给面含计数两字段)。 */
175
+ function fullDisk(text, cwd) {
176
+ let data
177
+ try { data = JSON.parse(text) } catch { return null }
178
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null
179
+ if (!contentUsable(data.version, data.cwd, Array.isArray(data.history), cwd)) return null
180
+ const meta = extractSlotMeta(data.history, data.activeProvider, data.updatedAt, isStr(data.title) ? data.title : "")
181
+ if (isNum(data.updatedAt)) meta.updatedAt = data.updatedAt
182
+ else delete meta.updatedAt // 缺键 ⇒ 退摘要 / mtime(缺省链;extractSlotMeta 的 now() 兜底在此不适用)
183
+ if (data.activeModel) meta.activeModel = data.activeModel
184
+ if (data.createdBy) meta.createdBy = data.createdBy
185
+ return meta
186
+ }
187
+
188
+ /** ③ 头窗供给面 → 盘面元数据(门不过 ⇒ `null`;`historySeen` 是内部标记,不入供给面)。 */
189
+ function headDisk(head, cwd) {
190
+ if (!contentUsable(head.version, head.cwd, head.historySeen === true, cwd)) return null
191
+ delete head.version
192
+ delete head.cwd
193
+ delete head.historySeen
194
+ return head
195
+ }
196
+
197
+ /** 盘面枚举(判据句 1):普通文件 + 数字槽号全锚;目录不存在 / 不可读 ⇒ `[]`。
198
+ * `sessionPath` 先行 = 短哈希迁移随该调用先行(先迁移、后枚举)。 */
199
+ function enumerateSlots(cwd) {
200
+ const base = sessionPath(cwd)
201
+ const dir = dirname(base)
202
+ const prefix = `${basename(base)}.`
203
+ let entries
204
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return [] }
205
+ const found = []
206
+ for (const e of entries) {
207
+ if (!e.isFile() || !e.name.startsWith(prefix)) continue
208
+ const suffix = e.name.slice(prefix.length)
209
+ if (!SLOT_SUFFIX.test(suffix)) continue
210
+ const path = join(dir, e.name)
211
+ try {
212
+ const st = statSync(path)
213
+ found.push({ slot: Number(suffix), path, size: st.size, mtime: st.mtimeMs })
214
+ } catch { /* 枚举与 stat 之间被删 / 不可达 ⇒ 视同不在盘 */ }
215
+ }
216
+ return found
217
+ }
218
+
219
+ /** ③ 头窗读(只读 `min(size, SCAN_HEAD_BYTES)` 字节——大档绝不整档读)。 */
220
+ function readHeadText(path) {
221
+ const fd = openSync(path, "r")
222
+ try {
223
+ const buf = Buffer.allocUnsafe(SCAN_HEAD_BYTES)
224
+ const size = readSync(fd, buf, 0, SCAN_HEAD_BYTES, 0)
225
+ return buf.subarray(0, size).toString("utf8")
226
+ } finally {
227
+ try { closeSync(fd) } catch { /* 关闭失败不影响读结果 */ }
228
+ }
229
+ }
230
+
231
+ /** 字段装配(判据句 2 优先级):盘面实读值 > 摘要值(任意新鲜度)> 缺省(`""` / `null` / mtime);计数两字段
232
+ * **不可得 = `null`**(非 `0`——真 0 与「不知道」可分;§6.25 判据句 4 / D-SE62)。`disk` = ②/③ 盘面供给面;
233
+ * `digest` = manifest 摘要;`dateFromStat` = 日期取 stat mtime(判据句 2 ④ 与判据句 3 降级档)。 */
234
+ function mergeFields(disk, digest, mtime, tsBase, dateFromStat) {
235
+ const d = disk ?? {}
236
+ const g = digest ?? {}
237
+ const num = (a, b, fallback) => isNum(a) ? a : (isNum(b) ? b : fallback)
238
+ const str = (a, b, fallback) => isStr(a) ? a : (isStr(b) ? b : fallback)
239
+ const meta = {
240
+ ts: tsBase,
241
+ messageCount: num(d.messageCount, g.messageCount, null),
242
+ turnCount: num(d.turnCount, g.turnCount, null),
243
+ firstMessage: str(d.firstMessage, g.firstMessage, ""),
244
+ activeProvider: str(d.activeProvider, g.activeProvider, ""),
245
+ updatedAt: dateFromStat ? mtime : num(d.updatedAt, g.updatedAt, mtime),
246
+ title: str(d.title, g.title, ""),
247
+ createdBy: str(d.createdBy, g.createdBy, ""),
248
+ }
249
+ const model = str(d.activeModel, g.activeModel, null)
250
+ if (model !== null) meta.activeModel = model
251
+ return meta
252
+ }
253
+
254
+ /** 行时间标基准:摘要 `ts` > 数字形旧条目值 > mtime(判据句 2 缺省链)。 */
255
+ function tsBaseOf(entry, mtime) {
256
+ if (isNum(entry?.ts)) return entry.ts
257
+ if (isNum(entry)) return entry
258
+ return mtime
259
+ }
260
+
261
+ /** 单档取数(判据句 2 四级的装配点):返回 `{ meta, spent }`——`spent` = 本次占用的预算字节。 */
262
+ function readSlot(f, entry, budget, cwd) {
263
+ const mtime = f.mtime
264
+ const digest = entry && typeof entry === "object" ? entry : null
265
+ const tsBase = tsBaseOf(entry, mtime)
266
+ // ① 摘要快路:摘要在场且 `ts`(摘要落盘时刻)≥ 文件 mtime ⇒ 整条由摘要供给、槽文件零字节读
267
+ if (digest && isNum(digest.ts) && digest.ts >= mtime) {
268
+ _scanStats.fastPath++
269
+ return { meta: mergeFields(null, digest, mtime, tsBase, false), spent: 0 }
270
+ }
271
+ // ②/③:预算内读盘;越预算 / 读失败 / 内容不可用 ⇒ 盘面缺位(日期退 mtime——判据句 2 ④ / 判据句 3)
272
+ const full = f.size <= SCAN_FULL_MAX
273
+ const cost = full ? f.size : SCAN_HEAD_BYTES
274
+ if (cost > budget) return { meta: mergeFields(null, digest, mtime, tsBase, true), spent: 0 }
275
+ let disk = null
276
+ try {
277
+ const text = full ? readFileSync(f.path, "utf8") : readHeadText(f.path)
278
+ _scanStats.bytes += cost
279
+ _scanStats.reads.push({ slot: f.slot, bytes: cost, window: full ? "full" : "head" })
280
+ disk = full ? fullDisk(text, cwd) : headDisk(scanHeadKeys(text), cwd)
281
+ } catch { /* 并发删除 / EPERM ⇒ 盘面缺位 */ }
282
+ return { meta: mergeFields(disk, digest, mtime, tsBase, disk === null), spent: cost }
283
+ }
284
+
285
+ /** 扫描本 cwd 的槽文件并装配每档元数据(判据句 1 + 2):返回 `[{ slot, meta, mtime }]`(序 = 枚举序——
286
+ * 行序归调用面投影;`mtime` = 文件 mtime,判定链 / 核实面新鲜度判据入参)。`digests` = manifest `m.slots`。 */
287
+ export function scanSlotMetas(cwd, digests = {}) {
288
+ _scanStats.calls++
289
+ const found = enumerateSlots(cwd)
290
+ found.sort((a, b) => b.mtime - a.mtime) // 预算耗用序 = mtime 降序
291
+ let budget = SCAN_BUDGET_BYTES
292
+ const out = []
293
+ for (const f of found) {
294
+ const { meta, spent } = readSlot(f, digests[f.slot], budget, cwd)
295
+ budget -= spent
296
+ out.push({ slot: f.slot, meta, mtime: f.mtime })
297
+ }
298
+ return out
299
+ }