@thincoder/core 0.10.2 → 0.10.3

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 (58) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/advisor/citations.mjs +13 -3
  3. package/advisor/convergence.mjs +1 -1
  4. package/advisor/messages.mjs +3 -3
  5. package/agent/dispatch-run.mjs +29 -12
  6. package/agent/record-results.mjs +6 -1
  7. package/agent/spawn-child.mjs +2 -1
  8. package/agent/subagent-event.mjs +71 -0
  9. package/agent/suspension.mjs +35 -9
  10. package/agent/turn-loop.mjs +6 -0
  11. package/agent-tools/advisor-async.mjs +11 -1
  12. package/agent-tools/advisor-settle.mjs +7 -9
  13. package/agent-tools/batch-lifecycle.mjs +3 -2
  14. package/agent-tools/batch-read.mjs +36 -0
  15. package/agent-tools/batch-skeleton.mjs +28 -0
  16. package/agent-tools/batch.mjs +5 -3
  17. package/agent-tools/design-token.mjs +56 -16
  18. package/agent-tools/subagent-async.mjs +6 -1
  19. package/agent-tools/subagent-panel.mjs +119 -36
  20. package/agent-tools/subagent.mjs +4 -2
  21. package/agent-tools/verify.mjs +5 -0
  22. package/auto-think.mjs +26 -2
  23. package/config-presets.mjs +8 -0
  24. package/config.mjs +30 -0
  25. package/declaration.mjs +26 -2
  26. package/embedding.mjs +35 -3
  27. package/ledger-cmd.mjs +13 -21
  28. package/ledger-db.mjs +34 -1
  29. package/ledger-migrate.mjs +4 -3
  30. package/ledger-read.mjs +94 -0
  31. package/ledger-surface.mjs +15 -46
  32. package/ledger.mjs +36 -62
  33. package/manifest-schema.mjs +13 -3
  34. package/manifest.mjs +27 -8
  35. package/mcp/transport-http.mjs +52 -4
  36. package/mcp/transport-stdio.mjs +33 -8
  37. package/mcp.mjs +13 -5
  38. package/memory/code-search.mjs +5 -3
  39. package/memory/core.mjs +6 -4
  40. package/memory/docs.mjs +5 -3
  41. package/memory/schema.mjs +2 -0
  42. package/package.json +1 -1
  43. package/prompts/advisor-design.md +3 -2
  44. package/prompts/advisor-round1.md +1 -0
  45. package/prompts/advisor-round2.md +8 -3
  46. package/prompts/advisor-round3.md +7 -2
  47. package/prompts/discipline-normal.md +2 -1
  48. package/prompts/persona-engineering.md +3 -1
  49. package/provider/core.mjs +10 -4
  50. package/provider/google.mjs +18 -2
  51. package/provider/normalize.mjs +57 -0
  52. package/provider/sse.mjs +45 -13
  53. package/proxy.mjs +13 -3
  54. package/session.mjs +13 -2
  55. package/stream-destroy.mjs +62 -0
  56. package/tool-docs/verify.md +1 -1
  57. package/tools/edit-batch.mjs +7 -2
  58. package/undo-stack.mjs +23 -9
package/manifest.mjs CHANGED
@@ -4,10 +4,12 @@
4
4
  * (机制代码在核,操作对象 = 被开发项目 cwd 根的 PROJECT-MANIFEST.json 数据档)。
5
5
  *
6
6
  * 契约速览(全量见模块设计 §2.2):
7
- * - readManifest(cwd) → { ok, manifest, missingKeys, errors, reason }:
7
+ * - readManifest(cwd) → { ok, manifest, missingKeys, unknownKeys, errors, reason }:
8
8
  * 读 + validateManifest(obj);缺键 → 产 missingKeys(非拒)→ 补默认值 →
9
9
  * 再校验通过;整档缺失 → { ok:false, reason:'missing' }(不静默 fallback)。
10
- * - validateManifest(obj) → { ok, errors, missingKeys }:枚举 / version 数值 /
10
+ * 未知键(#802 / KD-M1-36)→ 产 unknownKeys + 一行可见告警(console.warn + logEvent;
11
+ * 同档同键集去重、读到零未知键清该档 memo)——非拒定性零变。
12
+ * - validateManifest(obj) → { ok, errors, missingKeys, unknownKeys }:枚举 / version 数值 /
11
13
  * docRoot 子键值形态(串 | 数组,F7)+ checkConfig.lineCounts 元素层形态——**纯函数、零 fs**;不落盘。
12
14
  * - 三族声明键(`codePaths` / `index.{codeExtensions,docExtensions,publicRepos}` / `advisor.{docMap,standardsDoc}`——
13
15
  * KD-M1-31 / M1-32):缺键补默认;形态错 = 档非法(fail-closed,与 docRoot 子键同款);
@@ -39,6 +41,8 @@ import { readFileSync, writeFileSync } from "node:fs"
39
41
  import { join, resolve } from "node:path"
40
42
  import { DEFAULT_MANIFEST, isValidDocRootValue, validateManifest, fillDefaults } from "./manifest-schema.mjs"
41
43
  import { MANIFEST_REL, discoverProjects, owningProject, resolveProjectRoot } from "./manifest-discovery.mjs"
44
+ // #802(KD-M1-36):未知键读面告警的事件面——诊断日志(fire-and-forget,失败静默降级)。
45
+ import { logEvent } from "./log.mjs"
42
46
  // 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):发现 ∕ 归属族与校验 ∕ 默认值族出档
43
47
  // `manifest-discovery.mjs` ∕ `manifest-schema.mjs`;全迁移导出经本档转口——消费者 import 面零改。
44
48
  export { MANIFEST_REL, discoverProjects, discoverRepos, owningProject, projectRootView, resolveProjectRoot, _setProjectRootForTest, _resetProjectRootForTest } from "./manifest-discovery.mjs"
@@ -126,19 +130,33 @@ export function manifestFilePath(cwd) {
126
130
  return join(writeRoot(cwd), MANIFEST_REL)
127
131
  }
128
132
 
133
+ /** #802(KD-M1-36)未知键告警去重 memo:档路径 → 上次观测键集(进程内);读到零未知键即清该条。 */
134
+ const unknownKeysSeen = new Map()
135
+
136
+ /** 读面一行可见告警(#802):含键名;同档同键集恰一次——console.warn + logEvent('manifest:unknown-keys')。 */
137
+ function warnUnknownKeys(file, unknownKeys) {
138
+ if (unknownKeys.length === 0) { unknownKeysSeen.delete(file); return }
139
+ const sig = JSON.stringify([...unknownKeys].sort())
140
+ if (unknownKeysSeen.get(file) === sig) return
141
+ unknownKeysSeen.set(file, sig)
142
+ console.warn(`[manifest] ${MANIFEST_REL} unknown key(s) ignored: ${unknownKeys.join(", ")} — not an error; check spelling / version mismatch (KD-M1-36)`)
143
+ logEvent("manifest:unknown-keys", { path: file, keys: unknownKeys.join(",") })
144
+ }
145
+
129
146
  /**
130
147
  * 读 cwd 根数据档(模块设计 §2.2):读 + validateManifest(obj) → 缺键产
131
148
  * missingKeys(非拒)→ 补默认值 → 再校验通过。整档缺失 → { ok:false, reason:'missing' }
132
149
  * (绝不静默 fallback——KD-M1-2);JSON 非法 / 顶层非对象 → reason:'invalid'。
133
- * @returns {{ok:boolean, manifest:object|null, missingKeys:string[], errors:string[], reason?:string}}
150
+ * @returns {{ok:boolean, manifest:object|null, missingKeys:string[], unknownKeys:string[], errors:string[], reason?:string}}
134
151
  */
135
152
  export function readManifest(cwd) {
136
153
  let raw
154
+ const file = manifestFilePath(cwd) // #802:档路径单点(读 / 告警 memo 同源)
137
155
  try {
138
- raw = readFileSync(manifestFilePath(cwd), "utf8")
156
+ raw = readFileSync(file, "utf8")
139
157
  } catch (e) {
140
158
  if (e.code === "ENOENT") {
141
- return { ok: false, reason: "missing", errors: [], missingKeys: [], manifest: null }
159
+ return { ok: false, reason: "missing", errors: [], missingKeys: [], unknownKeys: [], manifest: null }
142
160
  }
143
161
  throw e // 权限/目录等其他读错——fail-closed 上抛,不伪装成缺失
144
162
  }
@@ -146,15 +164,16 @@ export function readManifest(cwd) {
146
164
  try {
147
165
  obj = JSON.parse(raw)
148
166
  } catch {
149
- return { ok: false, reason: "invalid", errors: [`${MANIFEST_REL} 不是合法 JSON`], missingKeys: [], manifest: null }
167
+ return { ok: false, reason: "invalid", errors: [`${MANIFEST_REL} 不是合法 JSON`], missingKeys: [], unknownKeys: [], manifest: null }
150
168
  }
151
169
  if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
152
- return { ok: false, reason: "invalid", errors: ["manifest 顶层必须是 JSON 对象"], missingKeys: [], manifest: null }
170
+ return { ok: false, reason: "invalid", errors: ["manifest 顶层必须是 JSON 对象"], missingKeys: [], unknownKeys: [], manifest: null }
153
171
  }
154
172
  const first = validateManifest(obj)
173
+ warnUnknownKeys(file, first.unknownKeys) // #802:读面单点告警(非拒——ok 定性零变)
155
174
  const manifest = fillDefaults(obj)
156
175
  const final = validateManifest(manifest)
157
- return { ok: final.ok, manifest, missingKeys: first.missingKeys, errors: final.errors, reason: final.ok ? undefined : "invalid" }
176
+ return { ok: final.ok, manifest, missingKeys: first.missingKeys, unknownKeys: first.unknownKeys, errors: final.errors, reason: final.ok ? undefined : "invalid" }
158
177
  }
159
178
 
160
179
  /**
@@ -17,6 +17,11 @@ export function httpTransport(baseURL, extraHeaders = {}) {
17
17
  let legacySSE = false
18
18
  let deadFired = false
19
19
  let deadListeners = new Set()
20
+ // #850 D-MC18:会话过期(HTTP 404)自愈链状态——重建单飞 promise ∥ initialize 参数单源
21
+ //(建连时 `setInitPayload` 注入)∥ 会话死态标记(未修复 ⇒ isAlive false ⇒ ensureAlive 走重连)。
22
+ let reinitPromise = null
23
+ let initPayload = null
24
+ let sessionDead = false
20
25
 
21
26
  /** 2026-08-31 MCP 会诊 P5:意外死亡通知(SSE 流断/error,非主动 close)。 */
22
27
  const fireDead = (msg) => {
@@ -118,7 +123,7 @@ export function httpTransport(baseURL, extraHeaders = {}) {
118
123
  await Promise.race([gotEndpoint, wait])
119
124
  }
120
125
 
121
- async function postRequest(method, params) {
126
+ async function postOnce(method, params, retried) {
122
127
  const id = rpcId()
123
128
  const body = JSON.stringify({ jsonrpc: "2.0", id, method, params })
124
129
 
@@ -167,7 +172,22 @@ export function httpTransport(baseURL, extraHeaders = {}) {
167
172
  if (resp.status === 202) return
168
173
  if (!resp.ok) {
169
174
  pending.delete(id)
170
- resolve({ id, error: { code: -32000, message: `HTTP ${resp.status}` } })
175
+ const error = { id, error: { code: -32000, message: `HTTP ${resp.status}` } }
176
+ // #850 D-MC18:404(MCP 会话失效语义)⇒ 自愈三步——清会话 ⇒ 重建(单飞)⇒ 重试原请求一次。
177
+ if (resp.status === 404 && !retried) {
178
+ sessionId = null // ① 清会话(后续请求不再携失效 id)
179
+ const healed = await reinitialize().then(() => true, () => false)
180
+ // ③ 只重试一次(close 竞态:关闭后自愈链随连接生命周期终止——不重试)
181
+ if (healed && !closed) { resolve(postOnce(method, params, true)); return }
182
+ }
183
+ if (resp.status === 404) {
184
+ // 重建失败 / 二次 404:不第三次——原错误透传 + fireDead(既有退避重连链兜底)
185
+ if (!closed) {
186
+ sessionDead = true
187
+ fireDead("session expired")
188
+ }
189
+ }
190
+ resolve(error)
171
191
  return
172
192
  }
173
193
  if (ct.includes("text/event-stream")) {
@@ -203,6 +223,9 @@ export function httpTransport(baseURL, extraHeaders = {}) {
203
223
  }).finally(() => pending.delete(id))
204
224
  }
205
225
 
226
+ /** 原请求入口(每次调用一个 `retried` 份额——自愈重试恒为 true,二次不再重试)。 */
227
+ const postRequest = (method, params) => postOnce(method, params, false)
228
+
206
229
  const send = async (method, params) => withTimeout(postRequest(method, params), CALL_TIMEOUT_MS)
207
230
 
208
231
  const notify = (method, params) => {
@@ -217,8 +240,32 @@ export function httpTransport(baseURL, extraHeaders = {}) {
217
240
  })
218
241
  }
219
242
 
243
+ /** #850 D-MC18:404 自愈重建(单飞)——重新 `initialize`(+ `notifications/initialized`)。
244
+ * 并发 404 共享同一重建;失败抛(调用面归入「不重试」+ fireDead + sessionDead)。
245
+ * 重建请求自身以 retried=true 走 postOnce——重建中再 404 不再触自愈。 */
246
+ const reinitialize = () => {
247
+ if (!reinitPromise) {
248
+ reinitPromise = (async () => {
249
+ // close 竞态:关闭后自愈不再继续(重建 ∥ 通知 ∥ 会话复活全停)
250
+ if (closed) throw new Error("connection closed")
251
+ if (!initPayload) throw new Error("initialize payload not set (setInitPayload)")
252
+ const resp = await postOnce("initialize", initPayload, true)
253
+ if (resp?.error) throw new Error(resp.error.message ?? "initialize failed")
254
+ if (closed) throw new Error("connection closed")
255
+ notify("notifications/initialized", {})
256
+ sessionDead = false
257
+ })().finally(() => { reinitPromise = null })
258
+ }
259
+ return reinitPromise
260
+ }
261
+
262
+ /** initialize 参数单源注入(建连方:mcp.mjs——握手与自愈重建共用同一参数)。 */
263
+ const setInitPayload = (payload) => { initPayload = payload }
264
+
220
265
  const close = () => {
221
266
  closed = true
267
+ // 关闭态由 closed 判死;会话死态标记随连接生命周期结束复位(无残留歧义)。
268
+ sessionDead = false
222
269
  abortController?.abort()
223
270
  if (sessionId) {
224
271
  fetch(postUrl, {
@@ -242,7 +289,8 @@ export function httpTransport(baseURL, extraHeaders = {}) {
242
289
  send, notify, close, openSSE, url, headers: extraHeaders,
243
290
  // F1:POST-only 降级(postOnly)与 legacy SSE 流(eventSource)都算活连接——
244
291
  // 不得因 eventSource == null 误判死(glm-websearch "reconnect failed" 根因)。
245
- isAlive: () => !closed && (eventSource != null || postOnly),
246
- onDead, markPostOnly,
292
+ // D-MC18:会话过期未修复(sessionDead)⇒ 判死——ensureAlive 据此走重连。
293
+ isAlive: () => !closed && !sessionDead && (eventSource != null || postOnly),
294
+ onDead, markPostOnly, setInitPayload,
247
295
  }
248
296
  }
@@ -1,30 +1,55 @@
1
1
  /**
2
2
  * mcp/transport-stdio.mjs — MCP stdio transport
3
3
  */
4
- import { spawn } from "node:child_process"
4
+ import { spawn, spawnSync } from "node:child_process"
5
5
  import { rpcId, CALL_TIMEOUT_MS, withTimeout, quoteArg } from "./helpers.mjs"
6
6
 
7
+ /** 退出相位标志(D-MC19):模块加载期注册的 exit 钩子置位——退出相位无事件循环,
8
+ * 只同步操作可落地(注册序恒早于宿主 cleanup——`thincoder-cli/src/tui/index.mjs:125`)。 */
9
+ let exiting = false
10
+ process.on("exit", () => { exiting = true })
11
+
12
+ /** 测试缝(先例 `provider/rate.mjs` `_rateHooks`):组杀 / 单进程杀 / 定时器可注入
13
+ * (POSIX 分流在生产 win32 机位的直驱 = 临时覆写 `process.platform`——见批内件 T-A12–A14)。 */
14
+ export const _killHooks = {
15
+ groupKill: (pid, sig) => process.kill(-pid, sig),
16
+ childKill: (child, sig) => child.kill(sig),
17
+ setTimer: (fn, ms) => setTimeout(fn, ms),
18
+ }
19
+
7
20
  /** Kill the child AND its whole process tree.
8
21
  * win32: cmd.exe 包装 spawn 的孙进程(npx/node)必须 taskkill /T /F 才能杀净
9
22
  * (2026-08-31 MCP 会诊 P2:此前只 child.kill() 杀 cmd.exe 壳,真 server 成僵尸
10
- * 并在 Windows 团队每人每次断开/重连泄漏一批);
11
- * POSIX: SIGTERM 后 2s 未退 SIGKILL 兜底。 */
23
+ * 并在 Windows 团队每人每次断开/重连泄漏一批);taskkill 必须 **spawnSync**(同步)——
24
+ * 调用链挂在 `process.on("exit")`(退出相位仅同步操作可落地:异步 spawn 的 taskkill
25
+ * 随父进程退出被一并带走 ⇒ 子树泄漏——#17 · 2026-10-03);
26
+ * POSIX(D-MC19):detached 新进程组(组长 pid = 组 id)⇒ 组杀 `process.kill(-pid, sig)`
27
+ * (组杀覆盖孙进程——原单进程 SIGTERM 只达直接子进程);正常相位 = 组 SIGTERM → 2s 组
28
+ * SIGKILL 升级;退出相位(升级 timer 不触发)⇒ 同步组 SIGKILL 直达(不做 SIGTERM 等待)。
29
+ * ESRCH(无此进程组)⇒ fallback 单进程 kill;其余错误 best-effort。 */
12
30
  function killTree(child) {
13
31
  if (!child.pid) return
14
32
  if (process.platform === "win32") {
15
- try { spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }) } catch { /* best effort */ }
33
+ try { spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }) } catch { /* best effort */ }
16
34
  return
17
35
  }
18
- try {
19
- child.kill("SIGTERM")
20
- setTimeout(() => { try { child.kill("SIGKILL") } catch { /* already gone */ } }, 2000).unref?.()
21
- } catch { /* best effort */ }
36
+ const groupKill = (sig) => {
37
+ try { _killHooks.groupKill(child.pid, sig) } catch (e) {
38
+ if (e?.code === "ESRCH") { try { _killHooks.childKill(child, sig) } catch { /* already gone */ } }
39
+ }
40
+ }
41
+ if (exiting) { groupKill("SIGKILL"); return }
42
+ groupKill("SIGTERM")
43
+ const timer = _killHooks.setTimer(() => groupKill("SIGKILL"), 2000)
44
+ timer?.unref?.()
22
45
  }
23
46
 
24
47
  /** Create an MCP stdio transport over a spawned child process.
25
48
  * @param {Object} [env] — extra environment variables merged on top of process.env */
26
49
  export function stdioTransport(command, args, env) {
27
50
  const spawnOptions = { stdio: ["pipe", "pipe", "pipe"], windowsHide: true, env: { ...process.env, ...env } }
51
+ // D-MC19:POSIX 加 detached——新进程组(组杀覆盖孙进程);win32 分支零改(taskkill /T /F 已覆盖)。
52
+ if (process.platform !== "win32") spawnOptions.detached = true
28
53
  const child =
29
54
  process.platform === "win32" && !/\.exe$/i.test(command)
30
55
  ? spawn("cmd.exe", ["/d", "/s", "/c", [command, ...(args ?? [])].map(quoteArg).join(" ")], {
package/mcp.mjs CHANGED
@@ -51,6 +51,16 @@ export const _mcpHooks = {
51
51
  reconnectDelays: [1000, 2000, 4000, 8000],
52
52
  }
53
53
 
54
+ /** initialize 请求参数单源(D-MC18):建连握手(doInitialize)与 transport 内 404 自愈重建共用——
55
+ * 建连时经 `transport.setInitPayload(...)` 注入。导出面供批内件复用(参数单源不可分叉)。 */
56
+ export function buildInitParams() {
57
+ return {
58
+ protocolVersion: "2024-11-05",
59
+ capabilities: {},
60
+ clientInfo: { name: "thincoder", version: "1.0.0" },
61
+ }
62
+ }
63
+
54
64
  /** 按 config 创建并完成握手的 transport(findTransportConfig 与 vscode 对齐)。 */
55
65
  async function createConnectedTransport(rawConfig, serverName) {
56
66
  // #57(provider-config-family 批):值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——传输建连单点
@@ -62,6 +72,8 @@ async function createConnectedTransport(rawConfig, serverName) {
62
72
  await transport.connect()
63
73
  } else if (config.url) {
64
74
  transport = httpTransport(config.url, config.headers ?? {})
75
+ // D-MC18:#850——initialize 参数单源注入(404 自愈重建走同一参数)
76
+ transport.setInitPayload(buildInitParams())
65
77
  try {
66
78
  await transport.openSSE()
67
79
  } catch {
@@ -194,11 +206,7 @@ function truncateMcpOutput(text) {
194
206
 
195
207
  async function doInitialize(transport, _name) {
196
208
  const initResp = await withTimeout(
197
- transport.send("initialize", {
198
- protocolVersion: "2024-11-05",
199
- capabilities: {},
200
- clientInfo: { name: "thincoder", version: "1.0.0" },
201
- }),
209
+ transport.send("initialize", buildInitParams()),
202
210
  INIT_TIMEOUT_MS,
203
211
  )
204
212
  if (initResp.error) throw new Error(`initialize error: ${initResp.error.message}`)
@@ -5,7 +5,7 @@
5
5
  * ∥ `codeSearchTool`——迁出块逐字;原档 `memory/code-sync.mjs` 经 `export { … } from` 转口保名
6
6
  * (消费面 / 批内件 import 面零改)。
7
7
  */
8
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
8
+ import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
9
9
  import { scanVectors, createTopK } from "./scan.mjs"
10
10
  import { normalizeOrigin } from "./origin.mjs"
11
11
  import { declaredPublicRoots } from "../conventions.mjs"
@@ -118,10 +118,12 @@ async function _runEnsureCodeEmbeddings(memory) {
118
118
  if (pending.length === 0) return
119
119
 
120
120
  const texts = pending.map((r) => `${r.path}${r.symbol_name ? " :: " + r.symbol_name : ""}\n${safeSliceUTF16(r.content, EMBED_TEXT_MAX_LEN)}`)
121
- const vecs = await embed(memory.embedder, texts)
121
+ // #859(MEMORY.md §6.3 ∥ D-MEM31):400 类毒行逐条隔离——null 行跳过不写(backlog 不堵)+ 一行可见回执
122
+ const { vectors: vecs, skipped } = await embedTolerant(memory.embedder, texts)
123
+ if (skipped.length) console.warn(`[code] embedding skipped ${skipped.length} row(s, first): ${skipped[0]}`)
122
124
 
123
125
  const update = memory.db.prepare(`UPDATE code_chunks SET embedding = ? WHERE rowid = ?`)
124
- pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
126
+ pending.forEach((r, i) => { if (vecs[i]) update.run(toBlob(vecs[i]), r.rowid) })
125
127
  }
126
128
 
127
129
  /** Generate the code_search tool (read-only). */
package/memory/core.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { parseEntry, serializeEntry, entryFilename } from "../markdown.mjs"
9
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
9
+ import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
10
10
  import { scanVectors, createTopK } from "./scan.mjs"
11
11
  import { normalizeOrigin } from "./origin.mjs"
12
12
  import { readFile, stat, readdir, writeFile, mkdir } from "node:fs/promises"
@@ -172,11 +172,13 @@ async function _runEnsureEmbeddings(memory) {
172
172
  if (pendingEntries.length + pendingFiles.length > 0) {
173
173
  const items = [...pendingEntries, ...pendingFiles]
174
174
  const texts = items.map((r) => `${r.title}\n${safeSliceUTF16(r.content, EMBED_TEXT_MAX_LEN)}`)
175
- const vecs = await embed(memory.embedder, texts)
175
+ // #859(MEMORY.md §6.3 ∥ D-MEM31):400 类毒行逐条隔离——null 行跳过不写(backlog 不堵)+ 一行可见回执
176
+ const { vectors: vecs, skipped } = await embedTolerant(memory.embedder, texts)
177
+ if (skipped.length) console.warn(`[memory] embedding skipped ${skipped.length} row(s, first): ${skipped[0]}`)
176
178
  const updateEntry = memory.db.prepare(`UPDATE entries SET embedding = ? WHERE id = ?`)
177
- pendingEntries.forEach((r, i) => updateEntry.run(toBlob(vecs[i]), r.id))
179
+ pendingEntries.forEach((r, i) => { if (vecs[i]) updateEntry.run(toBlob(vecs[i]), r.id) })
178
180
  const updateFile = memory.db.prepare(`UPDATE files SET embedding = ? WHERE rowid = ?`)
179
- pendingFiles.forEach((r, i) => updateFile.run(toBlob(vecs[pendingEntries.length + i]), r.rowid))
181
+ pendingFiles.forEach((r, i) => { if (vecs[pendingEntries.length + i]) updateFile.run(toBlob(vecs[pendingEntries.length + i]), r.rowid) })
180
182
  }
181
183
  }
182
184
  // 逐面判定 + 回填(code ∕ doc 面自持模型键——同判据;失配面同样零写回降级信号)
package/memory/docs.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { readFile, stat } from "node:fs/promises"
6
- import { embed, cosine, toBlob, fromBlob } from "../embedding.mjs"
6
+ import { embed, embedTolerant, cosine, toBlob, fromBlob } from "../embedding.mjs"
7
7
  import { scanVectors, createTopK, SCAN_YIELD_MS } from "./scan.mjs"
8
8
  import { normalizeOrigin } from "./origin.mjs"
9
9
  import { MAX_DOC_FILE_BYTES } from "./schema.mjs"
@@ -196,10 +196,12 @@ async function _runEnsureDocEmbeddings(memory) {
196
196
  if (pending.length === 0) return
197
197
 
198
198
  const texts = pending.map((r) => `${r.heading || r.path}\n${safeSliceUTF16(r.content, EMBED_TEXT_MAX_LEN)}`)
199
- const vecs = await embed(memory.embedder, texts)
199
+ // #859(MEMORY.md §6.3 ∥ D-MEM31):400 类毒行逐条隔离——null 行跳过不写(backlog 不堵)+ 一行可见回执
200
+ const { vectors: vecs, skipped } = await embedTolerant(memory.embedder, texts)
201
+ if (skipped.length) console.warn(`[docs] embedding skipped ${skipped.length} row(s, first): ${skipped[0]}`)
200
202
 
201
203
  const update = memory.db.prepare(`UPDATE doc_chunks SET embedding = ? WHERE rowid = ?`)
202
- pending.forEach((r, i) => update.run(toBlob(vecs[i]), r.rowid))
204
+ pending.forEach((r, i) => { if (vecs[i]) update.run(toBlob(vecs[i]), r.rowid) })
203
205
  }
204
206
 
205
207
  /** Generate the doc_search tool (read-only). */
package/memory/schema.mjs CHANGED
@@ -50,6 +50,8 @@ export const SKIP_DIRS = new Set([
50
50
  "AppData", "Application Data", "Desktop", "Documents", "Downloads",
51
51
  "Music", "Pictures", "Videos", "OneDrive", "Contacts", "Favorites",
52
52
  "Links", "Saved Games", "Searches",
53
+ // User-home common items (Library = macOS user dir; go = Go workspace) — basename match at any depth (false-prune face accepted)
54
+ "Library", "go",
53
55
  // Other common non-code directories
54
56
  "Program Files", "Program Files (x86)", "Windows", "$Recycle.Bin",
55
57
  ])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
@@ -16,11 +16,12 @@ You are an independent design reviewer for a design review inside an engineering
16
16
  - 🟡 Advisory — design could be improved; NOT a blocker for approval
17
17
  - 🔵 Note — optional observation; NOT a blocker Document ownership severity:
18
18
  - Wording that CONTRADICTS an existing document (same mechanism described differently in two places) → 🔴
19
- - Creating a new file for an existing section, or duplicating a description that already exists elsewhere → 🟡 ## Citation Discipline When you cite design-document text, use the exact `file:line` format (e.g. `path/to/file.ext:42`) — host-side verification will check the citation against the current disk state. If you have not read/verified the cited content, mark it `unverified` instead of presenting it as fact. ## Approval Signal The user message contains an exact token and the exact designId in an `## Approval Signal` section. Close your findings with a single verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as the line immediately before the token echo; the verdict is the closing decision, output nothing beyond the token echo below.
19
+ - Creating a new file for an existing section, or duplicating a description that already exists elsewhere → 🟡 ## Citation Discipline When you cite design-document text, use the exact `file:line` format (e.g. `path/to/file.ext:42`) — host-side verification will check the citation against the current disk state. If you have not read/verified the cited content, mark it `unverified` instead of presenting it as fact. Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
20
+ ## Approval Signal The user message contains an exact token and the exact designId in an `## Approval Signal` section. Close your findings with a single verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as the line immediately before the token echo; the verdict is the closing decision, output nothing beyond the token echo below.
20
21
  - VERDICT: pass = no 🔴 (Critical) issue remains — 🟡 (Advisory) and 🔵 (Note) findings do NOT block pass: list them in your table and still pass.
21
22
  - VERDICT: changes-required = any 🔴 (Critical) issue — then do NOT include the token or the designId below; list the issues instead.
22
23
  - The verdict line itself is the approval statement — no separate prose around it, no post-verdict commentary.
23
- - After the VERDICT line, the ONLY allowed content is the token echo: if — and ONLY if — your verdict is pass, echo this exact token: [DESIGN-TOKEN:<token>] and this exact designId: <designId>. Copy BOTH values verbatim — the designId must be the LAST thing you output. Important:
24
+ - After the VERDICT line, the ONLY allowed content is the token echo: if — and ONLY if — your verdict is pass, echo this exact token: [DESIGN-TOKEN:<token>] and this exact designId: <designId>. Copy BOTH values verbatim — every character, including the colon and the digits after the uuid inside the token brackets; a shortened echo is flagged as truncated. The designId must be the LAST thing you output. Important:
24
25
  - Review the design on its own merits — do NOT expect code to exist yet.
25
26
  - Read the design document fully. Judge against the Project Guide (when present in the review context) and the review criteria in this prompt — do not assume any particular project files.
26
27
  - Do NOT run git diff or look for code changes — there are none at this stage.
@@ -24,6 +24,7 @@ You have a budget of 20 tool rounds (chat turns) — plan your exploration accor
24
24
  - Cover everything now. Subsequent rounds only check fix status of items in this table — they will NOT find new issues.
25
25
  - Stop calling tools once you are ready to produce the review table.
26
26
  - **Host verification**: every `file:line: content` reference in your table is mechanically checked against the CURRENT file state by the host — quote exactly what `read` returned; a mismatch marks the finding unverified.
27
+ Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
27
28
  - **Closing verdict line** (rules pinned in `## Verdict Line` at the end of this prompt): after the table/findings, end your reply with exactly ONE verdict line — `VERDICT: pass` or `VERDICT: changes-required` — as its final line, and output NOTHING after it: the verdict is the closing decision.
28
29
  ## Judgment Rules (apply directly — do not re-derive) Apply each rule to the extent it matches the review type: design review — doc-state rules (R1, R7a-e) apply; code review — all rules apply. R1 Doc contradiction / state inconsistency → 🟡 (report-and-fix by the parent doc layer — NOT 🔴; exception: the same mechanism described differently in two places = Document ownership 🔴 — keep this convention — do not downgrade)
29
30
  R2 Implementation deviates from design (acceptance unmet / silent simplification) → 🔴 (must fix)
@@ -9,12 +9,17 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
9
9
  3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them or a fix appears to contradict the task itself.
10
10
  4. **ALWAYS `read` the current file before judging an item fixed or unfixed.** - Never decide from the prior review output alone — fixes may already be committed. - (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.) - Batch independent tool calls in one reply.
11
11
  5. Produce your review table. Budget: read only the files named in the prior-review items. If at 8 rounds you have not yet verified all items, wrap up. Rules:
12
+ If you must wrap up with items not fully verified, list each of them explicitly with its state (Status `Unfixed`, evidence not verified this round) — never drop an item silently; the verdict line still closes the round with a conclusion.
12
13
  - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
13
14
  - Primarily check fix status of items in the prior review output.
14
15
  - For items marked "fixed": verify they were actually fixed.
15
- - For items marked "not an issue": evaluate whether the reasoning is sound.
16
- - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
16
+ - For items left unfixed (marked "not an issue" / "Deferred", or not addressed at all): every non-fix MUST carry a reason — a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record; evaluate whether that reasoning holds.
17
+ - Where it does not hold, the item stays unresolved — push back (this round is the last one where a reasoned non-fix can be pushed back). Where it holds, close the item: list it with Status `Accepted`.
18
+ - Zero-reason silence or evasion never closes an item — list it as `Unfixed`.
19
+ - Status vocabulary: `Fixed` / `Accepted` (a reasoned non-fix — closed) / `Unfixed` (unresolved — including zero-reason non-fixes) / `New`. Accepted closures and unresolved items must both be listed — an item must not vanish from the table silently.
20
+ - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(() => {`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
17
21
  - **Host verification**: your `file:line: content` citations are mechanically checked against the CURRENT file state — quote exactly what `read` returned; a mismatch marks the finding unverified.
22
+ Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
18
23
  - **Fresh context**: this round's conversation contains NO read output from earlier rounds — every file must be re-read this round.
19
24
  - You may flag obvious new problems — but only if clearly visible in the reviewed files and would cause crashes, data loss, or logic errors.
20
25
  - Do NOT nitpick style or naming.
@@ -43,4 +48,4 @@ R7d Semantic dangling → 🟡 report the design gap (parent fixes)
43
48
  R7e Never block "pass" due to doc-state contradiction — contradiction = 🟡 report-and-pass (except mechanism-level description mismatch — = 🔴 — must be resolved before pass) Source: 7-round sample — verified judgments — continuously re-reviewed. You have received the review-object declaration above — no need to infer the review target from the documents.
44
49
  ## Verdict Line — the closing decision (nothing after it)
45
50
  After the table/findings, output exactly ONE verdict line as the FINAL line of your reply: `VERDICT: pass` or `VERDICT: changes-required` — a single value, never both, no counts or extra text on the line. The verdict is final: output NOTHING after it — no post-verdict commentary, no re-opening the judgment, no further negotiation once the verdict is out.
46
- Verdict meaning: pass = every prior-review 🔴 issue is resolved AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved, any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
51
+ Verdict meaning: pass = every prior-review 🔴 issue is resolved — fixed (verified against the current files) or accepted (a reasoned non-fix, closed at this round) — AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved (including any zero-reason non-fix — silence or evasion is never accepted), any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
@@ -8,10 +8,15 @@ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 round
8
8
  3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them.
9
9
  4. **ALWAYS `read` the current file before judging an item fixed or unfixed.** - Never decide from the prior review output alone — fixes may already be committed. - (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.) - Batch independent tool calls in one reply.
10
10
  5. Produce your review table. Budget: read only the files named in the prior-review items. If at 8 rounds you have not yet verified all items, wrap up. Rules:
11
+ If you must wrap up with items not fully verified, list each of them explicitly with its state (Status `Unfixed`, evidence not verified this round) — never drop an item silently; the verdict line still closes the round with a conclusion.
11
12
  - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
12
13
  - Only check fix status of items in the prior review output.
13
- - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(...)`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
14
+ - For prior items the agent left unfixed with a reason (a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record): ACCEPT them — at this round a reasoned non-fix closes the item (do not re-adjudicate the reasoning here; list it with Status `Accepted`); do not push back on the same item again.
15
+ - Items with no reason (silence, evasion, a bare claim without justification) stay unresolved — list them with Status `Unfixed`; a zero-reason non-fix is never accepted.
16
+ - Status vocabulary: `Fixed` / `Accepted` (a reasoned non-fix — closed at this round) / `Unfixed` (unresolved — including zero-reason non-fixes). Every prior item must receive one of these dispositions; accepted and unresolved items must be listed — never drop an item silently.
17
+ - Every "Unfixed" or "New" entry MUST quote the exact line content from THIS round's `read` output (e.g. `run.mjs:180: timeoutId = setTimeout(() => {`). Line numbers alone are NOT evidence — they may be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
14
18
  - **Host verification**: your `file:line: content` citations are mechanically checked against the CURRENT file state — quote exactly what `read` returned; a mismatch marks the finding unverified.
19
+ Never abbreviate a quote with an ellipsis ("…" or "...") — a citation must be one CONTIGUOUS substring of the cited line; an abbreviated quote is reported as a non-contiguous citation (quote one contiguous excerpt instead).
15
20
  - **Fresh context**: this round's conversation contains NO read output from earlier rounds — every file must be re-read this round.
16
21
  - Do NOT look for new issues. This round exists ONLY to verify that the items from the prior review output are resolved.
17
22
  - Do NOT nitpick style or naming.
@@ -39,4 +44,4 @@ R7d Semantic dangling → 🟡 report the design gap (parent fixes)
39
44
  R7e Never block "pass" due to doc-state contradiction — contradiction = 🟡 report-and-pass (except mechanism-level description mismatch — = 🔴 — must be resolved before pass) Source: 7-round sample — verified judgments — continuously re-reviewed. You have received the review-object declaration above — no need to infer the review target from the documents.
40
45
  ## Verdict Line — the closing decision (nothing after it)
41
46
  After the table/findings, output exactly ONE verdict line as the FINAL line of your reply: `VERDICT: pass` or `VERDICT: changes-required` — a single value, never both, no counts or extra text on the line. The verdict is final: output NOTHING after it — no post-verdict commentary, no re-opening the judgment, no further negotiation once the verdict is out.
42
- Verdict meaning: pass = every prior-review 🔴 issue is resolved AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved, any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
47
+ Verdict meaning: pass = every prior-review 🔴 issue is resolved — fixed (verified against the current files) or accepted (a reasoned non-fix, closed at this round) — AND the fixes introduced no new 🔴. changes-required = any prior 🔴 still unresolved (including any zero-reason non-fix — silence or evasion is never accepted), any new 🔴 introduced by the fixes, or any 🟡 the review marks as must-fix (a must-fix row states "must fix before implementation/approval" → changes-required). Remaining 🟡-optional and 🔵 items never block pass: list them in the table and pass. Any 🔴 issue → `VERDICT: changes-required`.
@@ -125,8 +125,9 @@ The report must contain: what changed / why, the paths of files touched, how you
125
125
  `Action` is one of exactly four values: `Fixed` (you edited the code — landed), `Dispatched` (fix round in flight — not yet landed), `Not an issue` (technical rebuttal with evidence), `Deferred` (admitted, not fixed now — with a reason).
126
126
  - `Detail` = what changed and where (file:line), or your evidence/reason.
127
127
  - **No "pre-existing" cop-out.** You own the whole code. "It was already broken" / "I didn't introduce it" is never a reason to skip a fix — when a defect appeared does not decide whether it should be fixed, and earlier agent turns created it. Rebut only on technical grounds, otherwise fix it.
128
- - **Do not bury 🔴.** A 🔴 you neither fix nor rebut blocks convergence. `Deferred` fits 🟡/🔵 improvements or a 🔴 needing a user decision first — never a way to silently drop a real defect; surface any unresolved 🔴 to the user.
128
+ - **Do not bury 🔴.** A 🔴 you neither fix nor rebut blocks convergence. `Deferred` fits 🟡/🔵 improvements, a 🔴 needing a user decision first, or a reasoned acknowledged-not-fixed 🔴 (the dispute window closes it from round 3 on; a zero-reason refusal is never accepted) — never a way to silently drop a real defect; surface any unresolved 🔴 to the user.
129
129
  - Round 2 verifies the prior table + flags obvious new issues; round 3+ strictly verifies only the prior table (no new-issue hunting). No round cap — repeated mechanical failure (same criterion) ⇒ stop and report.
130
+ Every non-fix must carry a reason (a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record); the reviewer may push back on insufficient reasons through round 2, and from round 3 accepts reasoned non-fixes — a zero-reason refusal is never accepted and stays unresolved.
130
131
  - When the advisor reports all clear (no 🔴 remaining), run `verify`.
131
132
 
132
133
  ## Common disciplines
@@ -76,6 +76,7 @@ Batch record, dispatch task books, verification conclusions, review firing, requ
76
76
  - **Parent does not ghost-write**: you do NOT write the design doc — anything needing change goes **via a fix round** to eng-designer; three exception categories: ① your own write domain (batch record §1/§4/§6 · ledger · requirement docs) ② purely mechanical form corrections (line folding / pointer form / counts) ③ **small edits** (single-line / table-level · no new semantics · verifiable one by one).
77
77
  **All three must be marked** ("parent direct execution" + revertable). **Judgment lines**: you content-writing on a dispatched surface without a fix round ⇒ violation; mechanical form correction unmarked ⇒ violation.
78
78
  - **Close three states (no "promises")**: each round's close allows **only three states** — ① **Do** (the action **was fired THIS round**: tool call / edit landed — the report only describes **what happened this round**); ② **Wait** (real dependency: waiting for the user's nod / a subagent's return — **must state what you're waiting for**); ③ **Stop** (anomaly / pending judgment — **state the stop point**). **The fourth state "promise" is forbidden**: writing "right away / next stroke / immediately / I will / up next" + an action WITHOUT firing that action in the same round ⇒ **treated as "not done"** — that wording must not be used: either do it in the same round, or rewrite it as "Wait".
79
+ - **Every claim carries its basis**: at close, each judgment / conclusion / reading must carry its basis — one of: a judgment source (what it stands on), a `file:line` coordinate, or an explicit `unverified`; a claim with none of the three must not be output as a conclusion; a basis present must be checkable, one absent must be marked — no format mandated, existing documents not retrofitted.
79
80
  - **Debts go on the list**: undispatched / unfinished items of your own ⇒ **immediately written into batch record §6 unresolved (or a ledger row)** — debts **must be visible**, never living only in report prose, a code comment, or an inline note waiting for the user to chase.
80
81
  - **Fix in place first**: one-off small-form items (wording / coordinates / indexes / cleanup) from reviews or sweeps get fixed on the spot — never booked. **Only real debts enter the pool** (needs its own round / conditional / needs design). Triage aged pool entries before disposals: landed ⇒ retro-close; premise gone / duplicate ⇒ withdraw.
81
82
  - **Drain first (auto mode)**: while subagents are in flight, **clear your own queue in parallel** (verification / closure / settlement / mechanical corrections) — "waiting" **only holds for real dependencies**; parking doable work on "waiting" ⇒ violation.
@@ -105,13 +106,14 @@ Batch record, dispatch task books, verification conclusions, review firing, requ
105
106
  No "pre-existing" cop-out: "it was already broken" is never a reason to skip a fix — you own the whole design/code, and when a defect appeared does not decide whether it should be fixed.
106
107
  Findings beyond the approved design's scope: surface them or propose a design update — never silently ignore.
107
108
  A 🔴 you neither fix nor surface blocks convergence.
108
- `Deferred` fits 🟡/🔵 improvements or a 🔴 needing the user's decision first — never a way to silently drop a real defect; unresolved 🔴 must be surfaced to the user.
109
+ `Deferred` fits 🟡/🔵 improvements, a 🔴 needing the user's decision first, or a reasoned acknowledged-not-fixed 🔴 (the dispute window closes it from round 3 on; a zero-reason refusal is never accepted) — never a way to silently drop a real defect; unresolved 🔴 must be surfaced to the user.
109
110
  - **Fix round ⇄ user approval timing** (post-review): after review passes you adjudicate one by one (adjudication table) — fixes your adjudication demands (design-doc revisions / implementation repairs)
110
111
  **land via a fix round and pass your verification BEFORE you may request user approval**; while a fix round is in flight you must NOT request approval — in-flight status is reported only, and the report carries no approval request.
111
112
  **Fix-round boundary**: only fixes directly derived from review findings and your adjudication — **no smuggled new semantics/scope**; smuggling = new content,
112
113
  to be explicitly laid before the user for a separate decision, never default-approved along with the approval request.
113
114
  In the approval request, the adjudication table's `Dispatched` rows must have converged to `Fixed` one by one (with landing evidence: file:line or design-doc section).
114
115
  - Round decay: round 2 verifies the prior table + flags obvious new issues; round 3+ strictly verifies only the prior table (no new-issue hunting). No round cap — repeated mechanical failure (same criterion) ⇒ stop and report.
116
+ Every non-fix must carry a reason (a technical rebuttal with evidence, or an explicit acknowledged-not-fixed record); the reviewer may push back on insufficient reasons through round 2, and from round 3 accepts reasoned non-fixes — a zero-reason refusal is never accepted and stays unresolved.
115
117
  When the advisor reports all clear (no 🔴 remaining), run `verify`.
116
118
  - Async: **advisor calls are async by default at the top level.** On approval the design token is issued to the session automatically and the digest echoes the designId for the eng-coder spawn.
117
119
 
package/provider/core.mjs CHANGED
@@ -126,6 +126,8 @@ async function chatImpl(provider, { messages, tools, onToken, onReasoning, onWai
126
126
  // window/clamping logic below reads the overridden value where it matters.
127
127
  const spec = providerSpec(provider)
128
128
  messages = stripImagesForTextModel(messages, spec)
129
+ // #853 D-PR32:历史图片累计字节预算——发送前对消息副本累计驱逐(最老先弃;history 本体零触)。
130
+ messages = capHistoryImages(messages)
129
131
  // SESSION.md §6.9 T-S3: local-only message fields (ts/transient) never reach the wire.
130
132
  // Stripped BEFORE format dispatch — anthropic/responses transports pass whole message
131
133
  // objects through verbatim (only the OpenAI path ran escapeMessages). Copy-on-write:
@@ -330,7 +332,7 @@ export function buildContinuationMessages(messages, result, spec) {
330
332
  // Pre-send payload normalization lives in normalize.mjs (2026-08-31 extract,
331
333
  // TODO #2); re-exported so provider/index.mjs and tool-pairing.test.mjs keep
332
334
  // their import paths.
333
- import { stripImagesForTextModel, normalizeToolPairing } from "./normalize.mjs"
335
+ import { stripImagesForTextModel, normalizeToolPairing, capHistoryImages } from "./normalize.mjs"
334
336
  export { stripImagesForTextModel, normalizeToolPairing }
335
337
  /** Merge tool calls from a retry/continuation into the accumulated result.
336
338
  * 2026-08-31 会诊 #7/#17:readSSE 输出的 tc 已 finalize(无 index 字段),
@@ -392,9 +394,9 @@ async function requestWithRetry(provider, body, signal, onWait) {
392
394
  _headerTimeoutMs: effectiveFetchTimeoutMs(provider),
393
395
  _bodyIdleMs: FETCH_BODY_IDLE_MS,
394
396
  }
395
- response = provider.proxyUri
396
- ? await proxyFetch(url, opts, provider.proxyUri)
397
- : await fetch(url, opts)
397
+ // #878 D-PX8:请求调用点收口——恒走 `proxyFetch`(无 proxyUri 即直连分支,内部 abort 通道单点建设;
398
+ // 原 call-site 分叉删除)。
399
+ response = await proxyFetch(url, opts, provider.proxyUri)
398
400
  } catch (error) {
399
401
  // AGENT-LOOP-SUBAGENT.md §6.12 站点 #2(第 24 批):fetch 拒否面补标来源(undici 拒否的 AbortError 现场无 reason)
400
402
  if (error.name === "AbortError") throw annotateAbort(error, signal, "provider", "request")
@@ -406,6 +408,10 @@ async function requestWithRetry(provider, body, signal, onWait) {
406
408
 
407
409
  const text = await response.text().catch(() => "")
408
410
  let message = `LLM API error ${response.status}: ${text}`
411
+ // #853 D-PR32:413 可操作化——长会话图片累积的高频形态,从裸 HTTP 码变为可执行指引。
412
+ if (response.status === 413) {
413
+ message += " — request body too large (HTTP 413): run /compact to compress the conversation history, or remove historical images."
414
+ }
409
415
  // 401 双平台提示 + 诊断回显(2026-08-31 会诊 #15):
410
416
  // Kimi 双平台 key 不互通的提示保留;通用加 baseURL host + key 前 6 位掩码,
411
417
  // 帮用户快速分辨"配错平台还是配错账号"。
@@ -8,6 +8,7 @@ import { proxyFetch } from "../proxy.mjs"
8
8
  import { requestWithRetry } from "./retry.mjs"
9
9
  import { effectiveFetchTimeoutMs } from "./core.mjs"
10
10
  import { abortError, timeoutError } from "../abort-provenance.mjs"
11
+ import { terminateBody } from "../stream-destroy.mjs"
11
12
 
12
13
  /** OpenAI 语义 tool_choice → Gemini FunctionCallingConfig(2026-08-31 能力层)。 */
13
14
  function mapFunctionCallingConfig(choice) {
@@ -197,10 +198,14 @@ async function parseGeminiStream(response, { onToken, onReasoning, signal }) {
197
198
  // 2026-09-01 读侧 idle 超时(同 sse.mjs):body 有数据流动即不超时;连续 120s 无新 chunk 判死
198
199
  const READ_IDLE_MS = 120_000
199
200
  let idleTimer = null
201
+ let idleError = null
200
202
  const armIdle = () => {
201
203
  if (idleTimer) clearTimeout(idleTimer)
202
204
  idleTimer = setTimeout(() => {
203
- try { response.body?.destroy(timeoutError(`SSE idle timeout: no data for ${READ_IDLE_MS / 1000}s`, "provider", "google-sse-idle")) } catch { /* already gone */ }
205
+ // #878:断流单点分流——直连 fetch(web 流)走 `IDLE_ABORT` 内部 abort 通道;
206
+ // proxy 形回落 destroyBody(原契约)。相位串 = 本腿专属 google-sse-idle。
207
+ idleError = timeoutError(`SSE idle timeout: no data for ${READ_IDLE_MS / 1000}s`, "provider", "google-sse-idle")
208
+ try { terminateBody(response, idleError) } catch { /* already gone */ }
204
209
  }, READ_IDLE_MS)
205
210
  idleTimer.unref?.()
206
211
  }
@@ -238,6 +243,16 @@ async function parseGeminiStream(response, { onToken, onReasoning, signal }) {
238
243
  result.interruptMessage = signal.reason.message
239
244
  return result
240
245
  }
246
+ // #878:看门狗终结两径归一(idleFired)——无内容 ⇒ idle 超时错误;有内容 ⇒ partial
247
+ //(已流出内容保留,不整轮报废)。非 idle 错误照旧走下方 hasPartial 径(原语义零变)。
248
+ if (idleError) {
249
+ if (result.content || result.reasoning || result.toolCalls.length) {
250
+ result.partial = true
251
+ result.networkError = idleError.message ?? String(idleError)
252
+ return result
253
+ }
254
+ throw idleError
255
+ }
241
256
  if (hasPartial(e)) {
242
257
  result.partial = true
243
258
  result.networkError = e.message ?? String(e)
@@ -251,7 +266,8 @@ async function parseGeminiStream(response, { onToken, onReasoning, signal }) {
251
266
  return result
252
267
  }
253
268
 
254
- /** google.mjs 无 hasChoices 追踪——只有流中途死且已有内容才标 partial(同 sse.mjs 语义的简化版) */
269
+ /** google.mjs 无 hasChoices 追踪——非 idle 网络错误按文案判 partial(同 sse.mjs 语义的简化版);
270
+ * idle 径两分(有内容 ⇒ partial ∥ 无内容 ⇒ 超时错误)归 catch(#878)。 */
255
271
  function hasPartial(e) {
256
272
  return /ECONNRESET|terminated|idle timeout|network/i.test(e?.message ?? "")
257
273
  }