@thincoder/core 0.9.5 → 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 (215) hide show
  1. package/CHANGELOG.md +36 -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 +12 -12
  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 +22 -12
  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 +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -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 +15 -11
  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 +89 -29
  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 +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  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 +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  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 +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  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 +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  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 +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  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/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -3,7 +3,8 @@
3
3
  * 硬限;各函数语义原样外提,SESSION.md §6.10 D-2 / §6.12 / §6.11 注记随迁):
4
4
  * 本端恢复入口包装(resumeSlot——残留 GC 钩子)/ 槽数据应用(applySession)/ 新建槽
5
5
  * (newSession)/ 运行态清空(resetSessionState)/ 槽切换(switchToSlot)/ 占用查询
6
- * (slotOccupancy)/ v1 老文件清扫(stripTruncatedToolArgs)。
6
+ * (slotOccupancy)/ 打开态读数(sessionReading——§6.24 只读纯投影,桌面残余批落)/
7
+ * v1 老文件清扫(stripTruncatedToolArgs)。
7
8
  *
8
9
  * 静态环(与 session-slots.mjs ↔ session.mjs 同形):本档引 `session.mjs` 的
9
10
  * `loadSlotFile`(data 层读留 session.mjs——2026-09-05 §6.10 D-2 拆分点),session.mjs re-export
@@ -20,24 +21,37 @@ import { existsSync } from "node:fs"
20
21
  import { resolveCompactThreshold } from "./config.mjs"
21
22
  import {
22
23
  slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
23
- writeEndMarker, ownerPid, ownerPids,
24
+ writeEndMarker, sessionEnd, ownerPid, ownerPids,
24
25
  resumeSlot as slotsResumeSlot,
25
26
  } from "./session-slots.mjs"
26
27
  import { probeOwnersSync, probeOwnersAsync, ownerState } from "./process-probe.mjs"
28
+ // 落点 A 顺手补写(§6.25 判据句 3 · LEDGER-RELIABILITY 批)——判据单源住清单档(与 resumeSlot 同函数)。
29
+ import { healDigest, slotMtime } from "./session-slots-manifest.mjs"
27
30
  // 环 import:见头注(loadSlotFile 属 session.mjs 的 data 层读面)。
28
31
  import { loadSlotFile } from "./session.mjs"
32
+ // 全新槽首物化构造器单源(SLOT-END-PARAM 批 §6.20 判据句 4 写面:「`newSession` 并入」——
33
+ // 与 VSC `newSlot` / `loadSlotForWrite` 全新分支同一构造器;环 import 同上,函数体内运行时使用)。
34
+ import { newSlotData } from "./session-slot-write.mjs"
29
35
  import { mergeAdjacentAssistantEchoes } from "./context.mjs"
36
+ // 打开态读数公式(§6.24 判据句 2——与 CLI 状态行 / VSC 同式;零新公式,只组合)。
37
+ import { historyPercent } from "./token-window.mjs"
30
38
  import { scheduleSessionGC } from "./session-gc.mjs"
31
39
  import { restoreEngTokens } from "./token-ttl.mjs"
32
40
  import { clearPlanMode } from "./agent-tools/plan.mjs"
33
41
  import { bindRecordStore, unbindRecordStore, RECORD_WINDOW_MESSAGES } from "./session-store.mjs"
42
+ // 会话级档位(§6.21 判据句 4)判据单源:off 形族(`think-off.mjs`)+ 逐模型枚举
43
+ // (`model-specs.mjs`)——两叶档(零 import)⇒ 不新增环,核内零第二份族别表。
44
+ import { specForModel } from "./model-specs.mjs"
45
+ import { thinkOffShape, thinkOffPath } from "./think-off.mjs"
34
46
 
35
47
  /** 恢复决策包装(SESSION.md §6.12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
36
48
  * scheduleSessionGC 内部**启动窗外延迟拍**(`GC_PASS_DELAY_MS` = 3s)+ 每进程每前缀去重,
37
- * **异步非阻塞**启动路径(N4;§6.17 D-SE39);**async**(F-MI7——调用面必须 await)。 */
38
- export async function resumeSlot(cwd) {
49
+ * **异步非阻塞**启动路径(N4;§6.17 D-SE39);**async**(F-MI7——调用面必须 await)。
50
+ * `opts.end`(§6.20 判据句 2——显式端参 > 进程端名):本端记录端名透传(端壳经绑定转口
51
+ * 传本端常量;缺省 = 进程端名 ⇒ CLI 全调用点零改)。 */
52
+ export async function resumeSlot(cwd, { end = sessionEnd() } = {}) {
39
53
  scheduleSessionGC(cwd)
40
- return slotsResumeSlot(cwd)
54
+ return slotsResumeSlot(cwd, { end })
41
55
  }
42
56
 
43
57
  /** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
@@ -61,9 +75,9 @@ function stripTruncatedToolArgs(m) {
61
75
  *
62
76
  * MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
63
77
  * "三支"误读):
64
- * ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel,
65
- * legacy 槽 null/空串回该渠道默认模型 `providers[].model`(单值——2026-09-10 MODEL-SELECTION
66
- * v2;老 models[] 首候选形态已退场))+ 重算
78
+ * ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel(**串值**;
79
+ * 非串 ∕ null ∕ 空串 = 未登记 ⇒ 回该渠道默认模型 `providers[].model` 单值——2026-09-10 MODEL-SELECTION
80
+ * v2;老 models[] 首候选形态已退场)+ 重算
67
81
  * compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
68
82
  * ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
69
83
  * 复验 validateProvider 弹重选)。
@@ -126,6 +140,10 @@ export function applySession(agent, data, opts = {}) {
126
140
  agent._verifyPassed = false
127
141
  agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
128
142
  agent._slotMtime = null
143
+ // 会话级档位携带(§6.21 判据句 5——保存面的一半):值 = 本槽 `effort` **原样**携带(表外 /
144
+ // 不可 off 的原值不静默改写——写面归一只在 `setSlotPrefs`);缺键(老槽)⇒ `undefined` ⇒
145
+ // `saveSession` 不落该键(禁回填——同 `createdBy` 先例;键在而值为 `null` ⇒ 原样携 `null`)。
146
+ agent._slotEffort = Object.hasOwn(data, "effort") ? (data.effort ?? null) : undefined
129
147
  // ── 记录存储绑定(TUI-OOM-ROOTCAUSE · SESSION.md §6.14 绑定与对账 / 追加时点)──
130
148
  // opts.slot 在场(启动恢复 / 非占用的 /session 切换 / ACP 钉槽)→ 绑定(身份核验 + 对账 +
131
149
  // 窗口)——人读线 = store.tail(200);未传(模式 F:被他人活进程占用的槽 / 测试 / ACP fork)
@@ -149,16 +167,37 @@ export function applySession(agent, data, opts = {}) {
149
167
  // 空槽 model 恒有值,provider.model 键不缺失)
150
168
  const prevName = agent.activeProvider
151
169
  const prevModel = agent.activeModel ?? null
152
- const slotModel = data.activeModel || slotProvider.model
170
+ // #638(adoption 边界归一):非串 `data.activeModel`(外端脏载 / 手工档)= 未登记 ⇒ 回退渠道
171
+ // 模型——类型脏值不入两状态字段(镜下方阈值重算门 ∕ 档位块 ∕ VSC `panel-turn-stages.mjs:53` 同形门)。
172
+ const slotModel = (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : slotProvider.model
153
173
  const switched = prevName !== slotProvider.name || prevModel !== slotModel
154
174
  agent.activeProvider = slotProvider.name
155
175
  agent.activeModel = slotModel
156
176
  agent.provider = { ...slotProvider }
157
177
  if (slotModel) agent.provider.model = slotModel
158
- // 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
159
- if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
178
+ // 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)。
179
+ // 非串 model(外端脏载 / 手工档)⇒ 不重算(#638 后查表族恒 total,本门保留为同形防御——与
180
+ // 下方档位块同口径);类型脏载不中断恢复。
181
+ if (switched && agent.config?.agent?.compactThresholdAuto && typeof agent.provider.model === "string" && agent.provider.model) {
160
182
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
161
183
  }
184
+ // 会话级档位施加(§6.21 判据句 4——在模型合并支之后,以**合并后**的 `provider.model` 判 spec;
185
+ // 三端共用本施加面):`"off"` 记号 ⇒ 该模型有 off 路径才设关思考形(effort 族该形为 `null`
186
+ // ⇒ 另置 `reasoningEffort: "none"`——§16.4 载荷门,单源 = `think-off.mjs`);枚举字面 ⇒ 直置;
187
+ // `null` / 缺键 / 表外串 ⇒ 不动(边界表第 1–3 行:老槽零行为变更,表外不静默改写)。
188
+ if (data.effort != null) {
189
+ // 非串 model(外端脏载 / 手工档)按未登记归一——本块不因类型脏载抛(同 `resolveEffortPatch` 口径)
190
+ const spec = specForModel(typeof agent.provider.model === "string" ? agent.provider.model : "")
191
+ if (data.effort === "off") {
192
+ if (thinkOffPath(spec)) {
193
+ const shape = thinkOffShape(spec)
194
+ agent.provider.thinking = shape
195
+ if (shape === null) agent.provider.reasoningEffort = "none"
196
+ }
197
+ } else if (spec?.reasoningEffortEnum?.includes(data.effort)) {
198
+ agent.provider.reasoningEffort = data.effort
199
+ }
200
+ }
162
201
  return switched
163
202
  }
164
203
  // ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
@@ -211,10 +250,13 @@ export async function newSession(cwd, opts = {}) {
211
250
  while (m.slots[slot] || existsSync(slotPath(cwd, slot)) || liveClaimed(slot)) slot++
212
251
 
213
252
  // Write empty session — 2026-09-01 advisor 🔵:补 contextHistory/planMode 字段与
214
- // VS Code newSlot 对齐(SESSION.md §6.3 v2 格式双端一致;两端读侧均有兜底,功能等价)
215
- const data = { version: 2, cwd, title: "", updatedAt: Date.now(), history: [], contextHistory: [], tasks: [], planMode: false, goal: null, autoApprove: false, advisor: null, pendingReminders: [], sessionStart: null }
253
+ // VS Code newSlot 对齐(SESSION.md §6.3 v2 格式双端一致;两端读侧均有兜底,功能等价)。
254
+ // SLOT-END-PARAM 批(§6.20 判据句 4 写面):改用首物化构造器单源 `newSlotData(cwd)`
255
+ // (原字面逐字同形——并入后随其带创建端 = 本端名)。
256
+ const data = newSlotData(cwd)
216
257
  writeSessionFile(slotPath(cwd, slot), data)
217
- m.slots[slot] = slotDigest(data)
258
+ // 摘要 `ts` 地板入参(#503 同族随动——保存面写后取 mtime;`slotMtime` 读失败 ⇒ null ⇒ 裸墙钟)
259
+ m.slots[slot] = slotDigest(data, slotMtime(cwd, slot))
218
260
  m.active = slot
219
261
  // 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
220
262
  // 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
@@ -262,6 +304,7 @@ export function resetSessionState(agent) {
262
304
  agent._pendingReminders = []
263
305
  agent._slot = null
264
306
  agent._slotMtime = null
307
+ agent._slotEffort = null // 新会话无档位(§6.21 判据句 5:全新槽规范形 = `effort: null`)
265
308
  agent._osReminderInjected = false
266
309
  // §6.11(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
267
310
  // _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
@@ -279,11 +322,16 @@ export function resetSessionState(agent) {
279
322
  * (无认领副作用);目标槽空闲则一并认领、被另一活进程占用则不认领(slotOccupancy——
280
323
  * 下次保存经 activeSlot 自然 fork 到新槽);只改 manifest 指针(setActive 意图)。
281
324
  * 2026-09-01 会诊三家 🟡:saveManifest 条目级合并 + setActive(不把并发方刚翻的指针回滚)。 */
282
- export function switchToSlot(cwd, slot) {
325
+ export function switchToSlot(cwd, slot, { end = sessionEnd() } = {}) {
283
326
  const m = loadManifest(cwd)
284
- if (!m.slots[slot]) return null
327
+ // §6.22 判据句 4(SESSION-LIST-DISK 批):准入 = 盘面文件存在 ∨ manifest 有条目——盘上独有槽
328
+ // 可切;旧分支逐字保留(有条目无文件 ⇒ 读槽返 null 同今日)。
329
+ if (!m.slots[slot] && !existsSync(slotPath(cwd, slot))) return null
285
330
  const data = loadSlotFile(cwd, slot)
286
331
  if (!data) return null
332
+ // 落点 A(§6.25 判据句 3):数据已在手 ⇒ 摘要顺手补写——随本次 saveManifest 落(**零额外读 /
333
+ // 零额外写**;新鲜条目不写——幂等,§6.25 谓词单源 `healNeeded`)。
334
+ healDigest(m, slot, data, slotMtime(cwd, slot))
287
335
  m.active = slot
288
336
  const occ = slotOccupancy(cwd, slot)
289
337
  if (!occ.occupied) {
@@ -294,8 +342,9 @@ export function switchToSlot(cwd, slot) {
294
342
  // (不认领目标 + 旧认领一并释放——下次保存经 `allocateFresh` fork,既有语义)。核受占 =
295
343
  // 切换成立(D-SE33:指针 / 记录按 D-6 / D-4 落点——fork 面依赖指针翻至目标槽)。
296
344
  saveManifest(cwd, m, null, { setActive: true, release: occ.occupied ? [] : [slot] })
297
- // 2026-09-05 §6.10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
298
- writeEndMarker(cwd, slot)
345
+ // 2026-09-05 §6.10 D-4:/session N 跟随"最后查看的槽"(成功切换才写);§6.20 判据句 2:
346
+ // 写点端名随动(显式端参 > 进程端名——端壳经绑定转口传本端常量)
347
+ writeEndMarker(cwd, slot, end)
299
348
  return data
300
349
  }
301
350
 
@@ -316,3 +365,26 @@ export function slotOccupancy(cwd, slot) {
316
365
  if (st === "unknown") return { occupied: true, unknown: true, owner }
317
366
  return { occupied: false }
318
367
  }
368
+
369
+ /** 打开态读数(SESSION.md §6.24 · 桌面残余批——恢复态播种的读数**单源**):槽数据 ⇒ 状态行上下文
370
+ * 读数百分数(整数)。与 `applySession` 后读数**同源同式**(四事同判——组合既有件,零新公式):
371
+ * ① 线选 = `contextHistory` 非空取之,否则 `history` 经 `stripTruncatedToolArgs` 回退(同 applySession);
372
+ * ② 回声归并 = `mergeAdjacentAssistantEchoes`(同 applySession 装线前一步);
373
+ * ③ 渠道合并 = `providers` 命中 `data.activeProvider` ⇒ 串值 `activeModel` 覆盖 `entry.model`(非串 ∕
374
+ * 空串 = 未登记 ⇒ 回退 `entry.model`——模型合并支 ① 同归一);未命中 ⇒ `fallback`(支 ②「静默保持现状」的装配口径 = `loadConfig().provider`);
375
+ * ④ 公式 = `historyPercent`(`thincoder-core/token-window.mjs`——与 CLI 状态行 / VSC 同式)。
376
+ * **零副作用**:`data` / `providers` / `fallback` 皆入参(不读盘、不写盘、不改任何进程态);
377
+ * `data` 非对象 ⇒ `0`(与空历史同值——「非正 ⇒ 零节点」显示门归端侧,沿 `historyPercent` 空历史口径)。 */
378
+ export function sessionReading(data, { providers, fallback } = {}) {
379
+ if (data === null || typeof data !== "object") return 0
380
+ const full = Array.isArray(data.history) ? data.history : []
381
+ const ch = data.contextHistory
382
+ const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
383
+ const line = mergeAdjacentAssistantEchoes(machine)
384
+ const entry = data.activeProvider ? providers?.find((pr) => pr.name === data.activeProvider) : null
385
+ // #638:非串 `activeModel` 归一(与 applySession 模型合并支 ① 同判)——脏值不达 `historyPercent`。
386
+ const provider = entry
387
+ ? { ...entry, model: (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : entry.model }
388
+ : fallback
389
+ return historyPercent(line, provider)
390
+ }
@@ -29,9 +29,13 @@ export function renameSlot(cwd, slot, title, _stat = statSync) {
29
29
  // 读与写之间文件被并发方改过 → 放弃(保留并发内容,重命名下次重试)
30
30
  if (_stat(p).mtimeMs !== t0) return { ok: false, reason: "mtime-conflict" }
31
31
  writeSessionFile(p, data)
32
+ // 写后取 mtime(标题写也是一次落盘)——摘要 `ts` 地板入参(#503 同族随动;读失败 ⇒ 缺省回溯墙钟。
33
+ // `statSync` 是真实 stat——`_stat` 只为 mtime-conflict 判据注入,地板不共用该缝)。
34
+ let wroteMtimeMs
35
+ try { wroteMtimeMs = statSync(p).mtimeMs } catch { /* 不可得 ⇒ 裸墙钟 */ }
32
36
  const m = loadManifest(cwd)
33
37
  if (m.slots[n]) {
34
- m.slots[n] = slotDigest(data)
38
+ m.slots[n] = slotDigest(data, wroteMtimeMs)
35
39
  saveManifest(cwd, m)
36
40
  }
37
41
  return { ok: true }
@@ -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
+ }