thincoder 0.12.62 → 0.12.63

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 (228) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +53 -27
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -379
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +1 -1
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +1 -1
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-config.mjs +8 -8
  26. package/src/tui/cmd-eng.mjs +25 -9
  27. package/src/tui/cmd-mcp.mjs +9 -8
  28. package/src/tui/cmd-model.mjs +1 -1
  29. package/src/tui/cmd-new.mjs +6 -5
  30. package/src/tui/cmd-reindex.mjs +1 -1
  31. package/src/tui/cmd-restore.mjs +2 -2
  32. package/src/tui/cmd-session.mjs +24 -1
  33. package/src/tui/cmd-skills.mjs +1 -1
  34. package/src/tui/cmd-think.mjs +22 -9
  35. package/src/tui/config-helpers.mjs +1 -1
  36. package/src/tui/display-budget.mjs +33 -11
  37. package/src/tui/index.mjs +9 -9
  38. package/src/tui/interaction.mjs +16 -7
  39. package/src/tui/key-modes.mjs +9 -4
  40. package/src/tui/ledger-surface.mjs +26 -10
  41. package/src/tui/model-catalog.mjs +4 -4
  42. package/src/tui/model-picker.mjs +8 -7
  43. package/src/tui/mouse.mjs +11 -6
  44. package/src/tui/pickers.mjs +15 -2
  45. package/src/tui/render-conversation.mjs +1 -1
  46. package/src/tui/render-frame.mjs +10 -5
  47. package/src/tui/render-loop.mjs +1 -1
  48. package/src/tui/render-segments.mjs +3 -1
  49. package/src/tui/slash-commands.mjs +1 -1
  50. package/src/tui/startup.mjs +14 -14
  51. package/src/tui/subagent-blocks.mjs +20 -3
  52. package/src/tui/subagent-freeze.mjs +73 -2
  53. package/src/tui/suspension-drive.mjs +46 -22
  54. package/src/tui/tool-events.mjs +11 -8
  55. package/src/tui/wizard.mjs +3 -3
  56. package/src/abort-provenance.mjs +0 -116
  57. package/src/advisor/citations.mjs +0 -139
  58. package/src/advisor/compaction.mjs +0 -174
  59. package/src/advisor/convergence.mjs +0 -80
  60. package/src/advisor/history.mjs +0 -77
  61. package/src/advisor/loop.mjs +0 -293
  62. package/src/advisor/messages.mjs +0 -299
  63. package/src/advisor/project-context.mjs +0 -194
  64. package/src/advisor/repos.mjs +0 -150
  65. package/src/advisor/run.mjs +0 -293
  66. package/src/advisor/truncate.mjs +0 -57
  67. package/src/advisor.mjs +0 -290
  68. package/src/agent/completion.mjs +0 -146
  69. package/src/agent/dispatch.mjs +0 -489
  70. package/src/agent/helpers.mjs +0 -384
  71. package/src/agent/post-turn.mjs +0 -70
  72. package/src/agent/record-results.mjs +0 -174
  73. package/src/agent/relay-prefix.mjs +0 -39
  74. package/src/agent/run-stages.mjs +0 -244
  75. package/src/agent/setup-reminders.mjs +0 -69
  76. package/src/agent/setup.mjs +0 -354
  77. package/src/agent/spawn-child.mjs +0 -243
  78. package/src/agent-tools/advisor-async.mjs +0 -346
  79. package/src/agent-tools/advisor-settle.mjs +0 -231
  80. package/src/agent-tools/advisor.mjs +0 -260
  81. package/src/agent-tools/async-settle.mjs +0 -204
  82. package/src/agent-tools/batch-segment.mjs +0 -195
  83. package/src/agent-tools/consult.mjs +0 -473
  84. package/src/agent-tools/design-token.mjs +0 -117
  85. package/src/agent-tools/digest-budget.mjs +0 -76
  86. package/src/agent-tools/eng.mjs +0 -67
  87. package/src/agent-tools/escalate-async.mjs +0 -295
  88. package/src/agent-tools/goal.mjs +0 -119
  89. package/src/agent-tools/plan.mjs +0 -81
  90. package/src/agent-tools/read-history.mjs +0 -309
  91. package/src/agent-tools/recent-changes.mjs +0 -24
  92. package/src/agent-tools/review-streak.mjs +0 -93
  93. package/src/agent-tools/settings.mjs +0 -265
  94. package/src/agent-tools/skill.mjs +0 -47
  95. package/src/agent-tools/subagent-actions.mjs +0 -482
  96. package/src/agent-tools/subagent-async.mjs +0 -434
  97. package/src/agent-tools/subagent-panel.mjs +0 -160
  98. package/src/agent-tools/subagent-run.mjs +0 -205
  99. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  100. package/src/agent-tools/subagent-spawn.mjs +0 -459
  101. package/src/agent-tools/subagent.mjs +0 -404
  102. package/src/agent-tools/task.mjs +0 -87
  103. package/src/agent-tools/timer.mjs +0 -46
  104. package/src/agent-tools/verify.mjs +0 -271
  105. package/src/agent-tools.mjs +0 -17
  106. package/src/agent.mjs +0 -417
  107. package/src/auto-think.mjs +0 -115
  108. package/src/config-migrate.mjs +0 -70
  109. package/src/config.mjs +0 -496
  110. package/src/context.mjs +0 -392
  111. package/src/conventions.mjs +0 -223
  112. package/src/embedding.mjs +0 -120
  113. package/src/escape.mjs +0 -152
  114. package/src/expand-home.mjs +0 -16
  115. package/src/explore-distill.mjs +0 -155
  116. package/src/generate-title.mjs +0 -88
  117. package/src/git/checkpoint.mjs +0 -448
  118. package/src/git/gitmem.mjs +0 -100
  119. package/src/hooks.mjs +0 -97
  120. package/src/ledger.mjs +0 -227
  121. package/src/log.mjs +0 -195
  122. package/src/markdown.mjs +0 -106
  123. package/src/mcp/helpers.mjs +0 -51
  124. package/src/mcp/transport-http.mjs +0 -248
  125. package/src/mcp/transport-stdio.mjs +0 -140
  126. package/src/mcp/transport-ws.mjs +0 -122
  127. package/src/mcp.mjs +0 -295
  128. package/src/memory/code-index.mjs +0 -219
  129. package/src/memory/code-sync.mjs +0 -415
  130. package/src/memory/core.mjs +0 -299
  131. package/src/memory/delete.mjs +0 -236
  132. package/src/memory/docs.mjs +0 -419
  133. package/src/memory/file-walk.mjs +0 -109
  134. package/src/memory/scan.mjs +0 -95
  135. package/src/memory/schema.mjs +0 -452
  136. package/src/memory.mjs +0 -21
  137. package/src/model-ref.mjs +0 -66
  138. package/src/model-specs.mjs +0 -179
  139. package/src/peer-domains.mjs +0 -265
  140. package/src/peer-instances.mjs +0 -231
  141. package/src/prompt-overlays.mjs +0 -82
  142. package/src/prompts/advisor-design.md +0 -41
  143. package/src/prompts/advisor-round1.md +0 -41
  144. package/src/prompts/advisor-round2.md +0 -46
  145. package/src/prompts/advisor-round3.md +0 -42
  146. package/src/prompts/common.md +0 -115
  147. package/src/prompts/consult-base.md +0 -19
  148. package/src/prompts/discipline-engineering.md +0 -258
  149. package/src/prompts/discipline-normal.md +0 -185
  150. package/src/prompts/persona-coder.md +0 -21
  151. package/src/prompts/persona-eng-coder.md +0 -37
  152. package/src/prompts/persona-eng-designer.md +0 -60
  153. package/src/prompts/persona-engineering.md +0 -55
  154. package/src/prompts/persona-explore.md +0 -15
  155. package/src/prompts/persona-normal.md +0 -27
  156. package/src/prompts/persona-plan.md +0 -26
  157. package/src/provider/anthropic.mjs +0 -225
  158. package/src/provider/core.mjs +0 -476
  159. package/src/provider/errors.mjs +0 -101
  160. package/src/provider/google.mjs +0 -257
  161. package/src/provider/index.mjs +0 -7
  162. package/src/provider/list-models.mjs +0 -93
  163. package/src/provider/normalize.mjs +0 -81
  164. package/src/provider/rate.mjs +0 -108
  165. package/src/provider/responses.mjs +0 -495
  166. package/src/provider/retry.mjs +0 -88
  167. package/src/provider/sse.mjs +0 -264
  168. package/src/proxy.mjs +0 -261
  169. package/src/rules.mjs +0 -53
  170. package/src/session-gc.mjs +0 -221
  171. package/src/session-guard.mjs +0 -59
  172. package/src/session-migrate.mjs +0 -48
  173. package/src/session-rename.mjs +0 -38
  174. package/src/session-segments.mjs +0 -100
  175. package/src/session-slots.mjs +0 -492
  176. package/src/session-store.mjs +0 -441
  177. package/src/session.mjs +0 -492
  178. package/src/skills.mjs +0 -153
  179. package/src/text-budget.mjs +0 -46
  180. package/src/token-ttl.mjs +0 -274
  181. package/src/tools/apply_patch.md +0 -15
  182. package/src/tools/bash.md +0 -37
  183. package/src/tools/bash.mjs +0 -268
  184. package/src/tools/checklist-sync.mjs +0 -181
  185. package/src/tools/checklist.md +0 -13
  186. package/src/tools/checklist.mjs +0 -299
  187. package/src/tools/delete.md +0 -13
  188. package/src/tools/edit-batch.mjs +0 -191
  189. package/src/tools/edit-diff.mjs +0 -348
  190. package/src/tools/edit.md +0 -30
  191. package/src/tools/execute.md +0 -21
  192. package/src/tools/execute.mjs +0 -228
  193. package/src/tools/fetch.md +0 -12
  194. package/src/tools/file.mjs +0 -469
  195. package/src/tools/file_ops.md +0 -17
  196. package/src/tools/get_current_time.md +0 -8
  197. package/src/tools/git-checkpoint.mjs +0 -143
  198. package/src/tools/git-ext.mjs +0 -173
  199. package/src/tools/git.md +0 -54
  200. package/src/tools/git.mjs +0 -356
  201. package/src/tools/glob-dialect.mjs +0 -130
  202. package/src/tools/glob.md +0 -11
  203. package/src/tools/grep.md +0 -19
  204. package/src/tools/hashline_edit.md +0 -14
  205. package/src/tools/index.mjs +0 -36
  206. package/src/tools/insert_after.md +0 -15
  207. package/src/tools/lint.md +0 -10
  208. package/src/tools/linter.mjs +0 -128
  209. package/src/tools/ls.md +0 -12
  210. package/src/tools/lsp.md +0 -10
  211. package/src/tools/lsp.mjs +0 -316
  212. package/src/tools/ops.mjs +0 -299
  213. package/src/tools/patch.mjs +0 -282
  214. package/src/tools/process.md +0 -10
  215. package/src/tools/question.md +0 -16
  216. package/src/tools/question.mjs +0 -26
  217. package/src/tools/read.md +0 -20
  218. package/src/tools/read_image.md +0 -8
  219. package/src/tools/repomap.mjs +0 -314
  220. package/src/tools/search.mjs +0 -236
  221. package/src/tools/shared.mjs +0 -446
  222. package/src/tools/tree.md +0 -14
  223. package/src/tools/tree.mjs +0 -66
  224. package/src/tools/wait_for.md +0 -22
  225. package/src/tools/web.mjs +0 -224
  226. package/src/tools/websearch.md +0 -16
  227. package/src/tools/write.md +0 -11
  228. package/src/traces/trace-store.mjs +0 -355
package/src/config.mjs DELETED
@@ -1,496 +0,0 @@
1
- /**
2
- * config.mjs — configuration loading and saving
3
- * Model-merge schema v2 (2026-09-10 MODEL-SELECTION): providers[] carry ONE default model
4
- * per channel (`providers[].model` — the new-install seed / empty-slot fallback); the
5
- * candidates list field models[] is gone — the available-model list is fetched from the
6
- * provider at runtime (`GET /models`, PROVIDER.md §16). config.defaultModel (top level,
7
- * "provider:model" composite) is the new-session starting point; activeProvider/activeModel
8
- * are gone from config (session slots keep their own double fields). Legacy fields migrate
9
- * on load (write-back failure never blocks startup).
10
- * Config file: ~/.thincoder/config.json
11
- * API key can fall back to environment variables (when not configured in providers).
12
- */
13
-
14
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
15
- import { homedir } from "node:os"
16
- import { dirname, join } from "node:path"
17
- import { parseModelRef, resolveRuntimeProvider, defaultModelReason } from "./model-ref.mjs"
18
- // 老形态迁移核(M7 v2——纯函数零依赖——本文件超 500 行硬限拆分,VSC 同构文件)
19
- import { migrateLegacyModelFields } from "./config-migrate.mjs"
20
- import { expandHome } from "./expand-home.mjs"
21
-
22
- export { parseModelRef, resolveRuntimeProvider, defaultModelReason, migrateLegacyModelFields }
23
-
24
- export const configDir = join(homedir(), ".thincoder")
25
- export const configPath = join(configDir, "config.json")
26
-
27
- /** Test seam: override the config file location (mirrors thincoder-vscode config-io.mjs). */
28
- let _pathOverride = null
29
- export function _setConfigPathForTest(p) { _pathOverride = p }
30
- export function _resetConfigPathForTest() { _pathOverride = null }
31
- function cfgPath() { return _pathOverride ?? configPath }
32
-
33
- /** Built-in provider presets: shared by /provider add <preset> and first-run wizard.
34
- * MODEL-SELECTION v2 (2026-09-10): each preset carries exactly ONE default model
35
- * (`model` — 新装启动种子/槽位空兜底; 候选清单不再预置——运行期从 provider 拉取). */
36
- export const PROVIDER_PRESETS = {
37
- deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-flash", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
38
- kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
39
- "kimi-code": { baseURL: "https://api.kimi.com/coding/v1", model: "k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi For Coding (platform.kimi.com — sk-kimi- keys; NOT interchangeable with Moonshot)" },
40
- glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM" },
41
- "glm-code": { baseURL: "https://open.bigmodel.cn/api/coding/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM Coding Plan (coding endpoint — same key as GLM; server-forced thinking)" },
42
- qwen: { baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", reasoningEffort: "high", maxTokens: 131072, desc: "Qwen / Alibaba" },
43
- qwenplan: { baseURL: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1", model: "qwen3.7-max", reasoningEffort: "high", maxTokens: 131072, desc: "Qwen Token Plan (百炼套餐)" },
44
- mimo: { baseURL: "https://api.xiaomimimo.com/v1", model: "mimo-v2.5-pro", thinking: { type: "enabled" }, maxTokens: 131072, desc: "MiMo (Xiaomi)" },
45
- mimoplan: { baseURL: "https://token-plan-cn.xiaomimimo.com/v1", model: "mimo-v2.5-pro", thinking: { type: "enabled" }, maxTokens: 131072, desc: "MiMo Token Plan (小米套餐 — tp- keys; 与按量付费 sk- 密钥不通用)" },
46
- minimax: { baseURL: "https://api.minimaxi.com/v1", model: "MiniMax-M3", thinking: { type: "adaptive" }, maxTokens: 128000, chatPath: "/text/chatcompletion_v2", desc: "MiniMax" },
47
- openai: { baseURL: "https://api.openai.com/v1", model: "gpt-4o", desc: "OpenAI" },
48
- claude: { baseURL: "https://api.anthropic.com/v1", model: "claude-sonnet-4", format: "anthropic", maxTokens: 8192, desc: "Claude (Anthropic)" },
49
- gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta", model: "gemini-2.5-flash", format: "google", maxTokens: 8192, desc: "Gemini (Google)" },
50
- grok: { baseURL: "https://api.x.ai/v1", model: "grok-4.5", maxTokens: 65536, desc: "Grok (xAI)" },
51
- mistral: { baseURL: "https://api.mistral.ai/v1", model: "mistral-large", maxTokens: 32768, desc: "Mistral" },
52
- volcengine: { baseURL: "https://ark.cn-beijing.volces.com/api/v3", model: "doubao-pro-32k", maxTokens: 32768, desc: "Volcengine Ark (豆包)" },
53
- hunyuan: { baseURL: "https://api.hunyuan.cloud.tencent.com/v1", model: "hunyuan-pro", maxTokens: 32768, desc: "Hunyuan (腾讯混元)" },
54
- siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32768, desc: "SiliconFlow (硅基流动)" },
55
- openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32768, desc: "OpenRouter" },
56
- groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32768, desc: "Groq" },
57
- }
58
-
59
- export const DEFAULTS = {
60
- defaultModel: null, // top-level "provider:model" composite — new-session starting point (F-1)
61
- agent: {
62
- maxTurns: 200,
63
- subagentTurns: 100,
64
- subagentModel: null, // default subagent model: "provider:model" | provider name | model name (parent provider); null = inherit parent provider
65
- subagentModels: {}, // per-type override: { explore, plan, coder, "eng-coder" } — priority: subagent tool model arg > this[role] > subagentModel > parent provider
66
- goalTurns: 200,
67
- compactThreshold: 100000,
68
- verifyGuard: false, // push model back to verify when files were mutated but verify not run (opt-in)
69
- // Multi-model consultation ("会诊") + escalate ("飞刀") — CLI parity with the VS Code plugin.
70
- // consultModels: candidate pool for BOTH consult and escalate ({ provider, model, effort? }, up to 5).
71
- consultModels: [],
72
- consultTurns: 40, // per-consultant tool-turn budget (diagnosis tasks)
73
- consultTimeoutMs: 600000, // wall-clock ceiling per consultant (10min)
74
- streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
75
- advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
76
- autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
77
- engineering: false, // strict methodology enforcement — design-before-code (design review + user approval before code)
78
- // Async pool limits (AGENT-LOOP.md §11.1 D-24a/R14 + §11.2 R13 — POOL-CONFIG-
79
- // UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
80
- // pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
81
- // 4 路" + advisor 评审池并入同一可配体系——三池统一默认 4)。engCoder/other
82
- // runtime-validated at every subagent pool admission (subagent-async
83
- // resolvePoolLimits); advisor runtime-validated at every advisor launch
84
- // (advisor-async advisorPoolLimitFor——F-2——两域读取器独立不共享)——非法/缺省
85
- // 回退 4——settings tool 与 /config 并发池菜单写此键——变更下个 spawn/launch 生效。
86
- // ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS(两键)/advisor-async.mjs
87
- // ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
88
- // 勿单侧改默认。
89
- poolLimits: { engCoder: 4, other: 4, advisor: 4 },
90
- },
91
- memory: {
92
- dbPath: join(configDir, "memory.db"),
93
- projectDir: ".thincoder/memory",
94
- team: null,
95
- },
96
- shell: null, // bash tool shell executable (e.g. "C:\\Program Files\\Git\\bin\\bash.exe" or "pwsh"); null = system default (cmd on Windows, /bin/sh elsewhere)
97
- embedding: {
98
- baseURL: "https://api.siliconflow.cn/v1",
99
- model: "BAAI/bge-m3",
100
- },
101
- mcp: {
102
- servers: [],
103
- },
104
- websearch: {
105
- // Structured search via Tavily when a key is set — empty apiKey → Bing RSS/HTML fallback (zero-config).
106
- apiKey: "", // Tavily key (tvly-...) — optional
107
- },
108
- traces: {
109
- enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
110
- retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
111
- },
112
- }
113
-
114
- // Model capability table + spec lookup live in model-specs.mjs (2026-08-31
115
- // extract — config.mjs had grown past the 300-line advisory). Re-exported here
116
- // so the 23 existing importers keep their import paths.
117
- import { specForModel, providerSpec, specMatch } from "./model-specs.mjs"
118
- export { specForModel, providerSpec, specMatch }
119
-
120
-
121
- // Window utilization threshold: compacts at 60% context, reserving 40% headroom
122
- // for injected context (directory tree, git context, outline, project instructions,
123
- // memory/doc search results) which can consume 30-50K tokens each turn.
124
- const COMPACT_RATIO = 0.6
125
-
126
- /** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority), otherwise auto-computed from model.
127
- * Second param accepts EITHER a model name string (pure spec lookup — legacy caller:
128
- * first-run wizard) OR a provider object (providerSpec — the providers[].context
129
- * override in K units is honored, PROVIDER.md §15 T-C2). */
130
- export function resolveCompactThreshold(explicit, modelOrProvider) {
131
- if (explicit != null) return { value: explicit, auto: false }
132
- const provider = typeof modelOrProvider === "string" ? { model: modelOrProvider } : (modelOrProvider ?? {})
133
- const spec = providerSpec(provider)
134
- const value = Math.floor(spec.context * COMPACT_RATIO)
135
- return { value, auto: true }
136
- }
137
-
138
- /**
139
- * Bailian (阿里云百炼) host check — enable_thinking is a Bailian-only extension parameter;
140
- * sending it to other endpoints (kimi/glm/custom proxies) would pollute the request.
141
- */
142
- export function isBailianHost(baseURL) {
143
- return typeof baseURL === "string"
144
- && (baseURL.includes("dashscope.aliyuncs.com") || baseURL.includes(".maas.aliyuncs.com"))
145
- }
146
-
147
- /**
148
- * Resolve the Bailian `enable_thinking` switch for qwen hybrid-thinking models (PROVIDER.md §12).
149
- * qwen3.x on Bailian defaults to thinking ON, so an explicit off must send enable_thinking:false
150
- * or the server silently keeps thinking. Whitelist: model name starts with "qwen" (excluding the
151
- * non-thinking qwen3-coder line) AND the provider points at a Bailian host.
152
- * provider.thinking === null → false (explicit off: /think off, panel off — NF1 convention)
153
- * provider.reasoningEffort → true (effort tier implies thinking on; rides with reasoning_effort)
154
- * otherwise → undefined (field omitted — server default stays, no behavior change)
155
- * NOTE: spec carries no model field today — the name comes from provider.model (spec?.model is
156
- * a forward-compatible fallback). Keep the body byte-aligned with thincoder-vscode config.mjs
157
- * (cross-repo parity test compares them).
158
- */
159
- export function resolveEnableThinking(provider, spec) {
160
- const model = (provider?.model ?? spec?.model ?? "").toLowerCase()
161
- if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
162
- if (!isBailianHost(provider?.baseURL)) return undefined
163
- if (provider.thinking === null) return false
164
- if (provider.reasoningEffort) return true
165
- return undefined
166
- }
167
-
168
- /** Module-level one-time warn dedupe for invalid providers[].context (PROVIDER.md §15 D-C1). */
169
- const warnedContextProviders = new Set()
170
-
171
- /** F-4 (IKCDMR) 软失败清洗(D-S1 范式——loadConfig 是 CLI 启动砖点):consultModels 非法
172
- * 条目过滤不 throw——非数组 → []、形状非法/未知渠道丢弃、超 5 截断(keep 前 5)。
173
- * 返回 { keep, dropped }——dropped 供一次性警告;merged.agent.consultModelsFiltered 挂载
174
- * 供后续首帧引导消费(数据层——本批文件面无消费方——同 providerInvalidReason 载体先例)。 */
175
- function sanitizeConsultModels(cm, providerNames) {
176
- if (cm === undefined || cm === null) return { keep: [], dropped: [] }
177
- if (!Array.isArray(cm)) {
178
- return { keep: [], dropped: [`agent.consultModels must be an array of { provider, model } entries (got ${typeof cm})`] }
179
- }
180
- const names = new Set(providerNames)
181
- const keep = []
182
- const dropped = []
183
- for (const entry of cm) {
184
- if (keep.length >= 5) { dropped.push(`over the 5-entry cap — dropped ${JSON.stringify(entry)}`); continue }
185
- if (!entry || typeof entry !== "object" || typeof entry.provider !== "string" || !entry.provider.trim()
186
- || typeof entry.model !== "string" || !entry.model.trim()) {
187
- dropped.push(`invalid entry (expected { provider: string, model: string }) — got ${JSON.stringify(entry)}`)
188
- continue
189
- }
190
- if (!names.has(entry.provider)) {
191
- dropped.push(`entry "${entry.provider}:${entry.model}" references unknown provider "${entry.provider}" (available: ${[...names].join(", ") || "none"})`)
192
- continue
193
- }
194
- keep.push(entry)
195
- }
196
- return { keep, dropped }
197
- }
198
-
199
- /** F-4:过滤警告——进程级一次性(loadConfig 同进程可多次调用:TUI 启动 + reloadConfig)。 */
200
- let warnedConsultModels = false
201
- function warnConsultModelsFiltered(dropped, path) {
202
- if (warnedConsultModels || dropped.length === 0) return
203
- warnedConsultModels = true
204
- console.warn(`[config] agent.consultModels: ${dropped.length} invalid entr${dropped.length === 1 ? "y ignored" : "ies ignored"} (filtered — startup continues; no crash):\n` +
205
- dropped.map((d) => ` - ${d}`).join("\n") +
206
- `\n Fix: clean agent.consultModels in ${path} or use /config → consult/escalate pool menu.`)
207
- }
208
-
209
- /**
210
- * Find provider by name in providers[].
211
- * Throws if name is non-empty but not found — a typo in activeProvider silently falling to the first provider would use the wrong key on the wrong endpoint.
212
- * Returns the first provider when name is empty.
213
- */
214
- export function findProvider(providers, name) {
215
- if (name) {
216
- const found = providers.find((p) => p.name === name)
217
- if (found) return found
218
- const available = providers.map((p) => p.name).join(", ") || "(empty)"
219
- throw new Error(`activeProvider "${name}" not in providers list (available: ${available}); check for a typo in: ${configPath}`)
220
- }
221
- return providers[0] ?? { name: "default", baseURL: "", model: "" }
222
- }
223
-
224
- /** Normalize proxy config to { uri, web, model } or undefined (uri/url both accepted; invalid types dropped) */
225
- export function normalizeProxy(proxy) {
226
- if (typeof proxy === "string") return proxy ? { uri: proxy, web: true, model: false } : undefined
227
- if (!proxy || typeof proxy !== "object" || Array.isArray(proxy)) return undefined
228
- const uri = proxy.uri || proxy.url || ""
229
- if (typeof uri !== "string" || !uri) return undefined
230
- return { uri, web: proxy.web !== false, model: proxy.model === true }
231
- }
232
-
233
- /**
234
- * Load configuration.
235
- * No env-var overrides — config.json is the single source of truth
236
- * (API keys, baseURL, model, activeProvider all come from the file).
237
- */
238
- /** Keep only { header: "string value" } pairs from a provider's headers field — anything
239
- * else (null, arrays, nested objects) is dropped so it can never reach a fetch call.
240
- * Authorization is built-in and cannot be overridden from headers (core.mjs spreads first). */
241
- function sanitizeProviderHeaders(p) {
242
- if (p.headers == null || typeof p.headers !== "object" || Array.isArray(p.headers)) { delete p.headers; return p }
243
- const clean = {}
244
- for (const [k, v] of Object.entries(p.headers)) {
245
- if (typeof v === "string" && k.toLowerCase() !== "authorization") clean[k] = v
246
- }
247
- if (Object.keys(clean).length > 0) p.headers = clean
248
- else delete p.headers
249
- return p
250
- }
251
-
252
- /** loadConfig 内联迁移核已迁 config-migrate.mjs(500 行硬限拆分——VSC 同构)——
253
- * migrateLegacyModelFields 纯函数 + 本文件 import/写回编排(折中 C 见其头注释)。 */
254
- export function loadConfig() {
255
- let config = {}
256
- const path = cfgPath()
257
- if (existsSync(path)) {
258
- try {
259
- config = JSON.parse(readFileSync(path, "utf8"))
260
- } catch (error) {
261
- throw new Error(`Config file is not valid JSON, check or delete it: ${path}\n ${error.message}`, { cause: error })
262
- }
263
- }
264
-
265
- // ── 迁移(M7 v2):检测老字段(models[] / active* / 垃圾值)→ 内存迁移态先行;写回失败绝不阻断(下次 load 重试——幂等)──
266
- if (migrateLegacyModelFields(config)) {
267
- try {
268
- const r = writeConfigAtomic(path, migrateLegacyModelFields) // 磁盘 fresh raw 同变换
269
- if (r.ok === false) console.warn(`[config] migration write-back skipped (${r.reason}) — memory state continues, retried on next load`)
270
- } catch (e) {
271
- console.warn(`[config] migration write-back failed — memory state continues, retried on next load: ${e.message}`)
272
- }
273
- }
274
-
275
- const merged = {
276
- ...DEFAULTS,
277
- ...config,
278
- defaultModel: typeof config.defaultModel === "string" && config.defaultModel.trim() ? config.defaultModel : null,
279
- providers: Array.isArray(config.providers)
280
- ? config.providers.map((p) => sanitizeProviderHeaders({ ...p }))
281
- : [],
282
- agent: { ...DEFAULTS.agent, ...config.agent },
283
- memory: { ...DEFAULTS.memory, ...config.memory },
284
- embedding: { ...DEFAULTS.embedding, ...config.embedding },
285
- traces: { ...DEFAULTS.traces, ...config.traces },
286
- }
287
-
288
- // 家目录展开(第 29 批):config 路径字段单一规范化点——只读归一(磁盘原文保留)
289
- merged.memory.dbPath = expandHome(merged.memory.dbPath)
290
- merged.memory.projectDir = expandHome(merged.memory.projectDir)
291
- const team = merged.memory.team
292
- if (team && typeof team === "object" && !Array.isArray(team) && team.dir !== undefined) {
293
- merged.memory.team = { ...team, dir: expandHome(team.dir) } // 无 dir 键不注入(零键面变化)
294
- }
295
- merged.shell = expandHome(merged.shell)
296
-
297
- // providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
298
- // 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
299
- for (const p of merged.providers) {
300
- if (typeof p.model === "string" && p.model.trim()) continue
301
- if (p.model !== undefined) delete p.model
302
- }
303
-
304
- // providers[].context (K units, PROVIDER.md §15 D-C1): positive integer only — invalid
305
- // values (0/negative/non-numeric) are IGNORED (spec value applies) with a ONE-TIME warn
306
- // per provider name (module-level dedupe, same precedent as warnedModels in model-specs.mjs).
307
- for (const p of merged.providers) {
308
- if (p.context === undefined) continue
309
- if (Number.isInteger(Number(p.context)) && Number(p.context) > 0) { p.context = Number(p.context); continue } // 数字字符串("128")归一为数字——两端语义统一(code review #1)
310
- if (!warnedContextProviders.has(p.name ?? "(unnamed)")) {
311
- warnedContextProviders.add(p.name ?? "(unnamed)")
312
- console.warn(`[config] provider "${p.name}" context must be a positive integer in K units (e.g. 128 = 128K) — got ${JSON.stringify(p.context)} — ignored, using the model spec value`)
313
- }
314
- delete p.context
315
- }
316
-
317
- // F-4 (IKCDMR) consultModels 软失败化——D-S1 范式(defaultModel 无效同族):非法条目
318
- // 过滤不 throw(去 startup brick——无修复入口的硬崩消);一次性启动警告内嵌修复指引
319
- // (引导清条目载体——保留 discoverability);过滤记录挂 merged.agent.consultModelsFiltered
320
- // (数据层——consumer-ready——首帧 UI 消费点同 D-S1 promptProviderIfInvalid 属后续批)。
321
- // VSC loadAgentSettings 同规则(共享 config——双端锁步)。
322
- const cmClean = sanitizeConsultModels(merged.agent.consultModels, merged.providers.map((p) => p.name))
323
- warnConsultModelsFiltered(cmClean.dropped, path)
324
- merged.agent.consultModels = cmClean.keep
325
- if (cmClean.dropped.length) merged.agent.consultModelsFiltered = cmClean.dropped
326
-
327
- // Backward compatibility: promote root-level config fields to agent sub-object
328
- if (config.verifyGuard !== undefined) {
329
- merged.agent.verifyGuard = config.verifyGuard
330
- }
331
-
332
- // Normalize baseURL trailing slash (prevents //chat/completions)
333
- for (const p of merged.providers) {
334
- if (p.baseURL) p.baseURL = p.baseURL.replace(/\/+$/, "")
335
- }
336
-
337
- // Normalize proxy: string → { uri, web:true, model:false }; object 补默认值;非法类型丢弃。
338
- // 保证 agent.config.proxy 永远是规范形态或 undefined
339
- merged.proxy = normalizeProxy(merged.proxy)
340
-
341
- // Runtime provider = config.defaultModel 复合解析(F-2——resolveRuntimeProvider)。
342
- // 无效/未设 → {} + providerInvalidReason(D-S1 处置不 throw——make-agent 打 _providerInvalid
343
- // 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用 findProvider
344
- // throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
345
- merged.provider = resolveRuntimeProvider(merged.providers, merged.defaultModel)
346
- merged.providerInvalidReason = merged.provider.name
347
- ? null
348
- : defaultModelReason(merged.providers, merged.defaultModel)
349
-
350
- // Compaction threshold follows the model (provider-level context override honored — providerSpec)
351
- const explicitThreshold = config.agent?.compactThreshold
352
- const { value, auto } = resolveCompactThreshold(explicitThreshold, merged.provider)
353
- merged.agent.compactThreshold = value
354
- merged.agent.compactThresholdAuto = auto
355
-
356
- // Write back to merged for convenient access by upper layers
357
- // fetch 超时可配置(2026-09-01:agent.fetchTimeoutMs——provider/core.mjs effectiveFetchTimeoutMs 消费)
358
- merged.provider.fetchTimeoutMs = Number.isFinite(merged.agent?.fetchTimeoutMs) && merged.agent.fetchTimeoutMs > 0
359
- ? merged.agent.fetchTimeoutMs : undefined
360
- merged.providersList = merged.providers
361
- merged.advisor = { ...merged.agent.advisor } // promote for consistent access (decoupled copy)
362
-
363
- return merged
364
- }
365
-
366
- /**
367
- * MCP.md §5 D-3 (2026-09-01): re-read config.json and replace ONLY the agent's mcp section
368
- * — the agent 代配 closed loop (agent edits config.json with its edit tool, /mcp picks it
369
- * up). Never touches other config sections (providers/activeProvider stay as loaded).
370
- *
371
- * Malformed disk config → memory state kept, { ok:false, error } returned (the /mcp menu
372
- * shows "⚠ disk config unreadable"). Never throws.
373
- *
374
- * 对账 (reconciliation, MCP.md §5 D-3 / T23): returns which disk servers CHANGED
375
- * (fingerprint differs) or are DELETED from disk while still connected — fingerprint =
376
- * endpoint + token + headers/env key order. Existing connections are NOT torn down (an
377
- * in-use server must not be dropped): a deleted-but-connected server KEEPS its memory
378
- * entry (appended after the disk list) so the /mcp list can still show the row with the
379
- * "⚠ disk changed" mark. A server that is merely NEW on disk is not drift. persistRaw
380
- * write + reload is idempotent (fingerprints equal → no drift mark).
381
- *
382
- * @param path optional config path override (tests inject a tmp file; default configPath)
383
- */
384
- export function reloadMcpFromDisk(agent, path) {
385
- const memoryServers = Array.isArray(agent.config?.mcp?.servers) ? agent.config.mcp.servers : []
386
- const fileExists = existsSync(path ?? configPath)
387
- const diskMcp = readMcpSection(path)
388
- if (!diskMcp.ok) return { ok: false, error: diskMcp.error, changedNames: [] }
389
- // Missing/deleted config file → keep whichever mcp servers the session had (never
390
- // silently drop user servers because the file vanished — same memory-keeps policy
391
- // as the malformed-disk fallback).
392
- let diskServers = diskMcp.servers
393
- if (diskServers.length === 0 && !fileExists) diskServers = memoryServers
394
- // Drift vs the RAW disk list: fingerprint-changed or deleted-from-disk (T23 ⚠ 标记依据)
395
- const diskNames = new Set(diskServers.filter((s) => s?.name).map((s) => s.name))
396
- const changedNames = diffMcpServers(memoryServers, diskServers)
397
- // Connected servers deleted from disk stay in the list (memory copy) — T23: the row
398
- // must remain visible (marked ⚠) and its live connection untouched. They are already
399
- // in changedNames (absent from disk), and stay flagged on every reload until the user
400
- // reconnects (re-persists them) or removes them — real drift, honestly reported.
401
- const connectedNames = new Set((agent.tools ?? []).filter((t) => t?._mcpName).map((t) => t._mcpName))
402
- const keptConnected = memoryServers.filter((s) => s?.name && connectedNames.has(s.name) && !diskNames.has(s.name))
403
- const finalServers = [...diskServers, ...keptConnected]
404
- agent.config ??= {}
405
- agent.config.mcp = { ...agent.config.mcp, servers: finalServers }
406
- return { ok: true, servers: finalServers, changedNames }
407
- }
408
-
409
- /** Disk read behind reloadMcpFromDisk — bounded, never throws. */
410
- function readMcpSection(path = configPath) {
411
- try {
412
- if (!existsSync(path)) return { ok: true, servers: [] }
413
- const raw = JSON.parse(readFileSync(path, "utf8"))
414
- const servers = raw?.mcp?.servers
415
- // 非数组 = 畸形磁盘配置(2.3 代码正确性批)——ok:false 走调用方既有畸形回退(reloadMcpFromDisk 早退)
416
- if (servers !== undefined && !Array.isArray(servers)) return { ok: false, error: "mcp.servers must be an array" }
417
- return { ok: true, servers: Array.isArray(servers) ? servers : [] }
418
- } catch (error) {
419
- return { ok: false, error: error?.message ?? String(error) }
420
- }
421
- }
422
-
423
- /** Fingerprint = endpoint + token + headers/env entries in key order (JSON.stringify
424
- * of a normalized subset — key order included, matching connectMcpServer's
425
- * configFingerprint semantics: any change the connect layer would see counts).
426
- * Drift = CHANGED (fingerprint differs) or DELETED (missing from disk) — a server
427
- * that is new on disk is not drift (no live connection to protect). */
428
- function diffMcpServers(memoryServers, diskServers) {
429
- const memFp = new Map(memoryServers.filter((s) => s?.name).map((s) => [s.name, mcpFingerprint(s)]))
430
- const diskFp = new Map(diskServers.filter((s) => s?.name).map((s) => [s.name, mcpFingerprint(s)]))
431
- const changed = []
432
- for (const [name, fp] of diskFp) if (memFp.has(name) && memFp.get(name) !== fp) changed.push(name)
433
- for (const name of memFp.keys()) if (!diskFp.has(name)) changed.push(name)
434
- return changed
435
- }
436
-
437
- function mcpFingerprint(s) {
438
- return JSON.stringify([
439
- s.wsUrl ?? s.url ?? s.command ?? null,
440
- s.args ?? null,
441
- s.token ?? null,
442
- s.headers ?? null,
443
- s.env ?? null,
444
- ])
445
- }
446
-
447
- /**
448
- * R10 F5(D-F5b,2026-09-06)——config.json 写前 mtime 门控收口函数(session-rename
449
- * mtime-conflict 先例同型——MULTI-INSTANCE-COLLAB.md §2a.2)。所有 config.json 写点
450
- * (config-helpers persistRaw / cmd-config saveProxy / cli setup-wizard / settings
451
- * writeDisk)都经它落盘。
452
- *
453
- * 本函数持有整条「新鲜读 → mutate 单操作 → 写前重 stat → 写」链:
454
- * - t0 = 写前重 stat 的比对基线,取在**新鲜读之前**(stat→read 序):若对端在本端
455
- * stat 与 read 之间的微窗口写入,只会造成假冲突(放弃重试),绝不会带着旧内容覆盖
456
- * 对端新值——read→stat 序存在漏检窗口(stat 已反映对端新 mtime → 门控放行旧内容)。
457
- * - 写前重 stat ≠ t0 → **放弃**本次写(D-F5a 后各流已是 fresh 单操作语义,磁盘上对端
458
- * 的新值保持在线不抹);先 copy `.bak-{ts}` 留现场(仅冲突时——config 低频写不膨胀;
459
- * copy 而非 rename:冲突即放弃、本体不动,"保现场"是额外副本,非轮转腾位)。
460
- * - 返回 { ok:false, reason:"mtime-conflict" },调用方提示 "config changed on disk
461
- * concurrently — retry"——不自动合并(config 是用户显式操作——重试比猜测合并安全,
462
- * 决策点① A)。
463
- * - 文件缺失(首写)→ t0 = null;对端在本端读后创建 → null ≠ 新 mtime → 冲突放弃。
464
- * - 畸形文件拒写(throw,绝不静默覆盖);写后 chmod 0600 尽力而为(saveConfig 旧语义)。
465
- *
466
- * @param path config.json 路径(生产默认 configPath;测试注入 tmp 路径)
467
- * @param mutate 在磁盘新鲜 raw 上执行单操作的同步回调(如 push/splice/单字段补丁)
468
- * @returns { ok: true } | { ok: false, reason: "mtime-conflict" }
469
- */
470
- export function writeConfigAtomic(path, mutate) {
471
- const mtimeOf = (p) => {
472
- try { return statSync(p).mtimeMs } catch { return null } // 缺失 → null(t0 比对基线)
473
- }
474
- const t0 = mtimeOf(path) // stat 先于 read(安全方向——见头注释)
475
- const text = existsSync(path) ? readFileSync(path, "utf8") : null
476
- let raw = {}
477
- if (text !== null) {
478
- try {
479
- raw = JSON.parse(text)
480
- } catch (error) {
481
- throw new Error(`config file not parseable — refusing to overwrite: ${path} — ${error.message}`, { cause: error })
482
- }
483
- }
484
- mutate(raw)
485
- const t1 = mtimeOf(path)
486
- if (t0 !== t1) {
487
- // 对端在我们新鲜读后改过磁盘 → 放弃本次写(对端内容保持在线);.bak 副本留现场
488
- try { if (existsSync(path)) copyFileSync(path, `${path}.bak-${Date.now()}`) } catch { /* 现场保留失败不阻断冲突报告 */ }
489
- return { ok: false, reason: "mtime-conflict" }
490
- }
491
- mkdirSync(dirname(path), { recursive: true })
492
- // 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
493
- writeFileSync(path, JSON.stringify(raw, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
494
- try { chmodSync(path, 0o600) } catch { /* may fail on Windows, ignore */ }
495
- return { ok: true }
496
- }