@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
package/session-slots.mjs CHANGED
@@ -7,9 +7,15 @@
7
7
  * ⇒ 按 `CORE-UNIFICATION.md` §2.8.1 表第 3 行**随批**外提清单 / 认领 / 属主面至
8
8
  * `session-slots-manifest.mjs`(语义原样迁移;本档 re-export 该档导出保既有 import 面——
9
9
  * 消费档 import 路径与名面不动)。本档保留 = 存储原语(cwd 哈希 / 路径 / 原子写 / 进程
10
- * sessionId)· 端记录(end marker)· 列表面(listSlots / loadSlotMeta)· 删槽 · 恢复决策面
10
+ * sessionId)· 端记录(end marker)· 列表面(listSlots——条目集 = 盘面实读;扫描面住
11
+ * `session-slot-scan.mjs`)· 删槽 · 恢复决策面
11
12
  * (usableSlot / loadLegacyFile / resumeSlot)。
12
13
  *
14
+ * 端名缝(SESSION.md §6.20 D-SE50 · 2026-09-25 SLOT-END-PARAM 批):端名 = **进程内的单值
15
+ * 声明**(`setSessionEnd` / `sessionEnd`;`END` 仅模块级初值——CLI 零声明即得「cli」)。
16
+ * marker 家族三式与写 marker 的核入口逐函数带**显式端参**(缺省 = 进程端名)——端壳经绑定
17
+ * 转口传本端常量(`(cwd) => coreX(cwd, END)`),核内零端名分支;未列写点一律走进程端名。
18
+ *
13
19
  * 模型:每个项目(cwd hash)拥有无限编号 slot;manifest 记录 active 指针 + 每个
14
20
  * slot 的属主进程(slotSessions: slot → "pid-timestamp-random",CLI ↔ VS Code
15
21
  * 共享 manifest 以互斥认领)。
@@ -39,12 +45,17 @@ import { probeOwnersAsync, isProcessAlive } from "./process-probe.mjs"
39
45
  // 清单 / 认领 / 属主面(init-block 批外提——双向静态环见头注):本档 import 取用 + re-export
40
46
  // 保既有 import 面(消费档路径与名面不动)。
41
47
  import {
42
- extractSlotMeta, loadManifest, saveManifest, ownerPids, ownerStateOf, cleanDeadOwners,
43
- allocateFresh, claimSlot, activeSlot,
48
+ loadManifest, saveManifest, ownerPids, ownerStateOf, cleanDeadOwners,
49
+ allocateFresh, claimSlot, activeSlot, healDigest, slotMtime,
44
50
  } from "./session-slots-manifest.mjs"
51
+ // 盘面扫描面(SESSION-LIST-DISK 批 §6.22):条目集 = 盘面实读——枚举 / 早键截读 / 取数链装配住
52
+ // 新档;本档 listSlots 收敛为组合 + 条目投影。静态环(本档 ↔ 扫描档;环安全见头注同形)。
53
+ import { scanSlotMetas } from "./session-slot-scan.mjs"
54
+ // 读面懒核实面(§6.25 判据句 3 落点 B——不可信清单调度:纯内存登记 + 启动窗外延迟拍);静态环同形。
55
+ import { scheduleVerify } from "./session-slot-verify.mjs"
45
56
  export {
46
57
  slotDigest, loadManifest, saveManifest, ownerPid, ownerPids,
47
- allocateFresh, claimSlot, activeSlot,
58
+ allocateFresh, claimSlot, activeSlot, ledgerHealth,
48
59
  } from "./session-slots-manifest.mjs"
49
60
 
50
61
  let currentSessionId = null
@@ -85,7 +96,7 @@ export function sessionPath(cwd) {
85
96
  }
86
97
 
87
98
  export function slotPath(cwd, n) { return sessionPath(cwd) + "." + n }
88
- export function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" }
99
+ export function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" } // 会话级槽索引——≠ 项目级 PROJECT-MANIFEST.json
89
100
 
90
101
  // ========== end marker(SESSION.md §6.10 端分离恢复——本端"最后使用槽位"记录)==========
91
102
 
@@ -93,16 +104,28 @@ export function manifestPath(cwd) { return sessionPath(cwd) + ".manifest" }
93
104
  * (NF1:本端记录是本端单写者文件——不新增跨端共享可变字段)。 */
94
105
  export const END = "cli"
95
106
 
96
- /** 本端记录路径:{manifest}.{END}(manifest 旁的独立小文件,非内嵌字段——NF1)。 */
97
- export function endMarkerPath(cwd) { return `${manifestPath(cwd)}.${END}` }
107
+ /** 进程端名(SESSION.md §6.20 判据句 1 / D-SE50):端名 = **端进程内的单值声明**——
108
+ * 模块级 `_end`,初值 = `END`;端壳(VS Code / 桌面)模块求值期一次 `setSessionEnd(<端名>)`,
109
+ * CLI 进程零声明即得「cli」。marker 家族三式与写 marker 的核入口**逐函数缺省取本值**
110
+ * (显式端参优先);端名单源 = 本对函数,核内零端名分支(端名是值不是分支)。 */
111
+ let _end = END
112
+ /** 声明本进程端名(端壳一次;核不校验端名——拼错 = 该端退化为「缺失」,§6.20 边界行 4)。 */
113
+ export function setSessionEnd(end) { _end = end }
114
+ /** 本进程端名(marker 家族缺省参的单源)。 */
115
+ export function sessionEnd() { return _end }
116
+
117
+ /** 本端记录路径:{manifest}.{end}(manifest 旁的独立小文件,非内嵌字段——NF1)。端参三式
118
+ * (本式 + `readEndMarker` / `writeEndMarker`)缺省 = 本进程端名(§6.20 判据句 2——显式参 >
119
+ * 进程端名);端壳经绑定转口传本端常量。 */
120
+ export function endMarkerPath(cwd, end = sessionEnd()) { return `${manifestPath(cwd)}.${end}` }
98
121
 
99
122
  /** 读本端记录:返回 { slot: <number|null>, updatedAt } 或 null。三态语义(D-1):
100
123
  * 文件缺失 = 从未记录(升级/首用迁移窗口——可能触发一次性继承);
101
124
  * JSON 解析失败/结构非法(损坏)= 按"缺失"降级——不 rename 不 unlink(幂等、不误伤,
102
125
  * T-M13);`slot: null` = 显式置空(删过本端记录槽——绝不触发继承,T-M4)。 */
103
- export function readEndMarker(cwd) {
126
+ export function readEndMarker(cwd, end = sessionEnd()) {
104
127
  try {
105
- const p = endMarkerPath(cwd)
128
+ const p = endMarkerPath(cwd, end)
106
129
  if (!existsSync(p)) return null
107
130
  const m = JSON.parse(readFileSync(p, "utf8"))
108
131
  if (m && m.slot === null) return { slot: null, updatedAt: m.updatedAt ?? null }
@@ -113,9 +136,9 @@ export function readEndMarker(cwd) {
113
136
 
114
137
  /** 写本端记录(原子 .tmp+rename,复用 writeSessionFile):slot = 目标槽号或 null(显式置空)。
115
138
  * 失败容忍(NF2):写失败按无记录路径降级——不抛错,不影响会话数据(T-M14)。 */
116
- export function writeEndMarker(cwd, slot) {
139
+ export function writeEndMarker(cwd, slot, end = sessionEnd()) {
117
140
  try {
118
- writeSessionFile(endMarkerPath(cwd), { slot, updatedAt: Date.now() })
141
+ writeSessionFile(endMarkerPath(cwd, end), { slot, updatedAt: Date.now() })
119
142
  } catch { /* NF2:失败容忍 */ }
120
143
  }
121
144
 
@@ -124,10 +147,13 @@ export function activePath(cwd) {
124
147
  return slotPath(cwd, activeSlot(cwd))
125
148
  }
126
149
 
127
- /** Atomic write: write to temp file then rename to replace, preventing truncated JSON from mid-write crash. */
128
- export function writeSessionFile(p, data) {
150
+ /** Atomic write: write to temp file then rename to replace, preventing truncated JSON from mid-write crash.
151
+ * `opts.tmpUnique`(§6.25 判据句 5 · D-SE65——**账本面专用**):临时名 = `${p}.${pid}-${seq}.tmp`
152
+ * (进程内自增)⇒ 清「并发写者共用 `${p}.tmp` ⇒ 互覆 / 混写」;槽文件 / 端标记面零改。返回所用临时名。 */
153
+ let tmpSeq = 0
154
+ export function writeSessionFile(p, data, { tmpUnique = false } = {}) {
129
155
  mkdirSync(dirname(p), { recursive: true })
130
- const tmp = `${p}.tmp`
156
+ const tmp = tmpUnique ? `${p}.${process.pid}-${++tmpSeq}.tmp` : `${p}.tmp`
131
157
  writeFileSync(tmp, JSON.stringify(data), "utf8")
132
158
  try {
133
159
  renameSync(tmp, p)
@@ -140,6 +166,7 @@ export function writeSessionFile(p, data) {
140
166
  writeFileSync(p, readFileSync(tmp, "utf8"), "utf8")
141
167
  }
142
168
  }
169
+ return tmp
143
170
  }
144
171
 
145
172
  // ========== 清单 / 认领 / 属主面(init-block 批外提——见头注)==========
@@ -152,60 +179,50 @@ export function writeSessionFile(p, data) {
152
179
  * **三态**:`true` 活 / `false` 死 / `undefined` 未知——未知不作死判据(D-MI10)。 */
153
180
  export { isProcessAlive }
154
181
 
155
- /** Lazy-load slot metadata from slot file (for old-format manifest entries that lack metadata) */
156
- function loadSlotMeta(cwd, slot, v) {
157
- if (typeof v === "object" && v !== null && "ts" in v) return v
158
- const ts = typeof v === "number" ? v : 0
159
- try {
160
- const p = slotPath(cwd, slot)
161
- if (!existsSync(p)) return { ts }
162
- const data = JSON.parse(readFileSync(p, "utf8"))
163
- const history = data.history ?? []
164
- const meta = extractSlotMeta(history, data.activeProvider, data.updatedAt ?? ts, data.title ?? "")
165
- if (data.activeModel) meta.activeModel = data.activeModel
166
- return { ts, ...meta }
167
- } catch {
168
- return { ts }
169
- }
170
- }
171
-
172
- /** List all slots, newest first. Includes isActive flag.
173
- * 2026-09-01 会诊 🟢:只读操作不认领——原实现 active 缺失时调 activeSlot(写 manifest
174
- * 副作用,ACP session/list 可触发)。m.active 缺失时全部 isActive=false,由下一次
175
- * activeSlot 正常认领。 */
182
+ /** List all slots **from disk**, newest first. Includes isActive flag.
183
+ * §6.22(SESSION-LIST-DISK 批 · 2026-09-28):条目集 = **盘面实读**(sessions 根下槽文件枚举 /
184
+ * 取数链装配住 `session-slot-scan.mjs`)——manifest `m.slots` 只作摘要缓存,不参与条目集
185
+ * (既不筛也不补);排序 = `updatedAt` 降序(同值按槽号降序);行形态逐字段零改。
186
+ * 计数不可得 = `null`(§6.25 判据句 4——非 0:真 0 与未知可分);**读面懒核实**(判据句 3 落点 B):
187
+ * 不可信档清单交核实面(纯内存登记 + 延迟拍——调用本体零写;核完回写 ⇒ 下次列表回快路)。
188
+ * 2026-09-01 会诊 🟢:只读操作不认领(清单链**零写**——不认领、不写 manifest、零探测)——
189
+ * 原实现 active 缺失时调 activeSlot(写 manifest 副作用,ACP session/list 可触发)。
190
+ * m.active 缺失时全部 isActive=false,由下一次 activeSlot 正常认领。 */
176
191
  export function listSlots(cwd) {
177
192
  const m = loadManifest(cwd)
178
193
  const active = m.active ?? null
179
- return Object.entries(m.slots)
180
- .filter(([n]) => /^\d+$/.test(n))
181
- .map(([n, v]) => {
182
- const meta = loadSlotMeta(cwd, Number(n), v)
183
- return {
184
- slot: Number(n),
185
- isActive: Number(n) === active,
186
- timestamp: meta.ts,
187
- date: new Date(meta.ts).toLocaleString(),
188
- messageCount: meta.messageCount ?? 0,
189
- turnCount: meta.turnCount ?? 0,
190
- firstMessage: meta.firstMessage ?? "",
191
- // MODEL-MERGE-SESSION 摘要 "p:m":activeProvider 保持裸渠道名——列表消费面显复合
192
- // (旧摘要无 activeModel → 回退裸渠道名——新老兼容;cmd-session/VSC sessions 行免改)
193
- activeProvider: meta.activeProvider ? (meta.activeModel ? `${meta.activeProvider}:${meta.activeModel}` : meta.activeProvider) : "",
194
- updatedAt: meta.updatedAt ?? meta.ts,
195
- updatedDate: new Date(meta.updatedAt ?? meta.ts).toLocaleString(),
196
- title: meta.title ?? "",
197
- }
198
- })
199
- .sort((a, b) => b.updatedAt - a.updatedAt)
194
+ const scanned = scanSlotMetas(cwd, m.slots ?? {})
195
+ scheduleVerify(cwd, scanned, m.slots ?? {})
196
+ return scanned
197
+ .map(({ slot, meta }) => ({
198
+ slot,
199
+ isActive: slot === active,
200
+ timestamp: meta.ts,
201
+ date: new Date(meta.ts).toLocaleString(),
202
+ messageCount: meta.messageCount ?? null,
203
+ turnCount: meta.turnCount ?? null,
204
+ firstMessage: meta.firstMessage ?? "",
205
+ // MODEL-MERGE-SESSION 摘要 "p:m":activeProvider 保持裸渠道名——列表消费面显复合
206
+ // (旧摘要无 activeModel → 回退裸渠道名——新老兼容;cmd-session/VSC sessions 行免改)
207
+ activeProvider: meta.activeProvider ? (meta.activeModel ? `${meta.activeProvider}:${meta.activeModel}` : meta.activeProvider) : "",
208
+ updatedAt: meta.updatedAt ?? meta.ts,
209
+ updatedDate: new Date(meta.updatedAt ?? meta.ts).toLocaleString(),
210
+ title: meta.title ?? "",
211
+ // 创建端(§6.20 判据句 4 读面——缺键/老槽 ⇒ "" = 未知;「未知」的渲染文案归消费面)
212
+ createdBy: meta.createdBy ?? "",
213
+ }))
214
+ .sort((a, b) => b.updatedAt - a.updatedAt || b.slot - a.slot)
200
215
  }
201
216
 
202
217
  /** Delete a slot: remove its file and manifest entry. Deleting the active slot
203
218
  * resets the manifest active pointer (the next claim re-creates one). */
204
- export function deleteSlot(cwd, slot) {
219
+ export function deleteSlot(cwd, slot, { end = sessionEnd() } = {}) {
205
220
  const n = Number(slot)
206
221
  if (!Number.isInteger(n) || n < 1) return false
207
222
  const m = loadManifest(cwd)
208
- if (!m.slots[n]) return false
223
+ // §6.22 判据句 4(SESSION-LIST-DISK 批):准入 = 盘面文件存在 ∨ manifest 有条目——盘上独有槽
224
+ // (文件在盘、条目缺失)可删;旧分支逐字保留(有条目无文件 ⇒ 仍清条目,行为同今日)。
225
+ if (!m.slots[n] && !existsSync(slotPath(cwd, n))) return false
209
226
  delete m.slots[n]
210
227
  delete m.slotSessions?.[n] // orphan session-id entries bloat the manifest forever
211
228
  try { unlinkSync(slotPath(cwd, n)) } catch { /* missing file is fine */ }
@@ -215,7 +232,7 @@ export function deleteSlot(cwd, slot) {
215
232
  // 保留 fresh.active,2026-09-01 会诊三家 🟡)
216
233
  saveManifest(cwd, m, { slots: [n], slotSessions: [n] }, { setActive: true })
217
234
  // 2026-09-05 §6.10 D-4/F4:删到本端记录槽 → 记录显式置空(文件保留 + slot:null——下次全新起步,不复活——T-M4/T-M8)
218
- if (readEndMarker(cwd)?.slot === n) writeEndMarker(cwd, null)
235
+ if (readEndMarker(cwd, end)?.slot === n) writeEndMarker(cwd, null, end)
219
236
  return true
220
237
  }
221
238
 
@@ -224,11 +241,12 @@ export function deleteSlot(cwd, slot) {
224
241
 
225
242
  // ========== resumeSlot(SESSION.md §6.10 D-2——端分离恢复决策)==========
226
243
 
227
- /** 本端记录槽可用判据(D-2):slot ∈ m.slots + 槽文件在盘 + 属主 空、死、本进程。
244
+ /** 本端记录槽可用判据(D-2):**槽文件在盘**(§6.22 判据句 4——条目缺失不作门)+ 属主
245
+ * 空、死、本进程。
228
246
  * F-MI7:占用判据查调用面束(零自有探测);属主 unknown(探测失败 / 缺行)⇒ **不可用**
229
247
  * (不认领——保守:回落到 allocateFresh 取全新号,绝不与"可能活着"的属主同槽)。 */
230
248
  function usableSlot(cwd, m, slot, bundle = null) {
231
- if (!m.slots[slot] || !existsSync(slotPath(cwd, slot))) return false
249
+ if (!existsSync(slotPath(cwd, slot))) return false
232
250
  const owner = m.slotSessions?.[slot]
233
251
  if (!owner || owner === getSessionId()) return true
234
252
  return ownerStateOf(owner, bundle) === "dead"
@@ -266,33 +284,43 @@ function loadLegacyFile(cwd) {
266
284
  * 恢复决策(SESSION.md §6.10 D-2)——TUI 启动的本端恢复入口(VS Code 面板同构镜像)。
267
285
  * **async**(F-MI7:入口一次异步束——探测不阻塞事件循环;调用面必须 await)。
268
286
  * 返回 { slot, data }(data 可为 null——全新起步或读槽失败)。判据:
269
- * ① 本端记录可用(slot ≠ null 且 ∈ m.slots 且槽文件在盘 且属主 空/死/本进程)→ claimSlot;
287
+ * ① 本端记录可用(slot ≠ null 且槽文件在盘 且属主 空/死/本进程——§6.22 判据句 4:manifest
288
+ * 条目缺失不作门)→ claimSlot;
270
289
  * ② 记录缺失(从未记录 = 升级/首用迁移窗口):②a active 属主 = 本进程(同进程重入——
271
290
  * ensureActive 早退语义镜像:认领后尚未写 slots 条目/文件的窗口)→ 直接沿用;
272
291
  * ②b 否则一次性继承 manifest.active(同判据;活属主绝不继承——全新槽起步,T-M3);
273
292
  * ③ 其余一切(slot:null 显式置空 / 槽被删 / 属主为活外人 / 继承失败)→ allocateFresh。
274
- * 每次落点都写本端记录;claim 后读槽失败(.corrupted/.unreadable——loadSlotFile 既有改名
275
- * 保全语义)→ 保持已 claim 槽 + data:null——不改 marker——下次保存原地重建(T-M15)。
293
+ * 每次落点都写本端记录;**读序前移**(§6.25 判据句 3 落点 A):槽数据读先于认领落盘——读槽失败
294
+ * (.corrupted/.unreadable——loadSlotFile 既有改名保全语义)→ 仍按上述判据落点 + data:null——不改
295
+ * marker——下次保存原地重建(T-M15);数据在手 ⇒ 摘要顺手补写随认领那次 saveManifest 落。
296
+ * `opts.end`(§6.20 判据句 2——显式端参 > 进程端名):本端记录的读写端名——端壳经绑定转口
297
+ * 传本端常量(消费核本体的端壳零副本);缺省 = 进程端名(CLI 全调用点零改)。
276
298
  */
277
- export async function resumeSlot(cwd) {
299
+ export async function resumeSlot(cwd, { end = sessionEnd() } = {}) {
278
300
  const m = loadManifest(cwd)
279
301
  m.slotSessions ??= {}
280
302
  // 入口一次**异步束**(F-MI7——整链 async):清理 / 可用 / 空闲全部查表,零逐 pid 探测。
281
303
  const bundle = await probeOwnersAsync(ownerPids(m))
282
304
  // 与 ensureActive 同型的死主清理(认领路径持久化——F5a 纪律:仅传 m 等于没删)
283
305
  const deadParam = cleanDeadOwners(m, bundle)
284
- const rec = readEndMarker(cwd) // null = 缺失/损坏;{slot:null|N} = 文件在(D-1)
306
+ const rec = readEndMarker(cwd, end) // null = 缺失/损坏;{slot:null|N} = 文件在(D-1)
285
307
  let slot = null
286
308
  if (rec?.slot != null && usableSlot(cwd, m, rec.slot, bundle)) slot = rec.slot // ① 本端记录可用
287
309
  else if (rec === null) {
288
310
  if (m.active && m.slotSessions?.[m.active] === getSessionId()) slot = m.active // ②a 同进程重入
289
311
  else if (m.active && usableSlot(cwd, m, m.active, bundle)) slot = m.active // ②b 一次性继承
290
312
  }
291
- if (slot !== null) claimSlot(cwd, slot, m, deadParam)
292
- else slot = allocateFresh(cwd, m, deadParam, bundle) // ③ 全新分配(slot:null 绝不继承——T-M4)
293
- // data 层:读已认领槽(loadSlotFile 自 session.mjs——环 import 见文件头);读失败/槽文件不在 → legacy 单文件兜底(仅 data)
294
- let data = loadSlotFile(cwd, slot)
313
+ // 落点 A(§6.25 判据句 3):**读序前移**——纯读先于认领落盘 ⇒ 数据在手,摘要补写随认领那次 saveManifest 落
314
+ // (零额外读 + 零额外写);认领决策树 / 保留集 / 端标记 / 改名语义逐条零变。
315
+ let data = slot !== null ? loadSlotFile(cwd, slot) : null
316
+ if (slot !== null) {
317
+ if (data) healDigest(m, slot, data, slotMtime(cwd, slot))
318
+ claimSlot(cwd, slot, m, deadParam)
319
+ } else {
320
+ slot = allocateFresh(cwd, m, deadParam, bundle) // ③ 全新分配(slot:null 绝不继承——T-M4)
321
+ }
322
+ // data 层:读已认领槽(loadSlotFile 自 session.mjs——环 import 见文件头);读失败/槽文件不在 → legacy 兑底(仅 data)
295
323
  if (!data) data = loadLegacyFile(cwd)
296
- writeEndMarker(cwd, slot)
324
+ writeEndMarker(cwd, slot, end)
297
325
  return { slot, data }
298
326
  }
package/session.mjs CHANGED
@@ -22,6 +22,8 @@ import {
22
22
  slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
23
23
  activeSlot, writeEndMarker,
24
24
  } from "./session-slots.mjs"
25
+ // 摘要打点单源(#503 地板——与 session-lifecycle 同径直引;静态环同族,函数体内运行时使用)。
26
+ import { digestTs } from "./session-slots-manifest.mjs"
25
27
  // F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
26
28
  import { guardForeignSlotFile } from "./session-guard.mjs"
27
29
  import { engTokenSlotFields } from "./token-ttl.mjs"
@@ -40,16 +42,18 @@ export {
40
42
  writeSessionFile, slotDigest, loadManifest, saveManifest, activeSlot, listSlots,
41
43
  deleteSlot, isProcessAlive, END, endMarkerPath,
42
44
  readEndMarker, writeEndMarker, claimSlot, allocateFresh,
45
+ // §6.25 判据句 4:账本健康出口随动(端壳警示面消费——CLI / VSC 接线另座)
46
+ ledgerHealth,
43
47
  } from "./session-slots.mjs"
44
48
  // §6.12 标题写契约使 session-slots.mjs 超 500 行硬限 → renameSlot 拆至 session-rename.mjs(§6.12 模块与实现约束)
45
49
  export { renameSlot } from "./session-rename.mjs"
46
50
  // 人读线惰性窗口面(`history-window.mjs`)——re-export 保调用方单一路径。
47
51
  export { historyWindow, HISTORY_PAGE_SIZE, isRealUserMsg } from "./history-window.mjs"
48
52
  // 会话生命周期面(init-block 批 · F-MI7 拆分——语义原样外提):本端恢复入口包装(含残留 GC
49
- // 钩子)/ 槽数据应用 / 新建 / 运行态清空 / 槽切换 / 占用查询。**resumeSlot/newSession 为 async**
50
- // (入口一次异步探测束——调用面必须 await)。
53
+ // 钩子)/ 槽数据应用 / 新建 / 运行态清空 / 槽切换 / 占用查询 / 打开态读数(桌面残余批 · §6.24——
54
+ // 只读纯投影)。**resumeSlot/newSession 为 async**(入口一次异步探测束——调用面必须 await)。
51
55
  export {
52
- resumeSlot, applySession, newSession, resetSessionState, switchToSlot, slotOccupancy,
56
+ resumeSlot, applySession, newSession, resetSessionState, switchToSlot, slotOccupancy, sessionReading,
53
57
  } from "./session-lifecycle.mjs"
54
58
  import { resumeSlot as lifecycleResumeSlot } from "./session-lifecycle.mjs"
55
59
 
@@ -79,8 +83,9 @@ export function sessionDescriptor(agent, data) {
79
83
  return { history: full, total: full.length, base: 0 }
80
84
  }
81
85
 
82
- /** 槽摘要(记录存储绑定路径——history 不物化):与 slotDigest(extractSlotMeta) 同形。 */
83
- function digestFromStore(fields, counters) {
86
+ /** 槽摘要(记录存储绑定路径——history 不物化):与 slotDigest(extractSlotMeta) 同形(含
87
+ * `ts` 打点单源 `digestTs`——#503:`mtimeMs` 为该档写后取(缺省 ⇒ 裸墙钟回溯)。 */
88
+ function digestFromStore(fields, counters, mtimeMs) {
84
89
  const meta = {
85
90
  messageCount: counters.total,
86
91
  turnCount: counters.userReal,
@@ -90,7 +95,9 @@ function digestFromStore(fields, counters) {
90
95
  title: fields.title ?? "",
91
96
  }
92
97
  if (fields.activeModel) meta.activeModel = fields.activeModel
93
- return { ts: Date.now(), ...meta }
98
+ // 创建端(SLOT-END-PARAM 批 §6.20 判据句 4 读面——有值才带;老槽无键 ⇒ 键缺席 = 未知)
99
+ if (fields.createdBy) meta.createdBy = fields.createdBy
100
+ return { ts: digestTs(mtimeMs), ...meta }
94
101
  }
95
102
 
96
103
  /** Save agent state to the active slot file (atomic write). `display` (the old
@@ -153,17 +160,30 @@ export function saveSession(agent) {
153
160
  // saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
154
161
  // mtime 缓存 _slotMtime:自写未变跳过全量解析)。
155
162
  const rotated = guardForeignSlotFile(agent, p, slot)
163
+ // 创建端(SLOT-END-PARAM 批 §6.20 判据句 4 写面):取值链在守卫内顺带解析(轮转/损坏/文件
164
+ // 不在盘 ⇒ 进程端名;解析通过 ⇒ 盘上键或 null)——**守卫之后**注入,本端首物化写一次,
165
+ // 此后透传盘上键;盘上无键(老槽)⇒ null ⇒ 键不落盘(禁回填)。
166
+ if (agent._slotCreatedBy != null) fields.createdBy = agent._slotCreatedBy
167
+ // 会话级档位携带(§6.21 判据句 5 · 防整对象抹除):值 = 当前生效档位(`applySession` 携带的槽
168
+ // 原值——`"off"` 记号原样写盘,不回填为具体档);缺之 ⇒ 下一次回合保存把槽写面结果整对象抹除。
169
+ // `undefined` = 本会话未携带(老槽无该键 / 未经 `applySession`)⇒ 键不落盘(禁回填)。
170
+ if (agent._slotEffort !== undefined) fields.effort = agent._slotEffort
156
171
  // 绑定态 → 流式投影(段原文拼接——VSC 兼容面逐字同形);未绑定 → 既有全量物化写
157
172
  if (agent._recordStore) saveProjectedSlot(agent, p, fields, contextHistory)
158
173
  else writeSessionFile(p, { ...fields, history: legacyHistory(agent), contextHistory })
159
- // 记录我们刚写的 mtime——下次保存跳过重复解析
160
- try { agent._slotMtime = statSync(p).mtimeMs } catch {}
174
+ // 记录我们刚写的 mtime——下次保存跳过解析(缓存 = `{p, mtimeMs}` 按路径命中:#386)+
175
+ // 摘要 `ts` 地板入参(#503:保存面**写后取** mtime——同族先例 session-slot-verify.mjs:99)。
176
+ let wroteMtimeMs
177
+ try {
178
+ wroteMtimeMs = statSync(p).mtimeMs
179
+ agent._slotMtime = { p, mtimeMs: wroteMtimeMs }
180
+ } catch { /* stat 失败 ⇒ 缓存不动 / 摘要裸墙钟回溯 */ }
161
181
  // Update slot metadata in manifest
162
182
  try {
163
183
  const m = loadManifest(agent.cwd)
164
184
  m.slots[slot] = agent._recordStore
165
- ? digestFromStore(fields, agent._recordStore.counters())
166
- : slotDigest({ ...fields, history: legacyHistory(agent) })
185
+ ? digestFromStore(fields, agent._recordStore.counters(), wroteMtimeMs)
186
+ : slotDigest({ ...fields, history: legacyHistory(agent) }, wroteMtimeMs)
167
187
  saveManifest(agent.cwd, m)
168
188
  } catch (e) {
169
189
  // Manifest update failure is non-fatal — data is safe, metadata will lazy-recover on next listSlots
@@ -0,0 +1,78 @@
1
+ /**
2
+ * shell-candidates.mjs — 平台 shell 候选探测**单源**(B10 S17 收编:桌面 `settings-env.mjs:27-62` ∥
3
+ * VSC `src/extension/settings.mjs:72-115` 两造 ≈逐字 ⇒ 候选表 ∕ 探测序 ∕ 超时 ∕ memo 上提本档;
4
+ * 两端薄壳只留 re-export —— 端侧零自持候选表)。
5
+ *
6
+ * 面:`shellCandidates()` ⇒ `Promise<{ name, value }[]>`——「System default」恒首 + 平台候选
7
+ * (`where` ∕ `sh -c 'command -v'` 探存);进程内 memo(成功结果缓存:本进程生命周期内 shell 路径
8
+ * 不热变化)+ 在飞去重(同一时刻重复请求共享同一批 —— 不叠发子进程)。
9
+ * 探测面 = **异步非阻塞**(`execFile` + `Promise.all` 并发;同步探测会占住宿主事件循环 ⇒ UI 假死 ——
10
+ * VSC F-W18 口径);本函数**绝不抛出**(探测失败 = 该候选缺席)。
11
+ * 测试缝(先例 = 核 `_setProbeImplForTest`):`_setShellDetectForTest` 替换真实 `execFile` 探测;
12
+ * 注入即清 memo ∕ 在飞态(免旧批结果串味);复位 = `null`。
13
+ * 纪律:零第三方依赖;本档只 probe 存在性 —— 不读配置、不出文案(候选名面 = 两端既有显示源)。
14
+ */
15
+ import { execFile } from "node:child_process"
16
+ import { existsSync } from "node:fs"
17
+
18
+ /** 探测超时(ms)—— 两端原值单源(VSC `settings.mjs` ∕ 桌面 `settings-env.mjs` 同值 3000)。 */
19
+ const PROBE_TIMEOUT_MS = 3000
20
+
21
+ /** Git Bash 安装路径表(Windows 候选面 —— 表序 = 探测优先序,首命中即取)。 */
22
+ const GIT_BASH_PATHS = Object.freeze([
23
+ "C:\\Program Files\\Git\\bin\\bash.exe",
24
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
25
+ `${process.env.LOCALAPPDATA ?? ""}\\Programs\\Git\\bin\\bash.exe`,
26
+ ])
27
+
28
+ /** memo(成功结果缓存)∕ 在飞(同批共享)—— 本档单点持有。 */
29
+ let _cache = null
30
+ let _inFlight = null
31
+
32
+ /** 测试缝:伪探测替掉真实 `execFile` 探测(可返 Promise——异步面同形);注入即清 memo ∕ 在飞态。 */
33
+ let _detectImpl = null
34
+ export function _setShellDetectForTest(fn) {
35
+ _detectImpl = typeof fn === "function" ? fn : null
36
+ _cache = null
37
+ _inFlight = null
38
+ }
39
+
40
+ /** 候选命令是否存在(异步探测:Windows `where` / POSIX `sh -c 'command -v'`;失败或超时 ⇒ false)。 */
41
+ function commandExists(cmd) {
42
+ return new Promise((resolve) => {
43
+ if (_detectImpl) { resolve(_detectImpl(cmd)); return }
44
+ const win = process.platform === "win32"
45
+ try {
46
+ execFile(win ? "where" : "sh", win ? [cmd] : ["-c", `command -v ${cmd}`], { timeout: PROBE_TIMEOUT_MS }, (err, stdout) => {
47
+ resolve(!err && String(stdout ?? "").trim().length > 0)
48
+ })
49
+ } catch { resolve(false) }
50
+ })
51
+ }
52
+
53
+ /** 平台候选表(「System default」恒首;`detect` 只作探测用 —— 出口两键 `{ name, value }` 不外发函数)。 */
54
+ function candidateTable() {
55
+ const candidates = [{ name: "System default", value: null, detect: () => true }]
56
+ if (process.platform === "win32") {
57
+ candidates.push({ name: "PowerShell (pwsh)", value: "pwsh", detect: () => commandExists("pwsh") })
58
+ candidates.push({ name: "Windows PowerShell (powershell)", value: "powershell", detect: () => commandExists("powershell") })
59
+ const gb = GIT_BASH_PATHS.find((p) => p && existsSync(p))
60
+ if (gb) candidates.push({ name: `Git Bash (${gb})`, value: gb, detect: () => true })
61
+ candidates.push({ name: "WSL bash (wsl)", value: "wsl", detect: () => commandExists("wsl") })
62
+ } else {
63
+ for (const sh of ["bash", "zsh", "fish"]) candidates.push({ name: sh, value: sh, detect: () => commandExists(sh) })
64
+ }
65
+ return candidates
66
+ }
67
+
68
+ /** 平台 shell 候选读数(memo + 在飞去重):候选集 = 探存命中项 `{ name, value }`(平台序)。 */
69
+ export function shellCandidates() {
70
+ if (_cache !== null) return Promise.resolve(_cache)
71
+ if (_inFlight) return _inFlight
72
+ const candidates = candidateTable()
73
+ const p = Promise.all(candidates.map(async (c) => ((await c.detect()) ? { name: c.name, value: c.value } : null)))
74
+ .then((hits) => { _cache = hits.filter(Boolean); return _cache })
75
+ .finally(() => { if (_inFlight === p) _inFlight = null })
76
+ _inFlight = p
77
+ return p
78
+ }
package/text-budget.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * text-budget.mjs — 文本额度纯函数(零依赖——TUI-OOM-ROOTCAUSE 批,AGENT-LOOP.md §23.3.1 / TUI.md §15.3.2)。
2
+ * text-budget.mjs — 文本额度纯函数(零依赖——TUI-OOM-ROOTCAUSE 批,AGENT-LOOP.md §6.15 / TUI-SESSION-VIEW.md §5.2)。
3
3
  *
4
4
  * 单一来源(D2):agent 侧捕获截断(spawn-child.mjs——子代理 `_capturedOutput`)与 TUI 面
5
5
  * 载体额度(tui/display-budget.mjs)共用本体的 `capText` / `appendCappedText`——两处各自
6
6
  * 复制截断逻辑的漂移面被消除。本模块零 import(可被任意层直接引用)。
7
7
  *
8
8
  * 计量口径 = **UTF-16 码元**(`String.length`——与 JS 字符串内存近似、确定、O(1) 计长;
9
- * TUI.md §15.2 表 1)。截断形态 = 头保 + 中段标记 + 尾保(标记含真实省略数 N)。
9
+ * TUI-SESSION-VIEW.md §5.1)。截断形态 = 头保 + 中段标记 + 尾保(标记含真实省略数 N)。
10
10
  *
11
11
  * 标记串约定:`marker` 模板中以字面 `N` 为省略数占位(逐字进测试断言——如
12
12
  * `… [captured output truncated: N chars omitted] …`)。
package/think-off.mjs ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * think-off.mjs — off 形族的单一实现源(叶档:零 import 纯函数)。
3
+ *
4
+ * 单一来源 = `docs/core/design/MODEL-SPECS.md`:写面取形 = §16.2 / §15.4-2 族别判据表;
5
+ * 「有效 off 路径」判据 = §16.4。四处生产者(CLI `/think` · CLI `/advisor` · VSC 主模型面
6
+ * `reasoning-mode.mjs` · VSC 面板写面 `settings-panel-write.mjs`)一律消费本档(端引核零副本——
7
+ * 副本即漂移源:VSC 全族一形 `null` 与 CLI 自定义族支 `undefined` 两处漂移已实证,§16.1-③)。
8
+ */
9
+
10
+ /** 写面取形(§15.4-2 族别判据的单一实现):`spec → null | { type:"disabled" }`。
11
+ * effort 族(`thinkApi === "effort"`)⇒ `null`——载荷层 off 门首款要求 `thinking === null`
12
+ * (单源 = `doc:PROVIDER.md:§6.12`;`{type:"disabled"}` 不开门 ⇒ 关思考静默失效);
13
+ * 其余(type 族默认 / 自定义开值族)⇒ `{ type: "disabled" }`——type 机制原生 off 形。 */
14
+ export function thinkOffShape(spec) {
15
+ return spec?.thinkApi === "effort" ? null : { type: "disabled" }
16
+ }
17
+
18
+ /** 有效 off 路径(「何时可宣称 OFF」判据本体 = §16.4)——由 `thinkOffShape` 派生(防两处漂移):
19
+ * `null` 形只有 effort 族的载荷门据它补发 `reasoning_effort:"none"` ⇒ 须枚举含 `"none"`
20
+ * (无枚举 / 无 `none` ⇒ 形不发任何字段,§16.4 表第 1 行);`{type:"disabled"}` 形 ⇒ 达载荷层。
21
+ * 两形共门 = `thinkAlwaysOn !== true`(服务端强制族:形被拒 / 恒思考 ⇒ 无效,§16.3 / §16.4 表第 2 行)。 */
22
+ export function thinkOffPath(spec) {
23
+ if (spec?.thinkAlwaysOn === true) return false
24
+ if (thinkOffShape(spec) === null) return (spec?.reasoningEffortEnum ?? []).includes("none")
25
+ return true
26
+ }
27
+
28
+ /** advisor 推理档写面三态(**写语义单源** —— 由 VSC `settings-panel-write.mjs:149-164` 上提;B10 S11:
29
+ * 桌面主侧 ∕ VSC 写面两端同引,零副本):`advisor` 对象原地作用于 `{ thinking, reasoningEffort }` 两键 ——
30
+ * ① `"none"`(关思考)⇒ `thinking = thinkOffShape(spec)`(族别 off 形)+ 删 `reasoningEffort`;
31
+ * ② 非空档字面 ⇒ `reasoningEffort = 值` + 清 off 形残记(`thinking` 为 `null` 字面 ∕ `{type:"disabled"}`
32
+ * 两 off 形皆清 —— 选档即要思考,B10 S11 收正:源式只清 `null` 字面,type 族 off 形残留会压过档位读回);
33
+ * ③ 其余(空 ∕ 非串 = 中性)⇒ 删 `reasoningEffort` + 清 off 形残记(读回中性 —— 同上收正)。
34
+ * 写后投影恒等(读回 = 所选:off 形 ⇒ `"none"`;字面档 ⇒ 该档;两清 ⇒ 中性)。*/
35
+ export function applyAdvisorEffort(advisor, value, spec) {
36
+ const target = advisor !== null && typeof advisor === "object" && !Array.isArray(advisor) ? advisor : {}
37
+ const offish = target.thinking === null || target.thinking?.type === "disabled"
38
+ if (value === "none") {
39
+ target.thinking = thinkOffShape(spec)
40
+ delete target.reasoningEffort
41
+ } else if (typeof value === "string" && value.trim()) {
42
+ target.reasoningEffort = value.trim()
43
+ if (offish) delete target.thinking
44
+ } else {
45
+ delete target.reasoningEffort
46
+ if (offish) delete target.thinking
47
+ }
48
+ return target
49
+ }
package/token-ttl.mjs CHANGED
@@ -30,7 +30,7 @@
30
30
  import { existsSync, readFileSync, statSync } from "node:fs"
31
31
  import {
32
32
  slotPath, writeSessionFile, activeSlot, loadManifest, saveManifest, slotDigest,
33
- writeEndMarker,
33
+ writeEndMarker, sessionEnd,
34
34
  } from "./session-slots.mjs"
35
35
  // F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
36
36
  import { guardForeignSlotFile } from "./session-guard.mjs"
@@ -258,8 +258,14 @@ export function persistEngTokens(agent) {
258
258
  engineering: agent.config?.agent?.engineering ?? false,
259
259
  goal: agent.goal ?? null,
260
260
  advisor: agent.config?.advisor ?? null,
261
+ // 会话级档位(#441③ 键齐——与 `newSlotData` 规范结构 / `saveSession` 携带面同源:
262
+ // 实况有值携带、无则 `null`;老槽禁回填不适用——本分支只产全新槽)。
263
+ effort: agent._slotEffort ?? null,
261
264
  pendingReminders: agent._pendingReminders ?? [],
262
265
  sessionStart: agent._sessionStart ?? null,
266
+ // 创建端(SLOT-END-PARAM 批 §6.20 判据句 4 写面——全新分支):取值链已由守卫(上方
267
+ // `guardForeignSlotFile`)解析入 `agent._slotCreatedBy`(文件不在盘 ⇒ 进程端名)。
268
+ createdBy: agent._slotCreatedBy ?? sessionEnd(),
263
269
  }
264
270
  }
265
271
  const fields = engTokenSlotFields(agent)
@@ -270,13 +276,18 @@ export function persistEngTokens(agent) {
270
276
  delete data.engDesignToken
271
277
  data.updatedAt = Date.now()
272
278
  writeSessionFile(p, data)
273
- // 记录我们刚写的 mtime——下次保存/守卫跳过重复解析(saveSession 同款)
274
- try { agent._slotMtime = statSync(p).mtimeMs } catch {}
279
+ // 记录我们刚写的 mtime——下次保存/守卫跳过重复解析(saveSession 同款;缓存按路径命中 #386)
280
+ // + 摘要 `ts` 地板入参(#503:保存面写后取 mtime——同族先例 session-slot-verify.mjs:99)。
281
+ let wroteMtimeMs
282
+ try {
283
+ wroteMtimeMs = statSync(p).mtimeMs
284
+ agent._slotMtime = { p, mtimeMs: wroteMtimeMs }
285
+ } catch { /* stat 失败 ⇒ 缓存不动 / 摘要裸墙钟回溯 */ }
275
286
  // Update slot metadata in manifest(saveSession 同款尾——非致命:数据已安全,
276
287
  // metadata 下次 listSlots 惰性恢复)
277
288
  try {
278
289
  const m = loadManifest(agent.cwd)
279
- m.slots[slot] = slotDigest(data)
290
+ m.slots[slot] = slotDigest(data, wroteMtimeMs)
280
291
  saveManifest(agent.cwd, m)
281
292
  } catch (e) {
282
293
  console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)