thincoder 0.12.67 → 0.12.69

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 (64) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +21 -5
  3. package/bin/thincoder.mjs +41 -344
  4. package/package.json +2 -2
  5. package/src/acp/bridge.mjs +5 -2
  6. package/src/acp/handlers-session.mjs +12 -1
  7. package/src/cli/make-agent.mjs +44 -119
  8. package/src/cli/memory-command.mjs +45 -3
  9. package/src/command-interactive.mjs +196 -0
  10. package/src/command-table.mjs +185 -0
  11. package/src/completions.mjs +36 -12
  12. package/src/crash-reports.mjs +42 -20
  13. package/src/distill.mjs +3 -3
  14. package/src/heap-watch.mjs +5 -14
  15. package/src/tui/agent-turn.mjs +88 -50
  16. package/src/tui/clipboard.mjs +74 -30
  17. package/src/tui/cmd-advisor.mjs +39 -13
  18. package/src/tui/cmd-clear.mjs +2 -2
  19. package/src/tui/cmd-config.mjs +17 -1
  20. package/src/tui/cmd-eng.mjs +1 -1
  21. package/src/tui/cmd-mcp.mjs +1 -1
  22. package/src/tui/cmd-new.mjs +2 -2
  23. package/src/tui/cmd-reindex.mjs +1 -1
  24. package/src/tui/cmd-session.mjs +13 -5
  25. package/src/tui/cmd-think.mjs +35 -17
  26. package/src/tui/cmd-timers.mjs +33 -0
  27. package/src/tui/conversation-writer.mjs +2 -2
  28. package/src/tui/display-budget.mjs +9 -9
  29. package/src/tui/fold-block.mjs +1 -1
  30. package/src/tui/index.mjs +54 -18
  31. package/src/tui/interaction.mjs +4 -2
  32. package/src/tui/key-handler-ctrlc.mjs +1 -1
  33. package/src/tui/key-handler-search.mjs +1 -1
  34. package/src/tui/key-handler.mjs +1 -1
  35. package/src/tui/key-modes.mjs +1 -1
  36. package/src/tui/layout.mjs +5 -5
  37. package/src/tui/lifecycle-records.mjs +199 -0
  38. package/src/tui/model-picker.mjs +16 -199
  39. package/src/tui/mouse.mjs +3 -3
  40. package/src/tui/pickers.mjs +6 -2
  41. package/src/tui/provider-admin.mjs +213 -0
  42. package/src/tui/queued-merge.mjs +4 -60
  43. package/src/tui/queued-pickup.mjs +3 -3
  44. package/src/tui/render-conversation.mjs +13 -4
  45. package/src/tui/render-frame.mjs +43 -6
  46. package/src/tui/render-loop.mjs +2 -3
  47. package/src/tui/render-segments.mjs +2 -2
  48. package/src/tui/render.mjs +1 -1
  49. package/src/tui/slash-commands.mjs +3 -0
  50. package/src/tui/startup.mjs +28 -3
  51. package/src/tui/subagent-blocks.mjs +27 -19
  52. package/src/tui/subagent-children.mjs +5 -5
  53. package/src/tui/subagent-freeze.mjs +39 -37
  54. package/src/tui/subagent-panel.mjs +7 -7
  55. package/src/tui/suspension-drive.mjs +172 -259
  56. package/src/tui/timer-watch.mjs +64 -0
  57. package/src/tui/tool-args.mjs +10 -71
  58. package/src/tui/tool-display.mjs +6 -6
  59. package/src/tui/tool-events.mjs +33 -28
  60. package/src/tui/tui-lifecycle.mjs +13 -4
  61. package/src/tui/tui-state.mjs +3 -2
  62. package/src/tui/turn-face.mjs +4 -1
  63. package/src/tui/wizard.mjs +7 -2
  64. package/src/tui/wrapped-spawn.mjs +3 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [0.12.69] — 2026-10-03
2
+
3
+ > 0.12.68 → 0.12.69(连续号——发布时定号)
4
+
5
+ ### Changed
6
+
7
+ - **核依赖升级**:`@thincoder/core` `^0.10.1` → `^0.10.2`(统一 provider 解析随核生效)。
8
+ - **无效 provider 态分流(#841)**:TUI——`fallback`(可运行但未设默认模型)⇒ 不弹选择器 + 一行明示(`模型` 缺 ⇒ 词形仅渠道名);真无效(无 provider ∕ 无 key)⇒ 选择器 + 引导提示行。headless——真无效 ⇒ 可读错误 + 明确退出码;`fallback` ⇒ stderr 一行 + **继续运行**(不退出)。
9
+
10
+ ## [0.12.68] — 2026-10-03
11
+
12
+ > 0.12.67 → 0.12.68(连续号——发布时定号)
13
+
14
+ ### Added
15
+
16
+ - **空闲唤醒**:挂起会话可被定时器唤醒——到点自动续跑(可见面 + 中止适配;timer-wake #443 ∕ #444)。
17
+ - **台账健康信号**:台账库异常 ⇒ TUI 警告行(空值以 `—` 占位)。
18
+ - **heap 快照采集运行期开关**(`diagnostics.heapSnapshot`——默认关)。
19
+ - **`PROJECT-MANIFEST.json` 声明面**:codePaths ∕ index ∕ advisor 三键入驻(`conventions.json` 退役);工程写门豁免 aux 路径(test/tests/scripts、`.thincoder/tmp`)。
20
+
21
+ ### Changed
22
+
23
+ - **核依赖升级**:`@thincoder/core` `^0.9.5` → `^0.10.1`(公开仓读取 ∥ 会话锚解析 ∥ 多仓机制 ∥ 纪律面随核生效)。
24
+ - **交互整批(parity B1–B10 + 走查轮)**:滚动退出 ∥ 挂起队列 ∥ 状态栏 ∥ 渲染性能 ∥ i18n ∥ 设置 ∥ 复制 ∥ 卡顿指示 ∥ 发送忙碌队列。
25
+ - **测试面重整**:旧套件族退役(runner 骨架保留——空 = 绿);单测 = 批内件(随批档)。
26
+
27
+ ### Fixed
28
+
29
+ - **tech-debt 轮 6 ∕ 8**:slash 直通 ∥ 模态门 ∥ sticky bit 重臂 ∥ 级联清除 ∥ aborted reason ∥ advisor 槽守卫 ∥ 台账写者 ∥ timer 支收敛。
30
+ - **批档 CRLF 容忍 + 粘贴生命周期**(#732 ∕ #733)。
31
+ - **会话台账读写守卫**(项目相对守卫面 + 读守卫)。
32
+
1
33
  ## [0.12.67] — 2026-09-25
2
34
 
3
35
  > 0.12.66 → 0.12.67(连续号——发布时定号)
package/README.md CHANGED
@@ -17,8 +17,8 @@ resolved from the npm registry for releases).
17
17
  - **Checkpoint system**: auto-snapshot before every user task, `list`/`create`/`rewind` tools for the model, single-file restore — rewinding itself is reversible (pre-rewind state auto-saved)
18
18
  - **Codebase understanding** ⭐0.5.0: `repo_outline` (dependency outline, auto-injected at startup), `code_search` (source FTS5 + vectors + JSDoc extraction), `doc_search` (docs chunked by ## headings) — background indexing, auto-incremental updates on file writes, three tools
19
19
  guided by "structure → intent → details"
20
- - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from twenty-one providers — DeepSeek / Kimi / Kimi For Coding / GLM / GLM Coding Plan (智谱 Coding) / Qwen / Qwen Token Plan / MiMo (小米) / MiMo Token Plan (小米套餐) / MiniMax / OpenAI / Claude / Gemini /
21
- Grok / Mistral / Volcengine Ark (豆包) / Hunyuan (腾讯混元) / Tencent TokenHub (腾讯混元网关) / SiliconFlow (硅基流动) / OpenRouter / Groq. No legacy model compatibility. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type /
20
+ - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from twenty-two providers — DeepSeek / Kimi / Kimi For Coding / GLM / GLM Coding Plan (智谱 Coding) / Qwen / Qwen Token Plan / MiMo (小米) / MiMo Token Plan (小米套餐) / MiniMax / OpenAI / Claude / Gemini /
21
+ Grok / Mistral / Volcengine Ark (豆包) / Hunyuan (腾讯混元) / Tencent TokenHub (腾讯混元网关) / Huawei Cloud MaaS (华为云 MaaS) / SiliconFlow (硅基流动) / OpenRouter / Groq. No legacy model compatibility. Auto-matched context windows, truncation-resume protocols (prefix/partial), thinking-mode APIs (thinking.type /
22
22
  reasoning_effort), reasoning_content echo strategies (reasoningEcho), output limits, temperature range clamping — all deeply adapted.
23
23
  - **Toolset**: `read` / `write` / `edit` / `bash` / `glob` (supports `**`) / `grep` / `websearch` / `ls` / `fetch` + `read_image` (image/video paste) + three retrieval tools + MCP — all zero-dependency, file tools confined to the working directory
24
24
  - **Memory system**: three layers (personal/project/team), FTS5 + vector RRF hybrid retrieval, git-friendly markdown format
@@ -80,6 +80,8 @@ thincoder
80
80
  First launch walks you through a setup wizard: arrow keys to pick a provider (built-in presets or a custom endpoint) → enter API key → optionally enter an embedding key (SiliconFlow, enables vector memory search, skippable) → arrow keys to pick a model — no hand-editing config
81
81
  files. Adjust anytime with `/model` (add/remove providers, set keys, custom endpoints) or `/config embedkey`. `chat`/`distill` also offer in-place interactive setup when no key is configured in a terminal (in pipes/CI they exit with an error and instructions).
82
82
 
83
+ Any OpenAI-compatible endpoint works without a built-in preset: pick **add provider → custom** in `/model` (name + baseURL + model), or hand-write the entry in `providers[]`. Regional endpoint variants work the same way — e.g. Huawei Cloud MaaS `https://api-ap-southeast-1.modelarts-maas.com/openai/v1`.
84
+
83
85
  You can also hand-write `~/.thincoder/config.json` (see "Configuration" below), then:
84
86
 
85
87
  ```bash
@@ -91,6 +93,7 @@ thincoder memory put --type=rule --title="code style" --content="no semicolons"
91
93
  thincoder memory search "code style"
92
94
  thincoder memory list
93
95
  thincoder memory remove 1
96
+ thincoder memory sweep
94
97
 
95
98
  # Team memory (optional, available after configuring memory.team)
96
99
  thincoder sync # pull the team repo and rebuild the index
@@ -110,7 +113,8 @@ encoding/command issues), `/think` (thinking mode toggle and reasoning effort),
110
113
  sessions), `/reindex` (rebuild the index), `/extract` (extract knowledge from the current session), `/restore` (restore checkpoint), `/clear`, `/exit`. High-frequency commands support abbreviations: `/h` `/x` `/m` `/p` `/t` `/c` `/n`. Typing `/` shows live matching hints in the
111
114
  status bar. Model picker supports search/filter — type to narrow down results.
112
115
 
113
- Configuration comes exclusively from `~/.thincoder/config.json` — no environment-variable configuration is supported.
116
+ Configuration comes exclusively from `~/.thincoder/config.json` — the single source of truth. Sensitive value fields (e.g. a provider's `apiKey` or `websearch.apiKey`)
117
+ may hold a `${env:VAR}` reference, resolved at use time — the value is then taken from the environment variable. There is no environment-variable-only configuration channel and no env-only override.
114
118
 
115
119
  > **Kimi note**: Kimi has **two separate platforms with non-interchangeable API keys** — Moonshot (`https://api.moonshot.cn/v1`, keys `sk-...`, platform.moonshot.cn) and **Kimi For Coding** (`https://api.kimi.com/coding/v1`, keys `sk-kimi-...`, platform.kimi.com, model ID `k3`).
116
120
  > Use the `kimi` preset for Moonshot and `kimi-code` for Kimi For Coding — putting one platform's key on the other's endpoint fails with 401 (a hint is appended when the key/baseURL look mismatched).
@@ -148,6 +152,7 @@ Configuration comes exclusively from `~/.thincoder/config.json` — no environme
148
152
  "subagentModel": null, // default subagent provider/model override: "provider:model" | provider name | model name; null = inherit parent provider. Per-call: subagent tool `model` arg
149
153
  "subagentModels": {}, // per-type override: { "explore": "...", "plan": "...", "coder": "...", "eng-coder": "...", "eng-designer": "..." }; priority: tool model arg > this > subagentModel > parent provider
150
154
  "compactThreshold": 100000, // context compaction threshold (approx. tokens)
155
+ "timerWake": true, // timer idle wake: expired timers are delivered on the spot and start a turn when the host is idle — supported face = CLI / VSC / desktop foreground ends (headless chat / ACP: step-boundary delivery only); false = step-boundary delivery only
151
156
  },
152
157
  "memory": {
153
158
  // Path fields (dbPath / projectDir / team.dir / shell) expand a leading ~ (~, ~/, ~\) to the home directory at load time
@@ -196,7 +201,7 @@ src/
196
201
  key handling, mouse/clipboard, the cmd-* command family, subagent panels
197
202
  tui.mjs re-export shim → src/tui/index.mjs
198
203
  upgrade.mjs version check and self-upgrade
199
- test/ node:test offline unit tests (npm test)
204
+ test/ suite entry (npm test) — integration set; unit tests = batch-local files (docs/batches/) — current state: ../docs/core/design/TESTING.md §2.3
200
205
  scripts/ repo-side scripts — release check, doc-impact, syntax/endpoint probes,
201
206
  real-environment verification (compaction, team sync)
202
207
  ```
@@ -213,7 +218,7 @@ Key design decisions:
213
218
  ## Development
214
219
 
215
220
  ```bash
216
- npm test # offline unit tests (node:test, with local mock servers)
221
+ npm test # repo suite (integration set, offline) — empty manifest after the 2026-09-28 reset; unit tests = docs/batches/ — current state: ../docs/core/design/TESTING.md §2.3
217
222
  node scripts/verify-compress.mjs # real-API verification of context compaction (needs valid config)
218
223
  node scripts/verify-team.mjs # team memory A->git->B full-chain verification (local git, offline)
219
224
  ```
@@ -226,6 +231,17 @@ Code conventions: pure `.mjs`, no semicolons, no third-party npm dependencies al
226
231
 
227
232
  ## Changelog
228
233
 
234
+ ### 0.12.69 (2026-10)
235
+ - **Provider-state unify (#841)** — one core resolution for the three ends (slot → `defaultModel` → first keyed channel) with three states: `ok`; `fallback` (runnable but no default model — one clear notice line, never blocked); `invalid` (no provider/key — guided picker; headless exits with a readable message and a clear code, while `fallback` prints one stderr line and keeps running). No more false "API key not configured" on a working channel.
236
+ - **Core `^0.10.2`** — unified provider resolution (`resolveProviderPlan` / `resolveChannelModel`), `providerState` / `providerStateReason` keys, slot key gate.
237
+
238
+ ### 0.12.68 (2026-10)
239
+ - **Idle wake** — suspended sessions wake to timers: they resume on schedule (visibility surface + abort accommodation).
240
+ - **Ledger health signals** — an unhealthy ledger database now shows a warning line in the TUI (empty values render as `—`).
241
+ - **Interaction batch (parity B1–B10)** — scroll exits, suspend queue, status line, render performance, i18n, settings, copy, stall indicator, send-while-busy queue.
242
+ - **Core `^0.10.1`** — public-repo reading (declared sources), session-anchor resolution for multi-repo workspaces, the multi-repo operation mechanism, and discipline-surface fixes ride in with the core.
243
+ - **Fixes** — tech-debt rounds 6/8 (slash passthrough, modal gate, sticky-bit re-arm, cascade clear, aborted reason, advisor slot guard, ledger writer); batch CRLF tolerance and paste lifecycle; ledger read/write guards.
244
+
229
245
  ### 0.12.67 (2026-09)
230
246
  - **Visible busy queue (multi-slot)** — queue up to 8 messages while a turn runs: a "to send" block shows each queued message in the stream, the status bar has four states, and batches are picked up at step boundaries (without interrupting in-flight tools) and merged into one message per turn (`/cmd` entries keep their order). New ledger commands: `thincoder ledger migrate --dry-run|--confirm` and `thincoder ledger audit`.
231
247
  - **Effort menus fixed** — the consult effort picker no longer shows a duplicated `none`; advisor effort "off" normalizes to `think_off` with an accurate receipt.
package/bin/thincoder.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  * thincoder — CLI entry point
5
5
  * thincoder Launch the interactive TUI
6
6
  * thincoder chat "..." One-shot agent run (tools enabled, streamed)
7
- * thincoder memory <sub> Memory management: list / search / put / remove
7
+ * thincoder memory <sub> Memory management: list / search / put / remove / sweep(sweep:origin 级库治理)
8
8
  * thincoder upgrade Update to the latest version from npm
9
9
  * thincoder completion <sh> Shell completion: bash / zsh / fish
10
10
  * thincoder session gc Session dir GC: --dry-run report / --confirm delete cold projects (SESSION.md §6.12)
@@ -15,48 +15,56 @@
15
15
  */
16
16
 
17
17
  import { readFileSync } from "node:fs"
18
- import { isAbsolute, join } from "node:path"
19
- import { runAgent } from "@thincoder/core/agent.mjs"
20
18
  import { loadConfig, configPath } from "@thincoder/core/config.mjs"
21
19
  import { tracesRoot } from "@thincoder/core/traces/trace-store.mjs"
22
20
  import { scheduleTraceCleanup } from "@thincoder/core/traces/trace-cleanup.mjs"
23
- import { createMemory, syncDir } from "@thincoder/core/memory.mjs"
24
- // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
25
- import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
26
- import { assembleAgent, attachManifest, teamConfig, gitAuthor, validateProvider } from "../src/cli/make-agent.mjs"
27
- import { memoryCommand } from "../src/cli/memory-command.mjs"
28
- import { setupWizard } from "../src/cli/setup-wizard.mjs"
29
- import { summarize, askPermission } from "../src/cli/permission.mjs"
30
- import { distillCommand } from "../src/cli/distill-command.mjs"
31
- import { prepareCrashReporting, recentCrashHint, writeCrashRecord } from "../src/crash-reports.mjs"
32
- import { setTuiActive, restoreTerminalAfterCrash } from "../src/tui/tui-lifecycle.mjs"
21
+ import { prepareCrashReporting, writeCrashRecord } from "../src/crash-reports.mjs"
22
+ import { setTuiActive, restoreTerminalAfterCrash, _setCleanupOutPathForTest } from "../src/tui/tui-lifecycle.mjs"
33
23
  import { spawnTuiWrapped } from "../src/tui/wrapped-spawn.mjs"
34
24
  import { configurePromptInjections } from "@thincoder/core/prompt-files.mjs"
35
25
  import { CLI_PROMPT_INJECTIONS } from "../src/prompt-injections.mjs"
26
+ // 拆档批 R4(2026-09-28):命令分发表外提——分发骨架 + 八薄命令族 + help ∕ version =
27
+ // `src/command-table.mjs`;交互长驻三命令(chat ∕ tui ∕ acp)= `src/command-interactive.mjs`。
28
+ import { runCommandTable } from "../src/command-table.mjs"
36
29
 
37
30
  // U2(CORE-UNIFICATION §2.6.3 专项补⑦ / §2.13.8(六)):CLI 端锚取值表——**进程入口、任何装配之前**注册一次(调用期应用 ⇒ 无导入序要求)。
38
31
  // 漏配 = 锚字面静默进模型 ⇒ 入口面用例(test/integration/cli-prompt-entry.test.mjs)显式覆盖本调用路径。
39
32
  configurePromptInjections(CLI_PROMPT_INJECTIONS)
40
33
 
41
- const [command, ...args] = process.argv.slice(2)
34
+ // CONFIG.md §6.2 ④:内部 argv 标志(生产零路径)。`--tui-wrapped`(包装门——注入点 =
35
+ // src/tui/wrapped-spawn.mjs)在 bin 顶部自剥离(不进命令解析);测试钩逐旗标
36
+ // (`--test-crash` / `--test-tui-active` / `--test-cleanup-out=<路径>`)在下方崩溃钩处按原始 argv
37
+ // (位置无关)判定后抛错接管——先于命令分派,永不作为命令被解释。
38
+ const _argvRaw = process.argv.slice(2)
39
+ const _tuiWrapped = _argvRaw.includes("--tui-wrapped")
40
+ const [command, ...args] = _argvRaw.filter((a) => a !== "--tui-wrapped")
42
41
  // TUI-STDERR-CAPTURE(F-1):TUI 启动(tui/无命令)默认包装——父 spawn 子 tee stderr 落盘(外部
43
42
  // 终止/native abort——第 4 类崩溃面——诊断默认捕获)。须在 prepareCrashReporting 前(父不预建/不设
44
- // report——子进程做——R25 保留)。env 门已设(包装内子进程)或包装失败 → 直行现逻辑(尽力面)。
45
- if ((command === undefined || command === "tui") && !process.env.THINCODER_TUI_WRAPPED) {
46
- if (spawnTuiWrapped()) await new Promise(() => {}) // 包装成功 → 挂起(tee/收尾退出全在 wrapped-spawn——不达下方 switch)
43
+ // report——子进程做——R25 保留)。argv 门已设(包装内子进程)或包装失败 → 直行现逻辑(尽力面)。
44
+ if ((command === undefined || command === "tui") && !_tuiWrapped) {
45
+ if (spawnTuiWrapped()) await new Promise(() => {}) // 包装成功 → 挂起(tee/收尾退出全在 wrapped-spawn——不达下方命令分发)
47
46
  }
48
47
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
49
48
 
49
+ // CONFIG.md §6.2 ①(用户面两开关——默认各向):判定单点 = 本入口——读配置键
50
+ // `diagnostics.{heapSnapshot,heapWatch}` 一次,经显式参数传入下方两消费者;读抛 ⇒ 各向默认
51
+ // (watch 开 ∥ 快照关——fail-closed)。
52
+ let _diagnostics = {}
53
+ try {
54
+ _diagnostics = loadConfig().diagnostics ?? {}
55
+ } catch { /* 配置缺失/损坏 → 各向默认(watch 开 ∥ 快照关——fail-closed) */ }
56
+
50
57
  // R25(F-R25b):crash-reports 预建 + process.report 启用——入口最前(一切重活前——缩编程
51
58
  // 期窗口)——V8 OOM/原生 fatal 自动写 report.*.json(实现批实测:目录缺失时 Node 静默不写
52
59
  // ——预建为必要动作)。失败不阻断启动(尽力面)。
53
- prepareCrashReporting()
60
+ prepareCrashReporting({ heapSnapshot: _diagnostics.heapSnapshot === true })
54
61
  // TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §8.3 武装点——全命令同源单点):堆遥测/看门狗——
55
62
  // 60s 采样 + 双档(70/85%)比例边缘预警(stderr + TUI 行 + 事件日志);定时器 unref
56
- // (一次性命令自然退出零阻塞);默认开 / THINCODER_HEAP_WATCH 关值集可不启。失败面全吞。
63
+ // (一次性命令自然退出零阻塞);默认开(配置键 `diagnostics.heapWatch` 取 `false` 可不启)。
64
+ // 失败面全吞。
57
65
  try {
58
66
  const { startHeapWatch } = await import("../src/heap-watch.mjs")
59
- startHeapWatch()
67
+ startHeapWatch({ enabled: _diagnostics.heapWatch !== false })
60
68
  } catch { /* 看门狗启动失败不阻断启动(尽力面) */ }
61
69
 
62
70
  // R25(F-R25a)异常钩子升级:原"只 console.error 一行(TUI 全屏下不可见)"→ ① 落盘
@@ -91,12 +99,15 @@ function handleFatal(type, error) {
91
99
  process.on("uncaughtException", (error) => handleFatal("uncaughtException", error))
92
100
  process.on("unhandledRejection", (error) => handleFatal("unhandledRejection", error))
93
101
 
94
- // R25 测试门(T-R25a.1/a.2——子进程 env 注入——生产零路径):THINCODER_TEST_CRASH=1 抛
95
- // 未捕获异常走完整崩溃序列;THINCODER_TEST_TUI_ACTIVE=1 模拟 TUI 活动态(同一 setter——
96
- // 测试缝同源);THINCODER_TEST_CLEANUP_OUT 指向文件时恢复序列写入该文件(tui-lifecycle 读)。
97
- if (process.env.THINCODER_TEST_CRASH === "1") {
98
- if (process.env.THINCODER_TEST_TUI_ACTIVE === "1") setTuiActive(true)
99
- throw new Error("R25 test crash (THINCODER_TEST_CRASH)")
102
+ // R25 测试门(T-R25a.1/a.2——子进程 argv 注入——生产零路径):`--test-crash` 抛未捕获异常
103
+ // 走完整崩溃序列;`--test-tui-active` 模拟 TUI 活动态(同一 setter——测试缝同源);
104
+ // `--test-cleanup-out=<路径>` 指到文件时恢复序列写入该文件(tui-lifecycle 缝——启动时读点)。
105
+ // 判定面 = 原始 argv(位置无关——与旧 env 门同语义:旗标可居首/居尾,不依赖命令位)。
106
+ const _cleanupOutArg = _argvRaw.find((a) => a.startsWith("--test-cleanup-out="))
107
+ if (_cleanupOutArg) _setCleanupOutPathForTest(_cleanupOutArg.slice("--test-cleanup-out=".length))
108
+ if (_argvRaw.includes("--test-crash")) {
109
+ if (_argvRaw.includes("--test-tui-active")) setTuiActive(true)
110
+ throw new Error("R25 test crash (--test-crash)")
100
111
  }
101
112
 
102
113
  const USAGE = `thincoder - thin coding agent
@@ -111,6 +122,7 @@ Usage:
111
122
  thincoder memory search <query> Search memory
112
123
  thincoder memory put --type=<t> --title=<t> --content=<c> [--tags=<t>]
113
124
  thincoder memory remove <id> Remove an entry
125
+ thincoder memory sweep [--origin <o>] [--dry-run|--confirm]
114
126
  thincoder sync Sync team memory repo (pull --rebase + reindex)
115
127
  thincoder reindex Rebuild the local index from markdown sources
116
128
  thincoder distill <file> [--yes] [--layer=<s>]
@@ -161,321 +173,6 @@ if (command === undefined || command === "tui" || command === "chat" || command
161
173
  } catch { /* 拍挂点失败静默——不阻断启动 */ }
162
174
  }
163
175
 
164
- switch (command) {
165
- case "chat": {
166
- const auto = args.includes("--auto")
167
- const prompt = args.filter((a) => a !== "--auto").join(" ").trim()
168
- if (!prompt) {
169
- console.error('Usage: thincoder chat [--auto] "<prompt>"')
170
- exitSoon(1)
171
- break
172
- }
173
-
174
- // R25(F-R25c):非交互/chat 模式——上次异常终止提示写 stderr 一行(无匹配不提示)
175
- const crashNotice = recentCrashHint()
176
- if (crashNotice) console.error(crashNotice)
177
-
178
- const agent = await assembleAgent()
179
- // SESSION.md §6.8 D-S4(F4)+ MODEL-MERGE-SESSION:headless 无 TUI —— 可读错误 + 退出码 1,
180
- // 不弹 UI、不崩溃;文案改引 defaultModel(F-6 引导 A headless 面——/config → 默认模型)
181
- if (agent._providerInvalid) {
182
- const dm = agent.config?.defaultModel
183
- console.error(`[error] 未配置有效模型(defaultModel "${dm ?? "(not set)"}":${agent._providerInvalidReason})。请运行 thincoder 进入 TUI 设置(/config → 默认模型;或 /model 选择后以会话槽生效),或编辑 ${configPath}`)
184
- exitSoon(1)
185
- break
186
- }
187
- if (!agent.provider.apiKey) {
188
- if (!process.stdin.isTTY) {
189
- console.error(noKeyMessage())
190
- exitSoon(1)
191
- break
192
- }
193
- const p = await setupWizard()
194
- if (!p) {
195
- exitSoon(1)
196
- break
197
- }
198
- agent.provider = p
199
- agent.activeProvider = p.name
200
- // Wizard may have configured an embedding key: attach vector search
201
- const fresh = loadConfig()
202
- if (fresh.embedding?.apiKey && agent.memory && !agent.memory.embedder) {
203
- const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
204
- agent.memory.embedder = createEmbedder(fresh.embedding)
205
- }
206
- }
207
- if (auto) agent.autoApprove = true
208
- // Accumulate token usage, output to stderr at the end (don't pollute stdout pipe)
209
- const usageTotal = { prompt: 0, completion: 0, cacheHit: 0, cacheMiss: 0 }
210
- try {
211
- await runAgent(agent, prompt, {
212
- onToken: (text) => process.stdout.write(text),
213
- onWait: (ev) => {
214
- const s = waitStatusText(ev)
215
- if (s) console.error(`[rate-limit] ${s}`)
216
- },
217
- onToolCall: (name, toolArgs) => {
218
- console.error(`\n[tool] ${name} ${summarize(toolArgs)}`)
219
- },
220
- onToolResult: (name, result) => {
221
- const preview = result.length > 200 ? result.slice(0, 200) + "..." : result
222
- console.error(`[done] ${name} -> ${preview.split("\n")[0]}`)
223
- },
224
- onToolOutput: (name, chunk) => process.stderr.write(chunk),
225
- onCompress: () => console.error(`\n[context] Context too long, auto-compacted (early conversation summarized by LLM)`),
226
- onTaskUpdate: (items) => {
227
- const done = items.filter((i) => i.status === "done").length
228
- const current = items.find((i) => i.status === "in_progress")
229
- console.error(`[task] ${done}/${items.length}${current ? ` ▶ ${current.title}` : ""}`)
230
- },
231
- onUsage: (usage) => {
232
- usageTotal.prompt += usage.prompt_tokens ?? 0
233
- usageTotal.completion += usage.completion_tokens ?? 0
234
- usageTotal.cacheHit += usage.prompt_cache_hit_tokens ?? 0
235
- usageTotal.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
236
- },
237
- onPermissionRequest: (name, toolArgs) => (agent.autoApprove ? true : askPermission(name, toolArgs)),
238
- })
239
- process.stdout.write("\n")
240
- if (usageTotal.prompt > 0) {
241
- const cacheTotal = usageTotal.cacheHit + usageTotal.cacheMiss
242
- const hitPart = cacheTotal > 0 ? ` cache-hit ${Math.round((usageTotal.cacheHit / cacheTotal) * 100)}%` : ""
243
- console.error(`[usage] prompt ${usageTotal.prompt} + completion ${usageTotal.completion}${hitPart}`)
244
- }
245
- } catch (error) {
246
- // 用 name 判断而非 instanceof:不依赖"与 runAgent 同一个模块实例"这一隐式约定
247
- if (error.name === "ContinueError") {
248
- console.error(`\n[paused] Agent stopped after ${error.turn} turns. Run in TUI to continue.`)
249
- exitSoon(0)
250
- } else {
251
- console.error(`\n[error] ${error.message}`)
252
- exitSoon(1)
253
- }
254
- }
255
- break
256
- }
257
-
258
- case "memory": {
259
- const config = loadConfig()
260
- const memory = createMemory({ dbPath: config.memory.dbPath })
261
- if (config.embedding?.apiKey) {
262
- const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
263
- memory.embedder = createEmbedder(config.embedding)
264
- }
265
- if (config.memory.projectDir) {
266
- memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
267
- }
268
- const code = await memoryCommand(memory, args)
269
- if (code) exitSoon(code)
270
- break
271
- }
272
-
273
- case "sync": {
274
- const config = loadConfig()
275
- const team = teamConfig(config)
276
- if (!team) {
277
- console.error("Team memory not configured. Set memory.team in ~/.thincoder/config.json:")
278
- console.error(' "team": { "name": "myteam", "repo": "git@github.com:org/team-memory.git" }')
279
- exitSoon(1)
280
- break
281
- }
282
- const memory = createMemory({ dbPath: config.memory.dbPath })
283
- const { ensureClone, pullTeam } = await import("@thincoder/core/git/gitmem.mjs")
284
- try {
285
- const cloned = await ensureClone(team)
286
- if (cloned) console.log(`Cloned team repo to ${team.dir}`)
287
- await pullTeam(team.dir)
288
- const stats = await syncDir(memory, { layer: "team", dir: team.dir })
289
- console.log(`Synced. Index: +${stats.added} ~${stats.updated} -${stats.removed}`)
290
- } catch (error) {
291
- console.error(`[error] ${error.message}`)
292
- exitSoon(1)
293
- }
294
- break
295
- }
296
-
297
- case "distill": {
298
- const code = await distillCommand(args, exitSoon)
299
- if (code) exitSoon(code)
300
- break
301
- }
302
-
303
- case "reindex": {
304
- const config = loadConfig()
305
- const memory = createMemory({ dbPath: config.memory.dbPath })
306
- // files 层(project/team 的 markdown 索引)全量重建;索引是易失品,真相在 markdown
307
- memory.db.prepare(`DELETE FROM files`).run()
308
- const cwd = process.cwd()
309
- let total = { added: 0, removed: 0 }
310
- if (config.memory.projectDir) {
311
- const s = await syncDir(memory, { layer: "project", dir: isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir) })
312
- total.added += s.added
313
- }
314
- const team = teamConfig(config)
315
- if (team) {
316
- const { ensureClone } = await import("@thincoder/core/git/gitmem.mjs")
317
- await ensureClone(team)
318
- const s = await syncDir(memory, { layer: "team", dir: team.dir })
319
- total.added += s.added
320
- }
321
- console.log(`Reindexed ${total.added} markdown entries (project${team ? " + team" : ""}). Vectors will be lazily regenerated on next search.`)
322
- break
323
- }
324
-
325
- case "tui":
326
- case undefined: {
327
- // 恢复上次的会话(同一项目目录);provider 按保存的名字切回(用户上次可能换过模型)
328
- // 2026-09-05 §6.10(R4):恢复决策按本端记录 resumeSlot(D-2 ①②③)——manifest active
329
- // 只作"无记录端"的一次性继承源,不再作本端恢复第一依据(D-6)。
330
- // 2026-09-18(装配门禁批 #30 · KD-M1-13 / KD-M1-15):resumeSlot **前移至装配之前**——
331
- // 装配期模式门判据 = 会话权威值(槽优先 + config 回退);恢复记录经 slotData 形参进
332
- // 装配钩子(同进程同一调用前移 ⇒ 零新增写动作;钩子内不调 resumeSlot——非纯读)。
333
- const { resumeSlot, applySession, sessionDescriptor } = await import("@thincoder/core/session.mjs")
334
- // F-MI7:resumeSlot = async(探测束不阻塞事件循环)——此处必须 await
335
- const { slot, data } = await resumeSlot(process.cwd())
336
- const agent = await assembleAgent({ slotData: data })
337
- // SESSION.md §6.8 D-S1:TUI 路径在 startTUI 前清空无效 provider——空 provider 不流入 runAgent
338
- // (崩溃源:chat() 缺 model → 网关 400 或 fetch("undefined/...") TypeError)
339
- if (agent._providerInvalid) agent.provider = null
340
- const config = loadConfig()
341
- if (data) {
342
- // applySession 内部已按槽复合重算 compactThreshold(auto 时)——不再需要 switched 分支
343
- // TUI-OOM-ROOTCAUSE(SESSION.md §6.14):传 slot → 绑定记录存储(身份核验 + 对账)
344
- // ——人读线 = 尾窗(磁盘为准);未传 = 模式 F(全量数组)
345
- applySession(agent, data, { slot })
346
- // SESSION.md §6.11(2026-09-08——N6 评审 🔴 修复):process restarted 句 = 进程级
347
- // 信号——仅本启动 resume 路径设一次(真进程重启恢复盘上会话);/session 切换与 ACP
348
- // 加载走同一 applySession 收敛但不设(不误报进程重启)。prepareRun 发句即清——进程
349
- // 内一次。resumed:yes 独立由 applySession 的恢复事件武装(data.history 非空)。
350
- agent._processRestartPending = true
351
- }
352
- // D-3 钉槽:applySession 清 _slot 后立即钉回恢复槽——首保存必落恢复槽(消除
353
- // "load → 首回合保存"窗口内并发方翻 active 导致的静默迁移);/new 经 resetSessionState
354
- // 清 _slot(newSession 已认领 + 写记录,语义保留)
355
- agent._slot = slot
356
- // M1 重估点(会话起点②——applySession 之后 · KD-M1-13 取值点②):CLI 的工程模式权威值 =
357
- // 会话槽(/eng 只写槽不写 config.json);此时 agent.config.agent.engineering 已由
358
- // applySession 按槽订正 ⇒ 与装配期判据同值 ⇒ 幂等重估(值同 ⇒ 无副作用)。
359
- attachManifest(agent)
360
- // D-S3 优先级补全:applySession 可能已用会话中的有效 provider 修复(config 无效 + 会话有效)——
361
- // 修复后复验清除标记,仅当两者都无效才弹重选(validateProvider 幂等)
362
- if (agent._providerInvalid) validateProvider(agent)
363
- // MCP 连接失败在 TUI alt-buffer 下 stderr 不可见,注入为下一条 user 消息后的提醒
364
- if (agent._mcpWarnings?.length) {
365
- agent._pendingReminders = agent._pendingReminders ?? []
366
- agent._pendingReminders.push(
367
- `[System reminder: ${agent._mcpWarnings.length} MCP server(s) failed to connect at startup:\n` +
368
- agent._mcpWarnings.map((w) => ` - ${w}`).join("\n") +
369
- `\nYou can try reconnecting with /mcp connect <name>.]`
370
- )
371
- }
372
- const { startTUI } = await import("../src/tui.mjs")
373
- try {
374
- await startTUI(agent, {
375
- projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
376
- team: teamConfig(config),
377
- author: gitAuthor(),
378
- restored: data ? sessionDescriptor(agent, data) : data,
379
- // R25(F-R25c):TUI 启动显示一行"上次运行异常终止"提示(showStartup 渲染——无匹配不传)
380
- crashNotice: recentCrashHint() ?? undefined,
381
- })
382
- } catch (error) {
383
- console.error(`[error] ${error.message}`)
384
- exitSoon(1)
385
- }
386
- break
387
- }
388
-
389
- case "completion": {
390
- const shell = args[0]
391
- if (!["bash", "zsh", "fish"].includes(shell)) {
392
- console.error(`Usage: thincoder completion <bash|zsh|fish>`)
393
- exitSoon(1)
394
- break
395
- }
396
- // §11.2 交付行数债(2026-09-08——500 行硬限触碰执行):直写段逐字迁出
397
- // src/completions.mjs(脚本字节不变——printCompletion 发射)
398
- const { printCompletion } = await import("../src/completions.mjs")
399
- printCompletion(shell)
400
- break
401
- }
402
-
403
- case "upgrade": {
404
- const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
405
- const local = pkg.version
406
- const { checkForUpdate } = await import("../src/upgrade.mjs")
407
- const result = await checkForUpdate(local)
408
- if (!result) {
409
- console.error("[upgrade] Unable to query npm registry — check your network connection and that npm is installed")
410
- exitSoon(1)
411
- break
412
- }
413
- if (!result.newer) {
414
- console.log(`ThinCoder ${local} is already the latest.`)
415
- } else {
416
- console.log(`Upgrading: ${local} → ${result.latest}`)
417
- const { execSync } = await import("node:child_process")
418
- execSync("npm install -g thincoder@latest", { stdio: "inherit" })
419
- console.log(`Upgraded to ${result.latest}`)
420
- }
421
- break
422
- }
423
-
424
- case "acp": {
425
- const { runAcpServer, runAcpLogin } = await import("../src/acp.mjs")
426
- // §11.2 改法 4:`args` 语义 = 追加到已配置的 agent 调用——`--login` = 终端认证流程,
427
- // 不进 stdio 服务模式(不带该旗标时行为零变);退出码语义 = 0 成功 / 非 0 失败。
428
- if (args.includes("--login")) {
429
- exitSoon(await runAcpLogin())
430
- break
431
- }
432
- await runAcpServer()
433
- break
434
- }
435
-
436
- case "session": {
437
- // SESSION.md §6.12 / §6.17:会话目录 GC 显式面(冷 cwd 90 天面 + 三合取存量组全量面)——
438
- // 双端同面(2026-09-21 端差注销:VS Code 端命令 `thincoder.sessionGc` 走同一核数据面)。
439
- // SESSION.md §6.19 D-SE46(2026-09-22 会话索引批):`session index` = 派生索引库显式面
440
- // (--status 读数 / --rebuild 全量重扫)——同一 `session` 分发族(壳侧分发,实现在核)。
441
- if (args[0] === "index") {
442
- const { runSessionIndex } = await import("@thincoder/core/session-index-cmd.mjs")
443
- process.exitCode = await runSessionIndex(args)
444
- break
445
- }
446
- const { runSessionGc } = await import("@thincoder/core/session-gc.mjs")
447
- process.exitCode = await runSessionGc(args)
448
- break
449
- }
450
-
451
- case "ledger": {
452
- // LEDGER.md §2.2(2026-09-25 批):台账存量收正面——变体键合并迁移(migrate)/ 残档审计(audit);
453
- // 核内实现(ledger-migrate.mjs),壳侧只分发(先例 = `session gc` 双档:--dry-run 只读 / --confirm 执行)。
454
- if (args[0] === "migrate" || args[0] === "audit") {
455
- const m = await import("@thincoder/core/ledger-migrate.mjs")
456
- process.exitCode = await (args[0] === "migrate" ? m.runLedgerMigrate(args) : m.runLedgerAudit(args))
457
- break
458
- }
459
- console.error("Usage: thincoder ledger migrate --dry-run | --confirm [--from <key>] | thincoder ledger audit [--root <dir>]")
460
- exitSoon(1)
461
- break
462
- }
463
-
464
- case "--help":
465
- case "-h": {
466
- process.stdout.write(USAGE)
467
- break
468
- }
469
-
470
- case "--version":
471
- case "-v": {
472
- console.log(VERSION)
473
- break
474
- }
475
-
476
- default: {
477
- console.error(`Unknown command: ${command}\n`)
478
- process.stdout.write(USAGE)
479
- exitSoon(1)
480
- }
481
- }
176
+ // 命令分发(2026-09-28 拆档批 R4:分发表外提 `src/command-table.mjs`)——依赖经 ctx 注入:
177
+ // USAGE ∕ VERSION ∕ 两 helper = 本进程入口装配面(消费方:分发骨架与交互长驻三命令)。
178
+ await runCommandTable(command, args, { usage: USAGE, version: VERSION, exitSoon, noKeyMessage })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.67",
3
+ "version": "0.12.69",
4
4
  "description": "Thin coding agent - zero third-party dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -20,7 +20,7 @@
20
20
  "node": ">=24"
21
21
  },
22
22
  "dependencies": {
23
- "@thincoder/core": "^0.9.5"
23
+ "@thincoder/core": "^0.10.2"
24
24
  },
25
25
  "files": [
26
26
  "bin/",
@@ -308,9 +308,12 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
308
308
  // 2026-09-08 D1:单形态按行号改(path + line/startLine/endLine + new_string——无
309
309
  // old_string)同样走 IDE 缓冲通道(hasLineParams 判定)——否则回落本地写盘会与
310
310
  // IDE 缓冲脱敏;editSingle → computeEditEntry 行号语义自动继承。
311
- if (base === "edit" && canReadFile && canWriteFile && (Array.isArray(args?.edits) || (path && typeof args?.new_string === "string" && (typeof args?.old_string === "string" || hasLineParams(args))))) {
311
+ // #327(TOOLS.md §6.17 裁定 3):路由判据**归一**——`edits` 真值判(与核 execute 同判据):
312
+ // 真值 ⇒ 批量分支(共享容器守卫、同一错误面);假值 ⇒ 单形态面——「桥径单形态应用」分支消除。
313
+ const hasEdits = Boolean(args?.edits)
314
+ if (base === "edit" && canReadFile && canWriteFile && (hasEdits || (path && typeof args?.new_string === "string" && (typeof args?.old_string === "string" || hasLineParams(args))))) {
312
315
  try {
313
- const text = Array.isArray(args?.edits) ? await editBatch(args) : await editSingle(path, args)
316
+ const text = hasEdits ? await editBatch(args) : await editSingle(path, args)
314
317
  return { handled: true, result: text }
315
318
  } catch (e) {
316
319
  return { handled: true, result: `Error: ${e.message}` }
@@ -18,6 +18,9 @@ import { loadManifest, normalizeCwd, bindRecordStore, newSession, saveManifest,
18
18
  // core `session.mjs` 未 re-export 该名——直引 manifest 档,先例 = `thincoder-vscode/src/extension/session-io.mjs`)。
19
19
  import { staleClaims } from "@thincoder/core/session-slots-manifest.mjs"
20
20
  import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
21
+ // #363①(off 形族单源——MODEL-SPECS.md §16.2):thinking 开关的取形引核(本档零副本)。
22
+ import { thinkOffShape } from "@thincoder/core/think-off.mjs"
23
+ import { providerSpec } from "@thincoder/core/config.mjs"
21
24
  import { ACP_ERRORS } from "./transport.mjs"
22
25
 
23
26
  /** ENG-PLAN-EXCLUSION(FR31 ② · E7「命令面逐面钉定」):工程真值 = 核单源同键
@@ -85,7 +88,15 @@ export function applyConfigOption(agent, configId, value) {
85
88
  }
86
89
  case "thinking": {
87
90
  if (typeof value !== "boolean" || !agent.provider) return false
88
- agent.provider.thinking = value ? { type: "enabled" } : { type: "disabled" }
91
+ // off 取形 = 核单源(MODEL-SPECS.md §16.2——#363①):effort 族 ⇒ `null`(载荷门据其补发
92
+ // `reasoning_effort:"none"` = 唯一有效 off 路径);其余族 ⇒ `{ type:"disabled" }`。旧形
93
+ // 全族一形 `{type:"disabled"}` 在 effort 族被载荷层 falsy 跳过 ⇒ 关思考静默失效。
94
+ // on 侧同引核(thinkEnabledValue——自定义开值族 MiniMax = "adaptive")。
95
+ const spec = providerSpec(agent.provider)
96
+ agent.provider.thinking = value ? { type: spec.thinkEnabledValue ?? "enabled" } : thinkOffShape(spec)
97
+ // effort 族:`null` 标记在载荷层由「无显式档」门控(provider/core.mjs:193 显式档优先会
98
+ // 跳过标记)⇒ off 清档位(与 CLI `/think off` 同法——cmd-think.mjs `applyThink` off 支)。
99
+ if (!value) delete agent.provider.reasoningEffort
89
100
  return true
90
101
  }
91
102
  case "mode": {