@thincoder/core 0.9.4 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -0,0 +1,304 @@
1
+ /**
2
+ * session-slot-verify.mjs — 读面懒核实面(SESSION.md §6.25 判据句 3 · LEDGER-RELIABILITY 批 · 2026-09-28)。
3
+ * 触发 = 读面交来的**不可信档清单**(谓词单源 `needsVerify`)。**调度 = 纯内存登记 + 启动窗外延迟拍**(`VERIFY_TICK_DELAY_MS` = 3s,`setTimeout` + `unref`——承 D-SE39:`setImmediate` 点火与启动链同循环)
4
+ * ⇒ 零 I/O / 零同步阻塞(首答 F-SL1 50 ms 不被拖慢)。核实 = 单档整档**分块流式**(计数只能全读获得——
5
+ * D-SE56):1 MiB 分块 + 逐块宏任务让出(零 ≥50 ms 连续同步段);**不整档物化**——走顶层键 + 逐元素结构
6
+ * 定界计数 / 切片解析(`jump` 每字符只扫一次;跨块续读、零正则)。预算 = 每轮 ≤ `SCAN_BUDGET_BYTES`(4 MiB
7
+ * ——单源住 `session-slot-scan.mjs`)∨ **超预算大档该轮至多一档**;未覆盖者留待下次列表调用;**不做全量扫**
8
+ * (D-SE67)。不重复三条:① 核完即回写摘要(`ts` 地板 = 该档 mtime——平台时间戳可领先 `Date.now()`,见回写行注释)⇒ 回快路;② **单飞**;③ **负缓存**(同 mtime 不重试)。
9
+ * 回写 = 尾部**一次合并写**(仅摘要条目);校验门不过(坏 JSON / 异 cwd / `version > 2`)⇒ **不写回**。
10
+ * 缝:`_verifyState` · `_verifyIdle()`(**禁时间等待**)· `_setVerifyDelayForTest` · `_resetVerifyStateForTest`。
11
+ * 在飞轮去向:目录切换 ⇒ **弃轮**(零跨根写);进程退出 ⇒ 拍 `unref` 不持活、在飞轮随终止丢弃;静态环:核内四引求值期零顶层调用 ⇒ 环安全。
12
+ */
13
+ import { closeSync, openSync, readSync, statSync } from "node:fs"
14
+ import { SCAN_BUDGET_BYTES } from "./session-slot-scan.mjs" // 预算常量单源
15
+ import { sessionPath, slotPath } from "./session-slots.mjs" // 槽前缀 / 路径单源
16
+ import { loadManifest, saveManifest } from "./session-slots-manifest.mjs" // 清单面单源
17
+ import { isRealUserMsg } from "./history-window.mjs" // 「真实用户消息」谓词单源(turnCount / firstMessage)
18
+ /** 启动窗外延迟拍(D-SE64 · 承 D-SE39):自调度点起 3s ⇒ 核实起点落于启动窗外。 */
19
+ export const VERIFY_TICK_DELAY_MS = 3000
20
+ /** 分块粒度(判据句 3):1 MiB + 逐块让出。 */
21
+ export const VERIFY_CHUNK_BYTES = 1024 * 1024
22
+ const HEAD_KEYS = new Set(["title", "activeProvider", "activeModel", "createdBy", "updatedAt", "version", "cwd"])
23
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v)
24
+ const isStr = (v) => typeof v === "string"
25
+ const isWs = (c) => c === " " || c === "\n" || c === "\r" || c === "\t"
26
+ const isDelim = (c) => c === undefined || isWs(c) || c === "," || c === "}" || c === "]"
27
+ const isBlank = (s) => { for (let i = 0; i < s.length; i++) if (!isWs(s[i])) return false; return true } // 尾随只许空白
28
+ const yieldToLoop = () => new Promise((resolve) => setImmediate(resolve)) // 逐块让出(唯一 setImmediate——非点火)
29
+ /** 观测计数(`_scanStats` 同惯例——生产零消费):`ticks` / `verified` 成功回写档数 / `failed` 门不过档数 /
30
+ * `skipped` 跳过档数(不在盘 / 负缓存 / 余量不足)/ `chunks` 分块读次数 / `bytes` 分块读字节。 */
31
+ export const _verifyState = { ticks: 0, verified: 0, failed: 0, skipped: 0, chunks: 0, bytes: 0 }
32
+ let verifyDelayMs = VERIFY_TICK_DELAY_MS
33
+ /** 延迟拍缝(置 0 点火勿真等;还原 = 置回 `VERIFY_TICK_DELAY_MS`)。 */
34
+ export function _setVerifyDelayForTest(ms) { verifyDelayMs = ms }
35
+ /** 不可信谓词(判据句 3 · **单源**):缺失 ∨ `ts < mtime`(陈旧)∨ 计数两字段任一非数(不可得);真 0
36
+ * 不触发(数值 0 = 可信);`mtime` 非数(stat 失败)⇒ 不妄断陈旧。 */
37
+ export function needsVerify(entry, mtime) {
38
+ if (!entry || typeof entry !== "object") return true
39
+ if (!isNum(entry.messageCount) || !isNum(entry.turnCount)) return true
40
+ if (!isNum(entry.ts)) return true
41
+ return isNum(mtime) ? entry.ts < mtime : false
42
+ }
43
+ const pending = new Map() // cwd → Set(slot):待核(拍在途——纯内存登记)
44
+ const timers = new Map() // cwd → Timeout:拍句柄(去重 / 复位)
45
+ const running = new Set() // cwd:在飞轮(单飞)
46
+ const failedCache = new Map() // cwd → Map(slot → mtime):负缓存
47
+
48
+ /** 读面交付点(`listSlots` 同步返回前调用——纯内存登记 + 延迟拍:零 I/O / 零同步阻塞):`scanned` = 扫描行
49
+ * `[{ slot, meta, mtime }]`;不可信档按 **mtime 降序**入队(用户最可能看的行先核);同档不重复入队;
50
+ * 已有拍 ⇒ 并入待核集(不重复点火)。 */
51
+ export function scheduleVerify(cwd, scanned, digests = {}) {
52
+ const targets = scanned.filter((s) => needsVerify(digests?.[s.slot], s.mtime)).sort((a, b) => b.mtime - a.mtime)
53
+ if (targets.length === 0) return
54
+ let prefix
55
+ try { prefix = sessionPath(cwd) } catch { return } // 前缀不可解析 ⇒ 不排
56
+ const list = pending.get(cwd) ?? new Set()
57
+ for (const s of targets) list.add(s.slot)
58
+ pending.set(cwd, list)
59
+ if (timers.has(cwd)) return
60
+ const t = setTimeout(() => { timers.delete(cwd); runPass(cwd, prefix).catch(() => { /* 静默:下轮重核 */ }) }, verifyDelayMs)
61
+ t.unref?.() // 不持活(进程退出随终止)
62
+ timers.set(cwd, t)
63
+ }
64
+
65
+ /** 一轮核实(拍点火):捕获前缀校验(目录切换 ⇒ 弃轮——零跨根写)→ 逐档(mtime 降序)分块流式核实 →
66
+ * 尾部一次合并写(仅摘要条目)。 */
67
+ async function runPass(cwd, prefix) {
68
+ const slots = pending.get(cwd)
69
+ pending.delete(cwd)
70
+ if (!slots || running.has(cwd)) return // 单飞:在飞即返(余档留待下次列表调用)
71
+ if (sessionPath(cwd) !== prefix) return
72
+ running.add(cwd)
73
+ _verifyState.ticks += 1
74
+ try {
75
+ const results = new Map() // slot → { meta, mtime }(meta 不含 ts——ts 写回时打点)
76
+ let budget = SCAN_BUDGET_BYTES
77
+ let oversizeTaken = false
78
+ for (const slot of slots) {
79
+ let st
80
+ try { st = statSync(slotPath(cwd, slot)) } catch { _verifyState.skipped += 1; continue } // 不在盘 ⇒ 跳
81
+ if (failedCache.get(cwd)?.get(slot) === st.mtimeMs) { _verifyState.skipped += 1; continue } // 负缓存
82
+ if (st.size > SCAN_BUDGET_BYTES) {
83
+ if (oversizeTaken) continue // 超预算大档该轮至多一档
84
+ oversizeTaken = true
85
+ } else if (st.size > budget) continue // 余量不足 ⇒ 留待下次列表调用
86
+ const meta = await scanOne(slotPath(cwd, slot), st.size, cwd)
87
+ budget = Math.max(0, budget - st.size)
88
+ if (meta) { results.set(slot, { meta, mtime: st.mtimeMs }); continue }
89
+ _verifyState.failed += 1 // 门不过 ⇒ 负缓存(不发明摘要)
90
+ const neg = failedCache.get(cwd) ?? new Map()
91
+ neg.set(slot, st.mtimeMs)
92
+ failedCache.set(cwd, neg)
93
+ }
94
+ // 回写:尾部一次合并写——仅摘要条目;写回前再校验捕获前缀(零跨根写)
95
+ if (results.size > 0 && sessionPath(cwd) === prefix) {
96
+ const m = loadManifest(cwd)
97
+ m.slots ??= {}
98
+ // `ts` 地板 = 该档 mtime(平台文件时间戳可领先 `Date.now()` 数 ms——延迟盖章):无地板则刚核完的档被判「陈旧」再读;判据比较(`ts ≥ mtime`)零改。
99
+ for (const [slot, r] of results) m.slots[slot] = { ts: Math.max(Date.now(), r.mtime), ...r.meta }
100
+ if (saveManifest(cwd, m) !== false) _verifyState.verified += results.size // 拒写(边界 ⑩)不计
101
+ }
102
+ } finally {
103
+ running.delete(cwd)
104
+ }
105
+ }
106
+
107
+ /** 可等待句柄(判据句 3 缝):「无 pending 拍 ∧ 无在飞轮」即决。 */
108
+ export async function _verifyIdle() {
109
+ while (pending.size > 0 || running.size > 0) await yieldToLoop()
110
+ }
111
+
112
+ /** 复位(用例跨例隔离):清 pending 拍 + 在飞登记 + 负缓存 + 观测计数。 */
113
+ export function _resetVerifyStateForTest() {
114
+ for (const t of timers.values()) clearTimeout(t)
115
+ timers.clear(); pending.clear(); running.clear(); failedCache.clear()
116
+ for (const k of Object.keys(_verifyState)) _verifyState[k] = 0
117
+ }
118
+
119
+ /** 单档分块流式核实:1 MiB 分块读 + 逐块让出;返回摘要字段(不含 `ts`)或 `null`(门不过)。 */
120
+ async function scanOne(path, size, cwd) {
121
+ const buf = Buffer.allocUnsafe(VERIFY_CHUNK_BYTES)
122
+ const dec = new TextDecoder("utf-8") // 流式解码:多字节字符跨块不劈
123
+ const scan = new StreamScan()
124
+ let fd
125
+ try { fd = openSync(path, "r") } catch { return null } // 打开失败(并发删除等)⇒ 门不过(负缓存)
126
+ try {
127
+ let pos = 0
128
+ while (pos < size) { // 读到文件末(`done` 后仍读——尾随非空白等于坏 JSON,见 `feed` / `result`)
129
+ const n = readSync(fd, buf, 0, Math.min(VERIFY_CHUNK_BYTES, size - pos), pos)
130
+ if (n <= 0) break // 截断(读不到声明长度)⇒ 门不过
131
+ pos += n
132
+ _verifyState.chunks += 1
133
+ _verifyState.bytes += n
134
+ scan.feed(dec.decode(buf.subarray(0, n), { stream: true }))
135
+ await yieldToLoop() // 逐块宏任务让出(零 ≥50 ms 连续同步段)
136
+ }
137
+ // EOF 尾段冲洗(#499):流式解码器在文件末滞留的**不完整 UTF-8 序列**(≤3 字节)不冲洗就
138
+ // 丢失——坏档尾段会逃过「尾随非空白 = 坏 JSON」门;无参 `decode()` 收尾 ⇒ 残缺字节出 U+FFFD
139
+ // ⇒ 按坏档门不过(完整多字节字符照常解码,零影响)。
140
+ scan.feed(dec.decode())
141
+ } catch {
142
+ return null // 读失败 ⇒ 门不过(负缓存)
143
+ } finally {
144
+ try { closeSync(fd) } catch { /* 关闭失败不影响读结果 */ }
145
+ }
146
+ return scan.result(cwd)
147
+ }
148
+
149
+ /** 跨块流式结构扫描器(判据句 3):顶层键取值 + `history` 逐元素定界计数;**消费段即时丢弃**(内存 =
150
+ * 未完成 token);零正则、零整档物化。**严格 JSON**(容器分隔态 `sep`;结构违规 ⇒ `corrupt()`)。
151
+ * st:key → value → after ∨ item(history 内);sep:start 首元素位 / need 刚吃 `,` / after 刚完一元素;
152
+ * k:跳跃态(1 串 / 2 结构 / 3 标量 / null 无)、p 跳跃位置(buf 内)、d 深度、inStr / esc 串与转义态
153
+ * (跨块保留——续扫不重扫);cursor = 已消费位置(在飞值时 = 值起点);open = 顶层 `{` 未取。 */
154
+ class StreamScan {
155
+ constructor() {
156
+ this.buf = ""; this.cursor = 0; this.st = "key"; this.open = true; this.sep = "start"; this.key = null
157
+ this.k = null; this.p = 0; this.d = 0; this.inStr = false; this.esc = false
158
+ this.count = 0; this.turns = 0; this.first = null; this.head = {}
159
+ this.historySeen = false; this.done = false; this.bad = false
160
+ }
161
+ /** 喂入一块文本(终结后只许空白——尾随非空白 = 坏 JSON,判据句 3 门面)。 */
162
+ feed(text) {
163
+ if (this.done) { if (!isBlank(text)) this.bad = true; return }
164
+ if (this.cursor > 0) {
165
+ this.buf = this.buf.slice(this.cursor)
166
+ if (this.k !== null) this.p -= this.cursor
167
+ this.cursor = 0
168
+ }
169
+ this.buf += text
170
+ while (this.done === false) {
171
+ const next = this.step(this.buf, this.cursor)
172
+ if (next === null) break // 请示更多输入(不完整 token——含坏档停滞)
173
+ this.cursor = next
174
+ }
175
+ }
176
+ /** 单步(返回新位置 / `null` = 请示更多输入);结构违规 ⇒ `corrupt()`(门不过)。 */
177
+ step(b, i) {
178
+ const n = b.length
179
+ while (i < n && isWs(b[i])) i++
180
+ if (this.st === "key") {
181
+ if (i >= n) return null
182
+ const c = b[i]
183
+ if (this.open) { // 顶层 `{`(首字符)
184
+ if (c !== "{") return this.corrupt()
185
+ this.open = false
186
+ return i + 1
187
+ }
188
+ if (c === "}") { if (this.sep === "need") return this.corrupt(); this.done = true; return i + 1 } // 尾逗号 ⇒ 违规
189
+ if (c !== '"') return this.corrupt() // 空键位 / 双逗号 / 非串键
190
+ const end = this.jump(b, i)
191
+ if (end === null) return null
192
+ try { this.key = JSON.parse(b.slice(i, end)) } catch { return this.corrupt() }
193
+ if (!isStr(this.key)) return this.corrupt()
194
+ let k = end
195
+ while (k < n && isWs(b[k])) k++
196
+ if (k >= n) return null // 冒号未到(键重扫——键短,零成本)
197
+ if (b[k] !== ":") return this.corrupt()
198
+ this.st = "value"
199
+ return k + 1
200
+ }
201
+ if (this.st === "value") {
202
+ if (i >= n) return null
203
+ const c = b[i]
204
+ if (this.key === "history") { // history 非数组 ⇒ 内容面不可用(门不过)
205
+ if (c !== "[") return this.corrupt()
206
+ this.historySeen = true
207
+ this.sep = "start"
208
+ this.st = "item"
209
+ return i + 1
210
+ }
211
+ const end = this.jump(b, i)
212
+ if (end === null) return null
213
+ if (end <= i) return this.corrupt() // 空值(无进展)⇒ 结构违规(防死循环)
214
+ if (HEAD_KEYS.has(this.key) && c !== "{" && c !== "[") {
215
+ try { this.head[this.key] = JSON.parse(b.slice(i, end)) } catch { return this.corrupt() }
216
+ }
217
+ this.st = "after"
218
+ return end
219
+ }
220
+ if (this.st === "after") { // 顶层:刚完一值 ⇒ 须是 `,` 或 `}`
221
+ if (i >= n) return null
222
+ if (b[i] === ",") { this.sep = "need"; this.st = "key"; return i + 1 }
223
+ if (b[i] === "}") { this.done = true; return i + 1 }
224
+ return this.corrupt()
225
+ }
226
+ // item:history 数组内(元素结构定界——逐元素切片解析)
227
+ if (i >= n) return null
228
+ const c = b[i]
229
+ if (c === "]") { if (this.sep === "need") return this.corrupt(); this.st = "after"; return i + 1 } // 尾逗号
230
+ if (c === ",") { if (this.sep !== "after") return this.corrupt(); this.sep = "need"; return i + 1 }
231
+ if (this.sep === "after") return this.corrupt() // 缺分隔符
232
+ const end = this.jump(b, i) // 值闭界(不完整 ⇒ 留值文本于 buf,续扫)
233
+ if (end === null) return null
234
+ if (end <= i) return this.corrupt() // 空值元素(无进展)⇒ 结构违规(防死循环)
235
+ this.sep = "after"
236
+ this.count += 1 // messageCount = history.length(逐元素计数——含非对象元素)
237
+ if (c === "{") {
238
+ let m
239
+ try { m = JSON.parse(b.slice(i, end)) } catch { return this.corrupt() }
240
+ if (isRealUserMsg(m)) {
241
+ this.turns += 1 // turnCount = 真实用户消息数(谓词单源 = history-window.mjs)
242
+ if (this.first === null) this.first = m.content.slice(0, 80)
243
+ }
244
+ }
245
+ return end
246
+ }
247
+ /** 跨块可续的值跳跃(「字符串 / 转义跨块续读」):自 `i` 跳到值末(回值后一位);输入不足 ⇒ `null`
248
+ * (**状态保留**——下次自 `this.p` 续扫,每字符只扫一次);标量**不吃分隔符**(切片 `[i, end)`)。 */
249
+ jump(b, i) {
250
+ if (this.k === null) {
251
+ const c = b[i]
252
+ if (c === undefined) return null
253
+ if (c === '"') { this.k = 1; this.p = i + 1; this.esc = false }
254
+ else if (c === "{" || c === "[") { this.k = 2; this.p = i + 1; this.d = 1; this.inStr = false; this.esc = false }
255
+ else { this.k = 3; this.p = i }
256
+ }
257
+ let j = this.p
258
+ if (this.k === 1) {
259
+ while (j < b.length) {
260
+ const c = b[j]
261
+ if (this.esc) { this.esc = false; j++; continue }
262
+ if (c === "\\") { this.esc = true; j++; continue }
263
+ if (c === '"') { this.k = null; return j + 1 }
264
+ j++
265
+ }
266
+ } else if (this.k === 2) {
267
+ while (j < b.length) {
268
+ const c = b[j]
269
+ if (this.inStr) {
270
+ if (this.esc) this.esc = false
271
+ else if (c === "\\") this.esc = true
272
+ else if (c === '"') this.inStr = false
273
+ } else if (c === '"') this.inStr = true
274
+ else if (c === "{" || c === "[") this.d += 1
275
+ else if ((c === "}" || c === "]") && --this.d === 0) { this.k = null; return j + 1 }
276
+ j++
277
+ }
278
+ } else {
279
+ while (j < b.length && !isDelim(b[j])) j++
280
+ if (j < b.length) { this.k = null; return j } // 标量末(不吃分隔符)
281
+ }
282
+ this.p = j // 输入不足 ⇒ 记位置续扫(触底 = 可能截断)
283
+ return null
284
+ }
285
+ corrupt() { this.bad = true; this.done = true; return null }
286
+ /** 校验门 + 摘要字段:结构完整(顶层配平 ∧ 见 `history` 数组)∧ `version ∈ {1,2}` ∧ cwd 一致(缺键 =
287
+ * 本档)⇒ 字段(不含 `ts`);否则 `null`(不发明摘要)。字段缺省与 `slotDigest` / `extractSlotMeta`
288
+ * 逐条同口径(单源判据 = 用例强制逐字段相等)。 */
289
+ result(cwd) {
290
+ if (this.bad || !this.done || !this.historySeen) return null
291
+ if (!isBlank(this.buf.slice(this.cursor))) return null // 顶层配平后非空白残留(未过 `feed` 的那段)⇒ 门不过
292
+ if (this.head.version !== 1 && this.head.version !== 2) return null
293
+ const fc = this.head.cwd
294
+ if (isStr(fc) && fc.toLowerCase() !== cwd.toLowerCase()) return null
295
+ const meta = {
296
+ messageCount: this.count, turnCount: this.turns, firstMessage: this.first ?? "",
297
+ activeProvider: this.head.activeProvider ?? "", updatedAt: this.head.updatedAt ?? Date.now(),
298
+ title: this.head.title ?? "",
299
+ }
300
+ if (this.head.activeModel) meta.activeModel = this.head.activeModel
301
+ if (this.head.createdBy) meta.createdBy = this.head.createdBy
302
+ return meta
303
+ }
304
+ }
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * session-slot-write.mjs — 会话槽「元数据开关」写面(Parnas 边界:开关写决策组——
3
- * autoApprove / planMode / engineering / advisor guard,外加全新槽默认记录与落盘单点)。
3
+ * autoApprove / planMode / engineering / advisor guard / 会话级偏好三键
4
+ * (provider / model / effort——§6.21),外加全新槽默认记录与落盘单点)。
4
5
  *
5
6
  * 归属:会话数据面(同上位 `session.mjs` / `session-slots.mjs` / `session-store.mjs`)。
6
7
  * 本档 = **(cwd, slot)形态**的槽写入面(读-改-写);另一形态 = `session.mjs` 的
@@ -23,10 +24,14 @@
23
24
 
24
25
  import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
25
26
 
26
- import { getSessionId, loadManifest, saveManifest, slotDigest, slotPath, writeSessionFile } from "./session-slots.mjs"
27
- import { loadSlotFile } from "./session.mjs"
27
+ import { getSessionId, loadManifest, saveManifest, slotDigest, slotPath, writeSessionFile, sessionEnd } from "./session-slots.mjs"
28
+ import { loadSlotFile, isLegacyTransient } from "./session.mjs"
28
29
  // token 形态单源(格式校验 + 到期时刻)——本档的保存面合并规则复用同一判定。
29
30
  import { tokenExpiryMs } from "./token-ttl.mjs"
31
+ // 会话级偏好写面(§6.21 判据句 3)的判据单源——档位值域(`specForModel(...).reasoningEffortEnum`)
32
+ // 与 off 可达性(`thinkOffPath`);两叶档(零 import)⇒ 不涉环,核内零第二份族别 / 枚举表。
33
+ import { specForModel } from "./model-specs.mjs"
34
+ import { thinkOffPath } from "./think-off.mjs"
30
35
 
31
36
  /** mtime 缓存(键 = 槽文件路径 → 上次所见 mtimeMs):同一槽连写时跳过重复解析。
32
37
  * 测试复位缝(与 `_setSessionsDirForTest` 同款):缓存为进程级单例,用例间需可复位
@@ -34,13 +39,17 @@ import { tokenExpiryMs } from "./token-ttl.mjs"
34
39
  const slotMtimeCache = new Map()
35
40
  export function _resetSlotMtimeCacheForTest() { slotMtimeCache.clear() }
36
41
 
37
- /** Fresh slot data default:全新空槽的规范结构(`newSession` 写盘面同形)。 */
42
+ /** Fresh slot data default:全新空槽的规范结构(`newSession` 写盘面**同源**——SLOT-END-PARAM
43
+ * 批起 `newSession` 与本构造器单源;`effort: null` = 会话级档位未设态——§6.21 判据句 5 规范
44
+ * 结构同源;老槽无该键由读侧按 `null` 容忍)。首物化即记创建端(§6.20 判据句 4 写面:本端
45
+ * 物化者 = 进程端名;「禁回填」不适用——本构造器只产全新槽)。 */
38
46
  export function newSlotData(cwd) {
39
47
  return {
40
48
  version: 2, cwd, title: "", updatedAt: Date.now(),
41
49
  history: [], contextHistory: [], tasks: [],
42
- planMode: false, goal: null, autoApprove: false, advisor: null,
50
+ planMode: false, goal: null, autoApprove: false, advisor: null, effort: null,
43
51
  pendingReminders: [], sessionStart: null,
52
+ createdBy: sessionEnd(),
44
53
  }
45
54
  }
46
55
 
@@ -62,8 +71,11 @@ function rotateIfForeign(p, slot, data) {
62
71
  disk.cwd.toLowerCase() !== data.cwd.toLowerCase()
63
72
  const diskStart = disk?.sessionStart ?? null
64
73
  const myStart = data?.sessionStart ?? null
74
+ // 盘面长度与快照**同口径**(#441①):快照 `data` 来自读槽回放(`loadSlotFile` 已按
75
+ // `isLegacyTransient` 过滤,`session.mjs` 读面)⇒ 盘侧同滤——否则坏档里的 legacy 注入
76
+ // 条目会凭空把盘面算长,无并发追加也误判轮转(.bak 白产 + 误报 concurrent append)。
65
77
  const diskLonger = Array.isArray(disk?.history) && Array.isArray(data?.history) &&
66
- disk.history.length > data.history.length
78
+ disk.history.filter((m) => !isLegacyTransient(m)).length > data.history.length
67
79
  if (!(diskIsNewer || diskForeign || (diskStart && diskStart !== myStart) || diskLonger)) return null
68
80
  const bak = `${p}.bak-${Date.now()}`
69
81
  renameSync(p, bak)
@@ -84,16 +96,24 @@ function rotateIfForeign(p, slot, data) {
84
96
  /**
85
97
  * 槽落盘单点(读-改-写面共用):轮转判定 → 写槽文件 → 回写 manifest 摘要。
86
98
  * 返回轮转出的 `.bak` 路径或 null(调用方按需透出)。
99
+ *
100
+ * 创建端(SLOT-END-PARAM 批 §6.20 判据句 4 写面):入参无 `createdBy` **且**槽文件不在盘
101
+ * (**轮转后**判据——`rotateIfForeign` 已让位现场)⇒ 本端即将首物化 ⇒ 落进程端名;
102
+ * 文件在盘(含老槽无键)⇒ 不动(禁回填)。
87
103
  */
88
104
  export function saveSlotData(cwd, slot, data) {
89
105
  data.updatedAt = Date.now()
90
106
  const p = slotPath(cwd, slot)
91
107
  const rotated = rotateIfForeign(p, slot, data)
108
+ if (data.createdBy === undefined && !existsSync(p)) data.createdBy = sessionEnd()
92
109
  writeSessionFile(p, data)
93
- slotMtimeCache.set(p, (() => { try { return statSync(p).mtimeMs } catch { return 0 } })())
110
+ // 写后取 mtime 单次采集(读失败 ⇒ `0` = 不可得):缓存(本档路径键)+ 摘要 `ts` 地板入参
111
+ // (#503——保存面写后取 mtime,同族先例 `session-slot-verify.mjs:99`)。
112
+ const wroteMtime = (() => { try { return statSync(p).mtimeMs } catch { return 0 } })()
113
+ slotMtimeCache.set(p, wroteMtime)
94
114
  try {
95
115
  const m = loadManifest(cwd)
96
- m.slots[slot] = slotDigest(data)
116
+ m.slots[slot] = slotDigest(data, wroteMtime)
97
117
  saveManifest(cwd, m)
98
118
  } catch { /* 摘要失败非致命——数据已安全,元数据下次 listSlots 惰性恢复 */ }
99
119
  return rotated
@@ -166,3 +186,49 @@ export function mergeEngTokensForSave(incoming, existing) {
166
186
  }
167
187
  return out
168
188
  }
189
+
190
+ /** 会话级偏好(§6.21)键闭集——表外键 = 整 patch 拒(写未发生)。 */
191
+ const SLOT_PREF_KEYS = new Set(["provider", "model", "effort"])
192
+
193
+ /**
194
+ * 会话级档位归一(§6.21 判据句 3——纯函数、不抛):`level` → 槽 `effort` 值。
195
+ * - `null` / `undefined` / `"auto"` ⇒ `null`(未设 ⇒ 回落配置面 / 渠道默认);
196
+ * - `"off"` ⇒ 该模型**有 off 路径**(判据单源 = `thinkOffPath`,勿在本档重写族别判定)
197
+ * ⇒ `"off"`、否则 `null`;
198
+ * - 其余 ⇒ 逐模型枚举(单源 = `specForModel(model).reasoningEffortEnum`)含之 ⇒ 原字面、
199
+ * 不含 / 无枚举 ⇒ `null`。
200
+ * `model` 非串(外端脏载 / 手工档)⇒ 按**未登记**归一(`DEFAULT_SPEC` 径)——「不抛」契约。
201
+ * 写面归一只此一处;读侧另容忍表外串(applySession 边界表——不设,不静默改写)。
202
+ */
203
+ export function resolveEffortPatch(level, model) {
204
+ if (level == null || level === "auto") return null
205
+ const spec = specForModel(typeof model === "string" ? model : "")
206
+ if (level === "off") return thinkOffPath(spec) ? "off" : null
207
+ return (spec?.reasoningEffortEnum ?? []).includes(level) ? level : null
208
+ }
209
+
210
+ /**
211
+ * 会话级偏好写出口(§6.21 判据句 2——沿 `setSlotAutoApprove` 同形,复用 `writeFlag` 单点):
212
+ * `patch` 键闭集 = `provider` / `model` / `effort`,**至少一键**(零键 / 表外键 ⇒ `false`,写未
213
+ * 发生);槽不可读 ⇒ `false`。`provider` / `model` 键入参映射既有槽字段 `activeProvider` /
214
+ * `activeModel`(不新增字段;`null` = 缺键语义——`provider` / `model` 无「未设」态);`effort` 经
215
+ * `resolveEffortPatch` 归一(`model` 取 `patch.model ?? 槽现值 activeModel`),归一 `null` = 未设
216
+ * (回落配置面 / 渠道默认)——**值置 `null` 不删键**(与 `newSlotData` 规范结构同源)。
217
+ * 只写盘:不碰当前内存态(施加面唯一 = `applySession`——§6.21 判据句 4 / 边界表第 5 行)。
218
+ *
219
+ * **值域面(#441② 明文——核侧有意不校)**:本出口只校**键闭集**,**不校值域**——`provider` /
220
+ * `model` 传 `null`(或表外字面)按**缺键语义**零改(不报错;`null` 仅对 `effort` 有「未设」
221
+ * 语义);值域名(`null` 仅 `effort` + `provider` / `model` 非空)由**端侧契约**把守
222
+ * (桌面 IPC「会话级偏好注」;端侧拒例 = `thincoder-desktop/src/main/agent-host.mjs`
223
+ * `prefsPatchFailure`)——读侧容忍为有意(跨端 / 手工改写档零行为变更),核内不立第二份值域校验。
224
+ */
225
+ export function setSlotPrefs(cwd, slot, patch) {
226
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) return false
227
+ const keys = Object.keys(patch)
228
+ if (keys.length === 0 || keys.some((k) => !SLOT_PREF_KEYS.has(k))) return false
229
+ return writeFlag(cwd, slot, (d) => {
230
+ if (patch.provider != null) d.activeProvider = patch.provider
231
+ if (patch.model != null) d.activeModel = patch.model
232
+ if ("effort" in patch) d.effort = resolveEffortPatch(patch.effort, patch.model ?? d.activeModel)
233
+ })
234
+ }