@thincoder/core 0.9.4 → 0.9.5

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 (52) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/advisor/loop.mjs +2 -2
  3. package/advisor/run.mjs +1 -1
  4. package/agent/dispatch.mjs +21 -18
  5. package/agent/family-tools.mjs +6 -6
  6. package/agent/helpers.mjs +12 -2
  7. package/agent/run-stages.mjs +5 -5
  8. package/agent/suspension.mjs +1 -1
  9. package/agent-tools/advisor-async.mjs +2 -2
  10. package/agent-tools/advisor-settle.mjs +5 -5
  11. package/agent-tools/advisor.mjs +3 -3
  12. package/agent-tools/async-settle.mjs +2 -2
  13. package/agent-tools/batch-lifecycle.mjs +8 -16
  14. package/agent-tools/batch-paths.mjs +142 -0
  15. package/agent-tools/batch.mjs +10 -54
  16. package/agent-tools/consult.mjs +2 -2
  17. package/agent-tools/escalate-async.mjs +2 -2
  18. package/agent-tools/panel-blocks.mjs +1 -1
  19. package/agent-tools/subagent-actions.mjs +21 -21
  20. package/agent-tools/subagent-async.mjs +11 -11
  21. package/agent-tools/subagent-panel.mjs +5 -5
  22. package/agent-tools/subagent-run.mjs +8 -8
  23. package/agent-tools/subagent-scheduler.mjs +1 -1
  24. package/agent-tools/subagent-spawn.mjs +13 -12
  25. package/agent-tools/subagent.mjs +13 -13
  26. package/agent.mjs +13 -7
  27. package/config-presets.mjs +1 -1
  28. package/git/checkpoint.mjs +4 -8
  29. package/ledger-db.mjs +25 -5
  30. package/ledger-migrate.mjs +299 -0
  31. package/ledger.mjs +2 -1
  32. package/model-specs.mjs +118 -62
  33. package/package.json +1 -1
  34. package/peer-claims.mjs +263 -0
  35. package/peer-domains.mjs +93 -60
  36. package/prompts/common.md +4 -0
  37. package/prompts/persona-coder.md +1 -1
  38. package/prompts/persona-eng-coder.md +2 -2
  39. package/prompts/persona-eng-designer.md +2 -2
  40. package/prompts/persona-engineering.md +9 -0
  41. package/prompts/persona-explore.md +1 -1
  42. package/prompts/persona-normal.md +9 -0
  43. package/prompts/persona-plan.md +3 -3
  44. package/session-gc.mjs +67 -6
  45. package/session-slots-manifest.mjs +5 -0
  46. package/tool-docs/question.md +1 -0
  47. package/tools/edit-batch.mjs +6 -7
  48. package/tools/edit-diff.mjs +29 -2
  49. package/tools/file.mjs +7 -3
  50. package/tools/glob-dialect.mjs +3 -3
  51. package/tools/search.mjs +2 -2
  52. package/tools/shared.mjs +1 -1
package/model-specs.mjs CHANGED
@@ -17,7 +17,6 @@
17
17
  * partialMode: Kimi/Qwen Partial Mode truncation continuation (assistant message with partial:true)
18
18
  * prefixMode: DeepSeek Prefix Completion truncation continuation (uses /beta endpoint, with prefix:true)
19
19
  * multimodal: whether multimodal (image/vision input supported)
20
- * cacheMode: context caching mode: "auto"=automatic / "prompt"=needs explicit / "none"=unsupported
21
20
  * thinkApi: thinking API type: "type"=thinking.type field / "effort"=reasoning_effort field
22
21
  * thinkEnabledValue: when thinkApi is "type", the value used to enable thinking (default "enabled"; MiniMax uses "adaptive")
23
22
  * reasoningEcho: reasoning_content cross-turn echo strategy: "required"=must echo (error if missing) / "optional"=echo optional (default: don't echo)
@@ -30,67 +29,94 @@ const MODEL_SPECS = [
30
29
  // beta, automatic disk cache, vision. The two legacy names below are RETIRED — the server
31
30
  // still accepts them and serves them from V4.1-Flash today (no switch window), so their
32
31
  // rows carry the V4.1-Flash parameters (v4-flash gains multimodal).
33
- ["deepseek-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
32
+ ["deepseek-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
34
33
  // deepseek-v4.1-flash = qwen-plan channel name for DeepSeek V4.1-Flash (token-plan GET /models verified 2026-09-15) — same fields as the row above; ".1" ≠ "-" so plain prefix lookup misses the existing rows (D-PR25)
35
- ["deepseek-v4.1-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
36
- // deepseek-v4-pro (V4-Pro-0813): fields unchanged — vision capability NOT verified, so no
37
- // multimodal (conservative). From 2026-09-14 12:00 Beijing all requests route to V4.1-Flash.
38
- ["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
34
+ ["deepseek-v4.1-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
35
+ // deepseek-v4-pro (V4-Pro-0813): fields unchanged — 视觉面 **未探**(不声明 multimodal——保守;
36
+ // 补探登记见 MODEL-SPECS.md §13.4). From 2026-09-14 12:00 Beijing all requests route to V4.1-Flash.
37
+ ["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
39
38
  // deepseek-v4-flash: RETIRED name — still accepted, served by V4.1-Flash today → aligned row
40
- ["deepseek-v4-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
39
+ ["deepseek-v4-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
41
40
  // deepseek-v4-flash-vision-exp: retired experimental vision name — still accepted (V4.1-Flash)
42
- ["deepseek-v4-flash-vision-exp", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
41
+ ["deepseek-v4-flash-vision-exp", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
43
42
  // Kimi series
44
- ["kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
43
+ ["kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
45
44
  // Qwen router prefixes model IDs with provider namespace: kimi/kimi-k3 → kimi-k3 (IK7K4V)
46
- ["kimi/kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
45
+ ["kimi/kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
47
46
  // Kimi For Coding endpoint uses the short model ID "k3" (same specs as kimi-k3) — IK5VGJ
48
- ["k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
47
+ ["k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
48
+ // kimi-for-coding / k3-256k / kimi-for-coding-highspeed(Kimi For Coding 端点三名 · 2026-09-25 批 §14.2 #7-9 · 活测 = §1.4 服务端 200 /models):
49
+ // context = **活测**逐行;maxOutput = **族沿用 k3 行**(点名来源行——本注即标级);effort 枚举 ["low","high","max"] = **活测**,
50
+ // 默认档 max(kimi-for-coding)/ high(k3-256k)落端差表;机制位(partialMode / thinkApi / reasoningEcho)**族沿用 k3 行**
51
+ // (有则沿、无则不声明);能力位按 §1.4 现盘口径(image ✓ ⇒ multimodal ✓)。highspeed = 尺寸行 + image ✓(§1.4)——
52
+ // 活测**无 effort 块** ⇒ thinking / 枚举 / 默认档均不声明;与 kimi-k2.7-code-highspeed(128_000)分值分行(防合并)。
53
+ ["kimi-for-coding-highspeed", { context: 262_144, maxOutput: 131_072, multimodal: true }],
54
+ ["kimi-for-coding", { context: 1_048_576, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
55
+ ["k3-256k", { context: 262_144, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
56
+ // kimi-k2.6 / kimi-k2.7-code / kimi-k2.7-code-highspeed(三行同形 · 2026-09-24 bench 名单批):temperature **仅受理 1** = **校验级**
57
+ // (400 原文「invalid temperature: only 1 is allowed for this model」· roster-expand §2.2);尺寸位 = **未取证**
58
+ // (沿现盘兜底值——建行不改变生效值);**不得声明 tempRange**(KD-31 例外档不变量:入档值 = 实发值);枚举 / 视觉面 = **未探**。
59
+ ["kimi-k2.6", { context: 128_000, maxOutput: 32_000 }],
60
+ ["kimi-k2.7-code", { context: 128_000, maxOutput: 32_000 }],
61
+ ["kimi-k2.7-code-highspeed", { context: 128_000, maxOutput: 32_000 }],
49
62
  // GLM series
50
63
  // GLM-5.3: thinking always-on (no "disabled"); effort converges to low/high/max — NOT the
51
- // 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08)
52
- ["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
53
- ["glm-5.3-flash", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
64
+ // 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08).
65
+ // 视觉面 **未探**(不声明 multimodal——保守;补探登记见 MODEL-SPECS.md §13.4)。
66
+ ["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
67
+ ["glm-5.3-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
54
68
  // glm-5.3-flashx(GLM-5.3-FlashX,2026-09 上线)——独立行(不再蹭 flash 前缀行):maxOutput 131_072 =
55
69
  // **校验级**(400「max_tokens…限制数值范围[1,131072]」);effort 枚举 ["low","high","max"] = **校验级**
56
70
  // (400 原文点名:low/high/max 受理,其余拒绝)。multimodal = **实测**(8×8 纯红 PNG → 答「红色」)。
57
71
  // thinking = **实测**且**始终思考**(`thinking:{type:"disabled"}` → 400;裸请求默认开)——off 动作在
58
72
  // UI 侧为 no-op(服务端无关闭路径)。其余 = **族沿用** flash 行(context 网络口径未探边;
59
- // cacheMode / thinkApi / reasoningEcho / tempRange / noUsageStream = 前缀命中路径日常在用)。
60
- // flash 行 128_000 系其自身口径,不得对齐本行(AC-3 零回归)。
61
- ["glm-5.3-flashx", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
62
- ["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
63
- ["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
64
- ["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
73
+ // thinkApi / reasoningEcho / tempRange / noUsageStream = 前缀命中路径日常在用)。
74
+ // flash 行 maxOutput 131_072 = **校验级**(#20 批 §1.2 同源 · 2026-09-25 批 §14.2 #1 收正)——两行独立(改一行不动另一行)。
75
+ ["glm-5.3-flashx", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
76
+ ["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
77
+ ["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
78
+ ["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
79
+ // glm-4.5-air(智谱轻档 · 2026-09-24 bench 名单批):temperature 0 受理 = **实测**(roster-expand §2.2);其余 = **族沿用 glm-4 行**
80
+ // (逐名官方口径未取证);枚举 / 视觉面 = **未探**(不声明)。medium 直发实测受理(batch-params-judge §1.5)。
81
+ ["glm-4.5-air", { context: 128_000, maxOutput: 32_000, thinking: true, thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
65
82
  // GPT series
66
- ["gpt-5.6-sol", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
67
- ["gpt-5.6", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
68
- ["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false, cacheMode: "prompt" }],
69
- ["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
83
+ ["gpt-5.6-sol", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true }],
84
+ ["gpt-5.6", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true }],
85
+ ["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false }],
86
+ ["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true }],
70
87
  // Qwen series
71
- // qwen3.7-max rejects image parts outright (DashScope 400 "Unexpected item type in content") — text-only
72
- ["qwen3.7-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
88
+ // qwen-flash / qwen-vl-max(2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧实测):**尺寸行**——maxOutput 32_768 = **实测**
89
+ // (服务端直报「Range of max_tokens should be [1, 32768]」;131072 / 262144 均 400)。context 未取到 ⇒ 零口径——取兜底值
90
+ // 128_000(与建行前生效值同值,§14.3 兜底语义续效);thinking / 视觉有读数(默认关可开 / 不支持)但不落字段(§14.3 五名口径)。
91
+ ["qwen-flash", { context: 128_000, maxOutput: 32_768 }],
92
+ ["qwen-vl-max", { context: 128_000, maxOutput: 32_768 }],
93
+ // qwen3.7-max:**实测拒图像**(DashScope 400 "Unexpected item type in content"——真值彩图探针,2026-09-20)
94
+ // ⇒ 无视觉 = **不声明**(T-7 语义单一:multimodal 仅 true / undefined;补探已毕,无需再探)
95
+ ["qwen3.7-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
73
96
  // qwen3.7-flash / qwen3.8-flash (2026-09-20): thinking + effort enum + maxOutput + image intake
74
97
  // are server-MEASURED (batch 2026-09-20-qwen-flash-specs §1.2 — bare request carries reasoning_tokens
75
98
  // 166/27; 400 literal lists the enum; max_tokens range [1, 131072]). 3.7-flash enum has NO "max"
76
99
  // (6 levels), 3.8-flash adds it (7); enum order = the server's literal order, first member "none"
77
100
  // (D-5) — never re-sort by strength. context = doc/web statement (1M class, not API-measured);
78
- // partialMode / cacheMode / thinkApi / tempRange = same-family carry-over (not independently
101
+ // partialMode / thinkApi / tempRange = same-family carry-over (not independently
79
102
  // measured on these rows); reasoningEcho stays undeclared (cross-turn echo unverified — R-4).
80
- ["qwen3.7-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
81
- ["qwen3.8-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], tempRange: [0, 2] }],
82
- ["qwen3.8-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
103
+ ["qwen3.7-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
104
+ // qwen3.7-plus(2026-09-24 bench 名单批):context = **官方口径(网络转述)**(2026-09-20 qwen-flash-specs 批 AC-6 已裁允许入表——「100 万长文档」族点名);
105
+ // maxOutput / partialMode / thinkApi / tempRange / thinking = **族沿用 qwen3.7-flash 行**;枚举 / 视觉面 = **未探**(不声明)。
106
+ ["qwen3.7-plus", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, thinkApi: "effort", tempRange: [0, 2] }],
107
+ ["qwen3.8-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], tempRange: [0, 2] }],
108
+ ["qwen3.8-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
83
109
  // qwen3.8-omni-flash / qwen3.8-27b — parent 80-token probe (batch §1.8-①): reasoning_content
84
110
  // present, reasoning_tokens 49, `effort:"none"` removes it ⇒ thinking MEASURED; enum / maxOutput
85
111
  // / image intake measured the same way. omni-flash: context = doc/web statement (1M class, not
86
- // API-measured); partialMode / cacheMode / thinkApi / tempRange = same-family carry-over;
112
+ // API-measured); partialMode / thinkApi / tempRange = same-family carry-over;
87
113
  // reasoningEcho undeclared (cross-turn echo unverified — R-4). The server also takes audio, this
88
114
  // repo's call path sends images only ⇒ multimodal below = image intake (no `modalities` field, §2.7).
89
- ["qwen3.8-omni-flash",{ context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], tempRange: [0, 2] }],
115
+ ["qwen3.8-omni-flash",{ context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], tempRange: [0, 2] }],
90
116
  // 27b: context 262_144 = doc/web statement, the native value (1M is YaRN extrapolation, local
91
- // deploy only); partialMode / cacheMode / thinkApi / tempRange same-family carry-over; reasoningEcho
117
+ // deploy only); partialMode / thinkApi / tempRange same-family carry-over; reasoningEcho
92
118
  // undeclared (cross-turn echo unverified — R-4).
93
- ["qwen3.8-27b", { context: 262_144, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], tempRange: [0, 2] }],
119
+ ["qwen3.8-27b", { context: 262_144, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], tempRange: [0, 2] }],
94
120
  // No generic "qwen" fallback row: unlisted qwen names deliberately resolve to DEFAULT_SPEC
95
121
  // (128K / 32K, no vision) — per-name disposition in docs/core/design/MODEL-SPECS.md §2.4.
96
122
  // Do not re-add a blanket prefix row (silent inheritance is how missing rows stayed invisible).
@@ -100,41 +126,54 @@ const MODEL_SPECS = [
100
126
  // ≠ 3.8 系七档);maxOutput 65_536 = **校验级**(400「Range of max_tokens should be [1, 65536]」;
101
127
  // 65536 → 200)。thinking / multimodal = **实测**(裸请求 rc=143–239 默认开、none/minimal rc=0 可关;
102
128
  // 64×64 纯红 PNG 五名受理)。context = **官方口径**(未探边,文档口径非 API 实测);partialMode /
103
- // cacheMode / thinkApi / tempRange = **族沿用**(qwen3.7/3.8 行先例);reasoningEcho 不声明
129
+ // thinkApi / tempRange = **族沿用**(qwen3.7/3.8 行先例);reasoningEcho 不声明
104
130
  // (跨轮回声未验——R-4)。日期戳快照名不登——前缀命中母名行(有意继承)。
105
- ["qwen3.6-flash", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
131
+ ["qwen3.6-flash", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
106
132
  // qwen3.6-plus:同形行(共享证据块见 flash 行上)——校验级 maxOutput / 六档枚举、官方口径 context、族沿用机制位。
107
- ["qwen3.6-plus", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
133
+ ["qwen3.6-plus", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
108
134
  // qwen3.6-max-preview:同形行——校验级 maxOutput / 六档枚举、官方口径 context、族沿用机制位;视觉通道实测 ✓ 但纯红图答「黑色」(抽样波动,如实记)。
109
- ["qwen3.6-max-preview", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
135
+ ["qwen3.6-max-preview", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
110
136
  // qwen3.6-27b:同形行——校验级 maxOutput / 六档枚举、官方口径 context(未探边)、族沿用机制位。
111
- ["qwen3.6-27b", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
137
+ ["qwen3.6-27b", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
138
+ // qwen3.5-27b(2026-09-24 bench 名单批):context 262_144 = **官方口径(定价页分档佐证——[128K, 256K) 档在售;规格页未逐名核)**;
139
+ // maxOutput / 机制位 = **族沿用 qwen3.6-27b 行**;枚举 / 视觉面 = **未探**(不声明)。
140
+ ["qwen3.5-27b", { context: 262_144, maxOutput: 65_536, thinking: true, partialMode: true, thinkApi: "effort", tempRange: [0, 2] }],
112
141
  // qwen3.6-35b-a3b:MoE 后缀(a3b = 激活参数量级)——校验级 maxOutput / 六档枚举、官方口径 context、族沿用机制位。
113
- ["qwen3.6-35b-a3b", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
142
+ ["qwen3.6-35b-a3b", { context: 1_000_000, maxOutput: 65_536, thinking: true, partialMode: true, multimodal: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh"], tempRange: [0, 2] }],
114
143
  // MiniMax series
115
- ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
144
+ // effort(`reasoning_effort`)在 MiniMax 族 = **无生效面**(**实测**:effort 被忽略——`none` / `medium` 均 rc 在场;
145
+ // off 路径 = `thinking:{type:"disabled"}` ✓ rc=0)——medium 照发仅口径统一(batch-params-judge §1.2 / KD-32④)。
146
+ ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
116
147
  // MiMo series (Xiaomi — OpenAI-compatible https://api.xiaomimimo.com/v1; deep thinking via
117
148
  // thinking.type, default ON). Family echo policy stays conservative ("required" — tool rounds
118
149
  // always echo); 2026-09-22 re-probe: value / missing field / empty string all 200 — the
119
150
  // 2026-09-20 "must be passed back" 400 was NOT reproduced. v2.5 rows aligned 2026-09-22:
120
- // maxOutput 131_072 = **校验级**; cacheMode "auto" = **实测** (2nd same-prefix round cached 18,816).
121
- ["mimo-v2.5-pro", { context: 1_000_000, maxOutput: 131_072, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
122
- ["mimo-v2.5", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
151
+ // maxOutput 131_072 = **校验级**.
152
+ ["mimo-v2.5-pro", { context: 1_000_000, maxOutput: 131_072, thinking: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
153
+ ["mimo-v2.5", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
123
154
  // MiMo V2.6 series (2026-09-22 launch — three independent rows, each probed individually).
124
155
  // maxOutput 131_072 / tempRange [0, 1.5] = **校验级** (400 "at most 131072 completion tokens" /
125
156
  // "temperature must be within [0, 1.5]", per model); thinking = **实测** (bare request carries
126
157
  // reasoning_content; thinking.type disabled → rc gone ⇒ thinkApi "type" = measured face) and
127
- // multimodal = **实测** (8×8 pure-red PNG, pro / flash answered "Red"); cacheMode "auto" = **实测**
128
- // (cached_tokens 18,688). context 1_000_000 = **官方口径** (docs); reasoningEcho = **族沿用**.
129
- ["mimo-v2.6-pro", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
158
+ // multimodal = **实测** (8×8 pure-red PNG, pro / flash answered "Red"); context 1_000_000 = **官方口径**
159
+ // (docs); reasoningEcho = **族沿用**.
160
+ // effort(`reasoning_effort`)在三款 V2.6 = **无生效面**(**实测**:effort 被忽略——`none` / `medium` 均 rc 在场;
161
+ // off 路径 = `thinking:{type:"disabled"}` ✓ rc=0)——medium 照发仅口径统一(batch-params-judge §1.2 / KD-32④)。
162
+ ["mimo-v2.6-pro", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
130
163
  // mimo-v2.6-flash: same row shape as pro — maxOutput 131_072 / tempRange [0, 1.5] = **校验级**,
131
- // thinking / multimodal / cacheMode = **实测**, context = **官方口径**, reasoningEcho = **族沿用**.
132
- ["mimo-v2.6-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
164
+ // thinking / multimodal = **实测**, context = **官方口径**, reasoningEcho = **族沿用**.
165
+ ["mimo-v2.6-flash", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
133
166
  // mimo-v2.6-pro-ultraspeed: same row shape — same grades as flash (**校验级** / **实测** /
134
167
  // **官方口径** / **族沿用**); image answer at a 64-token budget was truncated — "Red" at 512.
135
- ["mimo-v2.6-pro-ultraspeed", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
136
- ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
137
- ["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto", noUsageStream: true }],
168
+ ["mimo-v2.6-pro-ultraspeed", { context: 1_000_000, maxOutput: 131_072, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
169
+ // minimax-m3(小写前缀行——与上方 MiniMax-M3 行同值,同族两写法):effort 同按**无生效面**处置
170
+ // (**实测**:effort 被忽略;off 路径 = `thinking:{type:"disabled"}` ✓ rc=0——batch-params-judge §1.2 / KD-32④)。
171
+ ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
172
+ ["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, noUsageStream: true }],
173
+ // MiniMax-M2.7(前缀行——覆盖标准档与 -highspeed · 2026-09-24 bench 名单批):thinking = **实测**(裸请求 rc 在场);off 路径 **未找到**
174
+ // (`thinking:{type:"disabled"}` 不生效——如实,不声明 thinkApi);effort 被忽略 ⇒ **无生效面**(枚举不声明;medium 照发仅口径统一);
175
+ // 尺寸 / noUsageStream = **族沿用 minimax-m1 行**;视觉面 = **未探**(不声明)。
176
+ ["MiniMax-M2.7", { context: 256_000, maxOutput: 128_000, thinking: true, noUsageStream: true }],
138
177
  // Grok series (xAI — OpenAI-compatible)
139
178
  // grok-4.x: 500K context per xAI Grok 4.6 spec (corrected 2026-08; earlier entries said 1M)
140
179
  ["grok-4.6", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
@@ -145,15 +184,21 @@ const MODEL_SPECS = [
145
184
  ["mistral-large", { context: 128_000, maxOutput: 32_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
146
185
  ["codestral", { context: 256_000, maxOutput: 32_000, thinking: false, tempRange: [0, 2] }],
147
186
  // Claude series (Anthropic)
148
- ["claude-opus-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
149
- ["claude-sonnet-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
150
- ["claude-opus-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
151
- ["claude-sonnet-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
152
- ["claude-3.5-haiku", { context: 200_000, maxOutput: 8_192, thinking: false, cacheMode: "none", format: "anthropic" }],
187
+ ["claude-opus-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, format: "anthropic" }],
188
+ ["claude-sonnet-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, format: "anthropic" }],
189
+ ["claude-opus-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, format: "anthropic" }],
190
+ ["claude-sonnet-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, format: "anthropic" }],
191
+ ["claude-3.5-haiku", { context: 200_000, maxOutput: 8_192, thinking: false, format: "anthropic" }],
192
+ // claude-fable-5.1(2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧交付):**尺寸行**——context 1_000_000 / maxOutput 128_000
193
+ // (多源网络转述级,本机无该渠道——行值 + 行注即标级);thinking「always-on adaptive」= 弱源读数 ⇒ 零口径不落字段(§14.3)。
194
+ ["claude-fable-5.1", { context: 1_000_000, maxOutput: 128_000 }],
153
195
  // Gemini series (Google)
154
- ["gemini-3-pro", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
155
- ["gemini-2.5-pro", { context: 2_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
156
- ["gemini-2.5-flash", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
196
+ ["gemini-3-pro", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, format: "google", noUsageStream: true }],
197
+ ["gemini-2.5-pro", { context: 2_000_000, maxOutput: 64_000, thinking: false, multimodal: true, format: "google", noUsageStream: true }],
198
+ ["gemini-2.5-flash", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, format: "google", noUsageStream: true }],
199
+ // gemini-3.1-pro(2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧交付):**尺寸行**——context 1_048_576 / maxOutput 65_536(多源
200
+ // 网络转述级,本机无该渠道——行值 + 行注即标级);thinking「有·计入输出预算」= 弱源读数 ⇒ 零口径不落字段(§14.3)。
201
+ ["gemini-3.1-pro", { context: 1_048_576, maxOutput: 65_536 }],
157
202
  // Tencent Hunyuan (TokenHub — 聚合网关 `https://tokenhub.tencentmaas.com/v1`;批 2026-09-20-channel-onboarding)
158
203
  // hy3:thinking / thinkApi / effort 枚举 = **本渠道实测**——裸请求 reasoning_content 在场(tok=16),
159
204
  // `reasoning_effort:"none"` 即消失(实测唯一有效 off 路径)。枚举 = 七值**受理级探针**(七值全 200 受理;
@@ -163,7 +208,7 @@ const MODEL_SPECS = [
163
208
  // **参考实配**(网关对 max_tokens 不硬拒 ⇒ 上限未证);context 256_000 = **网络口径**(他仓/文档 256K,未本渠道实测)。
164
209
  ["hy3", { context: 256_000, maxOutput: 128_000, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
165
210
  // hy3-preview(在场实测:/models + 401 前验活;能力位**未探针**)——**尺寸行**:仅登记尺寸位,取值 =
166
- // hy3 **同族沿用**(maxOutput 参考实配 / context 网络口径)。D-11:能力位不跨名沿用 ⇒ thinking / 枚举 / 视觉全不声明。
211
+ // hy3 **同族沿用**(maxOutput 参考实配 / context 网络口径)。D-11(2026-09-25 收窄现口径):能力位**无族据**一律不声明 ⇒ 该名无族据 ⇒ thinking / 枚举 / 视觉全不声明。
167
212
  ["hy3-preview", { context: 256_000, maxOutput: 128_000 }],
168
213
  // hy4-preview(在场实测:/models + 401;能力位**未探针**)——尺寸行:取值 = hy3 族**同族沿用**
169
214
  // (maxOutput 参考实配级取值 / context 网络口径;批次档 §1.2 未给该名尺寸行 ⇒ 上报清单 2,行注即标级)。
@@ -177,8 +222,19 @@ const MODEL_SPECS = [
177
222
  // seed-lite = 同族全针(批次档 §1.2 lite 行:思考 tok=161 / 视觉 "Red" / 上限 131 072 / echo 真名)——
178
223
  // context 同按 **官方口径** + **实测**下界标注;行独立(同值集 ≠ 同行:改一行不动另一行)。
179
224
  ["doubao-seed-2-0-lite-260428", { context: 256_000, maxOutput: 131_072, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"], multimodal: true }],
225
+ // doubao-seed-2-1 三档(火山方舟 · 2026-09-24 bench 名单批 · 三行同形):thinking / thinkApi / 枚举 = **实测**
226
+ // (裸请求 rc 在场;`reasoning_effort:"none"` → rc=0;七值全 200、乱值 400 泛化 ⇒ **受理级**——等级低于 2.0 行的
227
+ // 「真校验」,同值集不混级);maxOutput 524_288 = **受理级**(262 144 / 524 288 均受理;**上限未证**);
228
+ // context = **族沿用 2.0 行**(官方口径未逐名取证);**视觉面未探**(不声明——如实标级,不冒充实测)。
229
+ ["doubao-seed-2-1-pro-260915", { context: 256_000, maxOutput: 524_288, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
230
+ ["doubao-seed-2-1-turbo-260628", { context: 256_000, maxOutput: 524_288, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
231
+ ["doubao-seed-2-1-lite-260915", { context: 256_000, maxOutput: 524_288, thinking: true, thinkApi: "effort", reasoningEffortEnum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"] }],
232
+ // step-3.7-flash(StepFun · 2026-09-25 批 §14.2 #2-6 · §1.7.1 父侧交付):**尺寸行**——context 262_144(多源 256K =
233
+ // 网络转述级;渠道未激活——400 "product is not activated");maxOutput 未取到 ⇒ 零口径——取兜底值 32_000(与建行前
234
+ // 生效值同值);行键 = 裸名(渠道名录形 stepfun/step-3.7-flash——命名空间剥离同命中)。
235
+ ["step-3.7-flash", { context: 262_144, maxOutput: 32_000 }],
180
236
  ]
181
- const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
237
+ const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000 }
182
238
 
183
239
  /** Look up spec by model name prefix (case-insensitive), conservative default for unknown models.
184
240
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thincoder/core",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "ThinCoder shared core — shared mechanism modules + shared prompt content (CLI ↔ VS Code extension).",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,263 @@
1
+ /**
2
+ * peer-claims.mjs — R10 多实例协作 L3 **意图认领面**(claims · TTL 租约——MULTI-INSTANCE-COLLAB
3
+ * §4.4 / D-MI17–D-MI21)。
4
+ *
5
+ * 语义(§4.4.1–§4.4.3):结构化写工具执行成功 ⇒ 本实例登记认领——同 `peers/{sessionId}.json`
6
+ * 加 `claims` / `claimsUpdatedAt` 两字段,与足迹面 `domains` / `updatedAt` **分字段分存**
7
+ * (落盘 = 字段级合并写,互不改写,未知字段逐字保留);租约 `CLAIM_TTL_MS`,同域再写续约
8
+ * (`claimedAt` 保持首次;覆盖去冗 = 方向性 `covers` 三分支,集内无被覆盖项);落盘 = 新目标
9
+ * 即刻 + 续约节流 `CLAIM_RENEW_FLUSH_MS`(届内零 IO);原子写经 `writeSessionFile`
10
+ * (.tmp + rename——rename 翻 peers 目录 mtime ⇒ 对端聚合缓存失效,认领回合内可见)。
11
+ * 释放 = 租约到期 / 属主进程死亡 / 记录被死清理——**无显式释放面**(D-MI19)。
12
+ *
13
+ * 读面(§4.4.4):按 `expiresAt > now` 过滤 + 本实例落盘时剪除(不 unlink——过期不是崩溃
14
+ * 残留);属主死亡 ⇒ 整条记录不可见并随既有死清理删除。命中 = 目标 ∩ 他实例未过期认领
15
+ * (`claimHits`——调用面 = 既有写前预检同点、同一聚合扫描);软提示文案 `claimNoteText`
16
+ * (双端逐字一致;`age` / `left` / `who` 规则同 §4.4.4);去重集 `agent._peerNoted`
17
+ * (每(目标 × 认领属主)每 run 至多一行——`markClaimNoted` 在提示真正附加时落标记)。
18
+ *
19
+ * 本档另归口 peers 目录路径与路径谓词(`pathsOverlap` / `covers`——`peer-domains.mjs`
20
+ * re-export 保既有 import 面)。测试缝:`_setPeersDirForTest` / `_resetPeersDirForTest`
21
+ * (沙箱)+ `_setPeerClaimsTestImpl({ nowFn })`(时钟——续约 / 过期两侧零真实等待)。
22
+ */
23
+ import { existsSync, readFileSync } from "node:fs"
24
+ import { join } from "node:path"
25
+ import { configDir } from "./config.mjs"
26
+ import { normalizeCwd, getSessionId, END, manifestPath, writeSessionFile } from "./session-slots.mjs"
27
+
28
+ /** 认领租约(§4.4.2):30 min——同目标再写 / 目标被已有认领覆盖 ⇒ `expiresAt = now + 本值`。 */
29
+ export const CLAIM_TTL_MS = 30 * 60 * 1000
30
+
31
+ /** 续约落盘节流(§4.4.3):新目标即刻落盘;纯续约距上次落盘 ≥ 本窗才落盘,其余零 IO。 */
32
+ export const CLAIM_RENEW_FLUSH_MS = 60 * 1000
33
+
34
+ // ─── peers 目录(测试注入缝——形态先例 = 端侧同名缝)────────────────────────
35
+ let peersDirOverride = null
36
+ export function _setPeersDirForTest(dir) { peersDirOverride = dir }
37
+ export function _resetPeersDirForTest() { peersDirOverride = null }
38
+
39
+ export function peersDir() {
40
+ return peersDirOverride ?? join(configDir, "peers")
41
+ }
42
+
43
+ export function peerFilePath(sessionId) {
44
+ return join(peersDir(), `${sessionId}.json`)
45
+ }
46
+
47
+ // ─── 时钟缝(续约 / 过期两侧确定性断言——零真实等待)──────────────────────
48
+ let _testNowFn = null
49
+
50
+ export function _setPeerClaimsTestImpl({ nowFn = undefined } = {}) {
51
+ const prev = _testNowFn
52
+ _testNowFn = nowFn ?? null
53
+ return prev
54
+ }
55
+
56
+ export function _resetPeerClaimsTestImpl() {
57
+ _testNowFn = null
58
+ }
59
+
60
+ /** 认领面时钟(读面命中判据与落盘同一时钟——时钟缝注入时两侧确定一致;缺省 `Date.now`)。 */
61
+ export function claimsNow() {
62
+ return _testNowFn ? _testNowFn() : Date.now()
63
+ }
64
+
65
+ // ─── 路径谓词(自 peer-domains.mjs 归口——单源)────────────────────────────
66
+
67
+ function normPath(p) {
68
+ const n = process.platform === "win32" ? String(p).toLowerCase() : String(p)
69
+ return (process.platform === "win32" ? n.replace(/\//g, "\\") : n.replace(/\\/g, "/")).replace(/[\\/]+$/, "")
70
+ }
71
+
72
+ /** 路径重叠:a 与 b 相等或互为目录包含(file_ops 目录级操作 / delete 整目录语义——既有
73
+ * 语义原样:区分大小写按平台;分隔符归一)。本地规范式 = `normPath` 单源。 */
74
+ export function pathsOverlap(a, b) {
75
+ const pa = normPath(a)
76
+ const pb = normPath(b)
77
+ if (pa === pb) return true
78
+ const sepN = process.platform === "win32" ? "\\" : "/"
79
+ const prefix = (p, q) => p === q || p.startsWith(q + sepN)
80
+ return prefix(pa, pb) || prefix(pb, pa)
81
+ }
82
+
83
+ /** 方向性包含(覆盖去冗判据 §4.4.1):`coarse` 覆盖 `fine` = 相等或 coarse 为 fine 的目录
84
+ * **前缀**(单方向——对称谓词 `pathsOverlap` 不作包含判据)。归一 / 大小写同款。 */
85
+ export function covers(coarse, fine) {
86
+ const pc = normPath(coarse)
87
+ const pf = normPath(fine)
88
+ if (pc === pf) return true
89
+ return pf.startsWith(pc + (process.platform === "win32" ? "\\" : "/"))
90
+ }
91
+
92
+ // ─── 认领登记 / 落盘(写成功钩子——§4.4.3)────────────────────────────────
93
+
94
+ /** 认领状态载体 = `agent._peerClaims`(Map<target, {target, claimedAt, expiresAt}>,与足迹面
95
+ * `agent._peerWritten` 同型);落盘账 = `agent._peerClaimsFlushed`(Set<target>)+
96
+ * `agent._peerClaimsFlushedAt`(上次落盘时刻——续约节流锚)。 */
97
+
98
+ function pendingClaims(agent) {
99
+ return agent._peerClaims ?? (agent._peerClaims = new Map())
100
+ }
101
+
102
+ /** 过期剪除(§4.4.2——读面过滤之外的落盘面剪除):租约已结束的认领不参与覆盖判定,
103
+ * 也不占据续约对象;剪除同时清落盘账(该目标在盘上已失效 ⇒ 再认领视同新目标)。 */
104
+ function pruneClaims(agent, now) {
105
+ const set = pendingClaims(agent)
106
+ const flushed = agent._peerClaimsFlushed
107
+ for (const [target, claim] of [...set]) {
108
+ if (claim.expiresAt > now) continue
109
+ set.delete(target)
110
+ flushed?.delete(target)
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 认领登记 + 落盘决策(§4.4.3——写工具成功钩子调用;零新手工动作)。三分支(§4.4.1):
116
+ * 已有认领 `covers` 新目标 ⇒ 只续约;新目标 `covers` 已有认领项 ⇒ 替换(被覆盖项出集);
117
+ * 无包含关系 ⇒ 新目标入集。落盘:含未落盘目标 ⇒ 即刻整写;否则距上次落盘 ≥
118
+ * `CLAIM_RENEW_FLUSH_MS` ⇒ 续约落盘;其余零 IO。失败容忍(NF2——下次写重试)。
119
+ */
120
+ export function recordPeerClaims(agent, targets) {
121
+ try {
122
+ const now = claimsNow()
123
+ pruneClaims(agent, now)
124
+ const set = pendingClaims(agent)
125
+ for (const target of targets) {
126
+ const covering = [...set.values()].find((c) => covers(c.target, target))
127
+ if (covering) {
128
+ covering.expiresAt = now + CLAIM_TTL_MS // 续约(claimedAt 保持首次)
129
+ continue
130
+ }
131
+ for (const [t, c] of [...set]) {
132
+ if (covers(target, c.target)) {
133
+ set.delete(t)
134
+ agent._peerClaimsFlushed?.delete(t)
135
+ }
136
+ }
137
+ set.set(target, { target, claimedAt: now, expiresAt: now + CLAIM_TTL_MS })
138
+ }
139
+ if (set.size === 0) return
140
+ const flushed = agent._peerClaimsFlushed ?? (agent._peerClaimsFlushed = new Set())
141
+ const hasUnflushed = [...set.keys()].some((t) => !flushed.has(t))
142
+ const due = now - (agent._peerClaimsFlushedAt ?? 0) >= CLAIM_RENEW_FLUSH_MS
143
+ if (hasUnflushed || due) flushPeerClaims(agent, now)
144
+ } catch {
145
+ /* 登记失败不影响工具结果(工具主流程永不受认领面影响) */
146
+ }
147
+ }
148
+
149
+ /** 读既有实例文件(字段级合并写基线):缺失 / 损坏 / 结构非法 ⇒ `{}`(按缺失降级——
150
+ * 损坏件不删,本次落盘覆盖)。 */
151
+ export function readPeerRecord(file) {
152
+ try {
153
+ const rec = JSON.parse(readFileSync(file, "utf8"))
154
+ return rec && typeof rec === "object" && !Array.isArray(rec) ? rec : {}
155
+ } catch {
156
+ return {}
157
+ }
158
+ }
159
+
160
+ /** 认领落盘(§4.4.1 字段级合并写):只改 `claims` / `claimsUpdatedAt`——其余字段(含足迹面
161
+ * `domains` / `updatedAt`、身份字段、未知字段)逐字保留;身份字段仅在缺失时补齐(本面
162
+ * 不写他面字段)。原子写 = `writeSessionFile`(.tmp + rename)。落盘门控 = 本 cwd 会话
163
+ * manifest 在场(测试卫生——无头 / 未绑定会话不向真实 peers 目录写任何东西;端侧既有
164
+ * L3 门同法)。过期条目在落盘时剪除。 */
165
+ export function flushPeerClaims(agent, now = claimsNow()) {
166
+ try {
167
+ const cwd = agent?.cwd
168
+ if (!cwd || !existsSync(manifestPath(cwd))) return
169
+ const sessionId = getSessionId()
170
+ const base = readPeerRecord(peerFilePath(sessionId))
171
+ const claims = [...pendingClaims(agent).values()]
172
+ .filter((c) => c.expiresAt > now)
173
+ .map((c) => ({ target: c.target, claimedAt: c.claimedAt, expiresAt: c.expiresAt }))
174
+ .sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
175
+ const payload = { ...base }
176
+ if (payload.sessionId === undefined) payload.sessionId = sessionId
177
+ if (payload.pid === undefined) payload.pid = process.pid
178
+ if (payload.end === undefined) payload.end = END
179
+ if (payload.cwd === undefined) payload.cwd = normalizeCwd(cwd)
180
+ payload.claims = claims
181
+ payload.claimsUpdatedAt = now
182
+ writeSessionFile(peerFilePath(sessionId), payload)
183
+ agent._peerClaimsFlushed = new Set(claims.map((c) => c.target))
184
+ agent._peerClaimsFlushedAt = now
185
+ } catch {
186
+ /* NF2:失败容忍——下次写重试(落盘账不推进) */
187
+ }
188
+ }
189
+
190
+ // ─── 命中判据 / 文案 / 去重(§4.4.4)──────────────────────────────────────
191
+
192
+ /** 聚合载荷中解析出的认领项形态(`scanPeersDir` 消费——结构非法按缺失)。 */
193
+ export function parseClaims(raw) {
194
+ if (!Array.isArray(raw)) return []
195
+ const out = []
196
+ for (const c of raw) {
197
+ if (!c || typeof c !== "object") continue
198
+ if (typeof c.target !== "string" || c.target.length === 0) continue
199
+ const claimedAt = Number(c.claimedAt) || 0
200
+ const expiresAt = Number(c.expiresAt) || 0
201
+ if (expiresAt <= 0) continue
202
+ out.push({ target: c.target, claimedAt, expiresAt })
203
+ }
204
+ return out
205
+ }
206
+
207
+ /** 命中判据(§4.4.4):`peers`(聚合载荷——同 cwd / self 已排除 / 属主已判活)中各实例的
208
+ * **未过期**认领 ∩ `target` ⇒ `[{end, pid, sessionId, claimedAt, expiresAt}]`(同属主同
209
+ * 目标至多一条)。纯只读、不抛。 */
210
+ export function claimHits(peers, target, now) {
211
+ const out = []
212
+ for (const p of peers ?? []) {
213
+ const hit = (p.claims ?? []).find((c) => c.expiresAt > now && pathsOverlap(target, c.target))
214
+ if (hit) out.push({ end: p.end, pid: p.pid, sessionId: p.sessionId, claimedAt: hit.claimedAt, expiresAt: hit.expiresAt })
215
+ }
216
+ return out
217
+ }
218
+
219
+ /** 认领时长(§4.4.4):`just now`(< 1 min)/ `${n} min ago`。 */
220
+ export function claimAge(claimedAt, now) {
221
+ const min = Math.floor((now - claimedAt) / 60000)
222
+ return min < 1 ? "just now" : `${min} min ago`
223
+ }
224
+
225
+ /** 剩余租约(§4.4.4):`max(1, ceil((expiresAt − now) / 60000))`(分钟)。 */
226
+ export function claimLeft(expiresAt, now) {
227
+ return Math.max(1, Math.ceil((expiresAt - now) / 60000))
228
+ }
229
+
230
+ /** `who` 串(§4.4.4 逐字规则):认领属主 `${end} pid=${pid}`(无 end 则 `pid=${pid}`);
231
+ * 仅足迹命中的属主追加 ` (recent write)`;以 ", " 连接。 */
232
+ export function claimWho(owners) {
233
+ return owners
234
+ .map((o) => `${o.end ? `${o.end} ` : ""}pid=${o.pid}${o.recentWrite ? " (recent write)" : ""}`)
235
+ .join(", ")
236
+ }
237
+
238
+ /** 认领软提示文案(逐字契约 §4.4.4——双端一致;禁自行解释)。 */
239
+ export function claimNoteText(target, who, age, left) {
240
+ return `[peer-collab] ${target} — another live instance (${who}) holds a live intent claim on it (claimed ${age}, lease ${left} min left); concurrent edits may overwrite each other. Write not blocked — coordinate before proceeding.`
241
+ }
242
+
243
+ /** 去重键(§4.4.4):同一(目标 × 认领属主)每 run 至多一行——键 = `${target}\\u0000${sessionId}`。
244
+ * `||`(非 `??`)兜底:缺 `sessionId` 的载荷不得与空串共键(端侧聚合允许空串)。 */
245
+ export function claimNoteKey(target, owner) {
246
+ return `${target}\u0000${owner.sessionId || owner.pid}`
247
+ }
248
+
249
+ export function claimNoted(agent, key) {
250
+ return agent?._peerNoted instanceof Set && agent._peerNoted.has(key)
251
+ }
252
+
253
+ /** 落去重标记(调用方**在提示真正附加时**调用——写失败路径不消耗额度)。 */
254
+ export function markClaimNoted(agent, keys) {
255
+ if (!agent || !Array.isArray(keys) || keys.length === 0) return
256
+ const set = agent._peerNoted instanceof Set ? agent._peerNoted : (agent._peerNoted = new Set())
257
+ for (const k of keys) set.add(k)
258
+ }
259
+
260
+ /** 去重集清空(§4.4.4——落点 = `flushPeerDomains` 首步:先于「无写入即返回」早退)。 */
261
+ export function clearClaimNoted(agent) {
262
+ if (agent?._peerNoted instanceof Set) agent._peerNoted.clear()
263
+ }