thincoder 0.12.68 → 0.12.70

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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [0.12.70] — 2026-10-04
2
+
3
+ > 0.12.69 → 0.12.70(连续号——发布时定号)
4
+
5
+ ### Added
6
+
7
+ - **ACP 面第二批(#870–#873)**:`resource_link`(Zed @ 文件引用——含三降级态)∥ `usage_update` {used,size} + `session/list` updatedAt ISO(超域钳位)∥ 会话身份归一(槽号串 ∥ delete→new 同键处置)∥ `set_config_option` 全量 `configOptions` 回执 + `SessionConfigOption.type` 判别键。
8
+ - **`thincoder ledger list --json`(#886)**:台账只读接口(多项目 ∥ 空集 ∥ 全程只读);schemaVersion + evidence 默认省。
9
+ - **OpenCode Go 预设(#906)**:`/model` 与配置面开箱可选(双协议侧)。
10
+
11
+ ### Changed
12
+
13
+ - **核依赖升级**:`@thincoder/core` `^0.10.2` → `^0.10.3`(MCP 自愈 ∥ 环边界中止前置 ∥ 回执汇总行等随核生效)。
14
+ - **会话选定写回默认模型(#883)**:会话内 `/model` 选定 ⇒ `defaultModel` 同拍写回(新会话自动采用;失败不反扑 ∥ 零静默)。
15
+ - **设置面扇出修复(#842)**:设置改动即时生效面收正(免重启)。
16
+
17
+ ### Fixed
18
+
19
+ - **ACP batch-2 跟进**:delete→new 处置 ∥ `updatedAt` 钳位(T1–T20 + 2 腿全绿)。
20
+ - **Home 启动整盘索引守卫(#867)**:home 检测双触发面 + 假旧引擎版本门(逐字错误 + exit 1)。
21
+ - **TUI-OOM 余项(#863)**:/undo 双上界 ∥ console 采集 cap ∥ advisor 轻量化。
22
+ - **台账幽灵根(#899)**:歧义根发现修复(空壳库不再遮蔽)。
23
+
24
+ ## [0.12.69] — 2026-10-03
25
+
26
+ > 0.12.68 → 0.12.69(连续号——发布时定号)
27
+
28
+ ### Changed
29
+
30
+ - **核依赖升级**:`@thincoder/core` `^0.10.1` → `^0.10.2`(统一 provider 解析随核生效)。
31
+ - **无效 provider 态分流(#841)**:TUI——`fallback`(可运行但未设默认模型)⇒ 不弹选择器 + 一行明示(`模型` 缺 ⇒ 词形仅渠道名);真无效(无 provider ∕ 无 key)⇒ 选择器 + 引导提示行。headless——真无效 ⇒ 可读错误 + 明确退出码;`fallback` ⇒ stderr 一行 + **继续运行**(不退出)。
32
+
1
33
  ## [0.12.68] — 2026-10-03
2
34
 
3
35
  > 0.12.67 → 0.12.68(连续号——发布时定号)
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-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 /
20
+ - **Model adaptation** ⭐: top-tier only, latest only. Built-in flagship models from twenty-four 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 / OpenCode Go (same key for both protocol sides: OpenAI-compatible + Anthropic Messages). 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
@@ -57,7 +57,7 @@ Three layers, all "query if present, skip if absent", unified hybrid retrieval:
57
57
  - IDE-native diffs — `write`/`edit` route through the editor buffer
58
58
  - Persisted sessions: list / load (history replay) / resume / delete
59
59
  - Per-session config: model / thinking / mode
60
- - Setup: [ACP 接入设计](../docs/cli/design/ACP-CLIENT.md)
60
+ - Setup: [ACP 接入指南](https://thincoder.com/acp.html)
61
61
  - Login: `thincoder acp --login` — one interactive terminal login; ACP clients and orchestrators then drive sessions headlessly
62
62
 
63
63
 
@@ -231,6 +231,18 @@ Code conventions: pure `.mjs`, no semicolons, no third-party npm dependencies al
231
231
 
232
232
  ## Changelog
233
233
 
234
+ ### 0.12.70 (2026-10)
235
+ - **ACP face round 2 (#870–#873)** — `resource_link` (@ file references from Zed, with three graceful degradations); `usage_update` `{used,size}` + ISO `updatedAt`; stable session identity (slot-key ids, delete→new); `set_config_option` returns full `configOptions` + a `type` discriminator.
236
+ - **`thincoder ledger list --json` (#886)** — read-only ledger interface (multi-project / empty set / fully read-only).
237
+ - **OpenCode Go presets (#906)** — `opencode-go` / `opencode-go-anthropic` available in `/model` and config (dual-protocol sides).
238
+ - **Session-default model carryover (#883)** — picking a model inside a session writes it back as `defaultModel` for new sessions (no silent failures).
239
+ - **Core `^0.10.3`** — MCP 404 self-heal, turn-loop abort front-guards (Stop wins over the continue prompt), big edit-receipt summary line, manifest unknown-keys warning.
240
+ - Fixed: ACP batch-2 follow-ups (delete→new disposal, `updatedAt` clamp); Home full-index version gate (#867); `/undo` bounds, console cap, advisor lightweighting (#863); ledger ghost-root discovery (#899).
241
+
242
+ ### 0.12.69 (2026-10)
243
+ - **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.
244
+ - **Core `^0.10.2`** — unified provider resolution (`resolveProviderPlan` / `resolveChannelModel`), `providerState` / `providerStateReason` keys, slot key gate.
245
+
234
246
  ### 0.12.68 (2026-10)
235
247
  - **Idle wake** — suspended sessions wake to timers: they resume on schedule (visibility surface + abort accommodation).
236
248
  - **Ledger health signals** — an unhealthy ledger database now shows a warning line in the TUI (empty values render as `—`).
@@ -0,0 +1,28 @@
1
+ /**
2
+ * node-version-gate.cjs — Node.js 主版本运行期校验(fail-fast · 2026-10-04 · 台账 #867 ∥ `docs/cli/design/CLI-ENTRY.md` §1)。
3
+ *
4
+ * 执行点 = `bin/thincoder.cjs`(shim)**首行**——先于 `.mjs` 链任何 ESM 静态 import 求值;不满足 ⇒
5
+ * 一行显式错误(当前版本 + 要求)+ `exit 1`(`node:sqlite` 等核 API 在旧版行为不可期)。
6
+ * 纯函数 `nodeVersionError(version = process.versions.node)` 版本串入参 ⇒ 供直测假版本;
7
+ * 零依赖(主版本整数比较)。射程 = npm bin 入口(`node bin/thincoder.mjs` 直跑 = 开发 ∥ 测试面)。
8
+ */
9
+ "use strict"
10
+
11
+ const REQUIRED_MAJOR = 24
12
+
13
+ /** 版本串 ⇒ `null`(达标:主版本 ≥ 24)∥ 逐字一行错误串(不达标 —— 含不可解析版本串)。 */
14
+ function nodeVersionError(version = process.versions.node) {
15
+ const major = Number.parseInt(String(version), 10)
16
+ if (Number.isInteger(major) && major >= REQUIRED_MAJOR) return null
17
+ return `thincoder requires Node.js >= 24 (current: ${version})`
18
+ }
19
+
20
+ /** 执行门(shim 首行调用):不达标 ⇒ 一行错误 + `exit 1`;达标 ⇒ 静默返回。 */
21
+ function enforceNodeMajor() {
22
+ const error = nodeVersionError()
23
+ if (error === null) return
24
+ console.error(error)
25
+ process.exit(1)
26
+ }
27
+
28
+ module.exports = { nodeVersionError, enforceNodeMajor }
package/bin/thincoder.cjs CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // CommonJS shim — npm 12 may reject ESM bin entries.
3
3
  // This is a tiny CJS wrapper that delegates to the real .mjs entry point.
4
+ // #867:Node 主版本前置校验(fail-fast——先于 `.mjs` 链任何 ESM 静态 import 求值)。
5
+ require("./node-version-gate.cjs").enforceNodeMajor()
4
6
  import("./thincoder.mjs")
package/bin/thincoder.mjs CHANGED
@@ -136,6 +136,8 @@ Usage:
136
136
  messages / tool calls + coverage + size; --rebuild reindexes every session file
137
137
  thincoder ledger migrate --dry-run | --confirm Ledger variant-key merge (dry-run report / backup then import; source recycled)
138
138
  thincoder ledger audit Read-only ledger dir audit (classify every db + suggestions)
139
+ thincoder ledger list --json [--full] [--family] [--cwd <dir>]
140
+ Read-only ledger export as single-segment JSON (--full adds evidence; --family exports the discovered family)
139
141
  thincoder upgrade Update to the latest version from npm
140
142
  thincoder completion <sh> Generate shell completion script (bash / zsh / fish)
141
143
  thincoder -v, --version Print version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.68",
3
+ "version": "0.12.70",
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.10.1"
23
+ "@thincoder/core": "^0.10.3"
24
24
  },
25
25
  "files": [
26
26
  "bin/",
@@ -5,9 +5,13 @@
5
5
  * Wire shapes verified against the ACP schema v1 + kimi acp-adapter:
6
6
  * - agent text → `agent_message_chunk` { content: { type: "text", text } }
7
7
  * - thinking → `agent_thought_chunk` (same content shape)
8
+ * - subagent ev → `session_info_update` { _meta: { "thincoder.dev/subagent":
9
+ * { role, id, state, progress?, detail? } } } (event token behind the relay
10
+ * prefix — the text face stays stripped; §12 extension contract)
8
11
  * - tool start → `tool_call` { toolCallId, title, kind, status: "in_progress", rawInput, content }
9
12
  * - tool result → `tool_call_update` { toolCallId, status: "completed"|"failed", content } (REPLACE semantics)
10
- * - usage → `usage_update` { usage }
13
+ * - usage → `usage_update` { used, size } (used = prompt + completion tokens;
14
+ * size = the model's context window — providerSpec single source)
11
15
  * - permission → reverse-RPC request `session/request_permission`
12
16
  * { sessionId, options, toolCall } → client responds with
13
17
  * { outcome: { outcome: "selected", optionId } | { outcome: "cancelled" } }
@@ -25,6 +29,11 @@ import { computeEditEntry, validateEditEntry, assertEditArgsExclusive, assertEdi
25
29
  import { parseRelayPath } from "@thincoder/core/agent/relay-prefix.mjs"
26
30
  // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(本面仅日志——PROVIDER.md §6.20)
27
31
  import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
32
+ // §2.1(ACP-PROTOCOL-COMPLIANCE):usage_update.size = 模型上下文窗口单源(含 providers[].context 覆写)。
33
+ import { providerSpec } from "@thincoder/core/config.mjs"
34
+ // 2026-10-04 批(ACP 协议面补全):事件 token 文法单源(形态判据 ∥ 逐事件投影——
35
+ // ACP-CLIENT.md §12.4)——剥离判据经本调用唯一决定,本档不再自持 ⟦ev⟧ 判据副本。
36
+ import { parseSubagentEvent } from "@thincoder/core/agent/subagent-event.mjs"
28
37
 
29
38
  /** ACP ToolKind inference (schema v1 enum) — best-effort, clients render by kind. */
30
39
  function inferToolKind(name) {
@@ -52,14 +61,31 @@ function permissionToBoolean(response) {
52
61
  return false
53
62
  }
54
63
 
64
+ /** §12.2(ACP-CLIENT.md):`_meta` 扩展键——命名空间防保留键碰撞(对外契约逐字)。 */
65
+ const SUBAGENT_META_KEY = "thincoder.dev/subagent"
66
+
67
+ /** 事件 token → `_meta["thincoder.dev/subagent"]` 载荷(§12.2 形状冻结——无值键缺席:
68
+ * progress/detail 键只在有值时在场;`state` = 事件名原文,形态域开集)。
69
+ * role/id = relay 前缀**最内段**(嵌套 `explore#1/eng-coder#2/…` ⇒ `eng-coder`/2——
70
+ * 段形 `role#id` 由 parseRelayPath 保证)。 */
71
+ function subagentMetaPayload(path, ev) {
72
+ const inner = path.inner.length > 0 ? path.inner[path.inner.length - 1] : path.head
73
+ const [role, id] = inner.split("#")
74
+ const meta = { role, id: Number(id), state: ev.name }
75
+ if (ev.progress) meta.progress = ev.progress
76
+ if (ev.detail) meta.detail = ev.detail
77
+ return meta
78
+ }
79
+
55
80
  /**
56
81
  * Build the runAgent callbacks for an ACP session.
57
- * @param {{ sessionId: string, notify: (m, p) => void, request: (m, p, o?) => Promise<any>, log?: (s) => void,
82
+ * @param {{ sessionId: string, agent?: object, notify: (m, p) => void, request: (m, p, o?) => Promise<any>, log?: (s) => void,
58
83
  * clientCaps?: { fs?: { readTextFile?: boolean, writeTextFile?: boolean } } }} deps
59
- * `clientCaps` = the §3.4 snapshot (taken at `session/new`); capabilities omitted by the
84
+ * `agent` = 会话的活引用(§2.4——`set_config_option` 切模型后 `usage_update.size` 随动;
85
+ * 不得在构造期固化数值);`clientCaps` = the §3.4 snapshot (taken at `session/new`); capabilities omitted by the
60
86
  * client are UNSUPPORTED ⇒ the fs reverse-RPC face stays local (§11.4 — never 30s 干等).
61
87
  */
62
- export function buildAcpCallbacks({ sessionId, notify, request, log = () => {}, clientCaps = {} }) {
88
+ export function buildAcpCallbacks({ sessionId, agent, notify, request, log = () => {}, clientCaps = {} }) {
63
89
  const update = (sessionUpdate, extra = {}) =>
64
90
  notify("session/update", { sessionId, update: { sessionUpdate, ...extra } })
65
91
  // §11.4 fs 能力位(默认 false——文档「MUST treat all capabilities omitted … as UNSUPPORTED」);
@@ -188,13 +214,18 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
188
214
  // TUI/webview display signal, not conversation content, and must not reach ACP clients.
189
215
  if (/^\[model\]/.test(payload)) return
190
216
  // D7 + 批 1 B2/D13(ACP-CLIENT.md §7.2):剥离判据 = **形态**(`⟦ev⟧<小写名>` + RS
191
- // 终止符同现),不再枚举事件名——枚举是漏项发生器(`queued` / `cancelled` 发射点在位
192
- // 而白名单未跟 ⇒ 随 agent_message_chunk 泄漏给客户端)。终止符约束 load-bearing:
193
- // 放宽为无终止符形态会吞真实正文(先例教训见 src/tui/render.mjs:250-252)。
194
- // 事件 token 是 TUI 显示信号;结构化 ACP 映射(tool_call_update)另行跟踪
195
- // (docs/TODO.md)。
196
- // 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
197
- if (/^⟦ev⟧[a-z]+\x1e/.test(payload)) return
217
+ // 终止符同现)——枚举是漏项发生器(`queued` / `cancelled` 发射点在位而白名单未跟 ⇒
218
+ // 随 agent_message_chunk 泄漏给客户端)。终止符约束 load-bearing:放宽为无终止符形态
219
+ // 会吞真实正文(先例教训见 src/tui/render.mjs:250-252)。
220
+ // 判据单源 = 核模块 `subagent-event.mjs`(2026-10-04 批统一——桥内联式退役,剥离语义
221
+ // 零改):`parseSubagentEvent` 非空 ⇒ 剥;§12 结构化发射 = 同一调用取投影载荷。
222
+ const ev = parseSubagentEvent(payload)
223
+ if (ev) {
224
+ // §12.1 发射规则:relay 前缀在场 ⇒ **增量**一条 `session_info_update`(文本面剥离
225
+ // 照旧——零 `agent_message_chunk`);无前缀 ⇒ 仅剥离零发射(防御面)。
226
+ if (path) update("session_info_update", { _meta: { [SUBAGENT_META_KEY]: subagentMetaPayload(path, ev) } })
227
+ return
228
+ }
198
229
  if (!payload) return // D6:剥后空载荷不发通知(防噪声空 chunk)
199
230
  update("agent_message_chunk", { content: { type: "text", text: payload } })
200
231
  },
@@ -205,7 +236,14 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
205
236
  if (!payload) return
206
237
  update("agent_thought_chunk", { content: { type: "text", text: payload } })
207
238
  },
208
- onUsage: (usage) => update("usage_update", { usage }),
239
+ // §2.1(ACP-PROTOCOL-COMPLIANCE):`UsageUpdate.required = ["used","size"]`——
240
+ // used = 本轮 prompt + completion(量级近似「当前在上下文中的 token」);size = 模型上下文
241
+ // 窗口(`providerSpec` 单源——含 providers[].context 覆写;未知模型 128_000 兜底)。
242
+ // agent 为活引用:切模型后 size 随动(不得构造期固化)。
243
+ onUsage: (usage) => update("usage_update", {
244
+ used: (usage?.prompt_tokens ?? 0) + (usage?.completion_tokens ?? 0),
245
+ size: providerSpec(agent?.provider).context,
246
+ }),
209
247
  onWait: (ev) => {
210
248
  const s = waitStatusText(ev)
211
249
  if (s) log(`[rate-limit] ${s}`)
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * handlers-session.mjs — in-memory ACP session family (ACP-CLIENT.md §3.5):
3
3
  * `session/{new,prompt,cancel,close,set_mode,set_config_option}` + `findSession` /
4
- * `applyConfigOption` + the shared credential gate consumed by every gated handler
5
- * across the four handler modules (§11.5).
4
+ * `applyConfigOption` / `sessionConfigOptions`(configOptions 全形投影单源——四响应 + 两通知)/
5
+ * `createSlotReleaser`(认领释放单点——close ∥ load/resume 替换同源)+ the shared credential
6
+ * gate consumed by every gated handler across the four handler modules (§11.5).
6
7
  *
7
8
  * 认证门 = **凭据状态即时判据**(§11.2 改法① / D15——撤 `authenticated` 闩锁):
8
9
  * 受门 handler 首行求值,真 ⇒ 放行、假 ⇒ `-32000`。客户端可以不调 `authenticate`
@@ -13,7 +14,7 @@
13
14
  * 释放该会话认领(保留集 = 本进程其余在存会话的活绑定槽,同 cwd;落盘判据三条沿用)。
14
15
  */
15
16
  import { resolve } from "node:path"
16
- import { loadManifest, normalizeCwd, bindRecordStore, newSession, saveManifest, slotPath } from "@thincoder/core/session.mjs"
17
+ import { loadManifest, normalizeCwd, bindRecordStore, deleteSlot, newSession, saveManifest, slotPath } from "@thincoder/core/session.mjs"
17
18
  // 认领释放谓词单源(SESSION.md §6.2 / §6.16——与 VSC 端壳 `releaseClaimsAll` 同源档;
18
19
  // core `session.mjs` 未 re-export 该名——直引 manifest 档,先例 = `thincoder-vscode/src/extension/session-io.mjs`)。
19
20
  import { staleClaims } from "@thincoder/core/session-slots-manifest.mjs"
@@ -22,18 +23,53 @@ import { PLAN_ENGINEERING_REFUSED } from "@thincoder/core/agent-tools/plan.mjs"
22
23
  import { thinkOffShape } from "@thincoder/core/think-off.mjs"
23
24
  import { providerSpec } from "@thincoder/core/config.mjs"
24
25
  import { ACP_ERRORS } from "./transport.mjs"
26
+ // §2.5(ACP-PROTOCOL-COMPLIANCE):prompt 内容块聚合(resource_link 内联 ∥ 降级标记——基线 MUST)。
27
+ import { buildPromptText } from "./resource-link.mjs"
25
28
 
26
29
  /** ENG-PLAN-EXCLUSION(FR31 ② · E7「命令面逐面钉定」):工程真值 = 核单源同键
27
30
  * `agent.config.agent.engineering`。 */
28
31
  const isEngineering = (agent) => agent?.config?.agent?.engineering === true
29
32
 
30
- /** Session-level config options exposed to clients — schema `SessionConfigOption.required = ["id","name"]`
31
- * (响应侧用 `id`;请求侧 `SetSessionConfigOptionRequest` 用 `configId`——不对称属契约本身 · §11.3 G2-8). */
32
- export const CONFIG_OPTIONS = [
33
- { id: "model", name: "Model" },
34
- { id: "thinking", name: "Thinking" },
35
- { id: "mode", name: "Mode" },
36
- ]
33
+ /** configOptions 全形单源投影(§2.3——四处响应 + 两处通知同源):`SessionConfigOption` =
34
+ * `required [id,name]` + oneOf select/boolean 判别键 `type`(select 另需 `currentValue` + `options[]`;
35
+ * boolean 需 `currentValue`);带**现值**投影(静态常量不可——`currentValue` 就地失真)。
36
+ * `model` 项在模型不可解析时**缺席**(`currentValue` 必填——无值不可虚构);`thinking` 现值 =
37
+ * 「非 off 形」(off 两形 = `null`(effort 族)∥ `{type:"disabled"}`;含自定义开值族——
38
+ * §11 语义登记:`undefined`(未显式设置)⇒ `false`,不发服务端生效态探测);数组序 =
39
+ * model(在位时)→ thinking → mode。 */
40
+ export function sessionConfigOptions(agent) {
41
+ const opts = []
42
+ const model = agent?.provider?.model
43
+ if (typeof model === "string" && model) {
44
+ const provider = agent?.provider?.name
45
+ const value = provider ? `${provider}:${model}` : model
46
+ opts.push({ id: "model", name: "Model", type: "select", currentValue: value, options: [{ value, name: value }] })
47
+ }
48
+ const th = agent?.provider?.thinking
49
+ opts.push({ id: "thinking", name: "Thinking", type: "boolean", currentValue: th != null && th.type !== "disabled" })
50
+ opts.push({ id: "mode", name: "Mode", type: "select", currentValue: agent?.planMode ? "plan" : "normal",
51
+ options: [{ value: "normal", name: "Normal" }, { value: "plan", name: "Plan" }] })
52
+ return opts
53
+ }
54
+
55
+ /** 认领释放单点(§2.4 提取;#168① 公式——SESSION.md §6.15 / §6.16 · 2026-09-22 定裁):
56
+ * 释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`;**保留集 = 本进程其余在存会话的活
57
+ * 绑定槽**(同 cwd——ACP 单 cwd 模型)。取值点:会话记录容器 = `ctx.sessions`
58
+ * (`Map<id, session>`——`thincoder-cli/src/acp.mjs` 单容器);槽值 = `session.agent._slot`
59
+ * (认领写入同源 = `handlers-slots.mjs` load/resume 钉槽 / 本档 `session/new` 认领点)。
60
+ * 调用点三处同源消费 = close ∥ load 替换 ∥ resume 替换(`acp.mjs` 建一次入
61
+ * `ctx.releaseClosedSlot`——同 `requireConfigured` 先例)。落盘判据三条由核 `saveManifest`
62
+ * `opts.release` 自动继承(fresh 同次快照 / 值条件删除 / 内存认领表移除)——**零新增写盘
63
+ * 路径**;本进程无残留认领 ⇒ 零写早退(判据单源 `staleClaims`——同核 `releaseClaimsAll` 早退形)。 */
64
+ export function createSlotReleaser({ getCwd, sessions }) {
65
+ return () => {
66
+ const cwd = getCwd()
67
+ const m = loadManifest(cwd)
68
+ const keep = [...sessions.values()].map((s) => s.agent?._slot).filter((slot) => slot != null)
69
+ if (staleClaims(m, keep).length === 0) return
70
+ saveManifest(cwd, m, null, { release: keep })
71
+ }
72
+ }
37
73
 
38
74
  /**
39
75
  * Credential gate (§11.5 判据拆分 · 评审 #1). Evaluated per call — **no cross-call latch**.
@@ -111,12 +147,11 @@ export function applyConfigOption(agent, configId, value) {
111
147
 
112
148
  /**
113
149
  * Build the in-memory session family. Shared state arrives via the single `ctx`
114
- * (§3.5 — `{ getCwd, sessions, allocSessionId, notifyRef, requestRef, createSession,
115
- * requireConfigured, log }`), so the id allocator and the slot family stay on one
116
- * namespace.
150
+ * (§3.5 — `{ getCwd, sessions, notifyRef, requestRef, createSession, requireConfigured,
151
+ * releaseClosedSlot, log }`);会话 id = 持久槽位号串(load/new 同命名空间——G5 收正)。
117
152
  */
118
153
  export function createSessionHandlers(ctx) {
119
- const { getCwd, sessions, allocSessionId, notifyRef, requestRef, createSession, requireConfigured, log } = ctx
154
+ const { getCwd, sessions, notifyRef, requestRef, createSession, requireConfigured, releaseClosedSlot, log } = ctx
120
155
 
121
156
  const findSession = (params) => {
122
157
  const s = sessions.get(String(params?.sessionId))
@@ -124,22 +159,6 @@ export function createSessionHandlers(ctx) {
124
159
  return { session: s }
125
160
  }
126
161
 
127
- /** #168①(SESSION.md §6.15 / §6.16——2026-09-22 定裁 · 台账 #168①):`session/close` ⇒
128
- * 释放被关闭会话的槽认领。公式代入:释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`;
129
- * **保留集 = 本进程其余在存会话的活绑定槽**(同 cwd——ACP 单 cwd 模型)。取值点(实施轮定位报告):
130
- * 会话记录容器 = `ctx.sessions`(`Map<id, session>`——`thincoder-cli/src/acp.mjs:98` 单容器);
131
- * 槽值 = `session.agent._slot`(认领写入同源 = `handlers-slots.mjs:96-100` / `:151-155` /
132
- * `handlers-session.mjs` 的 newSession 认领点)。落盘判据三条由核 `saveManifest` `opts.release`
133
- * 自动继承(fresh 同次快照 / 值条件删除 / 内存认领表移除)——**零新增写盘路径**;本进程无残留认领
134
- * ⇒ 零写早退(判据单源 `staleClaims`——同核 `releaseClaimsAll` 早退形)。 */
135
- const releaseClosedSessionSlot = () => {
136
- const cwd = getCwd()
137
- const m = loadManifest(cwd)
138
- const keep = [...sessions.values()].map((s) => s.agent?._slot).filter((slot) => slot != null)
139
- if (staleClaims(m, keep).length === 0) return
140
- saveManifest(cwd, m, null, { release: keep })
141
- }
142
-
143
162
  return {
144
163
  "session/new": async (params) => {
145
164
  const gate = requireConfigured()
@@ -163,12 +182,20 @@ export function createSessionHandlers(ctx) {
163
182
  if (params.mcpServers?.length) {
164
183
  log(`[acp] MCP forwarding is M2 scope — ignoring ${params.mcpServers.length} server(s)`)
165
184
  }
185
+ // §2.4(G5 收正):**先认领持久槽**——`createSession` 的 id 在构造期烧入 callbacks,
186
+ // 而 id = 槽号串(`String(slot)`——全族单一命名空间),故认领必须先于装配。
187
+ // 失败回滚(新序引入的孤儿面——必处置):装配抛错 ∥ `_providerInvalid` ⇒ `deleteSlot`
188
+ // (文件 + manifest 条目 + 认领由核单点清理);`committed` 旗标守卫——`sessions.set`
189
+ // 之后不回滚(在存会话的槽不因尾段异常被误删)。
190
+ let slot = null
191
+ let committed = false
166
192
  try {
167
- const id = allocSessionId()
193
+ slot = await newSession(getCwd())
194
+ const id = String(slot)
168
195
  const session = await createSession({ id, notify: notifyRef.current, request: requestRef.current, log })
169
196
  // §11.5 装配后检查(兜底,与门互补):门拦「key 缺失 / defaultModel 不可解析」;
170
197
  // 本行拦「门放行但 provider 仍不可用」的残余(如 defaultModel 可解析而 provider 缺
171
- // baseURL)——显式报错而**不建半死会话**(槽未认领、sessions 未登记——无半态)。
198
+ // baseURL)——显式报错而**不建半死会话**(槽已认领 ⇒ 由 finally 回滚撤除——无半态)。
172
199
  if (session?.agent?._providerInvalid) {
173
200
  return {
174
201
  error: {
@@ -182,15 +209,36 @@ export function createSessionHandlers(ctx) {
182
209
  // 走 _slot ??= activeSlot() → ensureActive 早退分支(slotSessions[active]===
183
210
  // mySessionId 同进程恒真)→ 第二个会话拿到与第一个相同的槽号 → 双写同槽
184
211
  // F2 互旋。getSessionId() 是进程级,_slot 是 agent 级——粒度错配必须在此切断。
185
- session.agent._slot = await newSession(getCwd())
212
+ session.agent._slot = slot
186
213
  // TUI-OOM-ROOTCAUSE(SESSION.md §6.14):新建槽绑定记录存储(baseHistory 空——
187
214
  // identity 待固化)——与 /new 同语义
188
215
  bindRecordStore(session.agent, { slotFile: slotPath(getCwd(), session.agent._slot), identity: session.agent._sessionStart ?? null, baseHistory: [] })
216
+ // §2.4 new 途处置点钉定(delete→new 槽号回收面 · 修正轮):`session/delete` 删档后槽号
217
+ // 回流,`session/new` 复得同号时会撞**同键在存实例**(键 = 槽号串而 `_slot` 已与键分离
218
+ // 的旧实例不受 delete 重钉环处置——`handlers-slots.mjs` 按 `_slot` 匹配)——close 同法
219
+ // 处置:cancel ∥ 删键。位次 = `sessions.set` 直前——此前任何一步失败均不改动旧实例。
220
+ const stale = sessions.get(id)
221
+ if (stale) {
222
+ stale.cancel()
223
+ sessions.delete(id)
224
+ }
189
225
  sessions.set(id, session)
190
- // §11.3 G2-5:NewSessionResponse.required = ["sessionId"](`id` / `configId` 键已收正)
191
- return { sessionId: id, configOptions: [...CONFIG_OPTIONS] }
226
+ committed = true
227
+ // §2.4 new 途认领释放位次(修正轮):置于 `sessions.set`/`committed` 之后——保留集须含
228
+ // 新会话槽(`createSlotReleaser` 保留集 = 在存 Map 槽);先释后装会把 `newSession` 刚写
229
+ // 的新认领当残留释放(`staleClaims` 谓词)⇒ 复开 F2 双写窗口;置 committed 后 ⇒ 释放
230
+ // 失败不误删已建会话(finally 回滚仅对未 committed 生效)。
231
+ if (stale) releaseClosedSlot()
232
+ // §11.3 G2-5:NewSessionResponse.required = ["sessionId"](`id` / `configId` 键已收正);
233
+ // configOptions = 全形(§2.3 单源投影)。
234
+ return { sessionId: id, configOptions: sessionConfigOptions(session.agent) }
192
235
  } catch (e) {
193
236
  return { error: { code: ACP_ERRORS.INTERNAL.code, message: `failed to create session: ${e.message}` } }
237
+ } finally {
238
+ if (slot != null && !committed) {
239
+ try { deleteSlot(getCwd(), slot) }
240
+ catch (e) { log(`[acp] failed to roll back slot ${slot} after a failed session/new: ${e.message}`) }
241
+ }
194
242
  }
195
243
  },
196
244
 
@@ -199,11 +247,12 @@ export function createSessionHandlers(ctx) {
199
247
  if (gate.error) return gate
200
248
  const found = findSession(params)
201
249
  if (found.error) return found
202
- // §11.3 G2-6:PromptRequest.required = ["sessionId","prompt"](`prompt` = ContentBlock[])——
203
- // 读取位置由 `params.content` 收正为 `params.prompt`;取块策略不变(首个 text 块——G6 登记项)。
250
+ // §11.3 G2-6:PromptRequest.required = ["sessionId","prompt"](`prompt` = ContentBlock[])
251
+ // ——读取位置由 `params.content` 收正为 `params.prompt`;§2.5:首个**合法** text 块 +
252
+ // resource_link 内联(基线 MUST——逐块失败降级不中断,标记内联)。
204
253
  const blocks = Array.isArray(params?.prompt) ? params.prompt : []
205
- const text = blocks.find((b) => b?.type === "text")?.text ?? ""
206
- if (!text) return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "prompt requires a text content block" } }
254
+ const text = await buildPromptText(blocks, { cwd: getCwd() })
255
+ if (!text) return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: "prompt requires a text or resource_link content block" } }
207
256
  try {
208
257
  await found.session.run(text)
209
258
  return { stopReason: "end_turn" }
@@ -234,7 +283,7 @@ export function createSessionHandlers(ctx) {
234
283
  found.session.cancel()
235
284
  // 先出容器再释放(保留集 = 其余在存会话——被关闭者不留在保留集内)。
236
285
  sessions.delete(String(params.sessionId))
237
- releaseClosedSessionSlot() // #168①:释放该会话认领(保留集 = 同 cwd 其余在存会话槽)
286
+ releaseClosedSlot() // #168①:释放该会话认领(保留集 = 同 cwd 其余在存会话槽)
238
287
  log(`session ${params.sessionId} closed by client`)
239
288
  }
240
289
  return {}
@@ -260,12 +309,13 @@ export function createSessionHandlers(ctx) {
260
309
  if (!applied) {
261
310
  return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: `unknown configId: ${configId}` } }
262
311
  }
263
- // Last-write-wins on the internal state; notify the client of the change.
312
+ // Last-write-wins on the internal state;§2.4:通知 = `ConfigOptionUpdate.required =
313
+ // ["configOptions"]`——**全量数组**(非增量 `{configId,value}`);响应同源全形(§2.3)。
264
314
  notifyRef.current("session/update", {
265
315
  sessionId: String(params.sessionId),
266
- update: { sessionUpdate: "config_option_update", configId, value },
316
+ update: { sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(found.session.agent) },
267
317
  })
268
- return {}
318
+ return { configOptions: sessionConfigOptions(found.session.agent) }
269
319
  },
270
320
 
271
321
  "session/set_mode": (params) => {
@@ -282,9 +332,10 @@ export function createSessionHandlers(ctx) {
282
332
  return { error: { ...ACP_ERRORS.INVALID_PARAMS, message: PLAN_ENGINEERING_REFUSED } }
283
333
  }
284
334
  found.session.agent.planMode = params.mode === "plan"
335
+ // §2.4:`CurrentModeUpdate.required = ["currentModeId"]`(`mode` 键与 schema 相抵——字段收正)。
285
336
  notifyRef.current("session/update", {
286
337
  sessionId: String(params.sessionId),
287
- update: { sessionUpdate: "current_mode_update", mode: params.mode },
338
+ update: { sessionUpdate: "current_mode_update", currentModeId: params.mode },
288
339
  })
289
340
  return {}
290
341
  },
@@ -3,13 +3,14 @@
3
3
  * `session/{list,load,resume,delete}` + the #41 engineering-manifest predicate
4
4
  * helpers (`slotEngineering` / `reattach`).
5
5
  *
6
- * id 命名空间边界(§11.3 · 评审 #7——本批只改字段名、不改语义):本族按**持久化槽位号**
7
- * 解析 `params.sessionId`(`Number(params.sessionId)`);而 `session/prompt` / `cancel` /
8
- * `close` / `set_*` 认的是 **ACP 会话 id**(入口分配器——与槽位号不同物,§6.1)⇒
9
- * 把 `session/list` 的输出直喂 `session/prompt` 仍会 `unknown session`(原 G5,本批不做)。
6
+ * id 语义(§2.4 · G5 收正):**全族单一命名空间**——`session id = 持久槽位号字符串`。
7
+ * 本族按槽位号解析 `params.sessionId`(`Number(...)`——list/load/resume/delete);
8
+ * `session/prompt` / `cancel` / `close` / `set_*` 认的 id 同源 = `session/new` 的槽号串 /
9
+ * load/resume 沿用的**客户端原文 id**(同字面读回即可命中)⇒ `session/list` 输出可直喂
10
+ * 全族方法(不变量:`session/new` 返回 id ∈ 随后 list 集)。
10
11
  */
11
12
  import { ACP_ERRORS } from "./transport.mjs"
12
- import { CONFIG_OPTIONS } from "./handlers-session.mjs"
13
+ import { sessionConfigOptions } from "./handlers-session.mjs"
13
14
  import { loadConfig } from "@thincoder/core/config.mjs"
14
15
  import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
15
16
  import { replayHistory } from "./bridge.mjs"
@@ -17,7 +18,7 @@ import { listSlots, applySession, deleteSlot, loadSlotFile, slotOccupancy, loadM
17
18
 
18
19
  /** Build the persisted-slot family. Shared state arrives via the single `ctx` (§3.5). */
19
20
  export function createSlotsHandlers(ctx) {
20
- const { getCwd, sessions, allocSessionId, notifyRef, requestRef, createSession, requireConfigured, log } = ctx
21
+ const { getCwd, sessions, notifyRef, requestRef, createSession, requireConfigured, releaseClosedSlot, log } = ctx
21
22
 
22
23
  /** #41 翻转族合值判据(MANIFEST.md §2.8 F3 触发条件口径 = §2.2 会话权威值):「槽带
23
24
  * `engineering` 字段 ? 槽值 : config 回退」。本路装配期(`applySession` 之前)无会话
@@ -43,13 +44,19 @@ export function createSlotsHandlers(ctx) {
43
44
  if (gate.error) return gate
44
45
  const slots = listSlots(getCwd())
45
46
  return {
46
- // §11.3 G2-7:SessionInfo.required = ["sessionId","cwd"](条目键 `id` 已收正)
47
+ // §11.3 G2-7:SessionInfo.required = ["sessionId","cwd"](条目键 `id` 已收正);
48
+ // §2.2:updatedAt = ISO 8601 字符串(核 `listSlots` 发出 epoch ms——此处收正);
49
+ // 值域钳(修正轮——防 `toISOString` RangeError ⇒ list 整方法 -32603):有限 ∧
50
+ // `|v| <= 8.64e15`(ECMA-262 TimeClip 上限)⇒ ISO;非法 ∥ 超域 ⇒ `undefined`
51
+ //(JSON 序列化 ⇒ 键缺席——null 语义;不用 epoch 0 占位——0 谎报 1970);
52
+ // `messageCount` 剔除(非 SessionInfo 字段——无 schema 位)。
47
53
  sessions: slots.map((s) => ({
48
54
  sessionId: String(s.slot),
49
55
  cwd: getCwd(), // single-cwd model (design §4.5)
50
- updatedAt: s.updatedAt ?? 0,
56
+ updatedAt: Number.isFinite(s.updatedAt) && Math.abs(s.updatedAt) <= 8.64e15
57
+ ? new Date(s.updatedAt).toISOString()
58
+ : undefined,
51
59
  title: s.title ?? "",
52
- messageCount: s.messageCount ?? 0,
53
60
  })),
54
61
  }
55
62
  },
@@ -76,7 +83,18 @@ export function createSlotsHandlers(ctx) {
76
83
  }
77
84
  }
78
85
  try {
79
- const id = allocSessionId()
86
+ // §2.4(G5 收正):会话 id = **客户端原文形态**(同值读回保续——`"008"` 类非规范
87
+ // 字面按原文注册,同字面读回即可命中);同 id 在存 ⇒ 替换(close 同法——置换点在
88
+ // 既有前置判据之后、`createSession` 之前:拒载路径零副作用,旧实例保留不误杀在飞
89
+ // 回合)。置前理由:① 旧实例的钉槽不再计入 `sameProcessPinned`(重载同槽 ⇒ 可正常
90
+ // 钉回原槽,不误 fork);② 防旧实例成为不可达孤儿(同 id 在 Map 上唯一)。
91
+ const id = String(params.sessionId)
92
+ const stale = sessions.get(id)
93
+ if (stale) {
94
+ stale.cancel()
95
+ sessions.delete(id)
96
+ releaseClosedSlot()
97
+ }
80
98
  const session = await createSession({ id, notify: notifyRef.current, request: requestRef.current, log })
81
99
  // 2026-08-31 advisor round2 🟡:钉 _slot 前查活主——目标槽被另一活进程(CLI/另一
82
100
  // IDE)占用时不得钉回(双方 sessionStart 一致 → F2 永不轮转 → 同槽 last-write-wins
@@ -110,8 +128,9 @@ export function createSlotsHandlers(ctx) {
110
128
  // client renders the restored conversation.
111
129
  replayHistory({ sessionId: id, notify: notifyRef.current, history: data.history, log })
112
130
  log(`session ${slot} loaded as session ${id} (${data.history?.length ?? 0} messages replayed)${occ.occupied || sameProcessPinned ? ` — slot busy, forked to ${session.agent._slot}` : ""}`)
113
- // §11.3:load/resume 响应 required = [](`id` / `cwd` 属多余键——同批收敛)
114
- return { configOptions: [...CONFIG_OPTIONS] }
131
+ // §11.3:load/resume 响应 required = [](`id` / `cwd` 属多余键——同批收敛);
132
+ // configOptions = 全形(§2.3 单源投影)。
133
+ return { configOptions: sessionConfigOptions(session.agent) }
115
134
  } catch (e) {
116
135
  return { error: { code: ACP_ERRORS.INTERNAL.code, message: `failed to load session ${slot}: ${e.message}` } }
117
136
  }
@@ -136,7 +155,15 @@ export function createSlotsHandlers(ctx) {
136
155
  }
137
156
  }
138
157
  try {
139
- const id = allocSessionId()
158
+ // §2.4:同 session/load——id = 客户端原文形态(同值读回保续);同 id 在存 ⇒ 替换
159
+ // (置换点在既有前置判据之后、`createSession` 之前——拒载路径零副作用)。
160
+ const id = String(params.sessionId)
161
+ const stale = sessions.get(id)
162
+ if (stale) {
163
+ stale.cancel()
164
+ sessions.delete(id)
165
+ releaseClosedSlot()
166
+ }
140
167
  const session = await createSession({ id, notify: notifyRef.current, request: requestRef.current, log })
141
168
  // 2026-08-31 advisor round2 🟡:同 session/load——活主占用的槽不钉回(防同槽双写,
142
169
  // 下次保存 fork 新槽);空闲则认领后钉回。
@@ -161,7 +188,7 @@ export function createSlotsHandlers(ctx) {
161
188
  sessions.set(id, session)
162
189
  // resume: no history replay — the client keeps its own rendering.
163
190
  log(`session ${slot} resumed as session ${id} (no replay)${occ.occupied || sameProcessPinned ? ` — slot busy, forked to ${session.agent._slot}` : ""}`)
164
- return { configOptions: [...CONFIG_OPTIONS] }
191
+ return { configOptions: sessionConfigOptions(session.agent) }
165
192
  } catch (e) {
166
193
  return { error: { code: ACP_ERRORS.INTERNAL.code, message: `failed to resume session ${slot}: ${e.message}` } }
167
194
  }