thincoder 0.12.67 → 0.12.68
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 +17 -5
- package/bin/thincoder.mjs +41 -344
- package/package.json +2 -2
- package/src/acp/bridge.mjs +5 -2
- package/src/acp/handlers-session.mjs +12 -1
- package/src/cli/make-agent.mjs +44 -119
- package/src/cli/memory-command.mjs +45 -3
- package/src/command-interactive.mjs +186 -0
- package/src/command-table.mjs +185 -0
- package/src/completions.mjs +36 -12
- package/src/crash-reports.mjs +42 -20
- package/src/distill.mjs +3 -3
- package/src/heap-watch.mjs +5 -14
- package/src/tui/agent-turn.mjs +88 -50
- package/src/tui/clipboard.mjs +74 -30
- package/src/tui/cmd-advisor.mjs +39 -13
- package/src/tui/cmd-clear.mjs +2 -2
- package/src/tui/cmd-config.mjs +17 -1
- package/src/tui/cmd-eng.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +1 -1
- package/src/tui/cmd-new.mjs +2 -2
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-session.mjs +13 -5
- package/src/tui/cmd-think.mjs +35 -17
- package/src/tui/cmd-timers.mjs +33 -0
- package/src/tui/conversation-writer.mjs +2 -2
- package/src/tui/display-budget.mjs +9 -9
- package/src/tui/fold-block.mjs +1 -1
- package/src/tui/index.mjs +20 -1
- package/src/tui/interaction.mjs +4 -2
- package/src/tui/key-handler-ctrlc.mjs +1 -1
- package/src/tui/key-handler-search.mjs +1 -1
- package/src/tui/key-handler.mjs +1 -1
- package/src/tui/key-modes.mjs +1 -1
- package/src/tui/layout.mjs +5 -5
- package/src/tui/lifecycle-records.mjs +199 -0
- package/src/tui/model-picker.mjs +16 -199
- package/src/tui/mouse.mjs +3 -3
- package/src/tui/pickers.mjs +6 -2
- package/src/tui/provider-admin.mjs +213 -0
- package/src/tui/queued-merge.mjs +4 -60
- package/src/tui/queued-pickup.mjs +3 -3
- package/src/tui/render-conversation.mjs +13 -4
- package/src/tui/render-frame.mjs +43 -6
- package/src/tui/render-loop.mjs +2 -3
- package/src/tui/render-segments.mjs +2 -2
- package/src/tui/render.mjs +1 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/tui/startup.mjs +28 -3
- package/src/tui/subagent-blocks.mjs +27 -19
- package/src/tui/subagent-children.mjs +5 -5
- package/src/tui/subagent-freeze.mjs +39 -37
- package/src/tui/subagent-panel.mjs +7 -7
- package/src/tui/suspension-drive.mjs +172 -259
- package/src/tui/timer-watch.mjs +64 -0
- package/src/tui/tool-args.mjs +10 -71
- package/src/tui/tool-display.mjs +6 -6
- package/src/tui/tool-events.mjs +33 -28
- package/src/tui/tui-lifecycle.mjs +13 -4
- package/src/tui/tui-state.mjs +3 -2
- package/src/tui/turn-face.mjs +4 -1
- package/src/tui/wizard.mjs +7 -2
- package/src/tui/wrapped-spawn.mjs +3 -3
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* display-budget.mjs — TUI 显示层额度(TUI-OOM-ROOTCAUSE 批——TUI.md §
|
|
2
|
+
* display-budget.mjs — TUI 显示层额度(TUI-OOM-ROOTCAUSE 批——TUI-SESSION-VIEW.md §5)。
|
|
3
3
|
*
|
|
4
|
-
* 职责(§
|
|
4
|
+
* 职责(§5.1/§5.2):常量单源(D-TB4)+ 行/载体计长(`lineChars`)+ `state.lines` 总量
|
|
5
5
|
* 对账(`syncLineBudget`)。纯函数本体(`capText` / `appendCappedText`)住
|
|
6
|
-
* `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §
|
|
6
|
+
* `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §6.15,D2 单源);
|
|
7
7
|
* 本模块只承载 TUI 面常量与对账。
|
|
8
8
|
*
|
|
9
9
|
* 依赖方向:本模块只引 `./ansi.mjs` + `@thincoder/core/text-budget.mjs`(叶子——不引 subagent 族,
|
|
10
10
|
* 冻结锚点平移由调用方经 `onTrim` 注入,避环)。
|
|
11
11
|
*
|
|
12
|
-
* 计量口径 = UTF-16 码元(`String.length`——TUI.md §
|
|
12
|
+
* 计量口径 = UTF-16 码元(`String.length`——TUI-SESSION-VIEW.md §5.1);行数维(5000 行环 /
|
|
13
13
|
* 500 行块环 / 200 条目环)**原样保留**,字符维为第二维(D-TB5——既有 N5/N6 语义不动)。
|
|
14
14
|
*/
|
|
15
15
|
import { C } from "./ansi.mjs"
|
|
@@ -17,7 +17,7 @@ import { capText, appendCappedText, fillMarker } from "@thincoder/core/text-budg
|
|
|
17
17
|
|
|
18
18
|
export { capText }
|
|
19
19
|
|
|
20
|
-
// ─── 常量(§
|
|
20
|
+
// ─── 常量(§5.1——单源;数值改动一处生效)────────────────────────────────
|
|
21
21
|
|
|
22
22
|
/** 单行(pushLine/pushLabel/恢复行/流式 flush 行)字符额度。 */
|
|
23
23
|
export const LINE_MAX_CHARS = 64_000
|
|
@@ -42,7 +42,7 @@ export const LINES_TRIM_FLOOR = 200
|
|
|
42
42
|
/** 搜索匹配计数上限(超出截断 + 提示行)。 */
|
|
43
43
|
export const SEARCH_MATCH_CAP = 10_000
|
|
44
44
|
|
|
45
|
-
// ─── 标记形态(逐字——§
|
|
45
|
+
// ─── 标记形态(逐字——§5.1 进测试断言)──────────────────────────────────
|
|
46
46
|
|
|
47
47
|
export const LINE_TRUNC_MARKER = "… [line truncated: N chars omitted]"
|
|
48
48
|
export const MIDDLE_TRUNC_MARKER = "… [middle truncated: N chars omitted]"
|
|
@@ -76,7 +76,7 @@ export function appendCapped(prev, add, opts) {
|
|
|
76
76
|
return appendCappedText(prev, add, opts)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** 评审文本单次截断(§
|
|
79
|
+
/** 评审文本单次截断(§5.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——与累积侧同源)。 */
|
|
80
80
|
export function capAdvisorText(text) {
|
|
81
81
|
return capText(text, { max: ADVISOR_TEXT_MAX_CHARS, keepHead: 32_000, keepTail: 96_000, marker: MIDDLE_TRUNC_MARKER })
|
|
82
82
|
}
|
|
@@ -109,7 +109,7 @@ export function capLines(lines, max, marker = MIDDLE_TRUNC_MARKER) {
|
|
|
109
109
|
return out
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
// ─── 计长与总量对账(§
|
|
112
|
+
// ─── 计长与总量对账(§5.2 / §5.4)───────────────────────────────────
|
|
113
113
|
|
|
114
114
|
const arrChars = (a) => {
|
|
115
115
|
if (!Array.isArray(a)) return 0
|
|
@@ -166,7 +166,7 @@ export function accountAll(state) {
|
|
|
166
166
|
const RECEIPT_RESERVE_CHARS = 96
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* `state.lines` 总量对账(§
|
|
169
|
+
* `state.lines` 总量对账(§5.3 落点表末行):超 `LINES_CHAR_BUDGET` → **最小步进**裁头 +
|
|
170
170
|
* 收据行,且不使 `lines.length` 低于 `LINES_TRIM_FLOOR`(触底 = 内层 break——接受超额,
|
|
171
171
|
* 不靠守卫退出:FLOOR+1 且残余超额时守卫仍为真)。每轮裁「至额度内所需最少行数」
|
|
172
172
|
* (need = 超额 + 收据上界预留;上限 = 可裁至保底的行数);`onTrim(state, removedCount)` 由
|
package/src/tui/fold-block.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 最高占屏 60%,底控件必落在视口内,展开永远可逆。
|
|
8
8
|
*
|
|
9
9
|
* 消费方(render-conversation.mjs 六处折叠点):
|
|
10
|
-
* 1. 子agent 活动区块(运行中 / 冻结,
|
|
10
|
+
* 1. 子agent 活动区块(运行中 / 冻结,docs/cli/design/TUI.md §6.8)
|
|
11
11
|
* 2. advisor 评审块(运行中 _advisorBlocks / 冻结 _frozenAdvisor)
|
|
12
12
|
* 3. 长消息折叠(long-N)/ 连续 dim 折叠(fold-N)
|
|
13
13
|
*
|
package/src/tui/index.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { createTuiState } from "./tui-state.mjs"
|
|
|
42
42
|
import { createInputFace } from "./input-face.mjs"
|
|
43
43
|
import { createConversationWriter } from "./conversation-writer.mjs"
|
|
44
44
|
import { createTurnFace } from "./turn-face.mjs"
|
|
45
|
+
import { createTimerWatch, fireTimerWake } from "./timer-watch.mjs"
|
|
45
46
|
|
|
46
47
|
export { upgradeFailureText, pendingNoticeReady } from "./update-notice.mjs"
|
|
47
48
|
|
|
@@ -141,10 +142,26 @@ export async function startTUI(agent, opts = {}) {
|
|
|
141
142
|
})
|
|
142
143
|
|
|
143
144
|
// 回合面(turn-face.mjs,原址 :366):submit / interaction(权限 + 提问)/ 图片粘贴 / turnCtx / turn
|
|
144
|
-
const { submit, turnCtx, askPermission, askQuestion, pasteClipboardImage } = createTurnFace({
|
|
145
|
+
const { submit, turn, turnCtx, askPermission, askQuestion, pasteClipboardImage } = createTurnFace({
|
|
145
146
|
agent, state, pushLine, pushLabel, render, ensureAssistantLabel, summarize, conversation,
|
|
146
147
|
handleSlash: (t) => handleSlash(t),
|
|
147
148
|
})
|
|
149
|
+
// timer 空闲自唤醒闩(AGENT-LOOP-ASYNC-POOL.md §6.30.5 载体②):装配点 = 回合面之后(`turn` 已就绪);
|
|
150
|
+
// 触发 ⇒ 到期批送达 + 开 timer 轮(顶层链——`{ autoTurn: true, timerTurn: true }`);
|
|
151
|
+
// 开关关(`agent.timerWake` false)⇒ 闩惰性(sync 内判——零注册)。
|
|
152
|
+
turnCtx.timerWatch = createTimerWatch({
|
|
153
|
+
agent,
|
|
154
|
+
// 火面异常兜底(先例 = key-handler `submit().catch(...)`):无人值守的空闲轮报错不得走
|
|
155
|
+
// 进程级 `unhandledRejection`(闩已自撤)——落错误行 + 重同步(在途项仍在 ⇒ 链尾/此处再武装)。
|
|
156
|
+
onFire: () => fireTimerWake(turnCtx, { runTurn: turn }).catch((e) => {
|
|
157
|
+
pushLine(`[error] ${e?.message ?? e}`, C.error)
|
|
158
|
+
turnCtx.timerWatch?.sync()
|
|
159
|
+
}),
|
|
160
|
+
})
|
|
161
|
+
// #448①「关闭后补评估」(KD-6):用户自发模态(picker ∕ wizard)全部退场 ⇒ 闩重武装——模态期被
|
|
162
|
+
// 抑制的到期件即达(无在途 / 开关关 ⇒ `sync()` 自判零注册——幂等)。两关闭点(picker 栈空 ∕
|
|
163
|
+
// wizard 退场)同注本回调;抑制本体 = `timer-watch.mjs` `fireTimerWake` 模态门(同谓词 `modalOpen`)。
|
|
164
|
+
const reevalTimerWake = () => { turnCtx.timerWatch?.sync() }
|
|
148
165
|
|
|
149
166
|
// ---------------------------------------------------------- Slash Commands
|
|
150
167
|
|
|
@@ -154,12 +171,14 @@ export async function startTUI(agent, opts = {}) {
|
|
|
154
171
|
// Model picker + generic picker: implemented in pickers.mjs
|
|
155
172
|
const { closePicker, showPicker, popPicker, renderPickerLines, openModelPicker, selectModel, setProviderKey, pickModelForSlot, confirmDelete } = createPickers({
|
|
156
173
|
agent, state, render, ansi, C, pushLine, pushLabel, persistRaw, askQuestion, maskKey,
|
|
174
|
+
onModalClose: reevalTimerWake, // #448①「关闭后补评估」(picker 栈空点)
|
|
157
175
|
})
|
|
158
176
|
|
|
159
177
|
// First-launch config wizard: implemented in wizard.mjs, closure deps passed via ctx
|
|
160
178
|
const { startWizard, renderWizard, wizardChooseProvider, wizardSubmitText, cancelWizard, wizardProviderItems } = createWizard({
|
|
161
179
|
agent, state, pushLine, pushLabel, render, persistRaw,
|
|
162
180
|
openModelPicker: () => openModelPicker(),
|
|
181
|
+
onModalClose: reevalTimerWake, // #448①「关闭后补评估」(wizard 退场点)
|
|
163
182
|
})
|
|
164
183
|
|
|
165
184
|
// /distill: impl in distill-cmd.mjs, ctx-passed
|
package/src/tui/interaction.mjs
CHANGED
|
@@ -63,7 +63,9 @@ export function createInteraction(ctx) {
|
|
|
63
63
|
|
|
64
64
|
function askPermission(name, args) {
|
|
65
65
|
// auto mode: fully authorized, no more prompts
|
|
66
|
-
|
|
66
|
+
// TURN-CAP-CONTINUE.md §1 #7 / D-TC15(2026-09-26):`continue` 不在 AUTO 授权面——续期恒须人答;
|
|
67
|
+
// 无人值守档(depth-0 digest / AUTO)撞帽即收口(静默自续 = F8 禁止)。
|
|
68
|
+
if (agent.autoApprove && name !== "continue") {
|
|
67
69
|
const argSummary = summarize(args)
|
|
68
70
|
pushLine(` [auto] ${name}${argSummary ? ` ${argSummary}` : ""}`, C.warn)
|
|
69
71
|
return Promise.resolve(true)
|
|
@@ -80,7 +82,7 @@ export function createInteraction(ctx) {
|
|
|
80
82
|
})
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
/** Batch permission ask (
|
|
85
|
+
/** Batch permission ask (D-B1): one merged prompt covering N
|
|
84
86
|
* non-readonly tools from the same toolCalls array — approve all / one by one /
|
|
85
87
|
* deny. Verdicts resolve as "approveAll" | "oneByOne" | "deny". */
|
|
86
88
|
const BATCH_PREVIEW_LINES_PER_TOOL = 8
|
|
@@ -33,7 +33,7 @@ export function handleCtrlCFamily(str, key, ctx) {
|
|
|
33
33
|
state.suspended
|
|
34
34
|
if (hasStopTarget) {
|
|
35
35
|
// 当前回合平 abort(无 interrupt——命中 agent.mjs 回合收尾清池分支——全停)
|
|
36
|
-
// §
|
|
36
|
+
// AGENT-LOOP-SUBAGENT.md §6.12 站点 #11(第 24 批):整批 / 会话级停 = stop(reason 载荷)
|
|
37
37
|
if (state.processing && state.controller) state.controller.abort({ abortTrigger: "stop", abortDetail: "session-stop" })
|
|
38
38
|
// 会话 abort 集合 = 链条内全部 controller(含 Ctrl+I/ContinueError 重建的旧
|
|
39
39
|
// controller——children 不逃逸,round1 偏差 #3)。挂起态才标记 _suspAborted +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Perform search on state.lines, update state.search.matches and clamp index
|
|
2
|
-
* TUI-OOM-ROOTCAUSE(TUI.md §
|
|
2
|
+
* TUI-OOM-ROOTCAUSE(TUI-SESSION-VIEW.md §5.1 SEARCH_MATCH_CAP):匹配计数上限——超限截断 +
|
|
3
3
|
* `state.search.capped` 提示标志(渲染面提示行;次要无界面)。
|
|
4
4
|
*/
|
|
5
5
|
import { SEARCH_MATCH_CAP } from "./display-budget.mjs"
|
package/src/tui/key-handler.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export function createKeyHandler(ctx) {
|
|
|
44
44
|
// 第 33 批(TUI §14.3(e) 键盘点):模态分派**之前**——任意按键 = 用户在场 ⇒ 清 attention
|
|
45
45
|
// 位(仅复位呈现态字段,按键语义 / 模态判定 / busy 门禁零改)。
|
|
46
46
|
clearAttention(state, render)
|
|
47
|
-
// permission confirm: y/n/a (a = approve + AUTO ON); batch (
|
|
47
|
+
// permission confirm: y/n/a (a = approve + AUTO ON); batch (D-B1): a/o/n (Esc = deny)
|
|
48
48
|
// ——模态实现 key-modes.mjs handlePermissionMode(2026-09-03 D-S4)
|
|
49
49
|
if (handlePermissionMode(str, key, { state, agent, pushLine, render })) return
|
|
50
50
|
|
package/src/tui/key-modes.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export function denyModalForOwner(state, key, { pushLine, render } = {}) {
|
|
|
34
34
|
return true
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/** permission 确认模态:y/n/a(a = approve + AUTO ON);batch
|
|
37
|
+
/** permission 确认模态:y/n/a(a = approve + AUTO ON);batch(D-B1):a/o/n(Esc = deny)。
|
|
38
38
|
* `continue` / `retry` = y/n-only 族(判据单源 `isYesNoModal`——X8 跟进①:`retry` 框不
|
|
39
39
|
* 广告 `a`,零会话级 AUTO 副作用)。
|
|
40
40
|
* consume:valid 键或 Esc 走 resolve 分支;其余键静默吞掉(模态独占)。 */
|
package/src/tui/layout.mjs
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* header → conversation → subagent 面板 → todo → picker → permission → input → status
|
|
7
7
|
* Running subagent activity renders in a FIXED bottom panel between the
|
|
8
|
-
* conversation and the todo panel (
|
|
8
|
+
* conversation and the todo panel (docs/cli/design/TUI.md §6.8) — full adaptive height
|
|
9
9
|
* (the conversation shrinks); compressed away first on small terminals (to 0 =
|
|
10
10
|
* hidden, data stays in the buffer). Done children are frozen into the
|
|
11
|
-
* conversation stream (
|
|
11
|
+
* conversation stream (unchanged). Output panels abolished.
|
|
12
12
|
* Fixed panels deducted first, conditional panels allocated by priority, remaining space to conversation.
|
|
13
13
|
*/
|
|
14
14
|
import { layoutInput, wrapText } from "./render.mjs"
|
|
@@ -109,7 +109,7 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
109
109
|
// Squeeze target: the divider line yields first under small terminals (the
|
|
110
110
|
// task rows themselves never compress away — put() truncates by panel h).
|
|
111
111
|
|
|
112
|
-
// Subagent panel (§
|
|
112
|
+
// Subagent panel (docs/cli/design/TUI.md §6.8 D1): RUNNING blocks only, between conversation and
|
|
113
113
|
// todo. Height = the FULL rendered height of every running block (F2 — fully
|
|
114
114
|
// adaptive, no cap; the conversation shrinks accordingly). Precomputed here
|
|
115
115
|
// via renderSubagentPanel (neutral module, no layout↔render-frame cycle);
|
|
@@ -152,7 +152,7 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
152
152
|
let todoFinalH = taskPanelH
|
|
153
153
|
const overflow = fixedH + convH - rows
|
|
154
154
|
if (overflow > 0) {
|
|
155
|
-
// 压缩链第 1
|
|
155
|
+
// 压缩链第 1 级(docs/cli/design/TUI.md §6.8 NF1/评审 #2 措辞统一):subagent 面板最先让位——
|
|
156
156
|
// 可压缩至 0 隐藏(运行中活动仍在缓冲区不丢),输入框/状态栏/会话区不可挤没。
|
|
157
157
|
let afterSub = fixedH
|
|
158
158
|
if (subagentH > 0) {
|
|
@@ -212,7 +212,7 @@ export function computeLayout(state, { cols, rows }) {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
* §
|
|
215
|
+
* docs/cli/design/TUI.md §6.8 评审 #4:面板部分压缩(subagentFinalH < subagentLines.length)时 render-frame
|
|
216
216
|
* 实际显示的行——**保底截断**:分隔线 + 末尾 (h-1) 行(最新启动区块优先;小终端上仍
|
|
217
217
|
* 能看到最新子 agent 活动,旧 slice(0,h) 保留顶部会把最新活动裁掉)。分隔线始终保留
|
|
218
218
|
* (面板边界语义)。h ≥ 全长 → 原样;h = 0 → [](layout 侧 panels.subagent 已为 null,
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lifecycle-records.mjs — 消化生命周期面记录承载件(#726 跨端消化面恢复批 —— CLI 承接面)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/core/design/SESSION.md` §6.26(记录形 ∥ 写缝 ∥ 读缝 ∥ 重建义务 ∥ 容差登记);
|
|
5
|
+
* CLI 承接细则 = `docs/cli/design/TUI-SESSION-VIEW.md` §6。本档 = 三写点 + 恢复面的**单一实现**:
|
|
6
|
+
* ① 记录形构建:`digest` 三型(start ∥ cap ∥ end)∥ `subagent` 快照(meta ∥ rows——rows 保尾 ≤500);
|
|
7
|
+
* ② 痕行文本:digest 记录的可见行文案——live(`digestTurn` ∥ cap 分支)与重建(`historyToLines`)同调;
|
|
8
|
+
* ③ 重建:`subagent` 记录 ⇒ `_frozenSubTask` 合成件(渲染端零改消费——折叠头 + tail-3);
|
|
9
|
+
* ④ 终态行回扫:end 记录页内缺席本轮 start ⇒ `store.page` 向更早逐段回读补齐 `n`(跨页零损)。
|
|
10
|
+
* 写点三处 = `suspension-drive.mjs` `digestTurn` ∥ `agent-turn.mjs` cap 分支 ∥ `subagent-freeze.mjs` `freezeSubTaskLines`。
|
|
11
|
+
*/
|
|
12
|
+
import { t } from "@thincoder/core/i18n.mjs"
|
|
13
|
+
import { RECORD_SEG_MESSAGES } from "@thincoder/core/session-store.mjs"
|
|
14
|
+
import { C } from "./ansi.mjs"
|
|
15
|
+
|
|
16
|
+
/** rows 保尾上界(显示行——§6.26「有界保尾」:超界弃最旧 ⇒ 前置省略标记行,标记自身不占额度)。 */
|
|
17
|
+
export const RECORD_ROWS_MAX_LINES = 500
|
|
18
|
+
|
|
19
|
+
/** 写缝载体(`state._agent` ∥ `ctx.agent`——端侧钉定二源,生产同对象;双缺 ⇒ null ⇒ 核口零动作)。 */
|
|
20
|
+
export function recordCarrier(state, agent) {
|
|
21
|
+
return state?._agent ?? agent ?? null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── ① 记录形构建(写点三处)─────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** digest 起跑记录(起跑两行同点):`n` = 起跑待消化数(可 0);`tier` 两档;ask 档携 `from`/`msg`。 */
|
|
27
|
+
export function digestStartRecord({ n, upstream, ask }) {
|
|
28
|
+
return { kind: "digest", status: "start", n, tier: upstream ? "ask" : "digest", ...(ask ?? {}) }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** digest 撞帽记录(cap 行同点;CLI 现无 auto 档产者——读取面按契约前向兼容)。 */
|
|
32
|
+
export function digestCapRecord(turns) {
|
|
33
|
+
return { kind: "digest", status: "cap", mode: "stop", turns }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** digest 收尾记录(终态行同点):`ok` ∥ `ms`(行文案 `seconds` 由同一 `ms` 派生——同值单算式)。 */
|
|
37
|
+
export function digestEndRecord({ ok, ms }) {
|
|
38
|
+
return { kind: "digest", status: "end", ok, ms }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** subagent 归档快照记录(冻结载体行插入同点):meta = 冻结时点块头事实(含 `pool`/`queued?`——§6.26)
|
|
42
|
+
* ∥ rows = 块内容行集(保尾 ≤500)。#790:`key` 写面归一 = 规范形 `sub:<role>#<id>`。 */
|
|
43
|
+
export function subagentRecord(sub) {
|
|
44
|
+
const localKey = String(sub.key ?? "")
|
|
45
|
+
const meta = {
|
|
46
|
+
key: localKey.startsWith("sub:") ? localKey : `sub:${localKey}`,
|
|
47
|
+
role: sub.role ?? null,
|
|
48
|
+
model: sub.model ?? null,
|
|
49
|
+
startedAt: sub.started ?? null,
|
|
50
|
+
doneAt: sub.doneAt ?? null,
|
|
51
|
+
turn: sub.turn ?? null,
|
|
52
|
+
maxTurns: sub.maxTurns ?? null,
|
|
53
|
+
status: sub.stopped ? "stopped" : sub.lastError ? "error" : "done",
|
|
54
|
+
}
|
|
55
|
+
if (sub.queued) meta.queued = true // 排队(未启动)⇒ 重建面 waiting 标注(`pool` 不写——未知)
|
|
56
|
+
else meta.pool = sub.async === true // 非排队显式两写(VSC 头段在场性判据 = `pool != null`——缺省会藏段)
|
|
57
|
+
if (sub.lastError) meta.error = sub.lastError // §6.26 meta `error?`(块头事实——重建面还原冻结头 errPart)
|
|
58
|
+
return { kind: "subagent", meta, rows: boundRows(rowsOf(sub)).map(({ kind, text }) => ({ kind, text })) }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 块内容行集(条目形 `{kind,text}` 同备;trim 标记随行——保尾裁量时不占额度)。 */
|
|
62
|
+
function rowsOf(sub) {
|
|
63
|
+
const out = []
|
|
64
|
+
for (const b of sub.blocks ?? []) {
|
|
65
|
+
if (!b || typeof b.text !== "string") continue
|
|
66
|
+
const row = { kind: typeof b.kind === "string" ? b.kind : "text", text: b.text }
|
|
67
|
+
if (b._trimMarker === true) row._trimMarker = true
|
|
68
|
+
out.push(row)
|
|
69
|
+
}
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 行显示行数(核 `countBlockLines` 同式:按 `\n` 切分、文末换行不计)。 */
|
|
74
|
+
function rowLines(row) {
|
|
75
|
+
const lines = String(row?.text ?? "").split("\n")
|
|
76
|
+
return lines[lines.length - 1] === "" ? lines.length - 1 : lines.length
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** rows 保尾上界(§6.26「有界保尾」):超界弃最旧 ⇒ 前置省略标记行(`N` = 实弃显示行数、
|
|
80
|
+
* 标记自身不占额度);单行超界保末行(行 = 最小单元,不切行中——桌面归档先例同式)。 */
|
|
81
|
+
function boundRows(rows) {
|
|
82
|
+
const list = Array.isArray(rows) ? rows : []
|
|
83
|
+
const isMarker = (r) => r?._trimMarker === true
|
|
84
|
+
const cost = (r) => (isMarker(r) ? 0 : rowLines(r))
|
|
85
|
+
let total = 0
|
|
86
|
+
let start = list.length
|
|
87
|
+
for (let i = list.length - 1; i >= 0; i -= 1) {
|
|
88
|
+
const n = cost(list[i])
|
|
89
|
+
if (total + n > RECORD_ROWS_MAX_LINES) break
|
|
90
|
+
total += n
|
|
91
|
+
start = i
|
|
92
|
+
}
|
|
93
|
+
if (start === list.length && list.length > 0) start = list.length - 1 // 单行超界:保末行(保尾)
|
|
94
|
+
const kept = [...list.slice(0, start).filter(isMarker), ...list.slice(start)]
|
|
95
|
+
if (kept.length === list.length) return list // 未超界:原引用(零写)
|
|
96
|
+
let dropped = 0
|
|
97
|
+
for (let i = 0; i < start; i += 1) dropped += cost(list[i])
|
|
98
|
+
return [{ kind: "meta", text: `… [rows truncated: ${dropped} lines omitted]` }, ...kept]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── ② 痕行文本(live ∥ 重建同调——单一实现零副本)─────────────────────────
|
|
102
|
+
|
|
103
|
+
/** digest 记录 ⇒ 痕行(文案与活流同算式)。`startN` = end 行的本轮起跑数(调用面解析——
|
|
104
|
+
* 页内直用 ∥ 存储回扫);缺省 / 非正数 / `ms` 非数 ⇒ 零行(活流同门 `pend0 > 0`)。 */
|
|
105
|
+
export function digestTraceLines(rec, startN = null) {
|
|
106
|
+
if (rec?.kind !== "digest") return []
|
|
107
|
+
if (rec.status === "start") {
|
|
108
|
+
const ask = rec.tier === "ask" && rec.from !== undefined ? { from: rec.from, msg: rec.msg } : null
|
|
109
|
+
const lines = [{ text: ask ? t("digest.turnLabelAsk", ask) : t("digest.turnLabel"), color: C.dim }]
|
|
110
|
+
if (rec.n > 0) lines.push({ text: t("digest.start", { n: rec.n }), color: C.dim })
|
|
111
|
+
return lines
|
|
112
|
+
}
|
|
113
|
+
if (rec.status === "cap") return [{ text: t("digest.capStop", { turns: rec.turns }), color: C.warn }]
|
|
114
|
+
if (rec.status === "end") {
|
|
115
|
+
if (!Number.isFinite(startN) || startN <= 0 || !Number.isFinite(rec.ms)) return []
|
|
116
|
+
const seconds = (rec.ms / 1000).toFixed(1) // 与记录 `ms` 同值单算式
|
|
117
|
+
return [{ text: t(rec.ok === false ? "digest.aborted" : "digest.done", { n: startN, seconds }), color: C.dim }]
|
|
118
|
+
}
|
|
119
|
+
return []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── ③ 重建(读面):subagent 记录 ⇒ 合成件 ────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/** subagent 记录 ⇒ `_frozenSubTask` 合成件(重建面;渲染端零改消费——折叠头 + tail-3 原式)。
|
|
125
|
+
* meta 块头事实逐项回填(渲染端读面:started→elapsed ∥ turn/maxTurns ∥ stopped ∥ error→errPart);
|
|
126
|
+
* #790:`pool`/`queued` ⇒ CLI 本地两事实(`async`/`queued`)∥ `key` 剥 `sub:` 前缀(显示 = 本地无前缀形)。 */
|
|
127
|
+
export function synthSubTask(rec) {
|
|
128
|
+
const meta = rec?.meta ?? {}
|
|
129
|
+
const rows = Array.isArray(rec?.rows) ? rec.rows : []
|
|
130
|
+
const blocks = rows.map((r) => ({ kind: typeof r?.kind === "string" ? r.kind : "text", text: String(r?.text ?? "") }))
|
|
131
|
+
const doneAt = Number.isFinite(meta.doneAt) ? meta.doneAt : null
|
|
132
|
+
const started = Number.isFinite(meta.startedAt) ? meta.startedAt : (doneAt ?? 0)
|
|
133
|
+
return {
|
|
134
|
+
key: String(meta.key ?? "").replace(/^sub:/, ""), // #790:读面容旧形——剥离判别(折叠键 ∥ 显示 = 本地无前缀形)
|
|
135
|
+
role: meta.role ?? null,
|
|
136
|
+
model: meta.model ?? null,
|
|
137
|
+
started,
|
|
138
|
+
done: true,
|
|
139
|
+
doneAt: doneAt ?? started,
|
|
140
|
+
turn: meta.turn ?? null,
|
|
141
|
+
maxTurns: meta.maxTurns ?? null,
|
|
142
|
+
async: meta.pool === true, // #790:记录 `pool` ⇒ 折叠头模式词(§6.26 命名映射:记录 pool ⇄ 本地 async)
|
|
143
|
+
queued: meta.queued === true, // #790:排队冻结 ⇒ 折叠头 waiting 标注
|
|
144
|
+
// #795 残项批:停止面词集扩三词(stopped ∥ cancelled ∥ terminated——词面判据单源 = §6.26;VSC/桌面写 cancelled)。
|
|
145
|
+
stopped: meta.status === "stopped" || meta.status === "cancelled"
|
|
146
|
+
|| meta.status === "terminated",
|
|
147
|
+
lastError: typeof meta.error === "string" ? meta.error : null,
|
|
148
|
+
blocks,
|
|
149
|
+
_charCount: blocks.reduce((n, b) => n + b.text.length, 0), // 行集字符和(§5.4 账实一致——恢复直算同源)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── ④ 终态行回扫(重建面:跨页分裂 ⇒ 存储逐段回读)─────────────────────────
|
|
154
|
+
|
|
155
|
+
/** 页内(含 ±1 页沿)本轮 start 直用:自 `idx` 向前命中最近 digest start ⇒ 其 `n`(无 ⇒ null)。 */
|
|
156
|
+
function pageStartN(records, idx) {
|
|
157
|
+
for (let j = idx - 1; j >= 0; j -= 1) {
|
|
158
|
+
const r = records[j]
|
|
159
|
+
if (r?.kind === "digest" && r.status === "start") return typeof r.n === "number" ? r.n : null
|
|
160
|
+
}
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** end 记录本轮 start 的 `n`(重建面解析序 = 预解析注记 `_startN`(跨页回扫产物)→ 页内回扫)。 */
|
|
165
|
+
export function resolvedStartN(records, idx) {
|
|
166
|
+
const m = records?.[idx]
|
|
167
|
+
if (m?._startN !== undefined) return m._startN
|
|
168
|
+
return pageStartN(records, idx)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 存储回扫:`store.page` 向更早逐段回读,至命中本轮 start 止(跨页零损——容差①于 CLI 不成立)。 */
|
|
172
|
+
export function scanStoreForStartN(store, endIdx) {
|
|
173
|
+
if (!store?.page || !Number.isFinite(endIdx) || endIdx <= 0) return null
|
|
174
|
+
let hi = Math.floor(endIdx)
|
|
175
|
+
while (hi > 0) {
|
|
176
|
+
const lo = Math.max(0, hi - RECORD_SEG_MESSAGES)
|
|
177
|
+
let messages = []
|
|
178
|
+
try { messages = store.page(lo, hi, { margin: 0 })?.messages ?? [] } catch { return null }
|
|
179
|
+
for (let k = messages.length - 1; k >= 0; k -= 1) {
|
|
180
|
+
const r = messages[k]
|
|
181
|
+
if (r?.kind === "digest" && r.status === "start") return typeof r.n === "number" ? r.n : null
|
|
182
|
+
}
|
|
183
|
+
hi = lo
|
|
184
|
+
}
|
|
185
|
+
return null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 预解析(调用面 = `restoreLines` ∥ `createLoadOlder`——持有存储读口):页内(含 ±1 页沿)缺席
|
|
189
|
+
* 本轮 start 的 end ⇒ 回扫 `n` 并注记 `_startN`(重建注记——不动存储形)。 */
|
|
190
|
+
export function resolveSplitTerminalNs(records, base, store) {
|
|
191
|
+
if (!Array.isArray(records) || !store?.page || !Number.isFinite(base)) return
|
|
192
|
+
for (let k = 0; k < records.length; k += 1) {
|
|
193
|
+
const m = records[k]
|
|
194
|
+
if (m?.kind !== "digest" || m.status !== "end" || m._startN !== undefined) continue
|
|
195
|
+
if (pageStartN(records, k) !== null) continue // 页内直用——零回扫
|
|
196
|
+
const n = scanStoreForStartN(store, base + k)
|
|
197
|
+
if (n !== null) m._startN = n
|
|
198
|
+
}
|
|
199
|
+
}
|