thincoder 0.12.69 → 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 +23 -0
- package/README.md +11 -3
- package/bin/node-version-gate.cjs +28 -0
- package/bin/thincoder.cjs +2 -0
- package/bin/thincoder.mjs +2 -0
- package/package.json +2 -2
- package/src/acp/bridge.mjs +50 -12
- package/src/acp/handlers-session.mjs +95 -44
- package/src/acp/handlers-slots.mjs +41 -14
- package/src/acp/read-data.mjs +127 -0
- package/src/acp/resource-link.mjs +124 -0
- package/src/acp/session.mjs +1 -1
- package/src/acp.mjs +15 -6
- package/src/command-interactive.mjs +1 -1
- package/src/command-table.mjs +8 -1
- package/src/completions.mjs +7 -4
- package/src/tui/cmd-model.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +10 -0
- package/src/tui/cmd-undo.mjs +16 -38
- package/src/tui/config-helpers.mjs +34 -1
- package/src/tui/index.mjs +5 -5
- package/src/tui/ledger-surface.mjs +5 -7
- package/src/tui/model-picker.mjs +15 -6
- package/src/tui/startup.mjs +12 -2
- package/src/tui/wizard.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
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
|
+
|
|
1
24
|
## [0.12.69] — 2026-10-03
|
|
2
25
|
|
|
3
26
|
> 0.12.68 → 0.12.69(连续号——发布时定号)
|
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-
|
|
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
|
|
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,14 @@ 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
|
+
|
|
234
242
|
### 0.12.69 (2026-10)
|
|
235
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.
|
|
236
244
|
- **Core `^0.10.2`** — unified provider resolution (`resolveProviderPlan` / `resolveChannelModel`), `providerState` / `providerStateReason` keys, slot key gate.
|
|
@@ -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.
|
|
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.
|
|
23
|
+
"@thincoder/core": "^0.10.3"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/",
|
package/src/acp/bridge.mjs
CHANGED
|
@@ -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` {
|
|
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
|
-
* `
|
|
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
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
if (
|
|
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
|
-
|
|
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`
|
|
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
|
-
/**
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,
|
|
115
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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",
|
|
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",
|
|
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
|
|
7
|
-
*
|
|
8
|
-
* `close` / `set_*`
|
|
9
|
-
*
|
|
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 {
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|