@thincoder/core 0.9.5 → 0.10.2

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 (216) hide show
  1. package/CHANGELOG.md +49 -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 +29 -12
  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-ref.mjs +96 -3
  119. package/model-specs.mjs +16 -7
  120. package/notify-policy.mjs +51 -0
  121. package/package.json +1 -1
  122. package/peer-claims.mjs +2 -2
  123. package/peer-domains.mjs +7 -11
  124. package/peer-instances.mjs +9 -10
  125. package/process-probe-exec.mjs +187 -0
  126. package/process-probe.mjs +27 -175
  127. package/prompt-overlays.mjs +11 -10
  128. package/prompts/advisor-design.md +2 -2
  129. package/prompts/advisor-round2.md +1 -1
  130. package/prompts/advisor-round3.md +1 -1
  131. package/prompts/common.md +16 -8
  132. package/prompts/discipline-engineering.md +57 -16
  133. package/prompts/discipline-normal.md +6 -3
  134. package/prompts/persona-coder.md +0 -1
  135. package/prompts/persona-eng-coder.md +1 -5
  136. package/prompts/persona-eng-designer.md +2 -3
  137. package/prompts/persona-engineering.md +32 -11
  138. package/prompts/persona-explore.md +0 -1
  139. package/prompts/persona-normal.md +1 -1
  140. package/prompts/persona-plan.md +3 -4
  141. package/prompts/subagent-base.md +14 -0
  142. package/provider/core.mjs +11 -48
  143. package/provider/list-models.mjs +3 -0
  144. package/provider/responses-request.mjs +237 -0
  145. package/provider/responses.mjs +6 -228
  146. package/provider-flows.mjs +248 -0
  147. package/queued.mjs +90 -0
  148. package/rules.mjs +209 -2
  149. package/session-guard.mjs +27 -7
  150. package/session-lifecycle.mjs +108 -27
  151. package/session-rename.mjs +5 -1
  152. package/session-slot-claims.mjs +230 -0
  153. package/session-slot-scan.mjs +299 -0
  154. package/session-slot-verify.mjs +304 -0
  155. package/session-slot-write.mjs +74 -8
  156. package/session-slots-manifest.mjs +163 -241
  157. package/session-slots.mjs +99 -71
  158. package/session.mjs +30 -10
  159. package/shell-candidates.mjs +78 -0
  160. package/text-budget.mjs +2 -2
  161. package/think-off.mjs +49 -0
  162. package/token-ttl.mjs +15 -4
  163. package/tool-args.mjs +73 -0
  164. package/tool-docs/advisor.md +9 -0
  165. package/tool-docs/batch.md +1 -0
  166. package/tool-docs/code_search.md +1 -0
  167. package/tool-docs/consult_start.md +5 -0
  168. package/tool-docs/consult_stop.md +3 -0
  169. package/tool-docs/context.md +7 -0
  170. package/tool-docs/doc_search.md +1 -0
  171. package/tool-docs/edit.md +2 -13
  172. package/tool-docs/eng.md +1 -0
  173. package/tool-docs/execute.md +1 -9
  174. package/tool-docs/git.md +25 -51
  175. package/tool-docs/goal.md +6 -0
  176. package/tool-docs/ledger_add.md +1 -0
  177. package/tool-docs/ledger_close.md +1 -0
  178. package/tool-docs/ledger_count.md +1 -0
  179. package/tool-docs/ledger_query.md +1 -0
  180. package/tool-docs/ledger_update.md +1 -0
  181. package/tool-docs/main_history.md +1 -0
  182. package/tool-docs/memory.md +10 -0
  183. package/tool-docs/notify_parent.md +9 -0
  184. package/tool-docs/peer_instances.md +1 -0
  185. package/tool-docs/plan.md +5 -0
  186. package/tool-docs/process.md +7 -3
  187. package/tool-docs/read_history.md +7 -0
  188. package/tool-docs/recent_changes.md +1 -0
  189. package/tool-docs/repo_outline.md +1 -0
  190. package/tool-docs/settings.md +6 -0
  191. package/tool-docs/skill.md +5 -0
  192. package/tool-docs/subagent.md +31 -0
  193. package/tool-docs/task.md +7 -0
  194. package/tool-docs/timer.md +6 -0
  195. package/tool-docs/verify.md +1 -0
  196. package/tool-docs/wait_for.md +2 -1
  197. package/tools/bash.mjs +28 -4
  198. package/tools/edit-batch.mjs +4 -4
  199. package/tools/edit-diff.mjs +4 -23
  200. package/tools/exec-run.mjs +102 -4
  201. package/tools/execute.mjs +5 -2
  202. package/tools/file.mjs +4 -9
  203. package/tools/git-run.mjs +1 -1
  204. package/tools/git.mjs +0 -24
  205. package/tools/index.mjs +2 -1
  206. package/tools/linter.mjs +2 -1
  207. package/tools/node-child.mjs +21 -0
  208. package/tools/ops.mjs +50 -13
  209. package/tools/patch.mjs +4 -2
  210. package/tools/repomap.mjs +20 -9
  211. package/tools/shared.mjs +8 -6
  212. package/tools/web.mjs +5 -2
  213. package/tools/write-path.mjs +1 -1
  214. package/traces/trace-store.mjs +17 -14
  215. package/vision-reader.mjs +76 -0
  216. package/agent-tools/batch-segment.mjs +0 -18
@@ -0,0 +1,248 @@
1
+ /**
2
+ * provider-flows.mjs — provider 加 ∕ 删 ∕ 密钥三流程 + 渠道准入探针判据(核件 —— 「桌面处理流 · VSC 对齐」
3
+ * 批 R8 上提产物;2026-09-28)。
4
+ *
5
+ * 上提源 = VSC `thincoder-vscode/src/extension/provider-flows.mjs`(`addProviderFlow:60` ∕
6
+ * `removeProviderFlow:129` ∕ `setKeyFlow:153` ∕ `probeProviderAdmission:32`)——**纯搬 + 转口,零语义改**
7
+ * (三流程步序 ∕ 字段校验 ∕ 拒因 ∕ 探针语义逐字);VSC 自持副本已随本批(parity-b4)迁移改指本档(双写窗口关闭——本档 = 桌面 ∕ VSC 消费单源)。
8
+ *
9
+ * 转口(宿主面 ⇒ 注入缝;本档零宿主依赖、零 `vscode` 引用):
10
+ * - **UI 壳**(VSC = `vscode.window.showQuickPick ∕ showInputBox ∕ showErrorMessage ∕ showWarningMessage ∕
11
+ * showInformationMessage`)⇒ `ui = { pick, input, error, warn, info }`;
12
+ * - **网络探针**(VSC = `probeChannelModels(name, probeTargetFromEntry(entry))`——目标构造随迁 = `probeTargetOf`)
13
+ * ⇒ `deps.probe(name, target)`(缺省 = 核真探 `probeChannelModels`;测试 ∕ 端侧可注入。**缝契约** = 同
14
+ * `probeChannelModels` 形:`{ ok:true, models }` ∕ `{ ok:false, error }`,**并自落账** `recordAdmission`
15
+ * ——未落账 ⇒ 失败分档回落 `classifyProbeFailure` 现算);
16
+ * - **宿主忙证据覆盖**(VSC = `loop-sampler.mjs` `overrideAdmissionIfHostBusy`,F-W19)⇒
17
+ * `deps.hostBusyOverride`(缺省 null = 无宿主证据 —— 核零宿主事件循环观测)。
18
+ * 持久化面 = 核 `config-io.mjs`(`addProviderEntry` ∕ `removeProviderEntry` ∕ `setProviderKey` ∕
19
+ * `resolveProviders`)——本档零副本。
20
+ *
21
+ * M9 语义定死:**准入永不阻断写** —— 探针只出读数 ∕ 失败消息(消息本体逐字长句),绝不抛出 ∕
22
+ * 绝不回滚已完成的保存;失败不缓存(下次配置动作重探)。
23
+ *
24
+ * 消费面:桌面 `thincoder-desktop/src/main/providers.mjs`(四通道壳——判据面:`FORMATS` ∕
25
+ * `customFieldsError` ∕ `probeAdmission`;端侧 reason 词法不动);三流程本体 = 桌面 ∕ VSC(已随本批迁移改指)消费,CLI 待迁。
26
+ */
27
+ import { PROVIDER_PRESETS } from "./config-presets.mjs"
28
+ import { normalizeProxy } from "./config.mjs"
29
+ import { addProviderEntry, loadRaw, removeProviderEntry, resolveProviders, setProviderKey } from "./config-io.mjs"
30
+ import { admissionOf, channelUnavailableMessage, classifyProbeFailure, probeChannelModels } from "./provider/list-models.mjs"
31
+
32
+ /** 自定形协议域(闭集三协议 —— VSC 同档 `FORMATS` 逐字;端侧形判 ∕ 表单选项集同值域)。 */
33
+ export const FORMATS = Object.freeze(["openai", "anthropic", "google"])
34
+
35
+ /** UI 壳缝校验(缺 ∕ 形违 ⇒ 抛 —— fail-loud:宿主未接线不得静默扮成零动作)。 */
36
+ function requireUi(ui) {
37
+ if (!ui || typeof ui.pick !== "function" || typeof ui.input !== "function"
38
+ || typeof ui.error !== "function" || typeof ui.warn !== "function" || typeof ui.info !== "function") {
39
+ throw new TypeError("provider-flows: ui seam missing — inject { pick, input, error, warn, info }")
40
+ }
41
+ return ui
42
+ }
43
+
44
+ /**
45
+ * 自定形渠道名校验(VSC `addProviderFlow` 名称步 `validateInput` 逐字):空 ⇒ `Name is required`;
46
+ * 已配 ∕ 撞预设名 ⇒ `Name already in use`;合法 ⇒ `null`。
47
+ * `existing` = 已配渠道名集(VSC 同源 = `resolveProviders().providers` 名集;缺省空集)。
48
+ */
49
+ export function providerNameError(name, existing = new Set()) {
50
+ const n = String(name ?? "").trim()
51
+ if (!n) return "Name is required"
52
+ if (existing.has(n) || PROVIDER_PRESETS[n]) return "Name already in use"
53
+ return null
54
+ }
55
+
56
+ /**
57
+ * 自定形必填字段校验(VSC `addProviderFlow` 自定径输入步步序 = baseURL → model → format;
58
+ * name 步单列见 `providerNameError`)。三拒因串 = 核持久化面**同条件串**逐字(`config-io.mjs`
59
+ * `addProviderEntry`)——VSC 该三步为静默中止位(无消息可搬)⇒ 取同条件核串,保「同条件同词」。
60
+ * `format` 缺省 = `"openai"`(沿端侧形判同式)。
61
+ * @returns {string|null} 首个失败字段的拒因串;全过 ⇒ `null`
62
+ */
63
+ export function customFieldsError(fields) {
64
+ const baseURL = String(fields?.baseURL ?? "").trim()
65
+ if (!baseURL) return "Base URL is required"
66
+ const model = String(fields?.model ?? "").trim()
67
+ if (!model) return "Model is required"
68
+ const format = fields?.format ?? "openai"
69
+ if (!FORMATS.includes(format)) return `Unknown API format: ${format} (expected ${FORMATS.join("/")})`
70
+ return null
71
+ }
72
+
73
+ /**
74
+ * M9 探针目标构造(上提源 = VSC `provider-flows.mjs:38` 的实参面 `presets.mjs` `probeTargetFromEntry`——
75
+ * 逐条随迁):渠道条目 ⇒ `listModels` 可消费的探针目标。代理链与 chat 请求同规则(per-provider `proxy: true`
76
+ * ∧ 全局 `proxy.model === true` ⇒ 探针也走该代理);apiKey 归一(trim;缺 ⇒ 空串——探针会如实失败 ⇒
77
+ * 渠道标「不可用」——准入判据 M8)。
78
+ */
79
+ export function probeTargetOf(entry) {
80
+ const raw = loadRaw()
81
+ const proxyCfg = normalizeProxy(raw.proxy)
82
+ const proxyUri = entry?.proxy === true && proxyCfg?.uri && proxyCfg.model === true ? proxyCfg.uri : undefined
83
+ return {
84
+ name: entry?.name,
85
+ baseURL: entry?.baseURL,
86
+ apiKey: typeof entry?.apiKey === "string" ? entry.apiKey.trim() : "",
87
+ format: entry?.format,
88
+ proxyUri,
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 探针 + 失败分档(已解析条目的低层面 —— 端侧「读账优先 ∕ 未落账回落现算」单源:
94
+ * `failure` ∈ { timeout, malformed, hostBusy },分类本体住核 `classifyProbeFailure`;网络探针经 `deps.probe`
95
+ * 注入,缺省 = 核真探;**探针目标由本档 `probeTargetOf` 构造**(同 VSC 源实参)。**绝不抛出**(探针族语义)。
96
+ * @returns {{ok:true, models:string[]} | {ok:false, failure:string, error:string}}
97
+ */
98
+ export async function probeAdmission(name, provider, deps = {}) {
99
+ const probe = typeof deps.probe === "function" ? deps.probe : probeChannelModels
100
+ const r = await probe(name, probeTargetOf(provider))
101
+ if (r?.ok) return { ok: true, models: r.models ?? [] }
102
+ const error = r?.error
103
+ const failure = admissionOf(name)?.failure ?? classifyProbeFailure({ message: error })
104
+ return { ok: false, failure, error }
105
+ }
106
+
107
+ /** M9 渠道准入探(配置写入面):对目标渠道探一次 `GET /models`。
108
+ * 探通 → 渠道可用(探得候选可直接用);探不通 → 记录失败展示态(providerStatus 行 `不可用` + 失败消息本体)
109
+ * 并返回失败消息供调用方提示。**绝不抛出、绝不阻断任何写**;失败不缓存——下次配置动作重探。 */
110
+ export async function probeProviderAdmission(name, deps = {}) {
111
+ if (!name) return { ok: false, error: "provider name is required" }
112
+ try {
113
+ const { providers } = resolveProviders()
114
+ const entry = providers.find((p) => p.name === name)
115
+ if (!entry) return { ok: false, error: `Unknown provider "${name}"` }
116
+ const r = await probeAdmission(name, entry, deps)
117
+ // F-W19:核分类只知超时 ∕ 畸形;宿主忙 = 端侧证据 ⇒ 覆盖落账分类为 `hostBusy`
118
+ // (`reason` 逐字不动;返回面零扩张)——宿主证据经注入缝给(核零宿主事件循环观测)。
119
+ if (!r.ok && typeof deps.hostBusyOverride === "function") deps.hostBusyOverride(name, r.error)
120
+ return r.ok ? { ok: true, models: r.models } : { ok: false, error: r.error }
121
+ } catch (e) {
122
+ return { ok: false, error: channelUnavailableMessage(e) }
123
+ }
124
+ }
125
+
126
+ /** M9:探一次并在失败时界面明示失败消息(消息本体逐字长句)——不阻断已完成的保存。 */
127
+ async function reportAdmission(name, ui, deps) {
128
+ const probe = await probeProviderAdmission(name, deps)
129
+ if (!probe.ok) ui.warn(probe.error)
130
+ return probe
131
+ }
132
+
133
+ // ─── Three interactive flows (host UI shell injected) ───
134
+
135
+ /** Add a provider interactively: pick an unused preset or configure custom manually. */
136
+ export async function addProviderFlow(ui, refresh, deps = {}) {
137
+ const host = requireUi(ui)
138
+ let providers
139
+ try {
140
+ ({ providers } = resolveProviders())
141
+ } catch (e) {
142
+ host.error(e.message)
143
+ return
144
+ }
145
+ const existing = new Set(providers.map((p) => p.name))
146
+
147
+ const items = Object.entries(PROVIDER_PRESETS)
148
+ .filter(([name]) => !existing.has(name))
149
+ .map(([name, p]) => ({ label: name, description: p.desc, detail: p.model ?? "", kind: "preset", name }))
150
+ items.push({ label: "Custom (manual config)", description: "enter baseURL/model/format", kind: "custom" })
151
+
152
+ const sel = await host.pick(items, {
153
+ placeHolder: "Add provider — select a preset",
154
+ matchOnDescription: true, matchOnDetail: true,
155
+ })
156
+ if (!sel) return
157
+
158
+ if (sel.kind === "custom") {
159
+ const name = (await host.input({
160
+ prompt: "Provider name",
161
+ validateInput: (v) => providerNameError(v, existing),
162
+ }))?.trim()
163
+ if (!name) return
164
+ const baseURL = (await host.input({
165
+ prompt: `Base URL for ${name}`,
166
+ placeHolder: "https://api.example.com/v1",
167
+ }))?.trim()
168
+ if (!baseURL) return
169
+ const model = (await host.input({ prompt: `Model name for ${name}` }))?.trim()
170
+ if (!model) return
171
+ const format = await host.pick(
172
+ FORMATS.map((f) => ({ label: f, description: f === "openai" ? "(default)" : f === "anthropic" ? "Messages API" : "streamGenerateContent" })),
173
+ { placeHolder: "API format" },
174
+ )
175
+ if (!format) return
176
+ const err = addProviderEntry({ custom: { name, baseURL, model, format: format.label } })
177
+ if (err) { host.error(err); return }
178
+ const key = await host.input({ prompt: `API key for ${name} (leave empty to skip)`, password: true })
179
+ if (key?.trim()) {
180
+ const kerr = setProviderKey(name, key.trim())
181
+ if (kerr) { host.error(kerr); return } // F5b:config 并发被改——放弃 + 提示重试
182
+ }
183
+ await refresh?.()
184
+ await reportAdmission(name, host, deps) // M9:加渠道的配置写入面——探一次 /models(失败标不可用,不阻断保存)
185
+ return
186
+ }
187
+
188
+ // Preset: entry auto-filled from PROVIDER_PRESETS, then ask for a key
189
+ const err = addProviderEntry({ preset: sel.name })
190
+ if (err) { host.error(err); return }
191
+ const key = await host.input({ prompt: `API key for ${sel.name} (leave empty to skip)`, password: true })
192
+ if (key?.trim()) {
193
+ const kerr = setProviderKey(sel.name, key.trim())
194
+ if (kerr) { host.error(kerr); return } // F5b:冲突放弃提示重试
195
+ }
196
+ await refresh?.()
197
+ await reportAdmission(sel.name, host, deps) // M9:同上
198
+ }
199
+
200
+ /** Remove a provider interactively (active one is not listed, CLI parity). */
201
+ export async function removeProviderFlow(ui, refresh) {
202
+ const host = requireUi(ui)
203
+ let providers, activeProvider
204
+ try {
205
+ ({ providers, activeProvider } = resolveProviders())
206
+ } catch (e) {
207
+ host.error(e.message)
208
+ return
209
+ }
210
+ const candidates = providers.filter((p) => p.name !== activeProvider)
211
+ if (candidates.length === 0) {
212
+ host.info("No removable providers — the active provider is kept.")
213
+ return
214
+ }
215
+ const sel = await host.pick(
216
+ candidates.map((p) => ({ label: p.name, description: p.model ?? "" })),
217
+ { placeHolder: "Remove provider" },
218
+ )
219
+ if (!sel) return
220
+ const err = removeProviderEntry(sel.label)
221
+ if (err) host.error(err)
222
+ else await refresh?.()
223
+ }
224
+
225
+ /** Set / replace an API key for a configured provider. */
226
+ export async function setKeyFlow(ui, refresh, deps = {}) {
227
+ const host = requireUi(ui)
228
+ let providers
229
+ try {
230
+ ({ providers } = resolveProviders())
231
+ } catch (e) {
232
+ host.error(e.message)
233
+ return
234
+ }
235
+ if (providers.length === 0) return
236
+ const sel = await host.pick(
237
+ providers.map((p) => ({ label: p.name, description: p.apiKey ? "(has key)" : "(no key)" })),
238
+ { placeHolder: "Set API key for provider" },
239
+ )
240
+ if (!sel) return
241
+ const key = await host.input({ prompt: `API key for ${sel.label}`, password: true })
242
+ if (key?.trim()) {
243
+ const kerr = setProviderKey(sel.label, key.trim())
244
+ if (kerr) { host.error(kerr); return } // F5b:config 并发被改——放弃 + 提示重试
245
+ await refresh?.()
246
+ }
247
+ await reportAdmission(sel.label, host, deps) // M9:设 API key 的配置写入面——探一次 /models(失败标不可用,不阻断保存)
248
+ }
package/queued.mjs ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * queued.mjs — busy 排队消息的合并消费纯函数族(核件——「桌面处理流 · VSC 对齐」批 R3 上提产物)。
3
+ *
4
+ * 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.8(「合并消费」条 + 「步边界 pickup」块)。
5
+ * 上提源 = VSC `thincoder-vscode/src/extension/queued-merge.mjs:15-36`(容量常量 ∕ 合并串 ∕ 批计划)
6
+ * + `queued-pickup.mjs:49`(`takeQueuedBatchItem` 取项)——**纯搬 + 转口,零语义改**(常量名 / 值 /
7
+ * 形态文案 / 批语义逐字);对拍锚 = CLI 副本 `thincoder-cli/src/tui/queued-merge.mjs`(同名同值)。
8
+ *
9
+ * 消费面:计划面(常量 ∕ 合并串 ∕ 计划)由桌面排队面消费(`thincoder-desktop/src/main/queued-input.mjs`);
10
+ * `takeQueuedBatchItem` = **取项单源**(携图批退化逐条 ∕ slash 单条取——统一语义 = 计划首动作即消费),
11
+ * 消费点在册五处 = VSC 三径(`panel-turn-stages.mjs` 退出兜底 ∕ `suspension.mjs` 取项缝 ∕ 同档退出残余)
12
+ * + 桌面两径(`turn-chain.mjs` 尾径 ∕ `suspension-drive.mjs` 窗输入面)。
13
+ *
14
+ * 留端(载体面不迁——各端 adapter 各持):队容器(按会话键 `Map` ∥ 单容器)· 容量门与满队回执 ·
15
+ * 快照投影 · 步边界守卫与送达面(附件判决 ∕ 回合链出站)· 显示面。
16
+ */
17
+
18
+ // ── 合并常量(三端逐字一致):单批 ≤ MAX_MERGE_ITEMS 条且合并注入 ≤ MAX_MERGE_CHARS 字符;
19
+ // 超限截批先行(余下留待下批——不丢不截断单条);单条 > MAX_MERGE_CHARS 直发不进批;
20
+ // /cmd 不进合并(逐条保序即时)。──
21
+ export const MAX_MERGE_ITEMS = 8
22
+ export const MAX_MERGE_CHARS = 2000
23
+
24
+ /** 队容量(条)——**三端同名常量**(无会话载体与会话在飞载体合计口径;满队 = 第 9 条 ⇒
25
+ * 拒收 + 提示 + 不回显——容量门与满队回执留端)。 */
26
+ export const QUEUED_MAX_ITEMS = 8
27
+
28
+ /** 合并注入形态(编号逐条——模型一次处理全部;zh 字面,三端逐字)。 */
29
+ export function formatMergedMessages(items) {
30
+ return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
31
+ }
32
+
33
+ /**
34
+ * R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列(输入 = 文本数组;
35
+ * 调用侧以 `queue.map((q) => q.text)` 取数)。
36
+ * - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
37
+ * - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤ MAX_MERGE_ITEMS 条;
38
+ * 合并注入(格式化后)≤ MAX_MERGE_CHARS 字符;count ≥2 → merged 编号注入;count === 1 → 原文
39
+ * 直发(含单条 > MAX_MERGE_CHARS——不进批);超限截批先行——余下条目留待下批(不丢)。
40
+ * 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
41
+ */
42
+ export function planQueuedInput(items) {
43
+ const actions = []
44
+ const n = items.length
45
+ let i = 0
46
+ while (i < n) {
47
+ const cur = String(items[i])
48
+ if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
49
+ let j = i
50
+ for (;;) {
51
+ if (j - i >= MAX_MERGE_ITEMS || j >= n) break
52
+ const it = String(items[j])
53
+ if (it.startsWith("/")) break
54
+ if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
55
+ const cand = items.slice(i, j + 1)
56
+ if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
57
+ j++
58
+ }
59
+ // 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
60
+ if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
61
+ const taken = items.slice(i, j)
62
+ if (taken.length === 1) {
63
+ actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
64
+ } else {
65
+ actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
66
+ }
67
+ i = j
68
+ }
69
+ return actions
70
+ }
71
+
72
+ /** 贴图判定(取项退化与步边界让位共用——图片随条目元数据走降级面,不静默丢)。 */
73
+ const hasImages = (q) => Array.isArray(q?.images) && q.images.length > 0
74
+
75
+ /**
76
+ * 载体条目面取批(纯函数——就地消费;**统一语义 = 计划首动作即消费**):`turn` 首动作 ⇒ 按计划取一批
77
+ * (多条合并 ∕ 超长直发);`slash` 首动作 ⇒ 单条取(count 1 ∕ 原文——保序 ∕ 不合并)。
78
+ * **贴图批退化逐条**(批内任一条目携 `images` ⇒ count = 1——图片随条目元数据走降级面);
79
+ * 无贴图 ⇒ 多条合并为一条(头条目元数据 + 合并文本)。
80
+ * @param {Array} queue 载体条目数组(条目 `{ text, images?, ... }`——就地 `splice` 消费)
81
+ * @returns {{item: object|null, merged: string|null}} 取出的载体条目 + 本批文本(快照 `merged` 源)
82
+ */
83
+ export function takeQueuedBatchItem(queue) {
84
+ if (!Array.isArray(queue) || queue.length === 0) return { item: null, merged: null }
85
+ const action = planQueuedInput(queue.map((q) => String(q?.text ?? "")))[0]
86
+ const count = queue.slice(0, action.count).some(hasImages) ? 1 : action.count
87
+ const taken = queue.splice(0, count)
88
+ const text = count > 1 ? action.text : String(taken[0]?.text ?? "")
89
+ return { item: { ...taken[0], text }, merged: text }
90
+ }
package/rules.mjs CHANGED
@@ -1,7 +1,12 @@
1
1
  /**
2
- * rules.mjs — stream rule discovery from .thincoder/rules/*.md
2
+ * rules.mjs — 规则面**单源**(两面):
3
+ * ① stream 规则(`.thincoder/rules/*.md`——两端;`discoverRules` ∕ `mergeFileRules`)
4
+ * ② 作用域规则(`.cursor/rules/*`——三端同面;`loadRules` 读取 ∕ 三分类 ∕ 尾块 ∕ JIT 注入全档单源)
3
5
  *
4
- * Rule file format (markdown + YAML frontmatter):
6
+ * 面定义(权威)= `docs/core/design/WORKSPACE.md` §2.3「规则发现面(一线程一语义)」:
7
+ * `.thincoder/rules/*` 恒 = stream 规则;`.cursor/rules/*` 恒 = 作用域规则。
8
+ *
9
+ * Stream rule file format (markdown + YAML frontmatter):
5
10
  * ---
6
11
  * pattern: "console\\.log"
7
12
  * action: abort # "abort"|"warn" (default: "warn")
@@ -10,10 +15,19 @@
10
15
  * Use the project logger instead of console.log.
11
16
  *
12
17
  * The body becomes the rule message; if empty, falls back to `message` frontmatter.
18
+ *
19
+ * 上提史(R10 → B7 3a):`.cursor/rules` **读取面**(`loadRules` + 私有 frontmatter 解析)自 VSC
20
+ * `thincoder-vscode/src/extension/rules.mjs` 纯搬(R10);**三分类 ∕ 尾块文本 ∕ JIT 注入判据**
21
+ * 自 VSC `src/agent/rules-face.mjs` 纯搬 · **glob 匹配**(`matchesGlob`)自 VSC
22
+ * `src/extension/rules.mjs` 纯搬 · **合并**(`mergeFileRules`)自核 `agent/assemble.mjs` 内联块
23
+ * 抽取(B7 3a——均零语义改;VSC 两档退役,desk ∕ CLI 乘核径取得)。尾块 ∕ JIT 两缝同门
24
+ * `depth === 0`(B7 §2 修正轮 1 ④——子代理不携 `.cursor/rules`)。
13
25
  */
14
26
  import { existsSync, readFileSync, readdirSync } from "node:fs"
15
27
  import { join } from "node:path"
16
28
  import { parseFrontmatter } from "./markdown.mjs"
29
+ // #327 触达路径单源谓词 + 写集(JIT 面 `PATH_TOOLS` 构造——B7 3a:与门禁 ∕ 记账同址单源)
30
+ import { FILE_MUTATORS, toolTouchPaths } from "./agent/helpers.mjs"
17
31
 
18
32
  /**
19
33
  * Scan `.thincoder/rules/` in the project root for `.md` rule files.
@@ -51,3 +65,196 @@ export function discoverRules(projectRoot) {
51
65
  }
52
66
  return rules
53
67
  }
68
+
69
+ // ─── A 面:stream 规则装配合并(`.thincoder/rules`——B7 3a 自核装配内联块抽取单源)────────
70
+
71
+ /** 装配合并:`.thincoder/rules/` 文件规则**置前**,config 规则按 `pattern` 去重(文件侧优先;
72
+ * 逐字镜像原 CLI `make-agent.mjs:44-48` 语义)。文件规则为空 ⇒ 归一数组原样返回(零拷贝零改)。
73
+ * `discover` 可注入(核装配 `DEFAULT_DEPS.discoverRules` 缝——U79 装配序可机验;VSC 端壳同取本件)。 */
74
+ export function mergeFileRules(configRules, cwd, discover = discoverRules) {
75
+ const rules = Array.isArray(configRules) ? configRules : []
76
+ const fileRules = discover(cwd)
77
+ if (fileRules.length === 0) return rules
78
+ const filePatterns = new Set(fileRules.map((r) => r.pattern))
79
+ return [...fileRules, ...rules.filter((r) => !filePatterns.has(r.pattern))]
80
+ }
81
+
82
+ // ─── B 面:作用域规则读取面(`.cursor/rules`——R10 上提;纯搬自 VSC `src/extension/rules.mjs:28-81`)──
83
+
84
+ /**
85
+ * Load all scoped rules from `.cursor/rules/`.
86
+ * Returns [{ name, content, globs, alwaysApply, description, source }, ...]
87
+ */
88
+ export function loadRules(cwd) {
89
+ const dir = join(cwd, ".cursor/rules")
90
+ if (!existsSync(dir)) return []
91
+ const results = []
92
+ let entries
93
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return results }
94
+ for (const e of entries) {
95
+ if (!e.isFile() || e.name.startsWith(".") || (!e.name.endsWith(".md") && !e.name.endsWith(".mdc"))) continue
96
+ try {
97
+ const raw = readFileSync(join(dir, e.name), "utf8").trim()
98
+ if (raw.length === 0) continue
99
+ const { content, meta } = parseScopedFrontmatter(raw)
100
+ results.push({
101
+ name: e.name.replace(/\.(md|mdc)$/, ""),
102
+ content,
103
+ globs: meta.globs ? String(meta.globs).split(/[;\n]/).map(s => s.trim()).filter(Boolean) : null,
104
+ alwaysApply: meta.alwaysApply === "true",
105
+ description: meta.description ? String(meta.description) : null,
106
+ source: ".cursor/rules",
107
+ })
108
+ } catch { /* skip unreadable */ }
109
+ }
110
+ return results
111
+ }
112
+
113
+ /**
114
+ * Split frontmatter block from content.
115
+ * Frontmatter starts and ends with `---` on its own line.
116
+ * (私有件——随搬;名改 `parseScopedFrontmatter` = 与 `./markdown.mjs` 的 `parseFrontmatter`
117
+ * 导入同名冲突的机械改名——**体逐字纯搬,零语义改**。)
118
+ */
119
+ function parseScopedFrontmatter(raw) {
120
+ const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/)
121
+ if (!m) return { content: raw, meta: {} }
122
+
123
+ const front = m[1]
124
+ const content = raw.slice(m[0].length).trim()
125
+ const meta = {}
126
+ for (const line of front.split("\n")) {
127
+ const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+?)\s*$/)
128
+ if (kv) {
129
+ const key = kv[1]
130
+ let val = kv[2]
131
+ // Strip surrounding quotes
132
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
133
+ val = val.slice(1, -1)
134
+ }
135
+ meta[key] = val
136
+ }
137
+ }
138
+ return { content, meta }
139
+ }
140
+
141
+ // ─── B 面判据:三分类 ∕ 尾块 ∕ glob ∕ JIT(B7 3a 自 VSC 端壳上提单源)─────────────────────────
142
+
143
+ /** 三分类(**按序判定·互斥**——权威定义 `docs/core/design/WORKSPACE.md` §2.3):
144
+ * ① `alwaysApply === true` ⇒ **常驻集**(**先判**——`globs` 同在不改分类)
145
+ * ② 有 `globs` ⇒ **作用域集**(命中路径的工具派发前置提醒)
146
+ * ③ 无 `globs` 且无 `description` ⇒ **常驻集**
147
+ * ④ 仅 `description`(Cursor 的 agent-requested 语义)⇒ 两集皆无(本端无该机制——登记)。 */
148
+ export function classifyRules(rules) {
149
+ const always = []
150
+ const scoped = []
151
+ for (const r of rules ?? []) {
152
+ if (r.alwaysApply === true) always.push(r)
153
+ else if (r.globs?.length) scoped.push(r)
154
+ else if (!r.description) always.push(r)
155
+ }
156
+ return { always, scoped }
157
+ }
158
+
159
+ /** 目录读点(每 run 一次——装配期):`.cursor/rules` 读取 + 三分类(本函数 = 分类入口单源)。 */
160
+ export function loadScopedRules(cwd) {
161
+ return classifyRules(loadRules(cwd))
162
+ }
163
+
164
+ /** 常驻集注入块([4] 层尾块——与项目指令块同槽:每 run 重建、不进 history)+ **本 run 唯一
165
+ * 一次目录读取**:结果缓存于 `agent._rules`(`{ always, scoped }`)——作用域集 JIT 判定
166
+ * 只读该缓存(零重读盘)。无命中 ⇒ 空串(零改字符串)。 */
167
+ export function scopedRulesBlock(agent, cwd) {
168
+ const rules = loadScopedRules(cwd)
169
+ if (agent) agent._rules = rules
170
+ if (rules.always.length === 0) return ""
171
+ return `\n\nProject rules (.cursor/rules):\n${rules.always.map((r) => `- ${r.name}: ${r.content}`).join("\n")}`
172
+ }
173
+
174
+ /** 文件路径 glob 匹配(简单 `*` / `**` / `?` 通配——无完整 glob 库;纯搬自 VSC
175
+ * `src/extension/rules.mjs:37-74`,零语义改)。 */
176
+ export function matchesGlob(filePath, patterns) {
177
+ if (!patterns || patterns.length === 0) return false
178
+ // Normalize to forward slashes
179
+ const normalized = filePath.replace(/\\/g, "/")
180
+ for (const pat of patterns) {
181
+ if (simpleGlobMatch(normalized, pat.replace(/\\/g, "/"))) return true
182
+ }
183
+ return false
184
+ }
185
+
186
+ /** Simple glob match: supports *, **, ? */
187
+ function simpleGlobMatch(path, pattern) {
188
+ // Convert glob pattern to regex
189
+ let regexStr = ""
190
+ let i = 0
191
+ while (i < pattern.length) {
192
+ if (pattern[i] === "*" && pattern[i + 1] === "*") {
193
+ // ** matches anything including /
194
+ regexStr += ".*"
195
+ i += 2
196
+ // Skip trailing /
197
+ if (pattern[i] === "/") i++
198
+ } else if (pattern[i] === "*") {
199
+ regexStr += "[^/]*"
200
+ i++
201
+ } else if (pattern[i] === "?") {
202
+ regexStr += "[^/]"
203
+ i++
204
+ } else {
205
+ // Escape regex special chars
206
+ const c = pattern[i]
207
+ if (".+^${}()|[]\\".includes(c)) regexStr += "\\" + c
208
+ else regexStr += c
209
+ i++
210
+ }
211
+ }
212
+ return new RegExp("^" + regexStr + "$").test(path)
213
+ }
214
+
215
+ const PATH_TOOLS = new Set([...FILE_MUTATORS, "file_ops", "read", "glob"])
216
+
217
+ /** 路径候选(绝对路径先归一为 cwd 相对——globs 语义 = 项目相对;`read` 可收绝对路径):
218
+ * 前缀比对**大小写不敏感**(VS Code `uri.fsPath` 的 Windows 盘符为小写,模型侧常给大写盘符
219
+ * ——`D:\proj\a.py` vs cwd `d:\proj`);另剥前导 `./`。 */
220
+ function matchCandidates(p, cwd) {
221
+ const norm = String(p).replace(/\\/g, "/").replace(/^\.\//, "")
222
+ const out = [norm]
223
+ const base = String(cwd ?? "").replace(/\\/g, "/").replace(/\/+$/, "")
224
+ const underCwd = base && norm.length > base.length
225
+ && norm.slice(0, base.length).toLowerCase() === base.toLowerCase() && norm[base.length] === "/"
226
+ if (underCwd) out.push(norm.slice(base.length + 1))
227
+ return out
228
+ }
229
+
230
+ /** 作用域集 JIT 注入(**派发前**——模型下一轮可见):文件路径类工具(`FILE_MUTATORS` ∪
231
+ * `file_ops` ∪ `read` / `glob`)的触达路径命中**未注入**规则 ⇒ `history` 注入
232
+ * `[System reminder — project rule "<name>" (globs: <g>): <content>]`。语义一句话 =
233
+ * 「Agent 将触碰匹配文件时,该文件作用域的规则先入上下文」。去重键 = `name`;去重域 =
234
+ * 会话(`agent._rulesInjected` 惰性建 Set——顶层 agent 单例;子代理 agent 每 run 新对象
235
+ * ⇒ 去重域 = 该子回合)。路径候选 = #327 单源谓词(`toolTouchPaths`——设计
236
+ * B-4 公式)——`read.filePath` 别名与 `glob.pattern` **不作候选**(登记边界)。
237
+ * @returns {number} 本次注入条数(测试直驱面)。 */
238
+ export function injectScopedRules(agent, history, calls) {
239
+ const scoped = agent?._rules?.scoped ?? []
240
+ if (scoped.length === 0) return 0
241
+ const injected = (agent._rulesInjected ??= new Set())
242
+ let count = 0
243
+ for (const { tool, args } of calls ?? []) {
244
+ if (!PATH_TOOLS.has(tool?.name)) continue
245
+ const raw = toolTouchPaths(tool, args)
246
+ const paths = (raw ?? []).filter((p) => typeof p === "string" && p).flatMap((p) => matchCandidates(p, agent.cwd))
247
+ if (paths.length === 0) continue
248
+ for (const rule of scoped) {
249
+ if (injected.has(rule.name)) continue
250
+ if (!paths.some((p) => matchesGlob(p, rule.globs))) continue
251
+ injected.add(rule.name)
252
+ history.push({
253
+ role: "user",
254
+ content: `[System reminder — project rule "${rule.name}" (globs: ${rule.globs.join("; ")}): ${rule.content}]`,
255
+ })
256
+ count++
257
+ }
258
+ }
259
+ return count
260
+ }
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
  }