thincoder 0.12.65 → 0.12.67
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 +37 -0
- package/README.md +9 -0
- package/bin/thincoder.mjs +15 -0
- package/package.json +2 -2
- package/src/acp/bridge.mjs +8 -10
- package/src/cli/permission.mjs +1 -1
- package/src/prompt-injections.mjs +1 -1
- package/src/tui/agent-turn.mjs +29 -22
- package/src/tui/cmd-advisor.mjs +19 -4
- package/src/tui/cmd-config.mjs +9 -1
- package/src/tui/index.mjs +3 -4
- package/src/tui/interaction.mjs +1 -1
- package/src/tui/key-handler-busy.mjs +17 -12
- package/src/tui/key-handler-ctrlc.mjs +4 -4
- package/src/tui/key-handler-edit.mjs +12 -8
- package/src/tui/key-handler.mjs +1 -1
- package/src/tui/ledger-surface.mjs +26 -5
- package/src/tui/mouse.mjs +2 -2
- package/src/tui/queued-merge.mjs +68 -0
- package/src/tui/queued-pickup.mjs +34 -0
- package/src/tui/render-conversation.mjs +37 -1
- package/src/tui/render-frame.mjs +11 -5
- package/src/tui/render-segments.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +13 -13
- package/src/tui/subagent-freeze.mjs +4 -4
- package/src/tui/subagent-panel.mjs +4 -4
- package/src/tui/suspension-drive.mjs +42 -35
- package/src/tui/tool-display.mjs +1 -1
- package/src/tui/tool-events.mjs +7 -7
- package/src/tui/tui-state.mjs +1 -1
- package/src/tui/turn-face.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## [0.12.67] — 2026-09-25
|
|
2
|
+
|
|
3
|
+
> 0.12.66 → 0.12.67(连续号——发布时定号)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **忙时排队扩面(多槽 + 合并消费 + 可见面)**:Enter 排队自单槽升为**队列**(上限 8 条)——排队期会话流尾显示**待发送块**(逐条原文,每条 ≤3 行、超限尾标);状态栏 `enterHint` **四态分流**(已排队 N 条 / 普通 busy / 挂起内用户回合 / 挂起内系统轮——后两态分别「当前步骤结束后自动发送」/「本轮结束后优先发送」);消费 = **步边界领取为主**(不打断在飞工具;`[sending queued message]` 回执),连续非 `/` 条目**合并为一条消息、一次回合**(每批 ≤8 条 ∧ ≤2000 字符;`/cmd` 逐条保序);回合尾兜底与驱动级取批两条既有通道零改。
|
|
8
|
+
- **台账迁移 / 审计命令**:`thincoder ledger migrate --dry-run | --confirm`(跨端双库键归一合并——备份后导入、源库回收不删)+ `thincoder ledger audit`(库目录只读审计与处置建议)。
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **核依赖升级**:`@thincoder/core` `^0.9.4` → `^0.9.5`(规格扩面 + 意图认领 + 台账键归一随核生效)。
|
|
13
|
+
- **会诊 effort 菜单**:`none` 双行消除(自有两 `none` 与枚举 `none` 叠加——列表构造单源化)。
|
|
14
|
+
- **advisor effort off 语义归一**:菜单选 off ⇒ 统一 `think_off` 形(不再直赋 `reasoningEffort: "none"`),回执准确。
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **edit 工具入参守卫**(ACP 桥同收):非数组 `edits` ⇒ 成形错误,不再裸 `TypeError`。
|
|
19
|
+
- **子代理工具表** `question` 排除(CLI 提示词注入面同步)。
|
|
20
|
+
- 多处 TUI 文句 / 指针收正(挂起面 / 子代理面板 / 工具显示 / 台账面板)。
|
|
21
|
+
|
|
22
|
+
## [0.12.66] — 2026-09-22
|
|
23
|
+
|
|
24
|
+
> 0.12.65 → 0.12.66(连续号——发布时定号)
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **核依赖升级**:`@thincoder/core` `^0.9.3` → `^0.9.4`(MiMo V2.6 规格行 + 预设改指随核生效)。
|
|
29
|
+
|
|
1
30
|
## [0.12.65] — 2026-09-22
|
|
2
31
|
|
|
3
32
|
> 0.12.64 → 0.12.65(连续号——发布时定号)
|
|
@@ -181,6 +210,10 @@
|
|
|
181
210
|
② **工具作用域限制全部移除**(§10.1):resolveInCwd 去边界断言(信任模型 + 权限门禁为唯一防线,与 bash 一致);git workdir / execute scriptFile / file_ops 目录限制一并移除;工具描述与提示词 "confined to workspace" 措辞清理(两端 byte-identical)
|
|
182
211
|
③ **模型上下文长度可配置**(PROVIDER.md §15):`providers[].context`(K 单位,如 128 = 128K)覆盖 MODEL_SPECS——providerSpec 拷贝覆盖不污染共享 spec;压缩阈值(auto ×0.6)/ TOKEN 窗口 / 状态栏显示 / advisor 预算全链路跟随;数字字符串("128")两端统一归一;非法值忽略 + 警告一次;CLI /model 管理流 + VS Code settings.json 配置界面
|
|
183
212
|
|
|
213
|
+
## [0.12.58] — 2026-09-02
|
|
214
|
+
|
|
215
|
+
- 维护性发布(本期无单独变更条目)
|
|
216
|
+
|
|
184
217
|
## [0.12.57] — 2026-09-02
|
|
185
218
|
|
|
186
219
|
### Added
|
|
@@ -342,6 +375,10 @@
|
|
|
342
375
|
|
|
343
376
|
- 新增 `test/session-eng-advisor.test.mjs`:/eng 双写断言(slot+config)、applySession slot 恢复(true/false/显式 false 压过 config true)、旧槽位无字段回退 config 锁定、saveSession 每 turn 往返、/advisor guard 双写(model 等仍 config-scoped)
|
|
344
377
|
|
|
378
|
+
## [0.12.50] — 2026-08-29
|
|
379
|
+
|
|
380
|
+
- 维护性发布(本期无单独变更条目)
|
|
381
|
+
|
|
345
382
|
## [0.12.49] — 2026-08-29
|
|
346
383
|
|
|
347
384
|
### Added
|
package/README.md
CHANGED
|
@@ -226,6 +226,15 @@ Code conventions: pure `.mjs`, no semicolons, no third-party npm dependencies al
|
|
|
226
226
|
|
|
227
227
|
## Changelog
|
|
228
228
|
|
|
229
|
+
### 0.12.67 (2026-09)
|
|
230
|
+
- **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
|
+
- **Effort menus fixed** — the consult effort picker no longer shows a duplicated `none`; advisor effort "off" normalizes to `think_off` with an accurate receipt.
|
|
232
|
+
- **Core `^0.9.5`** — wider model coverage (qwen-flash / qwen-vl-max / claude-fable-5.1 / gemini-3.1-pro / step-3.7-flash / kimi-code rows), the `cacheMode` dead field removed, deepseek preset aligned to 384K, plus peer intent claims and cross-platform ledger key fixes.
|
|
233
|
+
- **Fixes** — the edit tool now returns a formed error for non-array `edits` (ACP bridge too); `question` is excluded from subagent tool tables.
|
|
234
|
+
|
|
235
|
+
### 0.12.66 (2026-09)
|
|
236
|
+
- **MiMo V2.6 support** — the core `^0.9.4` dependency registers MiMo V2.6 (pro / flash / pro-ultraspeed: 1M context, 131K output, vision, auto caching) and repoints the `mimo` / `mimoplan` presets to `mimo-v2.6-pro`.
|
|
237
|
+
|
|
229
238
|
### 0.12.65 (2026-09)
|
|
230
239
|
- **Session index** — `thincoder session index [--status|--rebuild]`. `read_history` now answers over-limit sessions and searches across sessions (`path:"all"`), backed by a derived, rebuildable `node:sqlite` index; the session files remain the only source of truth.
|
|
231
240
|
- **Queue submissions while busy** — Enter during a running turn is no longer swallowed: it lands in a single slot with a visible `[sending queued message]` receipt and runs at the turn boundary. The same now applies inside suspended sessions. Ctrl+I stays the interrupt-style channel.
|
package/bin/thincoder.mjs
CHANGED
|
@@ -122,6 +122,8 @@ Usage:
|
|
|
122
122
|
thincoder session index [--status | --rebuild]
|
|
123
123
|
Derived session index (disposable, rebuilt on demand): --status reports sessions /
|
|
124
124
|
messages / tool calls + coverage + size; --rebuild reindexes every session file
|
|
125
|
+
thincoder ledger migrate --dry-run | --confirm Ledger variant-key merge (dry-run report / backup then import; source recycled)
|
|
126
|
+
thincoder ledger audit Read-only ledger dir audit (classify every db + suggestions)
|
|
125
127
|
thincoder upgrade Update to the latest version from npm
|
|
126
128
|
thincoder completion <sh> Generate shell completion script (bash / zsh / fish)
|
|
127
129
|
thincoder -v, --version Print version
|
|
@@ -446,6 +448,19 @@ switch (command) {
|
|
|
446
448
|
break
|
|
447
449
|
}
|
|
448
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
|
+
|
|
449
464
|
case "--help":
|
|
450
465
|
case "-h": {
|
|
451
466
|
process.stdout.write(USAGE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thincoder",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.67",
|
|
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.
|
|
23
|
+
"@thincoder/core": "^0.9.5"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/",
|
package/src/acp/bridge.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* `{ stopReason: "end_turn" }` (kimi session.ts parity).
|
|
21
21
|
*/
|
|
22
22
|
import { detectDanger, normalizeEOL, joinWithEol } from "@thincoder/core/tools/shared.mjs"
|
|
23
|
-
import { computeEditEntry, validateEditEntry, assertEditArgsExclusive, hasLineParams } from "@thincoder/core/tools/edit-diff.mjs"
|
|
23
|
+
import { computeEditEntry, validateEditEntry, assertEditArgsExclusive, assertEditsContainer, assertEditEntries, hasLineParams, EDIT_ENTRY_NO_PATH, EDIT_ABORT_PREFIX, editEntryLabel } from "@thincoder/core/tools/edit-diff.mjs"
|
|
24
24
|
// 第 27 批 §12.3①/③:relay 前缀文法单一权威(模块直连——不自持正则副本)。
|
|
25
25
|
import { parseRelayPath } from "@thincoder/core/agent/relay-prefix.mjs"
|
|
26
26
|
// 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(本面仅日志——PROVIDER.md §6.20)
|
|
@@ -106,7 +106,6 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
|
|
|
106
106
|
// (edit-diff.mjs——校验→判定序→应用:行级 LCS、零重叠→替换即删、replace_all 字面替换全部)。
|
|
107
107
|
// 桥只留「读 IDE 缓冲 → computeEditEntry → 写回 IDE 缓冲」——错误文本经抛错原样透传
|
|
108
108
|
// ——与本地通道逐字一致(NF15.6b / AC15.10:not found / occurrences / 空 old / 空 new)。
|
|
109
|
-
const EDIT_ABORT_PREFIX = "edit aborted (atomic — no files written): "
|
|
110
109
|
|
|
111
110
|
// 不变量(§11.7-2):**任何** fs/* 反向 RPC 发出前必过客户端能力位——守卫住在本函数内
|
|
112
111
|
// (防御面:路由分支先判,守卫兜底,漏判不发静默超时)。
|
|
@@ -137,24 +136,23 @@ export function buildAcpCallbacks({ sessionId, notify, request, log = () => {},
|
|
|
137
136
|
await writeBuffer(p, joinWithEol(normalizeEOL(out.updated).split("\n"), raw))
|
|
138
137
|
return `OK: edited ${p} via IDE (${out.occurrences} occurrence(s))${out.note ? ` — ${out.note}` : ""}`
|
|
139
138
|
}
|
|
140
|
-
/** 数组形态(D15.7
|
|
141
|
-
*
|
|
142
|
-
* 读全部涉及文件缓冲(同文件去重——一次读)→ 逐条 computeEditEntry(
|
|
139
|
+
/** 数组形态(D15.7):守卫与文案单源 = 本地 edit-diff(assertEditsContainer / assertEditEntries /
|
|
140
|
+
* validateEditEntry——EDIT.md §8 D-7:核 edit-batch 与桥同调用、桥零字面副本)→
|
|
141
|
+
* 读全部涉及文件缓冲(同文件去重——一次读)→ 逐条 computeEditEntry(EDIT_ABORT_PREFIX——批量
|
|
143
142
|
* 原子前缀;同文件条目按数组序串行累积——第二条基于第一条结果)→ 全部通过 → 逐文件写回
|
|
144
143
|
* 一次(判失败 → 零写;写失败 → 同本地 edit-batch 既有原子语义)。 */
|
|
145
144
|
const editBatch = async (args) => {
|
|
146
145
|
const edits = args.edits
|
|
147
|
-
|
|
148
|
-
throw new Error("edits must be a non-empty array of {path, old_string | line/startLine+endLine, new_string}")
|
|
149
|
-
}
|
|
146
|
+
assertEditsContainer(edits)
|
|
150
147
|
assertEditArgsExclusive(args)
|
|
148
|
+
assertEditEntries(edits)
|
|
151
149
|
const groups = new Map() // path → { path, raw, content, edits }
|
|
152
150
|
for (const e of edits) {
|
|
153
151
|
// 2026-09-05 用户裁定(CLI parity——本地 edit-batch 同句):条目自带 path 优先;
|
|
154
152
|
// 缺省回退顶层 path(pathOf——path/filePath 别名同单形态)
|
|
155
153
|
const p = e.path ?? pathOf(args)
|
|
156
|
-
if (!p) throw new Error(
|
|
157
|
-
validateEditEntry(e, { label:
|
|
154
|
+
if (!p) throw new Error(EDIT_ENTRY_NO_PATH)
|
|
155
|
+
validateEditEntry(e, { label: editEntryLabel(p), rich: false })
|
|
158
156
|
let g = groups.get(p)
|
|
159
157
|
if (!g) {
|
|
160
158
|
g = { path: p, raw: "", content: "", edits: [] }
|
package/src/cli/permission.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** permission.mjs — CLI 转口档(P2 机制层端差批 §2.20):闸语义 + 每工具请示文案单源 = 核
|
|
1
|
+
/** permission.mjs — CLI 转口档(P2 机制层端差批 docs/batches/2026-09-20-mechanism-parity-batch.md §2.20):闸语义 + 每工具请示文案单源 = 核
|
|
2
2
|
* `permission.mjs`(#165 落点——本端零第二实现);展示面由核的 `io.ask` 缝承载。
|
|
3
3
|
* 导出面与核逐字对齐,保既有 import 面(`bin/thincoder.mjs` · `distill-command.mjs`)。 */
|
|
4
4
|
export { summarize, formatPermission, askPermission } from "@thincoder/core/permission.mjs"
|
|
@@ -16,5 +16,5 @@ export const CLI_PROMPT_INJECTIONS = {
|
|
|
16
16
|
// bash 宿主终端面——CLI 无 terminal 参数(工具面 review 再统一)
|
|
17
17
|
"bash-terminal-face": "",
|
|
18
18
|
// question 工具可用性行(核内正文该行已移出正文、锚原位替换——本端填回 CLI 措辞)
|
|
19
|
-
"question-ui-face": "- Availability: this tool needs an interactive UI — in contexts without one (headless runs
|
|
19
|
+
"question-ui-face": "- Availability: this tool needs an interactive UI — in contexts without one (headless runs) it returns an error instead of asking; subagent children (depth>0) never get it (excluded from their tool tables); put the question in your reply text instead.",
|
|
20
20
|
}
|
package/src/tui/agent-turn.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* (routeSub* / finishSubTask / freeze*SubTasks)在 subagent-blocks.mjs;标题生成
|
|
9
9
|
* 在 generate-title.mjs ensureSessionTitle。
|
|
10
10
|
*
|
|
11
|
-
* 2026-09-05 module-split
|
|
11
|
+
* 2026-09-05 module-split:AGENT-LOOP-ASYNC-POOL.md §6.8 挂起会话段(suspensionSession/digestTurn/poolLive/
|
|
12
12
|
* poolCounts 等——agent-turn.mjs 535 > 500 硬限)verbatim 迁至 suspension-drive.mjs——
|
|
13
13
|
* 本文件回合尾经 suspensionSession 进入驱动器;驱动器内 digest/用户回合经 runAgentTurn
|
|
14
14
|
* 递归回本文件(函数级静态环——模块求值期无顶层调用,环安全——session-slots ↔
|
|
@@ -18,11 +18,13 @@ import { runAgent, ContinueError } from "@thincoder/core/agent.mjs"
|
|
|
18
18
|
import { saveSession } from "@thincoder/core/session.mjs"
|
|
19
19
|
import { ansi, C } from "./ansi.mjs"
|
|
20
20
|
import { buildToolCallbacks, sweepToolBlocks } from "./tool-events.mjs"
|
|
21
|
-
import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 §
|
|
21
|
+
import { freezeAllSubTasks } from "./subagent-blocks.mjs" // freezeReclaimDigestedBlocks 随 AGENT-LOOP-ASYNC-POOL.md §6.8 段迁 suspension-drive.mjs
|
|
22
22
|
import { ensureSessionTitle } from "@thincoder/core/generate-title.mjs"
|
|
23
23
|
import { logEvent, errText } from "@thincoder/core/log.mjs"
|
|
24
24
|
import { t } from "@thincoder/core/i18n.mjs"
|
|
25
25
|
import { suspensionSession, poolLive } from "./suspension-drive.mjs"
|
|
26
|
+
import { planQueuedInput } from "./queued-merge.mjs"
|
|
27
|
+
import { pickupQueuedAtStepBoundary } from "./queued-pickup.mjs"
|
|
26
28
|
|
|
27
29
|
/** Exit-flush bound for the async end-of-run distillation (SEND-STALL-DISTILL §2.5):
|
|
28
30
|
* wait at most this long for the in-flight distill before the final session save —
|
|
@@ -80,7 +82,10 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
80
82
|
// 可注入覆盖(测试用);默认走真实实现
|
|
81
83
|
const runAgentImpl = ctx.runAgent ?? runAgent
|
|
82
84
|
const saveSessionImpl = ctx.saveSession ?? saveSession
|
|
83
|
-
//
|
|
85
|
+
// F16 步边界 pickup(queue-visible 批 fix 轮 2026-09-24——TUI.md §7.5 消费时机①):用户回合在飞 ⇒
|
|
86
|
+
// 核循环头投递回调(不中断——下一步生效,参照系 = 子代理 `send`);系统轮不传(分流——两者轮不参与)
|
|
87
|
+
const consumeQueuedInput = autoTurn ? null : () => pickupQueuedAtStepBoundary(ctx)
|
|
88
|
+
// autoTurn(消化轮):无用户输入——不画 "❯ You:"(系统驱动回合,AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6)
|
|
84
89
|
if (!autoTurn) {
|
|
85
90
|
pushLabel(`❯ You:`, ansi.bold + C.user)
|
|
86
91
|
pushLine(text, C.text)
|
|
@@ -89,7 +94,7 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
89
94
|
ctx.assistantLabeled = false
|
|
90
95
|
state.processing = true
|
|
91
96
|
state.status = "Processing..."
|
|
92
|
-
// §
|
|
97
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8(advisor round2 🟡):回合启动即解除空闲退出武装——exitArmed 只属于空闲态
|
|
93
98
|
// 双确认,跨回合残留会让"停回合后的 armed 落空穿透 + 陈旧 exitArmed"组合把一次
|
|
94
99
|
// 意图为全停/退出的按下变成无二次确认的即时退出(key-handler 落空分支落回空闲
|
|
95
100
|
// 分支时 `!state.exitArmed` 判假即 exit);计时一并清(key-handler 空闲分支按
|
|
@@ -109,7 +114,7 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
109
114
|
// bounded by the N2 per-child 500-line ring buffer.
|
|
110
115
|
state.currentTool = null
|
|
111
116
|
state.processingStarted = Date.now()
|
|
112
|
-
// §
|
|
117
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 偏差修复 #3(会话 abort 全覆盖):回合链 controller 登记。链头 = 非挂起会话内
|
|
113
118
|
// 且非释放窗口期(suspended/_suspPending 均 false)开启的回合——登记表清零;队列
|
|
114
119
|
// 递归回合(_suspPending 置位期)与会话内回合(suspended=true)继续累积。链条内每次
|
|
115
120
|
// 重建(Ctrl+I 续跑 / ContinueError 续跑 / AUTO 续跑)都登记——挂起会话的 abort 集合
|
|
@@ -151,14 +156,14 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
151
156
|
try {
|
|
152
157
|
for (let resume = false; ; resume = true) {
|
|
153
158
|
try {
|
|
154
|
-
await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume, autoTurn, upstreamTurn, suspDriven: true })
|
|
159
|
+
await runAgentImpl(agent, text, callbacks, { signal: state.controller.signal, resume, autoTurn, upstreamTurn, suspDriven: true, consumeQueuedInput })
|
|
155
160
|
flushStream()
|
|
156
161
|
break // Normal completion, exit loop
|
|
157
162
|
} catch (error) {
|
|
158
163
|
flushStream()
|
|
159
164
|
if (error.name === "AbortError" || state.controller?.signal.aborted) {
|
|
160
165
|
const reason = state.controller?.signal?.reason
|
|
161
|
-
// §
|
|
166
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-C1(round1 #1 区分机制——2026-09-03):interrupt 两种语义——
|
|
162
167
|
// 有 message(Ctrl+I 注入——key-modes handleInterruptMode)= 重建 controller
|
|
163
168
|
// 续跑(既有语义——agent loop 已把消息注入 history,中止的 signal 不能重试);
|
|
164
169
|
// 无 message(Ctrl+C 首按停回合——key-handler abort({ interrupt: true })
|
|
@@ -188,7 +193,7 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
188
193
|
}
|
|
189
194
|
if (error instanceof ContinueError) {
|
|
190
195
|
if (autoTurn) {
|
|
191
|
-
// §
|
|
196
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 digest turn-cap 规则(D-S9 ContinueError 行):无面板——AUTO 档按
|
|
192
197
|
// §2 统一规则自动 resume(无人值守授权);手动档静默拒绝(部分消化留在
|
|
193
198
|
// 历史,会话回挂起——结果不丢,只是不再烧轮次)。
|
|
194
199
|
if (agent.autoApprove) {
|
|
@@ -257,12 +262,12 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
257
262
|
clearInterval(ticker)
|
|
258
263
|
state.processing = false
|
|
259
264
|
state._advisorBlocks = []
|
|
260
|
-
// §
|
|
265
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-S1/D-S8:回合正常结束且后台池仍 live → 挂起会话:子agent 区块保持 live
|
|
261
266
|
// (不冻结——各 settle 事件自行处理),本次回合 controller 交会话层作 abort 句柄
|
|
262
267
|
// (挂起期 Ctrl+C 中止全部后台子代理);池空 / 中断 / 错误 → 现状 freezeAllSubTasks
|
|
263
268
|
// (中断态块标 interrupted;正常态块已在 settle 时各自冻结)。
|
|
264
269
|
const willSuspend = poolLive(agent)
|
|
265
|
-
// §
|
|
270
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8:挂起会话内回合(digest/会话内用户回合——skipSession 且 suspended)的
|
|
266
271
|
// 收尾**不冻结驻留块**——已消化(pinned 且条目已注入)块由 suspensionSession 在
|
|
267
272
|
// run 返回后 freezeReclaimDigestedBlocks 逐条回收(settle 锚点 splice——digest 总览文本
|
|
268
273
|
// 之前——round1 #1 裁定,不等池空);
|
|
@@ -277,7 +282,7 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
277
282
|
} else if (!agent._sessionAbort) {
|
|
278
283
|
agent._sessionAbort = state.controller // 会话 abort 句柄(children 共享此 signal)
|
|
279
284
|
}
|
|
280
|
-
// §
|
|
285
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 偏差修复 #1(回合释放窗口守卫):willSuspend 判定后、进入任何 await 之前置位
|
|
281
286
|
// 挂起待定标志。suspensionSession 真正启动前还有真实 await(ensureSessionTitle /
|
|
282
287
|
// distill flush ≤5s / saveSession),其间 processing=false 且 suspended 尚未置位——
|
|
283
288
|
// 无此标志 key-handler Enter 会走 submit 并发开第二个 runAgentTurn(双驱动器竞态:
|
|
@@ -285,7 +290,7 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
285
290
|
// 有效区间 = 释放窗口(suspensionSession 启动即清除);会话内回合(skipSession)
|
|
286
291
|
// 由 state.suspended 覆盖;_suspAborted 时挂起会话不会启动(下方会话入口同条件)。
|
|
287
292
|
state._suspPending = willSuspend && !skipSession && !state._suspAborted
|
|
288
|
-
// §
|
|
293
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 偏差修复 #3(会话 abort 全覆盖):abort 集合快照 = 本链条内全部 controller
|
|
289
294
|
// (含 Ctrl+I/ContinueError 重建的旧 controller——其下 spawn 的 async children 持旧
|
|
290
295
|
// signal,Ctrl+C 只 abort 最后一个会让它们跑完整个 turn 预算,正常完成后仍
|
|
291
296
|
// mergeChildMutations 写父 guard 标记,绕过 advisor/verify 门)。会话内回合的 finally
|
|
@@ -327,17 +332,19 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
327
332
|
render()
|
|
328
333
|
}
|
|
329
334
|
|
|
330
|
-
// §
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
335
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 偏差 #1 兜底 + queue-visible 批(2026-09-24)收正:末步入队的排队消息(本 run 无后续
|
|
336
|
+
// 步边界 ⇒ 步边界 pickup 触不到)——按合并计划取批转 state.queue,队列 while 续发新回合;
|
|
337
|
+
// 池非空时挂起会话先消费 pendingInput(D-S5 输入优先),无需此处处理。超一批者截批先行
|
|
338
|
+
// (余下留待下批——仍在队列 ⇒ 待发送块可见,零丢失)。
|
|
334
339
|
if (!skipSession && !state._suspAborted && (state.pendingInput?.length ?? 0) > 0 && !poolLive(agent)) {
|
|
335
|
-
|
|
340
|
+
const action = planQueuedInput(state.pendingInput)[0]
|
|
341
|
+
state.pendingInput.splice(0, action.count)
|
|
342
|
+
state.queue.push({ text: action.text })
|
|
336
343
|
}
|
|
337
344
|
|
|
338
|
-
// 交接消息自动续发(
|
|
339
|
-
//
|
|
340
|
-
//
|
|
345
|
+
// 交接消息自动续发(queue-visible 批 2026-09-24——R15 攒批恢复):state.queue 条目 = 合并计划
|
|
346
|
+
// 的动作(残项:释放窗口兜底 / 挂起中止残余——零丢失承诺)——逐条直发;斜杠命令直接执行
|
|
347
|
+
// (保序);回合后的续发由递归层同循环续取。
|
|
341
348
|
while (state.queue.length > 0 && !state.processing) {
|
|
342
349
|
const head = state.queue.shift()
|
|
343
350
|
// F16(TUI.md §7.5):消费回执——queued dim 行(消费事实的可见锚;对位 [continuing…])
|
|
@@ -352,12 +359,12 @@ async function runAgentTurnInner(ctx, text, opts) {
|
|
|
352
359
|
return
|
|
353
360
|
}
|
|
354
361
|
|
|
355
|
-
// §
|
|
362
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-S2: 回合尾后台池非空 → 挂起会话(D-S9 状态机——输入放开、settle 驱动
|
|
356
363
|
// auto-turn 消化、池空自然退出回 idle)。skipSession:会话内回合(消化轮/会话内
|
|
357
364
|
// 用户回合)由 suspensionSession 统一调度,不再递归进入新会话。
|
|
358
365
|
if (!skipSession && poolLive(agent) && !state._suspAborted) {
|
|
359
366
|
await suspensionSession(ctx)
|
|
360
|
-
// §
|
|
367
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 round2 偏差 #1(_suspAborted 粘滞,code review round2 #1):挂起会话退出
|
|
361
368
|
// (自然耗尽或 Ctrl+C 中止)即复位中止标志——中止 unwind 已完成、池已清空/耗尽,
|
|
362
369
|
// 复位不会误触发重入。不清则 _suspPending 守卫与下方会话入口被同一标志永久门控:
|
|
363
370
|
// 中止后用户再 spawn async、回合尾池再 live 时永不再次进入挂起态(suspensionSession
|
package/src/tui/cmd-advisor.mjs
CHANGED
|
@@ -54,6 +54,15 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
54
54
|
return ` ${advisorStatus()}`.replace(/\|/g, ansi.dim + "|" + ansi.reset)
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** off 形落盘(`think_off` 与归一后的 `effort_none` 共用——单一口径,防同状态两公式):
|
|
58
|
+
* 自定义开值族(`thinkEnabledValue` ≠ "enabled")取 `null`(NF1 显式 off);其余取 `{type:"disabled"}`。 */
|
|
59
|
+
async function applyThinkOff() {
|
|
60
|
+
const { specForModel } = await import("@thincoder/core/config.mjs")
|
|
61
|
+
const spec = specForModel(getEffectiveModel(agent, cfg))
|
|
62
|
+
const isCustomThink = (spec.thinkEnabledValue ?? "enabled") !== "enabled"
|
|
63
|
+
cfg.thinking = isCustomThink ? null : { type: "disabled" }
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
// ── Model picker sub-loop ──
|
|
58
67
|
async function modelPicker() {
|
|
59
68
|
if (!modelCache) {
|
|
@@ -106,10 +115,16 @@ export async function handleAdvisorCommand(ctx) {
|
|
|
106
115
|
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
107
116
|
pushLine(`Thinking: ON`, C.tool)
|
|
108
117
|
} else if (c.action === "think_off") {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
await applyThinkOff()
|
|
119
|
+
await persist()
|
|
120
|
+
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
121
|
+
pushLine("Thinking: OFF", C.tool)
|
|
122
|
+
} else if (c.action === "effort_none") {
|
|
123
|
+
// 归一(镜像 `cmd-think.mjs:104` 归一式 / `:127` 删档 / `:82` 回执):档位 `none` 不是
|
|
124
|
+
// 「强度零」而是**关思考**——与菜单 off 项同语义。不归一则残留 `reasoningEffort:"none"`
|
|
125
|
+
// 被载荷层当强度档送(`provider/core.mjs:197-204`)⇒ 与 off 形矛盾同发、服务端仍思考。
|
|
126
|
+
delete cfg.reasoningEffort
|
|
127
|
+
await applyThinkOff()
|
|
113
128
|
await persist()
|
|
114
129
|
pushLabel("❯ Advisor", ansi.bold + C.tool)
|
|
115
130
|
pushLine("Thinking: OFF", C.tool)
|
package/src/tui/cmd-config.mjs
CHANGED
|
@@ -20,6 +20,14 @@ export function poolCur(pl, defaults) {
|
|
|
20
20
|
return (k) => (Number.isInteger(pl?.[k]) && pl[k] >= 1 ? pl[k] : defaults.agent.poolLimits[k])
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Effort 菜单档位构造(#16 双 `none` 去重——枚举自带 `none` 的族如 qwen3.8-flash 时原来出两行):
|
|
24
|
+
* `none` 恒为首项且唯一,其余档序保持(`filter` 保序)。语义面零改——`none` = 清档,
|
|
25
|
+
* 消费点 `:205`/`:232` 只认 `=== "none"`;空枚举在 `pickEffort` 处已早退 null、不经本函数。
|
|
26
|
+
* Exported for unit tests (`cmd-config-effort.test.mjs`). */
|
|
27
|
+
export function effortMenuLevels(enumList) {
|
|
28
|
+
return ["none", ...(enumList ?? []).filter((l) => l !== "none")]
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
/** /config command: view and set agent/embedding/proxy config. */
|
|
24
32
|
export async function handleConfigCommand(ctx, args = []) {
|
|
25
33
|
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, maskKey, pickModelForSlot } = ctx
|
|
@@ -177,7 +185,7 @@ export async function handleConfigCommand(ctx, args = []) {
|
|
|
177
185
|
// min/low/medium/high/max list made the user pick values that the runtime then
|
|
178
186
|
// silently dropped as out-of-enum (2026-08-17 audit). Show the model's real enum.
|
|
179
187
|
if (!enumList || enumList.length === 0) return null // model has no effort — skip
|
|
180
|
-
const levels =
|
|
188
|
+
const levels = effortMenuLevels(enumList) // "none" = clear the effort(自带去重——#16)
|
|
181
189
|
const entries = levels.map((l) => ({ type: "item", text: l === current ? `${l} ← current` : l, action: l }))
|
|
182
190
|
const c = await showPicker("Reasoning effort", entries, { defaultIndex: Math.max(0, levels.indexOf(current ?? "none")) })
|
|
183
191
|
return c ? c.action : null // Esc → null (keep unchanged)
|
package/src/tui/index.mjs
CHANGED
|
@@ -208,10 +208,9 @@ export async function startTUI(agent, opts = {}) {
|
|
|
208
208
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
209
209
|
// LEDGER-SURFACE(§2.30.3.4):台账可见面——首扫(setImmediate)+ 周期;dispose 挂进程退出(:278 cleanup 先例)
|
|
210
210
|
const ledgerSurface = startLedgerSurface({ state, agent, pushLine, render })
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
process.on("exit", () => { void Promise.resolve(ledgerSurface).then((s) => s?.dispose?.()) })
|
|
211
|
+
// 端壳契约 = **恒同步返回 `{ dispose }`**(K-LX3 归核后与核形一致——核句柄异步就绪后桥接;
|
|
212
|
+
// 载入失败 ⇒ 空句柄)⇒ 退钩同步 `.dispose()`(零 TypeError)。
|
|
213
|
+
process.on("exit", () => ledgerSurface.dispose())
|
|
215
214
|
backgroundIndex({ agent, state, render })
|
|
216
215
|
|
|
217
216
|
// Check for updates (non-blocking, after startup screen)——实现 update-notice.mjs
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export function createInteraction(ctx) {
|
|
|
68
68
|
pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
|
|
69
69
|
return Promise.resolve(true)
|
|
70
70
|
}
|
|
71
|
-
// 闸语义 / 请示流程 = 核单源(P2 批 §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
|
|
71
|
+
// 闸语义 / 请示流程 = 核单源(P2 批 docs/batches/2026-09-20-mechanism-parity-batch.md §2.20);本端只供展示面——卡片预览经 `io.ask` 缝
|
|
72
72
|
// 注入(permissionPreview 渲染在输入框上方,与 "Allow?" 提示同帧)。
|
|
73
73
|
return coreAskPermission(name, args, {
|
|
74
74
|
ask: ({ name: n, toolArgs }) => new Promise((resolve) => {
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
|
+
import { QUEUED_MAX_ITEMS } from "./queued-merge.mjs"
|
|
2
3
|
|
|
3
4
|
/** busy 门禁(2026-09-22 structure-debt §2.1 分族 4 · #226):processing(含 digest)期
|
|
4
|
-
* Enter
|
|
5
|
-
*
|
|
5
|
+
* Enter 队列受理(容量 QUEUED_MAX_ITEMS)+ 吞面四(模态 / 斜杠 / 空 / 队满)+ 满队提示 +
|
|
6
|
+
* 挂起面唤醒,Tab 恒吞;块体自 key-handler.mjs createKeyHandler 逐字搬移(仅去缩进;函数体自持原块入口判定)。
|
|
6
7
|
* 路由 = 分派器 createKeyHandler(守卫 = busy 块内返回条件:tab / 无 meta 的 Enter、回车)。 */
|
|
7
8
|
export function handleBusyEnter(str, key, state, ctx) {
|
|
8
9
|
const { pushLine, render } = ctx
|
|
9
10
|
if (state.processing) {
|
|
10
|
-
// busy 门禁(INPUT-LOCK C'——2026-09-09 + F16 busy
|
|
11
|
-
// 扩面 2026-09-22):processing(含 digest
|
|
12
|
-
// 提交 =
|
|
13
|
-
// ⇒ 清框入 pendingInput
|
|
14
|
-
//
|
|
11
|
+
// busy 门禁(INPUT-LOCK C'——2026-09-09 + F16 busy 队列受理——2026-09-21 · busy-extend
|
|
12
|
+
// 扩面 2026-09-22 · queue-visible 多槽 + 容量 8——2026-09-24):processing(含 digest——单一
|
|
13
|
+
// 判据)输入不禁——打字照常回显;Enter 提交 = 入队(放行判据五条 = TUI-INPUT-BOX.md §4.1:
|
|
14
|
+
// 非模态 · 非斜杠 · 非空 · 未满队 ⇒ 清框入 pendingInput——步边界 pickup / 回合尾 drain /
|
|
15
|
+
// 挂起 driver 送达;挂起两态不再是吞面);
|
|
16
|
+
// 吞面四 = 模态 / 斜杠 / 空 / 队满(吞 + 提示,文本保留;斜杠同禁发——白名单已删);
|
|
15
17
|
// 多行换行(meta/enter——编辑)照常放行;Tab 仍吞。**第 31 批**:↑↓ 分流见下方三规则块。
|
|
16
18
|
if (key.name === "tab") return
|
|
17
19
|
if ((key.name === "return" && !key.meta) || (str === "\r" && !key.meta)) {
|
|
@@ -23,18 +25,21 @@ export function handleBusyEnter(str, key, state, ctx) {
|
|
|
23
25
|
pushLine(`[主会话处理中 —— 消息未发送(回合结束后请重按 Enter)]`, C.warn)
|
|
24
26
|
render(); return
|
|
25
27
|
}
|
|
26
|
-
// 条件 5
|
|
27
|
-
if (state.pendingInput.length
|
|
28
|
-
pushLine(`[主会话处理中 ——
|
|
28
|
+
// 条件 5:满队(第 9 条)= 拒绝 + 提示 + 文本保留(容量 8——多槽裁定连带;与挂起态满队分支同构)
|
|
29
|
+
if (state.pendingInput.length >= QUEUED_MAX_ITEMS) {
|
|
30
|
+
pushLine(`[主会话处理中 —— 已排队 ${QUEUED_MAX_ITEMS} 条消息,请等其处理完成后再发送]`, C.warn)
|
|
29
31
|
render(); return
|
|
30
32
|
}
|
|
31
|
-
//
|
|
33
|
+
// 入队(判据全满足):清框 + history 收录 + 草稿复位 + 队列填充(§4.1 执行序)
|
|
32
34
|
state.input = []; state.cursor = 0
|
|
33
35
|
state.history.push(text)
|
|
34
36
|
state.historyIndex = -1
|
|
35
37
|
state._draft = null
|
|
36
38
|
state.pendingInput.push(text)
|
|
37
|
-
//
|
|
39
|
+
// F4「新提交消息 → 恢复跟随」(§7.5 跟随——排队提交同列;与 turn-face.mjs submit 同款两行)
|
|
40
|
+
state.scroll = 0
|
|
41
|
+
state._followTail = true
|
|
42
|
+
// 挂起面入队同款唤醒(§4.1)——busy 期该槽恒 null ⇒ 零动作;普通 busy 面零唤醒
|
|
38
43
|
if (state.suspended || state._suspPending) state._suspWake?.()
|
|
39
44
|
render(); return
|
|
40
45
|
}
|
|
@@ -15,7 +15,7 @@ export function handleCtrlCFamily(str, key, ctx) {
|
|
|
15
15
|
popPicker(null)
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
|
-
// §
|
|
18
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8(2026-09-03):武装窗口内第二次 Ctrl+C = 显式全停(D-C4 /abort 语义)。
|
|
19
19
|
// 在状态路由前检查(picker 取消分支例外——picker 打开时 Ctrl+C 语义 = 取消
|
|
20
20
|
// picker,武装让位;场景极窄——3s 窗口内需另有 picker 被打开)——两次按下之间
|
|
21
21
|
// 状态会迁移(首按停回合 → 释放窗口/挂起会话启动),武装必须跨态桥接:只查挂起
|
|
@@ -51,10 +51,10 @@ export function handleCtrlCFamily(str, key, ctx) {
|
|
|
51
51
|
// 无目标可停 → 落空继续到下方分支(processing 已停 → 空闲态 exitArmed 双确认)
|
|
52
52
|
}
|
|
53
53
|
if (state.suspended) {
|
|
54
|
-
// §
|
|
54
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-S9 + round2 偏差 #4(2026-09-02)+ AGENT-LOOP-ASYNC-POOL.md §6.8 修订(2026-09-03):挂起态
|
|
55
55
|
// Ctrl+C = 武装窗口两级中止——一次按键直接清池中止全部后台子代理的误触代价高
|
|
56
56
|
// (digest 刷屏时用户可能只想停住当前回合);仿空闲态退出武装语义(同下方空闲态
|
|
57
|
-
// exitArmed
|
|
57
|
+
// exitArmed 双确认分支)。AGENT-LOOP-ASYNC-POOL.md §6.8 修订 ①:中止当前回合的 abort 带 interrupt
|
|
58
58
|
// (无 message)——此前平 abort 命中 agent.mjs 回合收尾清池分支(aborted &&
|
|
59
59
|
// !reason.interrupt → 无条件 _asyncSubagents.clear()),一次首按即误杀全部后台
|
|
60
60
|
// 子代理(2026-09-03 用户两次实测——提示"再按才杀"与实际"一次就全杀"不符);
|
|
@@ -80,7 +80,7 @@ export function handleCtrlCFamily(str, key, ctx) {
|
|
|
80
80
|
return
|
|
81
81
|
}
|
|
82
82
|
if (state.processing && state.controller) {
|
|
83
|
-
// §
|
|
83
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-C1(2026-09-03 紧急修复——processing 态曾无武装窗口,第一按直接平
|
|
84
84
|
// abort() → 命中 agent.mjs 回合收尾清池分支 → 一次 Ctrl+C 误杀全部后台子代理,
|
|
85
85
|
// 用户两次实测被坑):首按 = interrupt 语义(无 message——停当前回合不续跑——
|
|
86
86
|
// interrupt 排除 agent.mjs 清池分支——后台池保留——agent-turn 区分不重建续跑)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
2
|
import { readClipboardText, insertPastedText } from "./clipboard.mjs"
|
|
3
|
+
import { QUEUED_MAX_ITEMS } from "./queued-merge.mjs"
|
|
3
4
|
|
|
4
5
|
/** 编辑族(2026-09-22 structure-debt §2.1 分族 5 · #226):Tab 补全 / Ctrl+U 清框 /
|
|
5
|
-
* 退格·删除 / Enter(多行换行 ·
|
|
6
|
+
* 退格·删除 / Enter(多行换行 · 挂起队列 · submit)/ 剪贴板文本与图片粘贴 / 可打印字符;
|
|
6
7
|
* 各块自 key-handler.mjs createKeyHandler 逐字搬移(仅去缩进)。
|
|
7
8
|
* 路由 = 分派器 createKeyHandler 末族(其后无可达分支 —— 块内 return 即终结本次按键)。 */
|
|
8
9
|
export function handleEditKeys(str, key, ctx) {
|
|
@@ -50,15 +51,15 @@ export function handleEditKeys(str, key, ctx) {
|
|
|
50
51
|
render()
|
|
51
52
|
} else {
|
|
52
53
|
const text = state.input.join("").trim()
|
|
53
|
-
// §
|
|
54
|
-
//
|
|
55
|
-
// pendingInput
|
|
56
|
-
//
|
|
57
|
-
//
|
|
54
|
+
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-S5/F3/F7 + 偏差 #1 + INPUT-LOCK 队列受理(F-6——2026-09-09 · queue-visible 多槽
|
|
55
|
+
// 容量 8——2026-09-24):挂起态(suspended 或释放窗口 _suspPending)Enter = 新回合输入
|
|
56
|
+
// (非打断)——填 pendingInput 队列(容量 QUEUED_MAX_ITEMS)由挂起会话调度;挂起会话内
|
|
57
|
+
// busy(含 digest)提交经 busy 门禁入同队列(F16 §4.1——两分支同队列、同款清理与唤醒);
|
|
58
|
+
// 输入框零干扰(F3)。满队(第 9 条——竞态防御——不覆盖不丢失)→ 吞 + 提示,文本保留在输入框。
|
|
58
59
|
if ((state.suspended || state._suspPending) && text && !text.startsWith("/")) {
|
|
59
60
|
state.pendingInput ??= []
|
|
60
|
-
if (state.pendingInput.length
|
|
61
|
-
pushLine(`[主会话处理中 ——
|
|
61
|
+
if (state.pendingInput.length >= QUEUED_MAX_ITEMS) {
|
|
62
|
+
pushLine(`[主会话处理中 —— 已排队 ${QUEUED_MAX_ITEMS} 条消息,请等其处理完成后再发送]`, C.warn)
|
|
62
63
|
render()
|
|
63
64
|
return
|
|
64
65
|
}
|
|
@@ -68,6 +69,9 @@ export function handleEditKeys(str, key, ctx) {
|
|
|
68
69
|
state.historyIndex = -1
|
|
69
70
|
state._draft = null
|
|
70
71
|
state.pendingInput.push(text)
|
|
72
|
+
// F4「新提交消息 → 恢复跟随」(§7.5 跟随——排队提交同列;与 turn-face.mjs submit 同款两行)
|
|
73
|
+
state.scroll = 0
|
|
74
|
+
state._followTail = true
|
|
71
75
|
state._suspWake?.()
|
|
72
76
|
render()
|
|
73
77
|
return
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -110,7 +110,7 @@ export function createKeyHandler(ctx) {
|
|
|
110
110
|
if (key.name === "up" || key.name === "down") { handleHistoryKeys(key, ctx); return }
|
|
111
111
|
if (key.name === "left" || key.name === "right" || key.name === "home" || key.name === "end") { handleCursorKeys(key, ctx); return }
|
|
112
112
|
|
|
113
|
-
// 序 4:busy 门禁(key-handler-busy.mjs)——Enter
|
|
113
|
+
// 序 4:busy 门禁(key-handler-busy.mjs)——Enter 队列受理(容量 8)+ 吞面四 / Tab 吞
|
|
114
114
|
// (守卫 = busy 块内返回条件:processing 期仅 tab 与无 meta 的 Enter/回车 被吞,余键落空至编辑族)
|
|
115
115
|
if (state.processing && (key.name === "tab" || ((key.name === "return" && !key.meta) || (str === "\r" && !key.meta)))) {
|
|
116
116
|
handleBusyEnter(str, key, state, ctx)
|