@whyshu/argent-engine 0.7.1 → 0.7.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 (28) hide show
  1. package/MANIFEST.json +19 -20
  2. package/engine/VERSION +1 -1
  3. package/engine/core/contracts/argent.contracts.json +1 -1
  4. package/engine/scripts/audit-all.mjs +63 -0
  5. package/engine/scripts/capability-packs.lock.json +19 -19
  6. package/engine/scripts/collect-evidence.mjs +52 -12
  7. package/engine/scripts/criteria-table.mjs +57 -0
  8. package/engine/scripts/drill-clean-room.sh +55 -0
  9. package/engine/scripts/drill-handover.sh +47 -0
  10. package/engine/scripts/dsh-upgrade.mjs +1 -18
  11. package/engine/scripts/final-check.mjs +6 -0
  12. package/engine/scripts/flow-requirements.json +12 -8
  13. package/engine/scripts/flow_config.py +15 -1
  14. package/engine/scripts/handover-sheet.mjs +67 -0
  15. package/engine/scripts/lib/argent-datadir.sh +1 -1
  16. package/engine/scripts/orchestrator.py +91 -10
  17. package/engine/scripts/patches/retire-check.sh +0 -4
  18. package/engine/scripts/release-argent.sh +45 -0
  19. package/engine/scripts/session-handoff.mjs +111 -0
  20. package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +30 -0
  21. package/engine/version.mjs +1 -1
  22. package/package.json +1 -1
  23. package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +0 -297
  24. package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +0 -11
  25. package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +0 -20
  26. package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +0 -39
  27. package/engine/scripts/patches/verify-bundled-migrators.mjs +0 -79
  28. package/engine/scripts/patches/verify-transport-diagnostics.mjs +0 -227
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ // `session-handoff` —— 给老会话产出"续接摘要"(S4 收尾配套)
3
+ // 目的:老格式会话日志一旦退役/升级后打不开,"工作能续上"由**摘要**承担。
4
+ // 真实形状(2026-10-16 实测):header `{type:"session",version,id,createdAt,cwd,agentPreset}`;
5
+ // 事件 `{type,seq,time,data}`;用户输入在 `agent/inbox/spliced → data.inserted[].content[].text`。
6
+ // 用法:--scan | --write [--format v3|v4|all] [--limit N]
7
+ // ⛔ 只读日志、只写摘要目录;多帧 zstd 按 magic 切帧逐帧解压。
8
+ import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, existsSync } from 'node:fs'
9
+ import { join } from 'node:path'
10
+ import { homedir } from 'node:os'
11
+ import { zstdDecompressSync } from 'node:zlib'
12
+
13
+ const args = process.argv.slice(2)
14
+ const has = (f) => args.includes(f)
15
+ const val = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d }
16
+ const HOME = process.env.HOME || homedir()
17
+ const ROOT = join(HOME, '.dsh', 'sessions')
18
+ const OUT = join(HOME, '.dsh', 'argent', 'session-handoffs')
19
+ const FORMAT = val('--format', 'v3'); const LIMIT = Number(val('--limit', '0')) || 0; const WRITE = has('--write')
20
+ const DAYS = Number(val('--days', '10')) || 0 // 只处理最近 N 天内动过的主会话(0 = 全部)
21
+ const CHILDREN = has('--include-children') // 默认**排除**子会话(delegationDepth>0)
22
+ const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
23
+
24
+ function readText(file) {
25
+ const buf = readFileSync(file); const pos = []; let i = 0
26
+ while ((i = buf.indexOf(MAGIC, i)) !== -1) { pos.push(i); i += 4 }
27
+ if (!pos.length) return ''
28
+ pos.push(buf.length); let out = ''
29
+ for (let k = 0; k < pos.length - 1; k++) { try { out += zstdDecompressSync(buf.subarray(pos[k], pos[k + 1])).toString('utf8') } catch { /* 坏帧跳过 */ } }
30
+ return out
31
+ }
32
+ /** 递归找所有文本块(形状容错:content[]/inserted[]/text) */
33
+ function texts(node, acc = []) {
34
+ if (!node) return acc
35
+ if (typeof node === 'string') { if (node.trim()) acc.push(node.trim()); return acc }
36
+ if (Array.isArray(node)) { for (const x of node) texts(x, acc); return acc }
37
+ if (typeof node === 'object') {
38
+ if (typeof node.text === 'string' && node.text.trim()) acc.push(node.text.trim())
39
+ for (const k of ['content', 'inserted', 'message', 'parts', 'blocks', 'data']) if (node[k] !== undefined) texts(node[k], acc)
40
+ }
41
+ return acc
42
+ }
43
+ const clean = (s, n) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, n)
44
+ const isUser = (e) => /inbox|user/.test(String(e.type)) || (e.data && e.data.source && e.data.source.kind === 'user')
45
+
46
+ function summarize(file, id, project, ver) {
47
+ const lines = readText(file).split('\n').filter(Boolean)
48
+ let header = null, ts0 = null, ts1 = null, events = 0
49
+ const users = [], assistants = [], all = []
50
+ for (const l of lines) {
51
+ let e; try { e = JSON.parse(l) } catch { continue }
52
+ if (e.type === 'session') { header = e; continue }
53
+ events++
54
+ const t = e.time || (e.data && e.data.time)
55
+ if (t) { const iso = new Date(t).toISOString(); ts0 = ts0 || iso; ts1 = iso }
56
+ const blocks = texts(e.data ?? e)
57
+ if (!blocks.length) continue
58
+ const joined = clean(blocks.join(' | '), 600)
59
+ all.push(joined)
60
+ if (isUser(e)) users.push(joined); else assistants.push(joined)
61
+ }
62
+ const todos = all.join('\n').split(/||\n/).map((s) => s.trim()).filter((l) => /(^\s*-\s*\[ \])|TODO|待办|下一步|下一步是/.test(l)).slice(0, 10)
63
+ return { id, project, ver, file, bytes: statSync(file).size, created: header && header.createdAt ? new Date(header.createdAt).toISOString() : null,
64
+ ts0, ts1, events, turns: users.length + assistants.length, preset: (header && header.agentPreset) || '', cwd: (header && header.cwd) || '', depth: (header && header.delegationDepth) || 0,
65
+ title: clean(users[0], 140), lastUser: clean(users[users.length - 1], 400), lastAsst: clean(assistants[assistants.length - 1], 400), todos }
66
+ }
67
+
68
+ function walk(dir, out = []) {
69
+ let es = []; try { es = readdirSync(dir, { withFileTypes: true }) } catch { return out }
70
+ for (const e of es) {
71
+ const p = join(dir, e.name)
72
+ if (e.isDirectory()) walk(p, out)
73
+ else {
74
+ const m = e.name.match(/^session\.(v\d+)\.jsonl\.zstd$/)
75
+ if (m && (FORMAT === 'all' || m[1] === FORMAT)) out.push({ file: p, ver: m[1], id: (p.match(/session-([0-9a-f-]{8,})/) || [])[1] || p.split('/').slice(-2)[0].replace(/^session[.-]?/, '') || e.name, project: p.replace(ROOT + '/', '').split('/')[0] })
76
+ }
77
+ }
78
+ return out
79
+ }
80
+
81
+ const CUT = DAYS ? Date.now() - DAYS * 86400000 : 0
82
+ const walkAll = walk(ROOT)
83
+ const recent = DAYS ? walkAll.filter((x) => statSync(x.file).mtimeMs >= CUT) : walkAll
84
+ const all = recent.sort((a, b) => statSync(b.file).mtimeMs - statSync(a.file).mtimeMs)
85
+ const list = LIMIT ? all.slice(0, LIMIT) : all
86
+ const rows = []; let bad = 0
87
+ for (const it of list) { try { rows.push(summarize(it.file, it.id, it.project, it.ver)) } catch { bad++ } }
88
+ const kept = CHILDREN ? rows : rows.filter((r) => !r.depth)
89
+ for (const r of rows) if (!kept.includes(r)) { /* 子会话:跳过(--include-children 可纳入) */ }
90
+ const titled = kept.filter((r) => r.title).length
91
+ console.log(JSON.stringify({ ok: true, days: DAYS, candidates: all.length, childrenExcluded: CHILDREN ? 0 : rows.length - kept.length, kept: kept.length, format: FORMAT, unreadable: bad, withTitle: titled, write: WRITE }))
92
+ if (!WRITE) { console.log(JSON.stringify(kept.slice(0, 3).map((r) => ({ id: r.id.slice(0, 8), ver: r.ver, preset: r.preset, events: r.events, turns: r.turns, title: r.title.slice(0, 70) })), null, 1)); process.exit(0) }
93
+ mkdirSync(OUT, { recursive: true })
94
+ const slug = (s) => s.replace(/[^\w.-]/g, '_')
95
+ for (const r of kept) {
96
+ writeFileSync(join(OUT, `${slug(r.project)}__${r.id}.${r.ver}.md`), [
97
+ `# 会话续接摘要|${r.id}(${r.ver})`, '',
98
+ `- 项目:\`${r.project}\` cwd:\`${r.cwd}\` preset:\`${r.preset}\``,
99
+ `- 日志:\`${r.file}\`(${(r.bytes / 1024).toFixed(0)}KB,${r.events} 事件 / ${r.turns} 轮)`,
100
+ `- 时间:${(r.created || r.ts0 || '?').slice(0, 16).replace('T', ' ')} → ${(r.ts1 || '?').slice(0, 16).replace('T', ' ')}`, '',
101
+ '## 首轮需求(标题)', '', r.title || '(未提取到)', '',
102
+ '## 最后一轮用户输入', '', r.lastUser || '(未提取到)', '',
103
+ '## 最后一条助手输出', '', r.lastAsst || '(未提取到)', '',
104
+ '## 疑似待办 / 下一步(原文行)', '', ...(r.todos.length ? r.todos.map((t) => `- ${clean(t, 200)}`) : ['(未提取到)']), '',
105
+ '> ⚠️ 自动提取(best effort):日志退役后,续接以本摘要 + 仓库文档/提交为准。', ''].join('\n'))
106
+ }
107
+ writeFileSync(join(OUT, 'INDEX.md'), ['# 老会话续接摘要索引', '', `- 生成:${new Date().toISOString().slice(0, 16).replace('T', ' ')}`,
108
+ `- 范围:format=${FORMAT} | 近 ${DAYS} 天 | 本次 ${kept.length} 条(候选 ${all.length} 条)| 有标题 ${titled} 条`, '',
109
+ '| 会话 | 版本 | preset | 时间 | 标题 | 摘要 |', '|---|---|---|---|---|---|',
110
+ ...rows.map((r) => `| \`${r.id.slice(0, 8)}\` | ${r.ver} | ${r.preset} | ${(r.created || r.ts0 || '?').slice(0, 10)} | ${(r.title || '').replace(/\|/g, '/').slice(0, 56)} | [md](${slug(r.project)}__${r.id}.${r.ver}.md) |`), ''].join('\n'))
111
+ console.log(`✅ 已写 ${kept.length} 份摘要(有标题 ${titled})+ INDEX.md ⇒ ${OUT}`)
@@ -75,3 +75,33 @@
75
75
  - **口径变化(有意)**:新文案含「需运营…」⇒ 分类从 `needs_agent`(唤醒角色去猜)
76
76
  变为 `needs_human`(落卡等人、运营改商品)。⛔ 这是本事故要的结果,不是副作用。
77
77
 
78
+ ## 卡票先问 hub 真相;⛔ 别单步硬跑(2026-10-16 现场 79301358)
79
+
80
+ **症状**:lip 表里一张票 `FULL状态=失败`(如「步骤7 上传箱唛失败」),但门户侧其实**早就全做完了**
81
+ (运营手工做完 / 页面早已下载完标签),反复重跑仍旧失败。
82
+
83
+ **根因(两条,都要记住)**:
84
+ 1. **单步模式会绕过前置判定**:`orchestrator.py --mode step --step 7 …` 直接进步骤,**跳过了**
85
+ `step1_prepare` 里那句「**先问 hub 真相,再下结论**」——那句会按 hub-v2 三项
86
+ (预约已取消 ∧ 产品标签已下载 ∧ 箱唛已下载)判定「**运营手动完成**」并结票。
87
+ ⇒ 用单步"复现失败"会把一张**其实已完成**的票重新打成失败。
88
+ 2. **「Declara al menos」不一定是残留**:旧逻辑把点击 `Generar etiquetas` 之前就存在的该提示
89
+ 一律当"残留文案"⇒ 干等 30s 后以"平台侧异常"收口(**冤枉平台**,运营也拿不到可执行信息)。
90
+ KB-136 起改为:先读 checkbox 勾选进度 + **Andes 勾选态**(`#2/#3`),声明确实没做完时
91
+ 重勾重试、仍不行则以 `[declaration_incomplete] 已声明 N/M` 精确收口。
92
+ ⚠️ 注意 `checkbox_total` **包含我们故意跳过的 `#1`(bultos)**,所以"勾选数 < 总数"本身
93
+ **不是**声明未完成的证据(正常态就是 2/4)。
94
+
95
+ **正确处置顺序(⛔ 不要一上来就 `--mode step`)**:
96
+ ```bash
97
+ # ① 先问 hub 真相(只读探针;必须带 --store-name,否则会解析成别的店)
98
+ python3 scripts/orchestrator.py --mode hub-status --record-id <rec> --shipment-id <货件号> \
99
+ --store-name "<店铺名>"
100
+ # ② 若 operationally_complete=true ⇒ 认定「运营手动完成」并**置位**(2026-10-16 起同时写 FULL状态=已完成)
101
+ python3 scripts/orchestrator.py --mode hub-status --hub-write-note \
102
+ --record-id <rec> --shipment-id <货件号> --store-name "<店铺名>"
103
+ # ③ 门户/数据侧确实没做完 ⇒ 才进单步或定点续跑(有失败收口时,步骤7 现在会先自己问一次 hub)
104
+ ```
105
+
106
+ **卡住不动的票**(触发器不再认领):`flow_stale.py --flow <流程> [--requeue]` 回收;
107
+ **流程跑得动但不接票**:`flow_trigger.sh --flow <流程>` 手动一轮(与定时器同一路径)。
@@ -1 +1 @@
1
- export const VERSION = "0.7.1"
1
+ export const VERSION = "0.7.3"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whyshu/argent-engine",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Argent 引擎(L1):流程/台账/重试/守卫/审计/浏览器自动化 —— 逐字节来自 argent-dsh 仓库",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,297 +0,0 @@
1
- # requires-dsh: >=0.1.7 (TRANSPORT 可诊断性;判定语义不变,只加可观测字段)
2
- #
3
- # 目标:@deepseek-ai/dsh-llm-deepseek/lib/index.js(官方 Messages 适配器;**问述代理复用同一份**,
4
- # 见 plugins/argent-core/src/llm-whyshu.js —— 它只把 provider 注册成 `whyshu`)。
5
- #
6
- # 为什么(2026-10-02):`generate()` 的 catch 把"非空闲超时、非用户中断"的**一切**错误折叠成
7
- # `DeepSeek Messages transport failed` / TRANSPORT,`failure` 只剩 message+code
8
- # ⇒ 落到会话日志的 `llm/retry` 事件也只有这两项(实测 session-878f3e3b:6 次连败,事后完全
9
- # 分不清 ECONNRESET / 提前关闭 / 5xx 断流),且无法回答"这次到底产出了没有"。
10
- #
11
- # 本补丁做三件事(**都不改 TIMEOUT/ABORTED/TRANSPORT 三分支判定**):
12
- # ① 捕获 provider 侧请求标识(request-id / x-request-id / x-deepseek-request-id)与 HTTP 状态,
13
- # 并作为 `requestId` 塞进 LlmError.failure ⇒ 随 `llm/retry` 事件**落进会话日志**;
14
- # ② 统计已产出内容(chunks / text / reasoning / toolArgs / blockStarts / settled / anyOutput);
15
- # ③ 打两行 warn:`[argent-transport] ...` 单行摘要(可 grep)+ 结构化 `TRANSPORT detail %o`。
16
- #
17
- # 回滚:git checkout 该文件(或用 patch -R)。重启不必要(模块在下次进程启动时加载)。
18
- --- /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/index.js.orig
19
- +++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/index.js
20
- @@ -2061,6 +2061,191 @@
21
- var e = new Error(message);
22
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
23
- });
24
- +/**
25
- + * Argent hotfix (2026-10-02) — TRANSPORT 可诊断性(**只加可观测字段,不改判定语义**)。
26
- + *
27
- + * 事故:`generate()` 的 catch 把"非空闲超时、非用户中断"的**一切**错误折叠成
28
- + * `DeepSeek Messages transport failed` / TRANSPORT,`failure` 里只剩 message+code
29
- + * ⇒ 落到会话日志的 `llm/retry` 事件也只有这两项,事后完全分不清是
30
- + * 连接被对端重置(ECONNRESET)、提前关闭(socket hang up)、读超时(ETIMEDOUT)、
31
- + * 还是 5xx 断流;也无法回答"这次到底产出了没有"(决定能不能安全重试)。
32
- + * 下面这些纯函数只负责收集与格式化,**不参与** TIMEOUT/ABORTED/TRANSPORT 的分支判定。
33
- + */
34
- +let argentTransportCallSeq = 0;
35
- +/** 建一次请求的诊断累加器(每回合一个)。 */
36
- +function argentTransportDiagnostics(options, connection) {
37
- + return {
38
- + provider: options.provider,
39
- + model: options.model,
40
- + sessionId: options.sessionId === void 0 ? void 0 : String(options.sessionId),
41
- + purpose: options.purpose,
42
- + callId: `${Date.now().toString(36)}-${++argentTransportCallSeq}`,
43
- + startedAt: Date.now(),
44
- + signalAbortedAtStart: options.signal?.aborted === true,
45
- + idleTimeoutMs: connection.streamIdleTimeoutMs,
46
- + requestId: void 0,
47
- + httpStatus: void 0,
48
- + responseAt: void 0,
49
- + bodySnippet: void 0,
50
- + produced: {
51
- + chunks: 0,
52
- + blockStarts: 0,
53
- + blocksEnded: 0,
54
- + textChars: 0,
55
- + reasoningChars: 0,
56
- + toolCallChars: 0,
57
- + toolCallNames: [],
58
- + settled: false
59
- + },
60
- + lastChunkAt: void 0
61
- + };
62
- +}
63
- +/** 记一条已产出的流分片 —— "有没有输出" = `chunks>0` 且任一类计数非零。 */
64
- +function argentRecordProducedChunk(diagnostics, chunk) {
65
- + if (diagnostics === void 0 || chunk === null || typeof chunk !== "object") return;
66
- + const produced = diagnostics.produced;
67
- + produced.chunks += 1;
68
- + diagnostics.lastChunkAt = Date.now();
69
- + if (chunk.type === "text-delta") produced.textChars += typeof chunk.text === "string" ? chunk.text.length : 0;
70
- + else if (chunk.type === "reasoning-delta") produced.reasoningChars += typeof chunk.text === "string" ? chunk.text.length : 0;
71
- + else if (chunk.type === "tool-call-delta") {
72
- + produced.toolCallChars += typeof chunk.argumentsDelta === "string" ? chunk.argumentsDelta.length : 0;
73
- + if (typeof chunk.name === "string" && chunk.name.length > 0 && !produced.toolCallNames.includes(chunk.name)) produced.toolCallNames.push(chunk.name);
74
- + } else if (chunk.type === "block-start") produced.blockStarts += 1;
75
- + else if (chunk.type === "block-end") produced.blocksEnded += 1;
76
- + else if (chunk.type === "finish" || chunk.type === "usage") produced.settled = true;
77
- +}
78
- +/** `produced` 的摘要字段(写进日志,也用于"是否可安全重试"的判断依据)。 */
79
- +function argentProducedSummary(diagnostics) {
80
- + const produced = diagnostics?.produced;
81
- + if (produced === void 0) return { chunks: 0, textChars: 0, reasoningChars: 0, toolCallChars: 0, blockStarts: 0, blocksEnded: 0, toolCallNames: [], settled: false, anyOutput: false };
82
- + return {
83
- + chunks: produced.chunks,
84
- + textChars: produced.textChars,
85
- + reasoningChars: produced.reasoningChars,
86
- + toolCallChars: produced.toolCallChars,
87
- + blockStarts: produced.blockStarts,
88
- + blocksEnded: produced.blocksEnded,
89
- + toolCallNames: [...produced.toolCallNames],
90
- + settled: produced.settled,
91
- + anyOutput: produced.textChars + produced.reasoningChars + produced.toolCallChars + produced.blockStarts > 0
92
- + };
93
- +}
94
- +/** 从响应头取 provider 侧请求标识(与 `providerError()` 同一组头、同一优先级)。 */
95
- +function argentRequestIdOf(headers) {
96
- + if (headers === void 0 || headers === null || typeof headers.get !== "function") return void 0;
97
- + const id = headers.get("request-id") ?? headers.get("x-request-id") ?? headers.get("x-deepseek-request-id");
98
- + return id === null || id === void 0 || id.length === 0 ? void 0 : ProviderRequestId(id);
99
- +}
100
- +/** 响应一到就记下状态码与请求标识(**即便后面流断掉**,这些也已经拿到了)。 */
101
- +function argentNoteResponse(diagnostics, response) {
102
- + if (diagnostics === void 0) return;
103
- + diagnostics.responseAt = Date.now();
104
- + diagnostics.httpStatus = response.status;
105
- + const id = argentRequestIdOf(response.headers);
106
- + if (id !== void 0) diagnostics.requestId = id;
107
- +}
108
- +/** 记非 2xx 的响应体片段(判 5xx 断流 vs 网关错误页用;截断,避免日志被 HTML 淹没)。 */
109
- +function argentNoteHttpFailure(diagnostics, response, text) {
110
- + if (diagnostics === void 0) return;
111
- + diagnostics.httpStatus = response.status;
112
- + const id = argentRequestIdOf(response.headers);
113
- + if (id !== void 0) diagnostics.requestId = id;
114
- + diagnostics.bodySnippet = typeof text === "string" ? text.slice(0, 400) : void 0;
115
- +}
116
- +/**
117
- + * 把任意 error 折成**可读的 cause 形态**。覆盖:
118
- + * · `ECONNRESET` / `socket hang up` / `ETIMEDOUT` / `UND_ERR_*` —— Node fetch(undici) 的
119
- + * 连接层错误,**通常在 `error.cause` 上**(undici 会包一层 `TypeError: fetch failed` 或
120
- + * `TypeError: terminated`);
121
- + * · 非 2xx 状态码 + 响应体片段 —— provider 侧网关/上游错误;
122
- + * · `AbortError` —— 区分"空闲 watchdog 触发"(已归 TIMEOUT)与"其它中断"。
123
- + * @param error - catch 到的原始错误。
124
- + * @param depth - 递归深度(只跟 cause 链,最多 4 层)。
125
- + * @returns 平铺的 cause 事实(可直接 JSON 化)。
126
- + */
127
- +function argentDescribeTransportCause(error, depth = 0) {
128
- + const out = {
129
- + depth,
130
- + name: void 0,
131
- + message: void 0,
132
- + code: void 0,
133
- + errno: void 0,
134
- + syscall: void 0,
135
- + abort: false,
136
- + kind: "unknown"
137
- + };
138
- + if (error === null || error === void 0 || typeof error !== "object") {
139
- + out.message = String(error);
140
- + return out;
141
- + }
142
- + out.name = typeof error.name === "string" ? error.name : void 0;
143
- + out.message = typeof error.message === "string" ? error.message : String(error);
144
- + out.code = typeof error.code === "string" ? error.code : void 0;
145
- + out.errno = error.errno === void 0 ? void 0 : String(error.errno);
146
- + out.syscall = typeof error.syscall === "string" ? error.syscall : void 0;
147
- + out.abort = out.name === "AbortError" || out.code === "ABORT_ERR";
148
- + const text = `${out.name ?? ""} ${out.code ?? ""} ${out.message ?? ""}`;
149
- + if (out.abort) out.kind = "AbortError";
150
- + else if (out.code === "ECONNRESET" || /socket hang up|ECONNRESET/i.test(text)) out.kind = "ECONNRESET";
151
- + else if (out.code === "ETIMEDOUT" || /ETIMEDOUT|timed? ?out/i.test(text)) out.kind = "ETIMEDOUT";
152
- + else if (out.code === "ECONNREFUSED" || /ECONNREFUSED/i.test(text)) out.kind = "ECONNREFUSED";
153
- + else if (out.code === "EPIPE") out.kind = "EPIPE";
154
- + else if (out.code === "UND_ERR_SOCKET" || out.code === "ERR_STREAM_PREMATURE_CLOSE") out.kind = "EARLY_CLOSE";
155
- + else if (out.code === "UND_ERR_ABORTED") out.kind = "AbortError";
156
- + else if (out.code === "UND_ERR_CONNECT_TIMEOUT" || out.code === "UND_ERR_HEADERS_TIMEOUT" || out.code === "UND_ERR_BODY_TIMEOUT") out.kind = "ETIMEDOUT";
157
- + else if (typeof out.code === "string" && out.code.startsWith("UND_ERR")) out.kind = out.code;
158
- + else if (/terminated|premature close|other side closed/i.test(text)) out.kind = "EARLY_CLOSE";
159
- + else if (/fetch failed/i.test(text)) out.kind = "FETCH_FAILED";
160
- + return out;
161
- +}
162
- +/** 汇总本回合的全部诊断事实(一次调用产出日志行 + 结构化字段)。 */
163
- +function argentTransportReport(diagnostics, error, connection) {
164
- + const now = Date.now();
165
- + const cause = argentDescribeTransportCause(error?.cause ?? error);
166
- + const produced = argentProducedSummary(diagnostics);
167
- + const requestId = diagnostics?.requestId ?? (typeof error?.failure?.requestId === "string" ? error.failure.requestId : void 0);
168
- + const status = diagnostics?.httpStatus;
169
- + return {
170
- + provider: diagnostics?.provider,
171
- + model: diagnostics?.model,
172
- + sessionId: diagnostics?.sessionId,
173
- + purpose: diagnostics?.purpose,
174
- + callId: diagnostics?.callId,
175
- + requestId,
176
- + httpStatus: status,
177
- + produced,
178
- + elapsedMs: diagnostics === void 0 ? void 0 : now - diagnostics.startedAt,
179
- + msSinceLastChunk: diagnostics?.lastChunkAt === void 0 ? void 0 : now - diagnostics.lastChunkAt,
180
- + idleTimeoutMs: connection.streamIdleTimeoutMs,
181
- + cause,
182
- + causeChain: (() => {
183
- + const chain = [];
184
- + let node = error;
185
- + for (let i = 0; i < 4 && node !== null && node !== void 0 && typeof node === "object"; i += 1) {
186
- + chain.push(argentDescribeTransportCause(node, i));
187
- + node = node.cause;
188
- + }
189
- + return chain;
190
- + })(),
191
- + bodySnippet: diagnostics?.bodySnippet,
192
- + aborted: false
193
- + };
194
- +}
195
- +/** 单行、可 grep 的日志文本(`[argent-transport]` 前缀固定,便于 tail/grep 定位)。 */
196
- +function argentTransportLogLine(report) {
197
- + const p = report.produced;
198
- + return `[argent-transport] route=${report.provider ?? "?"}/${report.model ?? "?"}`
199
- + + ` callId=${report.callId ?? "?"} providerRequestId=${report.requestId ?? "(none)"}`
200
- + + ` httpStatus=${report.httpStatus ?? "(no response)"} cause=${report.cause.kind}`
201
- + + ` causeName=${report.cause.name ?? "?"} causeCode=${report.cause.code ?? "?"} causeErrno=${report.cause.errno ?? "?"}`
202
- + + ` causeMessage=${JSON.stringify(String(report.cause.message ?? "").slice(0, 200))}`
203
- + + ` produced={chunks:${p.chunks},text:${p.textChars},reasoning:${p.reasoningChars},toolArgs:${p.toolCallChars},blockStarts:${p.blockStarts},settled:${p.settled},anyOutput:${p.anyOutput}}`
204
- + + ` toolCalls=${JSON.stringify(report.produced.toolCallNames ?? [])}`
205
- + + ` elapsedMs=${report.elapsedMs ?? "?"} msSinceLastChunk=${report.msSinceLastChunk ?? "?"}`
206
- + + ` idleTimeoutMs=${report.idleTimeoutMs ?? "?"} sessionId=${report.sessionId ?? "(none)"}`
207
- + + (report.bodySnippet === void 0 ? "" : ` bodySnippet=${JSON.stringify(report.bodySnippet.slice(0, 200))}`);
208
- +}
209
- /** DeepSeek provider using Messages content and native thinking replay. */
210
- var DeepSeekAdapter = class extends LlmAdapter {
211
- dependencies;
212
- @@ -2111,20 +2296,32 @@
213
- try {
214
- const consumer = new AbortController();
215
- const watchdog = __addDisposableResource(env_1, idleWatchdog(options.signal === void 0 ? consumer.signal : AbortSignal.any([consumer.signal, options.signal]), connection.streamIdleTimeoutMs, "MESSAGES_IDLE"), false);
216
- + /* Argent hotfix (2026-10-02):TRANSPORT 诊断累加器(只观测,不参与判定)。 */
217
- + const transportDiagnostics = argentTransportDiagnostics(options, connection);
218
- const iterator = this.request(options, connection, watchdog.signal, () => {
219
- watchdog.pulse();
220
- - });
221
- + }, transportDiagnostics);
222
- try {
223
- while (true) {
224
- const next = await watchdog.next(iterator);
225
- if (next.done) return;
226
- + argentRecordProducedChunk(transportDiagnostics, next.value);
227
- yield next.value;
228
- }
229
- } catch (error) {
230
- if (timeoutOf(watchdog.signal, "MESSAGES_IDLE") !== void 0) throw new LlmError("DeepSeek Messages stream idle timeout", "TIMEOUT", { cause: error });
231
- if (options.signal?.aborted) throw new LlmError("DeepSeek Messages request aborted", "ABORTED", { cause: error });
232
- if (error instanceof LlmError) throw error;
233
- - throw new LlmError("DeepSeek Messages transport failed", "TRANSPORT", { cause: error });
234
- + /* Argent hotfix (2026-10-02):**判定不变**(仍是 TRANSPORT),只补可观测字段:
235
- + * ① 通过 onTransportFailure 打一行可 grep 的 cause/streamId/已产出长度;
236
- + * ② 把 provider 侧请求标识塞进 LlmError.failure ⇒ 随 llm/retry 事件**落进会话日志**
237
- + * (此前 failure 只有 message+code,事后无法复盘)。 */
238
- + const transportReport = argentTransportReport(transportDiagnostics, error, connection);
239
- + this.dependencies.onTransportFailure?.(transportReport);
240
- + throw new LlmError("DeepSeek Messages transport failed", "TRANSPORT", {
241
- + cause: error,
242
- + ...transportReport.requestId === void 0 ? {} : { requestId: transportReport.requestId }
243
- + });
244
- } finally {
245
- consumer.abort();
246
- try {
247
- @@ -2138,7 +2335,7 @@
248
- __disposeResources(env_1);
249
- }
250
- }
251
- - async *request(options, connection, signal, activity) {
252
- + async *request(options, connection, signal, activity, transportDiagnostics) {
253
- signal.throwIfAborted();
254
- const { messages, versions } = await prepareImages(options.messages, connection, options.model, this.dependencies.resolveAttachments?.(), this.imageAccess, signal);
255
- const auth = await this.dependencies.resolveAuth(connection);
256
- @@ -2197,8 +2394,12 @@
257
- ...options.purpose === "compaction" ? { "x-deepseek-harness-compact": "1" } : {}
258
- }
259
- });
260
- + /* Argent hotfix (2026-10-02):响应一到就记下状态码 + provider 请求标识
261
- + * ——**即便随后流被对端切断**,这两个也已拿到,是事后定位的唯一线索。 */
262
- + argentNoteResponse(transportDiagnostics, response);
263
- if (!response.ok) {
264
- const text = await response.text();
265
- + argentNoteHttpFailure(transportDiagnostics, response, text);
266
- let raw;
267
- try {
268
- raw = JSON.parse(text);
269
- @@ -2252,6 +2453,27 @@
270
- onExtensionsOmitted: ({ provider, model, fields, error }) => {
271
- ctx.logger.warn(`llm-deepseek: sending route "${provider}/${model}" without request extension fields ${fields.join(", ")} because they failed to serialize: %o`, error);
272
- },
273
- + /* Argent hotfix (2026-10-02):TRANSPORT 的可诊断性。
274
- + * 一次失败打两行:① 单行、可 grep 的摘要(cause/streamId/已产出长度);
275
- + * ② 结构化对象(`%o`)供 `dsh` 日志详情/后续工具解析。判定语义不变。 */
276
- + onTransportFailure: (report) => {
277
- + ctx.logger.warn(argentTransportLogLine(report));
278
- + ctx.logger.warn("llm-deepseek: TRANSPORT detail %o", {
279
- + route: `${report.provider ?? "?"}/${report.model ?? "?"}`,
280
- + callId: report.callId,
281
- + providerRequestId: report.requestId,
282
- + httpStatus: report.httpStatus,
283
- + produced: report.produced,
284
- + elapsedMs: report.elapsedMs,
285
- + msSinceLastChunk: report.msSinceLastChunk,
286
- + idleTimeoutMs: report.idleTimeoutMs,
287
- + cause: report.cause,
288
- + causeChain: report.causeChain,
289
- + bodySnippet: report.bodySnippet,
290
- + sessionId: report.sessionId,
291
- + purpose: report.purpose
292
- + });
293
- + },
294
- resolveAttachments: () => ctx.get("attachments"),
295
- resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
296
- prepareExtensions: (request) => ctx.get("deepseekLlmApiExtensions")?.prepare(request) ?? Promise.resolve({
297
-
@@ -1,11 +0,0 @@
1
- --- /tmp/dsh-fix-test/index.js.orig 2026-09-10 16:21:27
2
- +++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v0-to-v1/lib/index.js 2026-09-10 16:21:27
3
- @@ -116,7 +116,7 @@
4
- "retry"
5
- ]),
6
- "model/selection": disposition(["provider", "model"], ["reasoningEffort"]),
7
- - "permission/preset": disposition(["preset"]),
8
- + "permission/preset": disposition(["preset"], ["origin"]),
9
- "plan/mode": disposition(["active"]),
10
- "request/context": disposition(["provider", "model"], ["contextWindow"]),
11
- "request/header": disposition(["header", "reason"], ["startsSeries"]),
@@ -1,20 +0,0 @@
1
- # requires-dsh: >=0.1.7 (v3→v4 迁移器 0.1.7 新增;修复"turn 外替换型 tool/result"过严拒绝)
2
- --- /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v3-to-v4/lib/index.js.orig
3
- +++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v3-to-v4/lib/index.js
4
- @@ -617,7 +617,15 @@
5
- }
6
- tool(event, data) {
7
- if (event.type === "tool/result" && event["surfaceOp"] !== "append") {
8
- - this.requireTurn(event.type);
9
- + /* Hardening (2026-09-29, argent-dsh hotfix for 0.1.7): v3 允许"turn 结束后的替换型
10
- + tool/result"(编辑/重放场景:重写早先 turn 的工具结果,如 surface 自替换)。
11
- + 原 requireTurn 过严 → 整个 session 迁移失败('tool/result is outside an
12
- + open turn',实测 06732069/34c3e088 等)。现在:turn 外时校验 data.turn 指向
13
- + 已关闭的历史 turn(防真损坏,放行合法历史)。 */
14
- + if (this.turn === null) {
15
- + const t = data["turn"];
16
- + if (!Number.isSafeInteger(t) || t >= this.nextTurn) throw new SessionFormatError(`${event.type} is outside an open turn`);
17
- + }
18
- return;
19
- }
20
- this.requireStep(event, data);
@@ -1,39 +0,0 @@
1
- # requires-dsh: >=0.1.7 (迁移校验 worker 的**打包副本**——模块版 hotfix 打不到它,必须单独打)
2
- #
3
- # 背景(2026-09-29 事故,实例 session-06732069「Argent项目」):
4
- # v3→v4 迁移由 @deepseek-ai/dsh-session-persistence-jsonl 起 worker 线程做最终校验
5
- # (lib/worker.cjs,539KB 打包件,内联了一整份 v0→v1 与 v3→v4 的迁移器/校验器)。
6
- # 此前所有 hotfix 只打在 lib/index.js 这些**模块**上 ⇒
7
- # · 离线校验器(verify-session-migration.mjs)import 模块 → 一直假绿;
8
- # · 真实 resume 走 worker 的**旧副本** → 抛
9
- # SessionPersistenceCorruptionError: stored log is corrupt:
10
- # Error: tool/result is outside an open turn
11
- # ⇒ 前端发送消息弹白 toast、会话打不开。
12
- # 本补丁把两处 hotfix 同步进 worker 打包副本:
13
- # ① v3→v4 `tool()`:turn 结束后的**替换型** tool/result 在 v3 里合法(编辑/重放),
14
- # 原 requireTurn 过严 → 整个迁移失败;
15
- # ② v0 `permission/preset`:容忍旧数据里的 `origin` 字段(= 2026-09-10 hotfix 的镜像,
16
- # 此前同样只打在模块上)。
17
- --- /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/worker.cjs.orig
18
- +++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-persistence-jsonl/lib/worker.cjs
19
- @@ -6749,4 +6749,4 @@
20
- ]),
21
- "model/selection": disposition(["provider", "model"], ["reasoningEffort"]),
22
- - "permission/preset": disposition(["preset"]),
23
- + "permission/preset": disposition(["preset"], ["origin"]),
24
- "plan/mode": disposition(["active"]),
25
- @@ -11331,7 +11331,13 @@
26
- }
27
- tool(event, data) {
28
- if (event.type === "tool/result" && event["surfaceOp"] !== "append") {
29
- - this.requireTurn(event.type);
30
- + /* Hardening (2026-09-29, argent-dsh hotfix, mirrored into the bundled worker):
31
- + v3 allows a REPLACEMENT tool/result after its turn closed (edit/replay); the strict
32
- + requireTurn killed the entire migration. Same rule as the module patch. */
33
- + if (this.turn === null) {
34
- + const t = data["turn"];
35
- + if (!Number.isSafeInteger(t) || t >= this.nextTurn) throw new SessionFormatError(`${event.type} is outside an open turn`);
36
- + }
37
- return;
38
- }
39
- this.requireStep(event, data);
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * verify-bundled-migrators.mjs — 校验**打包副本**里的迁移器/校验器也带上了 hotfix。
4
- *
5
- * 由来(2026-09-29 事故,实例 session-06732069「Argent项目」):
6
- * `@deepseek-ai/dsh-session-persistence-jsonl` 在做 v3→v4 迁移时,会另起一个
7
- * **worker 线程**对迁移产物做最终校验,而那个 worker 是**打包件**
8
- * (`lib/worker.cjs`,~540KB,内联了一整份 v0→v1 与 v3→v4 的迁移器/校验器)。
9
- * `scripts/patches/*.patch` 历来只打在 `lib/index.js` 这些**模块**上 ⇒
10
- * · `verify-session-migration.mjs`(import 模块)**一直假绿**;
11
- * · 真实 resume 走 worker 的旧副本 → `SessionPersistenceCorruptionError:
12
- * stored log is corrupt: Error: tool/result is outside an open turn`
13
- * ⇒ 前端发消息弹 toast、会话打不开。
14
- *
15
- * 判据(文本级,诚实说明边界):worker 是打包件、无导出,无法直接做行为断言;
16
- * 这里用**紧贴语义的代码形状**断言两处 hotfix 是否在打包副本里:
17
- * A. v3→v4 `tool()`:turn 外的替换型 tool/result 必须走"校验 data.turn 指向已关闭
18
- * 历史 turn"的放宽分支,**不得**再是裸 `this.requireTurn(event.type);`。
19
- * B. v0 `permission/preset`:必须容忍旧数据里的 `origin` 键。
20
- * 任一不满足 → exit 1,提示先跑 `node scripts/dsh-upgrade.mjs --patches-only`。
21
- *
22
- * 用法:node scripts/patches/verify-bundled-migrators.mjs
23
- * 退出码:0 通过(或本机无该打包件,跳过)/ 1 失守
24
- */
25
- import { readFileSync, existsSync } from 'node:fs'
26
- import { requireDshScope } from '../lib/dsh-scope.mjs'
27
-
28
- // 0.6.0 F1 修复批(2026-10-14):探不到 scope ⇒ 响亮失败(⛔ 不用假路径 ⇒ 否则本脚本会
29
- // "本机无该打包件,跳过"地**假绿** —— 而它正是防"worker 打包副本漏打 hotfix"的那道门禁)。
30
- let SCOPE
31
- try { SCOPE = requireDshScope({ what: '打包副本校验(verify-bundled-migrators)' }) }
32
- catch (e) { console.error(` ❌ ${e.message}`); process.exit(1) }
33
- const WORKER = `${SCOPE}/dsh-session-persistence-jsonl/lib/worker.cjs`
34
-
35
- let pass = 0, fail = 0
36
- const ok = (c, n) => { c ? (pass++, console.log(` ✅ ${n}`)) : (fail++, console.log(` ❌ ${n}`)) }
37
-
38
- console.log('【打包副本】dsh-session-persistence-jsonl/lib/worker.cjs 的迁移器 hotfix')
39
-
40
- if (!existsSync(WORKER)) {
41
- console.log(` ⏭ 本机没有该打包件(${WORKER})——跳过(上游可能改了实现)`)
42
- process.exit(0)
43
- }
44
-
45
- const src = readFileSync(WORKER, 'utf8')
46
-
47
- // ── A. v3→v4 tool():放宽 turn 外的替换型 tool/result ──────────────────────────
48
- const toolAt = src.indexOf('\ttool(event, data) {')
49
- if (toolAt === -1) {
50
- ok(false, 'worker 里能找到 v3→v4 的 tool() 方法(上游结构可能已变,本门禁需同步)')
51
- } else {
52
- // 取该方法开头到 `this.requireStep(` 之间的片段(即 surfaceOp !== "append" 分支)
53
- const stepAt = src.indexOf('this.requireStep(event, data);', toolAt)
54
- const branch = stepAt === -1 ? src.slice(toolAt, toolAt + 1200) : src.slice(toolAt, stepAt)
55
- ok(/Hardening \(2026-09-29, argent-dsh hotfix, mirrored into the bundled worker\)/.test(branch),
56
- 'A1 打补丁标记存在(mirrored into the bundled worker)')
57
- ok(!/^\s*this\.requireTurn\(event\.type\);\s*$/m.test(branch),
58
- 'A2 已不再是裸 requireTurn(否则 turn 外替换型 tool/result 会否掉整个迁移)')
59
- ok(/const t = data\["turn"\];/.test(branch) && /t >= this\.nextTurn/.test(branch),
60
- 'A3 放宽分支在校验 data.turn 指向已关闭的历史 turn(不是无条件放行)')
61
- }
62
-
63
- // ── B. v0 permission/preset:容忍 origin ────────────────────────────────────
64
- ok(/"permission\/preset": disposition\(\["preset"\], \["origin"\]\)/.test(src),
65
- 'B1 v0 的 permission/preset 容忍旧数据的 origin 键(2026-09-10 hotfix 的镜像)')
66
- ok(!/"permission\/preset": disposition\(\["preset"\]\),/.test(src),
67
- 'B2 旧的严格 disposition 已不存在')
68
-
69
- // ── C. 共享的 requireTurn 仍应保持严格(放宽只针对替换型 tool/result)─────────
70
- ok(/requireTurn\(type\) \{\s*\n\s*if \(this\.turn === null\) throw new SessionFormatError\(`\$\{type\} is outside an open turn`\);/.test(src),
71
- 'C1 requireTurn 本身仍是严格的(放宽没有波及别的下游事件类型)')
72
-
73
- console.log(`\n── 合计:通过 ${pass} / 失败 ${fail}`)
74
- if (fail > 0) {
75
- console.log('❌ 打包副本未同步 hotfix —— 真实 resume 会否掉会话(前端发送消息报 toast)')
76
- console.log(' 修法:node scripts/dsh-upgrade.mjs --patches-only (必须重启前跑)')
77
- process.exit(1)
78
- }
79
- console.log('✅ 打包副本的迁移器 hotfix 已同步(模块 + worker 两侧一致)')