@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
package/session-gc.mjs CHANGED
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * F1 残留 GC(自动):进程启动时对当前 cwd hash 前缀做一次轻量清理——.corrupted /
5
5
  * .unreadable / .manifest.corrupted / .bak-* 保留 30 天,孤儿 .tmp 保留 7 天
6
- *(mtime < now − retention 才删,等于保留期保留——older-than 边界语义)。
6
+ *(mtime < now − retention 才删,等于保留期保留——older-than 边界语义);**孤儿 sidecar
7
+ * 记录目录 `.d`**(主文件 `{prefix}.{N}` 不存在)保留 30 天(数据现场族口径)⇒ **回收**
8
+ *(rename 进 `sessions-trash`——目录承载记录存储本体,不可直删;D-SE36 同语义)。
7
9
  * 安全(N1):活跃槽(主文件在 + manifest slotSessions[N] 属主活)的现场一律保留;
8
10
  * manifest 主文件 / end marker / 数据主文件永不进入候选(后缀预过滤只匹配残留后缀)。
9
11
  *
@@ -35,6 +37,9 @@ import { configDir } from "./config.mjs"
35
37
  import { sessionPath, ownerPid } from "./session-slots.mjs"
36
38
  import { probeOwnersAsync, ownerState } from "./process-probe.mjs"
37
39
  import { listStaleCwds, deleteStaleCwd, recycleGroup, sweepStale, trashRootFor } from "./session-stale.mjs"
40
+ // sidecar 记录目录后缀常量(单源 = `session-segments.mjs:13`——本档经该常量判后缀形态;
41
+ // 两处名形匹配式(`classifyResidue` / `sweepOrphanRecordDirs`)按 `{prefix}.{N}` + 后缀内联,不构成第二单源)。
42
+ import { RECORD_DIR_SUFFIX } from "./session-segments.mjs"
38
43
 
39
44
  /** 保留期(§6.12):损坏现场(.corrupted/.unreadable/.manifest.corrupted)与并发轮转
40
45
  * 备份(.bak-*)30 天;孤儿 .tmp 7 天(崩溃现场恢复窗口)。 */
@@ -46,8 +51,9 @@ export const COLD_CWD_RETENTION_MS = 90 * 24 * 3600 * 1000
46
51
  function sessionsDir() { return join(configDir, "sessions") }
47
52
 
48
53
  /** 残留分类(§6.12 后缀表):name 须以 `${prefix}.` 开头(prefix = `${hash}.json`)。
49
- * 返回 { retention, slot, tmp } 或 null(主文件/end marker/他前缀——不动)。
50
- * slot = 关联槽号(`.N.` 段),无则 null(如 .manifest.corrupted)。 */
54
+ * 返回 { retention, slot, tmp, dir } 或 null(主文件/end marker/他前缀——不动)。
55
+ * slot = 关联槽号(`.N.` 段),无则 null(如 .manifest.corrupted);`dir` = sidecar 记录目录
56
+ * 形(`{prefix}.{N}.d`——孤儿判据反向:**主文件在 ⇒ 跳过**)。 */
51
57
  function classifyResidue(name, prefix) {
52
58
  if (!name.startsWith(prefix + ".")) return null
53
59
  const slotMatch = name.slice(prefix.length).match(/^\.(\d+)\./)
@@ -55,6 +61,7 @@ function classifyResidue(name, prefix) {
55
61
  if (name.endsWith(".corrupted") || name.endsWith(".unreadable")) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
56
62
  if (/\.bak-\d+$/.test(name)) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
57
63
  if (name.endsWith(".tmp")) return { retention: ORPHAN_TMP_RETENTION_MS, slot, tmp: true }
64
+ if (name.endsWith(RECORD_DIR_SUFFIX) && /\.\d+\.d$/.test(name)) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false, dir: true }
58
65
  return null
59
66
  }
60
67
 
@@ -105,17 +112,60 @@ export async function gcResidue({ dir = sessionsDir(), prefix, now = Date.now(),
105
112
  if (c.slot !== null && active.has(c.slot)) continue // N1:活跃槽现场一律保留(T3)
106
113
  const p = join(dir, c.name)
107
114
  if (c.tmp && existsSync(p.slice(0, -".tmp".length))) continue // 非孤儿 .tmp(主文件在——写中/回退候选)
115
+ if (c.dir && existsSync(p.slice(0, -RECORD_DIR_SUFFIX.length))) continue // 非孤儿 sidecar(主文件在 ⇒ 记录存储本体——不动;仅孤儿目录入候选)
108
116
  let st
109
117
  try { st = await stat(p) } catch { continue }
110
118
  if (st.mtimeMs >= now - c.retention) continue // 边界:older-than 才删,等于保留期保留(§6.12)
111
119
  result.candidates.push(c.name)
112
- if (!dryRun) {
113
- try { await unlink(p); result.deleted.push(c.name) } catch { /* 占用/竞态——跳过 */ }
120
+ if (dryRun) continue
121
+ if (c.dir) {
122
+ // 目录承载记录存储本体 ⇒ **回收**(rename 进 `sessions-trash/<批次>/`——不 unlink / 不同步 fs;
123
+ // 失败 ⇒ 跳过并计数(candidates − deleted)+ 原目录零删除)
124
+ const r = await recycleGroup({ prefix, files: [c.name] }, { dir, now })
125
+ if (r.moved.length) result.deleted.push(c.name)
126
+ continue
114
127
  }
128
+ try { await unlink(p); result.deleted.push(c.name) } catch { /* 占用/竞态——跳过 */ }
115
129
  }
116
130
  return result
117
131
  }
118
132
 
133
+ /** ② 显式面清运腿(§6.12 / §6.17 补充——存量通路):全目录孤儿 sidecar 记录目录
134
+ *(`{prefix}.{N}.d`——主文件不存在 ∧ 该槽非活跃)⇒ 逐条**回收**(rename 进 `sessions-trash/<批次>/`,同 ① 语义)。
135
+ * 与 ① 两面之差:① = 自动面(单前缀 / 当前 cwd / **带 30 天保留期**——只治本前缀未来新增);
136
+ * 本函数 = **用户显式面**(跳前缀遍历全目录 / **不设保留期**——存量孤儿跨多前缀且多为近期孤儿,
137
+ * 自动面望不到;进程入口 = `--dry-run` 先列 / `--confirm --all` 才动,且回收可回退)。
138
+ * `dryRun` ⇒ 只列不删;目录不可读 ⇒ 空集。失败 ⇒ 跳过(不 unlink 兜底——原目录零删除)。 */
139
+ export async function sweepOrphanRecordDirs({ dir = sessionsDir(), now = Date.now(), dryRun = false, probeFn = probeOwnersAsync, entries = null } = {}) {
140
+ const out = { candidates: [], recycled: [] }
141
+ let names = entries
142
+ if (!names) {
143
+ try { names = await readdir(dir) } catch { return out }
144
+ }
145
+ const present = new Set(names)
146
+ const byPrefix = new Map() // prefix → [{ name, slot }]
147
+ for (const name of names) {
148
+ const m = /^(.+\.json)\.(\d+)\.d$/.exec(name)
149
+ if (!m) continue
150
+ if (present.has(`${m[1]}.${m[2]}`)) continue // 主文件在 ⇒ 非孤儿(记录存储本体——不动)
151
+ if (!byPrefix.has(m[1])) byPrefix.set(m[1], [])
152
+ byPrefix.get(m[1]).push({ name, slot: Number(m[2]) })
153
+ }
154
+ for (const [prefix, items] of byPrefix) {
155
+ const active = await liveSlots(dir, prefix, probeFn) // N1:活跃槽现场一律保留
156
+ for (const it of items) {
157
+ if (active.has(it.slot)) continue
158
+ let st
159
+ try { st = await stat(join(dir, it.name)) } catch { continue }
160
+ if (st) out.candidates.push(it.name)
161
+ if (dryRun) continue
162
+ const r = await recycleGroup({ prefix, files: [it.name] }, { dir, now })
163
+ if (r.moved.length) out.recycled.push(it.name)
164
+ }
165
+ }
166
+ return out
167
+ }
168
+
119
169
  /** 启动**窗外延迟拍** pass(§6.17 编排——点火见 `scheduleSessionGC`):① 一次**异步目录快照**
120
170
  * (readdir 恰一次——两面共享)② 残留面(gcResidue 既有判据)③ 存量面(总评估有界
121
171
  * ≤ STALE_SWEEP_LIMIT(含 ① 面 manifest 读——D-SE40)+ 超期回收批清运)。
@@ -265,6 +315,10 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
265
315
  const residue = p ? await gcResidue({ dir, prefix: p, now, dryRun: true, probeFn }) : { candidates: [] }
266
316
  out(`Residue candidates for current project (${p ?? "unknown"}): ${residue.candidates.length}`)
267
317
  for (const name of residue.candidates) out(` ${name}`)
318
+ // 孤儿 sidecar 记录目录(② 存量面——全目录;主文件不在 + 该槽非活跃——不设保留期)
319
+ const orphanDirs = await sweepOrphanRecordDirs({ dir, now, dryRun: true, probeFn })
320
+ out(`Orphan record-dir (.d) candidates (main file gone; explicit face — no retention): ${orphanDirs.candidates.length}`)
321
+ for (const name of orphanDirs.candidates) out(` ${name}`)
268
322
  out(`Cold/stale project candidates (cold = manifest idle > 90 days; stale = no live owner + cwd unreachable/empty + 7-day window): ${candidates.length}`)
269
323
  for (const c of candidates) out(` ${c.hash} reason ${c.reason} files ${c.files.length}`)
270
324
  if (candidates.length >= GC_PROGRESS_MIN) out(`Estimate: ${fmtGcEstimate(candidates.length)} to scan ${candidates.length} candidates (measured ≈${GC_MS_PER_CANDIDATE}ms/candidate).`)
@@ -279,7 +333,9 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
279
333
  try { loopEntries = await readdir(dir) } catch { loopEntries = null }
280
334
  }
281
335
  const targets = confirmTarget === "--all" ? candidates : candidates.filter((c) => c.hash === confirmTarget)
282
- if (!targets.length) {
336
+ // ② 面候选预列(孤儿 sidecar 记录目录——存量通路;只列不删——实回收在组循环后)
337
+ const orphanDirs = confirmTarget === "--all" ? await sweepOrphanRecordDirs({ dir, now, dryRun: true, probeFn, entries: loopEntries }) : { candidates: [] }
338
+ if (!targets.length && !orphanDirs.candidates.length) {
283
339
  err(`Refused: ${confirmTarget} is not a cold/stale project (active, recent, or unknown) — nothing deleted.`)
284
340
  return 1
285
341
  }
@@ -299,5 +355,10 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
299
355
  }
300
356
  out(`Moved ${r.deleted.length} files for ${t.hash} into the recycle bin (${r.batch})${r.skipped?.length ? ` — skipped ${r.skipped.length} (locked/racing, kept in place)` : ""}.`)
301
357
  }
358
+ // ② 显式面清运腿(孤儿 sidecar 记录目录——存量通路;与 ① 同语义:rename 进回收批)
359
+ if (confirmTarget === "--all" && orphanDirs.candidates.length) {
360
+ const moved = await sweepOrphanRecordDirs({ dir, now, probeFn, entries: loopEntries })
361
+ out(`Orphan record dirs (.d): moved ${moved.recycled.length}/${moved.candidates.length} into the recycle bin (${trashRootFor(dir)}).`)
362
+ }
302
363
  return 0
303
364
  }
package/session-guard.mjs CHANGED
@@ -4,20 +4,29 @@
4
4
  * 行随迁,verbatim 零语义变):saveSession 与 token-ttl persistEngTokens 共用同一份。
5
5
  * 写前校验磁盘槽文件是否本进程会话的现场——磁盘文件 sessionStart 与本进程会话不符
6
6
  * (另一进程/会话的现场)/版本更新/异 cwd → 先轮转 .bak 保留再写。检查按 mtime 缓存
7
- * (agent._slotMtime):文件自上次检查/自写未变就跳过全量解析——每次保存都
7
+ * (agent._slotMtime = `{p, mtimeMs}`——**按槽文件路径**命中,见 #386 复核):文件自上次检查/自写未变就跳过全量解析——每次保存都
8
8
  * readFileSync+JSON.parse 多 MB 会话 → O(n²) 退化。文件存在但不可读(损坏/半写)→
9
9
  * 改名 .corrupted 保留现场。返回轮转的 .bak 路径或 null。
10
+ *
11
+ * 创建端取值链(SLOT-END-PARAM 批 §6.20 判据句 4):本守卫是全量解析盘面的唯一写前窗口
12
+ * ⇒ 创建端**顺带解析**(副作用回写 `agent._slotCreatedBy`,供 saveSession 在守卫**之后**
13
+ * 注入 fields——分支:文件不在盘 / 轮转 / 损坏改名 ⇒ `sessionEnd()`(本端将首物化这份文件);
14
+ * 解析通过未轮转 ⇒ `disk.createdBy ?? null`(透传盘上键;老槽无键 ⇒ null ⇒ 禁回填);
15
+ * mtime 命中 ⇒ 本函数未解析盘面 ⇒ 值不动)。
10
16
  */
11
17
 
12
18
  import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
13
19
  import { basename } from "node:path"
14
- // §14.3.8①(修正轮 #1):.bak 轮转对**非本会话活动绑定面**联动改名 sidecar(`{p}.d → {bak}.d`)。
20
+ // SESSION.md §6.12①(修正轮 #1):.bak 轮转对**非本会话活动绑定面**联动改名 sidecar(`{p}.d → {bak}.d`)。
15
21
  import { RECORD_DIR_SUFFIX } from "./session-store.mjs"
22
+ // 本进程端名(创建端取值链缺省值)——静态环:本档 ↔ session-slots.mjs(经 session.mjs,同
23
+ // 两档头注:函数体内运行时使用、求值期零顶层调用 ⇒ 环安全)。
24
+ import { sessionEnd } from "./session-slots.mjs"
16
25
 
17
26
  /** 轮转时随迁记录存储(.bak 路径联动)——本会话活动绑定面跳过(防拔掉活动存储)。 */
18
27
  function moveSidecar(src, dst, agent) {
19
28
  const dir = src + RECORD_DIR_SUFFIX
20
- if (agent?._recordStore?.dir === dir) return // 本会话活动绑定面——原地保留(§14.3.8①)
29
+ if (agent?._recordStore?.dir === dir) return // 本会话活动绑定面——原地保留(SESSION.md §6.12①)
21
30
  try {
22
31
  if (existsSync(dir)) renameSync(dir, dst + RECORD_DIR_SUFFIX)
23
32
  } catch { /* 随迁失败不阻断轮转(现场原地保留——读侧身份核验兜底) */ }
@@ -25,9 +34,17 @@ function moveSidecar(src, dst, agent) {
25
34
 
26
35
  export function guardForeignSlotFile(agent, p, slot) {
27
36
  try {
28
- if (!existsSync(p)) return null
37
+ if (!existsSync(p)) {
38
+ agent._slotCreatedBy = sessionEnd() // 文件不在盘 ⇒ 本端即将首物化(= 创建端)
39
+ return null
40
+ }
29
41
  const st = statSync(p)
30
- if (st.mtimeMs === (agent._slotMtime ?? -1)) return null
42
+ // mtime 命中(**同路径** ∧ 同 mtime):未解析盘面 ⇒ 值不动。缓存带路径标签(#386 复核)——
43
+ // 单 agent 换槽复用(`_slot` 直钉新槽,未经 applySession / resetSessionState 清缓存)时,
44
+ // 旧槽 mtime 不得命中新槽(同 mtime 巧合 ⇒ 漏轮转 / 漏 createdBy 透传);「换槽即清缓存」
45
+ // 不再是被依赖的隐式前提。
46
+ const hit = agent._slotMtime
47
+ if (hit && hit.p === p && st.mtimeMs === hit.mtimeMs) return null
31
48
  const disk = JSON.parse(readFileSync(p, "utf8"))
32
49
  // version>2 的新版文件无论 sessionStart 一律轮转(loadSlotFile 对 v3 返回 null 不动
33
50
  // 文件——若其 sessionStart 为 null,旧版首次保存会静默覆盖;轮转 .bak 保证新版文件
@@ -42,10 +59,12 @@ export function guardForeignSlotFile(agent, p, slot) {
42
59
  renameSync(p, bak)
43
60
  moveSidecar(p, bak, agent)
44
61
  console.error(`[session] slot ${slot} holds ${diskIsNewer ? `a newer-version file (v${disk.version})` : diskForeign ? `a foreign-cwd file (${disk.cwd})` : `another session (start ${diskStart}, ours ${myStart})`} — preserved as ${basename(bak)}`)
45
- agent._slotMtime = st.mtimeMs
62
+ agent._slotMtime = { p, mtimeMs: st.mtimeMs }
63
+ agent._slotCreatedBy = sessionEnd() // 轮转:现场已让位 ⇒ 本端将首物化新文件
46
64
  return bak
47
65
  }
48
- agent._slotMtime = st.mtimeMs
66
+ agent._slotMtime = { p, mtimeMs: st.mtimeMs }
67
+ agent._slotCreatedBy = disk?.createdBy ?? null // 解析通过:透传盘上键(无键 ⇒ null ⇒ 禁回填)
49
68
  return null
50
69
  } catch {
51
70
  // 文件存在但不可读(损坏/半写):改名 .corrupted 保留现场(2026-09-01 advisor 🔵——
@@ -54,6 +73,7 @@ export function guardForeignSlotFile(agent, p, slot) {
54
73
  if (existsSync(p)) {
55
74
  try { renameSync(p, `${p}.corrupted`) } catch {}
56
75
  }
76
+ agent._slotCreatedBy = sessionEnd() // 损坏改名:现场已让位 ⇒ 本端将首物化新文件
57
77
  return null
58
78
  }
59
79
  }
@@ -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 }