@thincoder/core 0.9.4 → 0.10.1

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 (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
package/auto-think.mjs CHANGED
@@ -81,7 +81,7 @@ export async function classifyAndApply(agent, turn) {
81
81
  ],
82
82
  tools: [],
83
83
  signal: AbortSignal.timeout(5_000),
84
- // D-TS12 (AGENT-LOOP.md §18.7): full logCtx field set at the chat call
84
+ // D-TS12 (TRACES.md §6.1): full logCtx field set at the chat call
85
85
  // point — traces/session/cwd/role/depth/kind (this call point carried
86
86
  // only {stage,turn,child}). The traces field closes the D-TR6 "off = no
87
87
  // persist" switch: without it the tracer treated the auto-think call as
@@ -14,7 +14,7 @@
14
14
 
15
15
  /** Built-in provider presets: shared by /provider add <preset> and first-run wizard. */
16
16
  export const PROVIDER_PRESETS = {
17
- deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-flash", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 393216, desc: "DeepSeek" },
17
+ deepseek: { baseURL: "https://api.deepseek.com", model: "deepseek-flash", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 384_000, desc: "DeepSeek" },
18
18
  kimi: { baseURL: "https://api.moonshot.cn/v1", model: "kimi-k3", thinking: null, reasoningEffort: "max", maxTokens: 131072, desc: "Kimi / Moonshot" },
19
19
  "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)" },
20
20
  glm: { baseURL: "https://open.bigmodel.cn/api/paas/v4", model: "glm-5.2", thinking: { type: "enabled" }, reasoningEffort: "max", maxTokens: 128000, desc: "Zhipu GLM" },
@@ -27,16 +27,21 @@ export const PROVIDER_PRESETS = {
27
27
  openai: { baseURL: "https://api.openai.com/v1", model: "gpt-4o", desc: "OpenAI" },
28
28
  claude: { baseURL: "https://api.anthropic.com/v1", model: "claude-sonnet-4", format: "anthropic", maxTokens: 8192, desc: "Claude (Anthropic)" },
29
29
  gemini: { baseURL: "https://generativelanguage.googleapis.com/v1beta", model: "gemini-2.5-flash", format: "google", maxTokens: 8192, desc: "Gemini (Google)" },
30
- grok: { baseURL: "https://api.x.ai/v1", model: "grok-4.5", maxTokens: 65536, desc: "Grok (xAI)" },
31
- mistral: { baseURL: "https://api.mistral.ai/v1", model: "mistral-large", maxTokens: 32768, desc: "Mistral" },
30
+ grok: { baseURL: "https://api.x.ai/v1", model: "grok-4.5", maxTokens: 64_000, desc: "Grok (xAI)" },
31
+ mistral: { baseURL: "https://api.mistral.ai/v1", model: "mistral-large", maxTokens: 32_000, desc: "Mistral" },
32
32
  volcengine: { baseURL: "https://ark.cn-beijing.volces.com/api/v3", model: "doubao-seed-2-0-code-preview-260215", maxTokens: 131072, desc: "Volcengine Ark (豆包)" },
33
- hunyuan: { baseURL: "https://api.hunyuan.cloud.tencent.com/v1", model: "hunyuan-pro", maxTokens: 32768, desc: "Hunyuan (腾讯混元)" },
33
+ hunyuan: { baseURL: "https://api.hunyuan.cloud.tencent.com/v1", model: "hunyuan-pro", maxTokens: 32_000, desc: "Hunyuan (腾讯混元)" },
34
34
  // TokenHub = Tencent MaaS 聚合网关(另一主机;`hy3` 在本规格表有行)。`thinking` / `reasoningEffort` /
35
35
  // `maxTokens` 一律**不设 = 不发**(两新渠道的 thinking 载荷与 max_tokens 行为未测 —— MODEL-SPECS §9.6 D-13)。
36
36
  tokenhub: { baseURL: "https://tokenhub.tencentmaas.com/v1", model: "hy3", desc: "Tencent TokenHub (腾讯混元网关)" },
37
- siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32768, desc: "SiliconFlow (硅基流动)" },
38
- openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32768, desc: "OpenRouter" },
39
- groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32768, desc: "Groq" },
37
+ // Huawei Cloud MaaS(ModelArts Studio)——OpenAI 兼容 `…/openai/v1`(区域端点变体走自定义渠道
38
+ // 自助路径——PROVIDER.md §6.21)。默认模型 `glm-5.3` = 官方口径(MaaS 最佳实践 ∕ 快速体验页的
39
+ // QS 示例模型 · 官方 API 样例 `model` 逐字)· **待验(无 key——未实拉 /models;接入后按实拉取值复核)**;
40
+ // `thinking` / `reasoningEffort` / `maxTokens` 不设 = 不发(载荷面未测——D-13)。
41
+ huawei: { baseURL: "https://api.modelarts-maas.com/openai/v1", model: "glm-5.3", desc: "Huawei Cloud ModelArts Studio (华为云 MaaS)" },
42
+ siliconflow: { baseURL: "https://api.siliconflow.cn/v1", model: "deepseek-ai/DeepSeek-V3", maxTokens: 32_000, desc: "SiliconFlow (硅基流动)" },
43
+ openrouter: { baseURL: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4", maxTokens: 32_000, desc: "OpenRouter" },
44
+ groq: { baseURL: "https://api.groq.com/openai/v1", model: "llama-3.3-70b-versatile", maxTokens: 32_000, desc: "Groq" },
40
45
  }
41
46
 
42
47
  /** Build the stored provider entry from a preset — strip the display field, keep the rest
@@ -0,0 +1,83 @@
1
+ /**
2
+ * config-watch.mjs — config.json 外部写盘感知·**纯逻辑面**(R8 · 桌面功能对位批「上提」KD-T2——
3
+ * 自 `thincoder-vscode/src/extension/config-watch.mjs:35-77` 逐字随迁:去抖 300ms ∕ stat 元组
4
+ * (`mtimeMs:size`)比对 ∕ 自写抑制;原批 = 第 21 批 B5 · `docs/design/SETTINGS.md` §2.6)。
5
+ *
6
+ * `~/.thincoder/config.json` 为多端共享单文件:CLI(`/advisor`、`settings set`)或手工编辑写盘后,
7
+ * 常开面板原本零感知(只在打开面板/保存时拉快照)。机制(零语义改):
8
+ * 宿主 fs 事件 →(端落子)`onEvent` → 去抖 → 元组比对 → 变化才 `onChange`(事件驱动零空转)。
9
+ * 自写抑制:本端自身写盘同样改元组 ⇒ 裸推送会在用户编辑中被快照重建;写盘成功后核 `config-io`
10
+ * `saveRaw` 同步回调 `onConfigSelfWrite`(`config-io.mjs:107`)⇒ `noteSelfWrite()` 把基线刷到当前
11
+ * 元组 ⇒ 事件到达时元组已等于基线 ⇒ 零推送;外部写无回调 ⇒ 元组异于基线 ⇒ 推送。
12
+ *
13
+ * 平台面两注入(核内零宿主绑定):① `attach`(fs 事件源工厂——端落子:VSC = `createFileSystemWatcher`、
14
+ * 桌面 = `node:fs.watch`)② `setTimer` ∕ `clearTimer`(定时器;缺省 = 全局)。`tupleOf`(stat 探针)缺省 =
15
+ * 本核 `statSync` 实现(两端宿主皆 Node);`subscribeSelfWrite` 缺省 = 核自写订阅。
16
+ * 降级(无 path ∕ `attach` 缺失 ∕ 构造抛错 ∕ 返回空)⇒ no-op 面(不阻断端激活——面板打开拉新的既有路径兜底)。
17
+ * 装配序微差(对源件):事件订阅随 `attach` 一次落(源件 = 构造 → 基线 → 订阅三事件)——订阅更早,
18
+ * 不丢事件;基线仍于 `attach` 后取(同步段内零窗口)。零语义差。
19
+ */
20
+ import { statSync } from "node:fs"
21
+ import { _configPath, onConfigSelfWrite } from "./config-io.mjs"
22
+
23
+ /** stat 元组(mtimeMs + size——同 tick 快写 mtime 可同,size 兜底);缺失 ∕ 不可读 ⇒ null。 */
24
+ export function configTupleOf(path) {
25
+ try {
26
+ const s = statSync(path)
27
+ return `${s.mtimeMs}:${s.size}`
28
+ } catch { return null }
29
+ }
30
+
31
+ /**
32
+ * 建配置监视面(纯逻辑)。
33
+ * @param {{ configPath?: string, onChange?: () => void, debounceMs?: number,
34
+ * attach?: (arg: { path: string, onEvent: () => void }) => { dispose?: () => void } | null,
35
+ * tupleOf?: (path: string) => string | null,
36
+ * setTimer?: (fn: () => void, ms: number) => unknown, clearTimer?: (handle: unknown) => void,
37
+ * subscribeSelfWrite?: (fn: () => void) => (() => void) | void }} [opts]
38
+ * `configPath` 缺省 = 核 `_configPath()` 当前值(测试缝同源);`attach` = 平台落子(fs 事件源)。
39
+ * @returns {{ dispose: () => void, noteSelfWrite: () => void }} 降级时返回 no-op(不阻断激活)。
40
+ */
41
+ export function createConfigWatch({
42
+ configPath, onChange, debounceMs = 300, attach, tupleOf = configTupleOf,
43
+ setTimer = setTimeout, clearTimer = clearTimeout, subscribeSelfWrite = onConfigSelfWrite,
44
+ } = {}) {
45
+ const noop = { dispose() {}, noteSelfWrite() {} }
46
+ const path = configPath || _configPath()
47
+ if (!path) return noop
48
+ if (typeof attach !== "function") return noop
49
+
50
+ let baseline = null
51
+ let timer = null
52
+ /** 元组比对(去抖落点):未变 → 零推送(隐藏 N5 判据保持);变 ⇒ 基线刷新 + `onChange`(抛不炸宿主)。 */
53
+ const check = () => {
54
+ timer = null
55
+ const now = tupleOf(path)
56
+ if (now === baseline) return
57
+ baseline = now // 比对后基线更新为当前元组
58
+ try { onChange?.() } catch (e) { console.warn(`[config-watch] onChange failed: ${e.message}`) }
59
+ }
60
+ /** 去抖(同拍多事件合并——窗口内重排)。 */
61
+ const schedule = () => {
62
+ if (timer) clearTimer(timer)
63
+ timer = setTimer(check, debounceMs)
64
+ }
65
+
66
+ let listener
67
+ try { listener = attach({ path, onEvent: schedule }) } catch { return noop }
68
+ if (!listener) return noop
69
+ baseline = tupleOf(path)
70
+ const noteSelfWrite = () => { baseline = tupleOf(path) }
71
+
72
+ let unsubSelfWrite = null
73
+ try { unsubSelfWrite = subscribeSelfWrite(noteSelfWrite) } catch { /* 订阅失败仅弱化自写抑制 */ }
74
+
75
+ return {
76
+ noteSelfWrite,
77
+ dispose() {
78
+ try { listener.dispose?.() } catch { /* already disposed */ }
79
+ if (timer) { clearTimer(timer); timer = null }
80
+ try { unsubSelfWrite?.() } catch { /* ignore */ }
81
+ },
82
+ }
83
+ }
package/config.mjs CHANGED
@@ -48,7 +48,7 @@ export const DEFAULTS = {
48
48
  streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
49
49
  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)
50
50
  autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
51
- engineering: false, // strict methodology enforcement — design-before-code (design review + user approval before code)
51
+ engineering: false, // initial default only — the runtime value is the session authority (MANIFEST.md §2.2 / ENGINEERING-MODE-V2.md §2.3 E5.1). Strict methodology enforcement — design-before-code (design review + user approval before code)
52
52
  // Async pool limits (AGENT-LOOP-ASYNC-POOL.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
53
53
  // UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
54
54
  // pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
@@ -61,6 +61,9 @@ export const DEFAULTS = {
61
61
  // ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
62
62
  // 勿单侧改默认。
63
63
  poolLimits: { engCoder: 4, other: 4, advisor: 4 },
64
+ // §6.30.3 门三件③(D-TW5):timer 空闲自唤醒开关——默认开(工具描述面已承诺到点提醒);
65
+ // false ⇒ 端侧不武装闩(回既有回合边界投递——零调度器面)。键面随 DEFAULTS 派生进 settings 类型表。
66
+ timerWake: true,
64
67
  },
65
68
  memory: {
66
69
  dbPath: join(configDir, "memory.db"),
@@ -83,6 +86,10 @@ export const DEFAULTS = {
83
86
  enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
84
87
  retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
85
88
  },
89
+ // 诊断开关(用户面 ① 配置键——CONFIG.md §6.2 名录):`heapWatch` 默认开(设 false 关闭)——自愈臂
90
+ // 遥测;`heapSnapshot` 默认关——采集 opt-in(堆转储敏感度 ≥ traces ⇒ 对齐发布隐私先例 D-TR6;
91
+ // 2026-09-30 采集收网批;桌面端运行期热读 ∥ CLI 本进程下次启动)
92
+ diagnostics: { heapWatch: true, heapSnapshot: false },
86
93
  }
87
94
 
88
95
  // Model capability table + spec lookup live in model-specs.mjs (2026-08-31
@@ -127,11 +134,12 @@ export function isBailianHost(baseURL) {
127
134
  * provider.reasoningEffort → true (effort tier implies thinking on; rides with reasoning_effort)
128
135
  * otherwise → undefined (field omitted — server default stays, no behavior change)
129
136
  * NOTE: spec carries no model field today — the name comes from provider.model (spec?.model is
130
- * a forward-compatible fallback). Keep the body byte-aligned with thincoder-vscode config.mjs
131
- * (cross-repo parity test compares them).
137
+ * a forward-compatible fallback). 非串 model(脏载 / 手工档)归一等价空串(未登记 ⇒ undefined)——
138
+ * 本函数恒 total、不因类型脏载抛(#638 查表族同归一)。
132
139
  */
133
140
  export function resolveEnableThinking(provider, spec) {
134
- const model = (provider?.model ?? spec?.model ?? "").toLowerCase()
141
+ const raw = provider?.model ?? spec?.model ?? ""
142
+ const model = (typeof raw === "string" ? raw : "").toLowerCase()
135
143
  if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
136
144
  if (!isBailianHost(provider?.baseURL)) return undefined
137
145
  if (provider.thinking === null) return false
@@ -257,6 +265,7 @@ export function loadConfig() {
257
265
  memory: { ...DEFAULTS.memory, ...config.memory },
258
266
  embedding: { ...DEFAULTS.embedding, ...config.embedding },
259
267
  traces: { ...DEFAULTS.traces, ...config.traces },
268
+ diagnostics: { ...DEFAULTS.diagnostics },
260
269
  }
261
270
 
262
271
  // 家目录展开(第 29 批):config 路径字段单一规范化点——只读归一(磁盘原文保留)
@@ -268,6 +277,10 @@ export function loadConfig() {
268
277
  }
269
278
  merged.shell = expandHome(merged.shell)
270
279
 
280
+ // diagnostics(① 配置键,CONFIG.md §6.2 名录):两键只认布尔值——非法 / 缺失回退默认(不 throw)
281
+ const _diagRaw = config.diagnostics && typeof config.diagnostics === "object" && !Array.isArray(config.diagnostics) ? config.diagnostics : {}
282
+ for (const key of ["heapWatch", "heapSnapshot"]) if (typeof _diagRaw[key] === "boolean") merged.diagnostics[key] = _diagRaw[key]
283
+
271
284
  // providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
272
285
  // 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
273
286
  for (const p of merged.providers) {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * context-degrade.mjs — 确定性降级面(无 LLM 的有损收缩 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
3
+ * 内容 = `pruneStub` ∥ `pruneStaleToolOutputs`(F-CC3 陈旧工具输出清理)∥ `OVERSIZE_CONTENT_LIMIT` ∥
4
+ * `shrinkOversized`(splitHistory 无中间段时的单条超限收缩)——迁出块逐字。
5
+ * 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改);
6
+ * 跨档被引的原私有面(`shrinkOversized`)补 `export` 关键字。
7
+ */
8
+ import { collectStaleToolOutputs } from "./token-window.mjs"
9
+
10
+ /** prune stub 逐字(§6.16.3):只给「已清理 + 原长度 + 重跑路径」——prune 是**删**不是摘要,不声称可复原。 */
11
+ const pruneStub = (chars) => `[pruned: stale tool output dropped (${chars} chars) — re-run the tool if you need it again.]`
12
+
13
+ /**
14
+ * 陈旧工具输出清理(F-CC3 · §6.16.3):合格集 = `token-window.mjs` 单源(保护尾外 ∧ `role:"tool"` ∧ ≥ 门槛);
15
+ * 命中项**原位换「内容」**(`history[i] = { ...m, content: stub }`——数组引用 / 长度 / 索引 / `tool_call_id` 全不变
16
+ * ⇒ 配对**结构上不可能被拆**)。记录面零改(copy-on-write:消息对象与人读线共享);基线失效同 `shrinkOversized` 先例。
17
+ * @returns {{pruned:number, freed:number, candidates:number, tailKept:number, belowMin:number}}
18
+ */
19
+ export function pruneStaleToolOutputs(agent) {
20
+ const history = agent.history
21
+ const stale = collectStaleToolOutputs(history, agent.provider)
22
+ const counts = {
23
+ pruned: stale.indexes.length,
24
+ freed: stale.tokens,
25
+ candidates: stale.candidates,
26
+ tailKept: stale.tailKept,
27
+ belowMin: stale.belowMin,
28
+ }
29
+ if (counts.pruned === 0) return counts
30
+ for (const i of stale.indexes) {
31
+ const m = history[i]
32
+ // 多模态 tool 结果(content 数组)整体替换为 stub ⇒ 图像 part 丢弃(不可再取——prune 是删;回执只给重跑路径)
33
+ const chars = typeof m.content === "string" ? m.content.length : JSON.stringify(m.content ?? "").length
34
+ history[i] = { ...m, content: pruneStub(chars) }
35
+ }
36
+ agent._lastPromptTokens = null
37
+ agent._usageAtLen = null
38
+ return counts
39
+ }
40
+
41
+ /** Hard truncation limit for a single message body: when exceeded and the splitter can't find a middle section, truncate to a stub (prevents one giant message from blocking compaction) */
42
+ const OVERSIZE_CONTENT_LIMIT = 8_000
43
+
44
+ /**
45
+ * Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
46
+ * Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
47
+ * does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400
48
+ * risk from this path (this module's echo-safety face is `applyCompression`: the compaction placeholder is never
49
+ * committed as a separate reasoning-less assistant next to another assistant — D-CC18, 2026-09-16 batch).
50
+ * Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
51
+ */
52
+ export function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
53
+ let shrunk = false
54
+ // Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
55
+ // message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
56
+ // line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
57
+ // the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
58
+ // already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
59
+ const next = agent.history.map((m) => {
60
+ if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
61
+ if (m.content.length <= limit) return m
62
+ // Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
63
+ const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
64
+ const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
65
+ shrunk = true
66
+ return {
67
+ ...m,
68
+ content:
69
+ m.content.slice(0, keepHead) +
70
+ `\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
71
+ m.content.slice(-keepTail),
72
+ }
73
+ })
74
+ if (shrunk) {
75
+ agent.history = next
76
+ // Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
77
+ agent._lastPromptTokens = null
78
+ agent._usageAtLen = null
79
+ }
80
+ return shrunk
81
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * context-echo.mjs — 回声安全面(D-CC18 ∥ D-CC19 恢复面归并 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
3
+ * 内容 = `COMPACTION_PLACEHOLDER` ∥ 前缀器族(`prefixContent` / `withPlaceholderPrefix`)∥ 回声对归并
4
+ * (`isAssistantEchoPair` / `contentTextOf` / `absorbEchoContent` / `mergeAdjacentAssistantEchoes`)——迁出块逐字。
5
+ * 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改);
6
+ * 跨档被引的原私有面(`COMPACTION_PLACEHOLDER` ∥ `withPlaceholderPrefix`)补 `export` 关键字。
7
+ */
8
+
9
+ /** Placeholder assistant reply committed right after compaction (D9); D-CC18 merges it into an adjacent tail assistant instead of emitting it as a separate message */
10
+ export const COMPACTION_PLACEHOLDER = "Understood. I'll continue from these notes, re-verifying anything transient."
11
+
12
+ /**
13
+ * Content-shape-safe prefixing (D-CC18, generalized for D-CC19 merge reuse):
14
+ * string → text + blank line + original; multimodal array → text part prepended;
15
+ * empty string / null / undefined / other → text alone.
16
+ */
17
+ function prefixContent(content, text) {
18
+ if (typeof content === "string" && content.length > 0) return `${text}\n\n${content}`
19
+ if (Array.isArray(content)) return [{ type: "text", text }, ...content]
20
+ return text
21
+ }
22
+
23
+ /**
24
+ * D-CC18 echo safety: prefix the placeholder onto an existing assistant message's content.
25
+ * Thin wrapper over prefixContent (behavioral semantics unchanged).
26
+ */
27
+ export function withPlaceholderPrefix(content) {
28
+ return prefixContent(content, COMPACTION_PLACEHOLDER)
29
+ }
30
+
31
+ /**
32
+ * D-CC19 restore-path echo merge (2026-09-16 ENGINE-DEBT 批 ED-1): persisted `contextHistory`
33
+ * is loaded back VERBATIM on session restore, so the D-CC18 pathological shape (an assistant
34
+ * WITHOUT reasoning_content directly followed by another assistant — DeepSeek-family thinking
35
+ * mode rejects the first request with 400) can revive from disk. Pure in-core function: scans
36
+ * only at restore time, never prompts the user, never rewrites the session file. Merge direction
37
+ * matches D-CC18 (the reasoning-less message is absorbed INTO its follower — the follower's
38
+ * tool_calls / reasoning_content / other fields are kept verbatim; texts joined with a blank
39
+ * line). Iterates to a fixed point (chains collapse in full). Copy-on-write: messages may be
40
+ * shared with other lines, so the merged message is always a NEW object; clean input returns
41
+ * the SAME array reference (zero copy).
42
+ */
43
+
44
+ /** Pair predicate: prev = assistant with no/empty reasoning_content and no tool_calls,
45
+ * directly followed by another assistant. (Prev WITH tool_calls is never merged — pairing
46
+ * safety, F-3.) */
47
+ export function isAssistantEchoPair(prev, next) {
48
+ return prev?.role === "assistant"
49
+ && next?.role === "assistant"
50
+ && !prev.reasoning_content
51
+ && !(Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0)
52
+ }
53
+
54
+ /** Text of a message content in any supported shape (string / parts array / null-ish). */
55
+ function contentTextOf(content) {
56
+ if (typeof content === "string") return content
57
+ if (Array.isArray(content)) {
58
+ return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n")
59
+ }
60
+ return ""
61
+ }
62
+
63
+ /** Absorb prev's text into next's content (blank-line join; empty prev text → next unchanged). */
64
+ function absorbEchoContent(prevContent, nextContent) {
65
+ const text = contentTextOf(prevContent)
66
+ if (text.length === 0) return nextContent
67
+ return prefixContent(nextContent, text)
68
+ }
69
+
70
+ export function mergeAdjacentAssistantEchoes(history) {
71
+ if (!Array.isArray(history)) return history
72
+ let src = history
73
+ for (;;) {
74
+ let changed = false
75
+ const next = []
76
+ for (let i = 0; i < src.length; i++) {
77
+ if (i + 1 < src.length && isAssistantEchoPair(src[i], src[i + 1])) {
78
+ next.push({ ...src[i + 1], content: absorbEchoContent(src[i].content, src[i + 1].content) })
79
+ i += 1
80
+ changed = true
81
+ } else {
82
+ next.push(src[i])
83
+ }
84
+ }
85
+ if (!changed) return src === history ? history : src
86
+ src = next
87
+ }
88
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * context-push.mjs — 消息双线写入单点(写缝面 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
3
+ * `pushReal` = 真实消息写缝(机器线 `agent.history` + 人读线 `agent._fullHistory` 同源双写);
4
+ * `pushRecord` = 人读线留档记录单点(机器线零触)。迁出块逐字;原档 `context.mjs` 经
5
+ * `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
6
+ */
7
+
8
+ /**
9
+ * pushReal — the single entry point for REAL conversation messages.
10
+ * A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
11
+ * agent.history — the machine context (compaction shrinks this)
12
+ * agent._fullHistory — the human-readable record (persistence source)
13
+ * Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
14
+ * are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
15
+ * The two lines are written independently at the source — no after-the-fact delta sync.
16
+ * Message timestamps (SESSION.md §6.9): stamped HERE once at push time (epoch ms) — a single
17
+ * point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
18
+ * is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
19
+ * ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
20
+ *
21
+ * TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14)——人读线内存有界 + 磁盘为准:
22
+ * ① `agent._recordStore?.append(msg)`:记录同步追加(磁盘为准——append-only sidecar);
23
+ * ② 窗口驱逐:绑定态(agent._historyWindow = 200)下 _fullHistory 只保最近窗口条——
24
+ * 更早内容仅存磁盘(翻页/检索/保存从盘按需读)。未绑定(模式 F)不驱逐(零回归)。
25
+ * 追加失败不阻断回合(独立 try/catch——尽力面 N-S6;store 内部另置 degraded 并停写)。
26
+ */
27
+ export function pushReal(agent, msg) {
28
+ if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
29
+ if (msg && msg.ts === undefined) msg.ts = Date.now()
30
+ agent._fullHistory.push(msg)
31
+ try { agent._recordStore?.append(msg) } catch { /* 尽力面:落盘失败不阻断回合(N-S6) */ }
32
+ const win = agent._historyWindow
33
+ if (win > 0 && agent._fullHistory.length > win) {
34
+ agent._fullHistory.splice(0, agent._fullHistory.length - win)
35
+ }
36
+ agent.history.push(msg)
37
+ }
38
+
39
+ /**
40
+ * pushRecord — 人读线留档记录单点(消化生命周期面 · #726;形 ∥ 写缝 ∥ 判据单源 = SESSION.md §6.26)。
41
+ * `pushReal` 双胞(直复用——零算法副本):`ts` 打点 ∥ 记录存储追加(`_recordStore?.append`——§6.14)∥
42
+ * 尾窗驱逐三面同源;**机器线零触** = `history` 以一次性弃数组承接 ⇒ 记录不入 `agent.history` /
43
+ * `contextHistory`(不喂模型)。未绑定(`_recordStore` 缺)⇒ 人读线追加照常 ∥ 存储腿空转(零抛——模式 F
44
+ * 零回归);载体缺位(无活跃会话)⇒ 零动作(零抛——日志归端侧)。
45
+ */
46
+ export function pushRecord(agent, record) {
47
+ if (!agent || typeof agent !== "object") return // 载体缺位:零动作(零抛——§6.26 失败面)
48
+ if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
49
+ // 载体形人读线半提取(先例 = 桌面 `session-io.appendRecord`):`history` 弃数组承接 ⇒ 机器线零触
50
+ pushReal({ _fullHistory: agent._fullHistory, _recordStore: agent._recordStore, _historyWindow: agent._historyWindow, history: [] }, record)
51
+ }