thincoder 0.12.61 → 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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  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 -371
  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 +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  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-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
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
- }