@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.
- package/CHANGELOG.md +33 -0
- package/advisor/citations.mjs +13 -3
- package/advisor/convergence.mjs +1 -1
- package/advisor/messages.mjs +3 -3
- package/agent/dispatch-run.mjs +29 -12
- package/agent/record-results.mjs +6 -1
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +35 -9
- package/agent/turn-loop.mjs +6 -0
- package/agent-tools/advisor-async.mjs +11 -1
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/batch-lifecycle.mjs +3 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +28 -0
- package/agent-tools/batch.mjs +5 -3
- package/agent-tools/design-token.mjs +56 -16
- package/agent-tools/subagent-async.mjs +6 -1
- package/agent-tools/subagent-panel.mjs +119 -36
- package/agent-tools/subagent.mjs +4 -2
- package/agent-tools/verify.mjs +5 -0
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +30 -0
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- package/ledger-cmd.mjs +13 -21
- package/ledger-db.mjs +34 -1
- package/ledger-migrate.mjs +4 -3
- package/ledger-read.mjs +94 -0
- package/ledger-surface.mjs +15 -46
- package/ledger.mjs +36 -62
- package/manifest-schema.mjs +13 -3
- package/manifest.mjs +27 -8
- package/mcp/transport-http.mjs +52 -4
- package/mcp/transport-stdio.mjs +33 -8
- package/mcp.mjs +13 -5
- package/memory/code-search.mjs +5 -3
- package/memory/core.mjs +6 -4
- package/memory/docs.mjs +5 -3
- package/memory/schema.mjs +2 -0
- package/package.json +1 -1
- package/prompts/advisor-design.md +3 -2
- package/prompts/advisor-round1.md +1 -0
- package/prompts/advisor-round2.md +8 -3
- package/prompts/advisor-round3.md +7 -2
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +3 -1
- package/provider/core.mjs +10 -4
- package/provider/google.mjs +18 -2
- package/provider/normalize.mjs +57 -0
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session.mjs +13 -2
- package/stream-destroy.mjs +62 -0
- package/tool-docs/verify.md +1 -1
- package/tools/edit-batch.mjs +7 -2
- 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
|
-
*
|
|
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(
|
|
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
|
/**
|
package/mcp/transport-http.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
292
|
+
// D-MC18:会话过期未修复(sessionDead)⇒ 判死——ensureAlive 据此走重连。
|
|
293
|
+
isAlive: () => !closed && !sessionDead && (eventSource != null || postOnly),
|
|
294
|
+
onDead, markPostOnly, setInitPayload,
|
|
247
295
|
}
|
|
248
296
|
}
|
package/mcp/transport-stdio.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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 {
|
|
33
|
+
try { spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }) } catch { /* best effort */ }
|
|
16
34
|
return
|
|
17
35
|
}
|
|
18
|
-
|
|
19
|
-
child.
|
|
20
|
-
|
|
21
|
-
|
|
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}`)
|
package/memory/code-search.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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
|
|
16
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
// 帮用户快速分辨"配错平台还是配错账号"。
|
package/provider/google.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
}
|