thincoder 0.12.61 → 0.12.63
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 +34 -1
- package/README.md +13 -12
- package/bin/thincoder.mjs +63 -28
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -371
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +32 -10
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +88 -0
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-clear.mjs +2 -0
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +10 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +31 -4
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +206 -0
- package/src/tui/index.mjs +40 -12
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-handler-search.mjs +9 -1
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +85 -0
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +17 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +49 -17
- package/src/tui/subagent-blocks.mjs +21 -3
- package/src/tui/subagent-children.mjs +86 -14
- package/src/tui/subagent-freeze.mjs +80 -3
- package/src/tui/suspension-drive.mjs +48 -22
- package/src/tui/tool-args.mjs +5 -2
- package/src/tui/tool-display.mjs +16 -2
- package/src/tui/tool-events.mjs +64 -22
- package/src/tui/tui-lifecycle.mjs +9 -2
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +21 -5
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -242
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -228
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -191
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -468
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -289
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -294
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -479
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -453
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -413
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -381
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -83
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -413
- package/src/memory/core.mjs +0 -300
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -417
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -217
- package/src/prompts/discipline-normal.md +0 -179
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -55
- package/src/prompts/persona-engineering.md +0 -54
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -214
- package/src/session-guard.mjs +0 -47
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-slots.mjs +0 -489
- package/src/session.mjs +0 -475
- package/src/skills.mjs +0 -153
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -224
package/src/tui/cmd-mcp.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
2
2
|
import { cloneEntry, fieldPicker, maskToken } from "./cmd-mcp-form.mjs"
|
|
3
3
|
|
|
4
4
|
/** /mcp command handler: view/add/edit/remove/test/reconnect MCP server.
|
|
5
|
-
* ctx: { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, ansi, C }
|
|
5
|
+
* ctx: { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, ansi, C, confirmDelete }
|
|
6
6
|
* MCP.md §5(2026-09-02,v2):列表即菜单(F7/D-2)、edit/add 统一字段 picker 表单
|
|
7
7
|
* (F3/F3b/D-1——表单机制在 cmd-mcp-form.mjs,本文件只调 fieldPicker)、保存前预览
|
|
8
8
|
* +探活(F2:探活失败回同一 fieldPicker——AC2)、AI 降 transport picker 末位(F4)、
|
|
@@ -37,7 +37,7 @@ async function addAndConnect(ctx, srv) {
|
|
|
37
37
|
agent.config.mcp.servers.push(srv)
|
|
38
38
|
try {
|
|
39
39
|
pushLine(`[mcp] Connecting ${srv.name}...`, C.dim)
|
|
40
|
-
const { connectMcpServer } = await import("
|
|
40
|
+
const { connectMcpServer } = await import("@thincoder/core/mcp.mjs")
|
|
41
41
|
const tools = await connectMcpServer(srv)
|
|
42
42
|
agent.tools.push(...tools)
|
|
43
43
|
pushLabel(`❯ MCP`, ansi.bold + C.tool)
|
|
@@ -50,7 +50,7 @@ async function addAndConnect(ctx, srv) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export async function handleMcpCommand(ctx, args = []) {
|
|
53
|
-
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw } = ctx
|
|
53
|
+
const { agent, pushLine, pushLabel, showPicker, askQuestion, persistRaw, confirmDelete = () => { throw new Error("ctx.confirmDelete missing — deletion refused (fail-closed)") } } = ctx
|
|
54
54
|
// 每轮重读:原本无 mcp 配置时 `?? []` 会拿到游离数组,Add server 后快照过期
|
|
55
55
|
const getServers = () => agent.config?.mcp?.servers ?? []
|
|
56
56
|
// D-3/T24:本菜单会话内最新一次磁盘重读是否失败(畸形 config.json → 内存态兜底)
|
|
@@ -61,7 +61,7 @@ export async function handleMcpCommand(ctx, args = []) {
|
|
|
61
61
|
* 测试注入 tmp config 路径用(生产 undefined → 默认 ~/.thincoder)。 */
|
|
62
62
|
let changedNames = [] // 最近一次成功重读的对账结果(T23 ⚠ 标记)
|
|
63
63
|
async function reloadFromDisk() {
|
|
64
|
-
const { reloadMcpFromDisk } = await import("
|
|
64
|
+
const { reloadMcpFromDisk } = await import("@thincoder/core/config.mjs")
|
|
65
65
|
const r = reloadMcpFromDisk(agent, ctx.configPath)
|
|
66
66
|
diskUnreadable = !r.ok
|
|
67
67
|
if (r.ok) changedNames = r.changedNames
|
|
@@ -85,6 +85,7 @@ export async function handleMcpCommand(ctx, args = []) {
|
|
|
85
85
|
// D-F5a(#7)先盘后存:磁盘 fresh raw.mcp.servers 上只删目标条目——不整节写回内存
|
|
86
86
|
// agent.config.mcp.servers(内存含 reloadMcpFromDisk 的 keptConnected 尾巴——
|
|
87
87
|
// 整节写回会把对端磁盘上其他 server 的新改动一起抹掉);冲突放弃不留内存 ghost
|
|
88
|
+
if (!(await confirmDelete(`Remove MCP server "${name}"?`))) return
|
|
88
89
|
await persistRaw((raw) => {
|
|
89
90
|
raw.mcp ??= { servers: [] }
|
|
90
91
|
if (!Array.isArray(raw.mcp.servers)) return
|
|
@@ -92,14 +93,14 @@ export async function handleMcpCommand(ctx, args = []) {
|
|
|
92
93
|
})
|
|
93
94
|
agent.config.mcp.servers = getServers().filter((s) => s.name !== name)
|
|
94
95
|
// Remove from tool list
|
|
95
|
-
const { removeMcpTools } = await import("
|
|
96
|
+
const { removeMcpTools } = await import("@thincoder/core/mcp.mjs")
|
|
96
97
|
removeMcpTools(agent, name)
|
|
97
98
|
pushLine(`[mcp] ${name} removed`, C.tool)
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
async function connectServer(name) {
|
|
101
102
|
const srv = getServers().find((s) => s.name === name)
|
|
102
|
-
const { removeMcpTools, connectMcpServer } = await import("
|
|
103
|
+
const { removeMcpTools, connectMcpServer } = await import("@thincoder/core/mcp.mjs")
|
|
103
104
|
removeMcpTools(agent, name)
|
|
104
105
|
try {
|
|
105
106
|
pushLine(`[mcp] Reconnecting ${name}...`, C.dim)
|
|
@@ -130,7 +131,7 @@ export async function handleMcpCommand(ctx, args = []) {
|
|
|
130
131
|
* agent.tools、探完即关)。未保存的临时 entry 直接传 probe(§5 D-1)。 */
|
|
131
132
|
async function probeLineFor(entry) {
|
|
132
133
|
pushLine(`[mcp] Probing ${entry.name}...`, C.dim)
|
|
133
|
-
const { probeMcpServer } = await import("
|
|
134
|
+
const { probeMcpServer } = await import("@thincoder/core/mcp.mjs")
|
|
134
135
|
const r = await probeMcpServer(entry)
|
|
135
136
|
return r.ok ? `✓ ${r.toolCount} tools, ${r.latencyMs}ms` : `✗ ${r.error}`
|
|
136
137
|
}
|
|
@@ -160,7 +161,7 @@ export async function handleMcpCommand(ctx, args = []) {
|
|
|
160
161
|
if (!description) return
|
|
161
162
|
pushLine("[mcp] Generating config from description...", C.dim)
|
|
162
163
|
try {
|
|
163
|
-
const { chat } = await import("
|
|
164
|
+
const { chat } = await import("@thincoder/core/provider/index.mjs")
|
|
164
165
|
const res = await chat(agent.provider, {
|
|
165
166
|
messages: [{
|
|
166
167
|
role: "user",
|
package/src/tui/cmd-model.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { C } from "./ansi.mjs"
|
|
2
|
-
import { parseModelRef } from "
|
|
2
|
+
import { parseModelRef } from "@thincoder/core/config.mjs"
|
|
3
3
|
|
|
4
4
|
/** /model command: open the session model picker, or switch the SESSION model directly via
|
|
5
5
|
* `/model <provider>:<model>`(MODEL-MERGE-SESSION——裁定③:裸 provider 拒——显式 p:m;
|
package/src/tui/cmd-new.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { newSession, resetSessionState } from "
|
|
1
|
+
import { newSession, resetSessionState, bindRecordStore, slotPath } from "@thincoder/core/session.mjs"
|
|
2
2
|
import { C } from "./ansi.mjs"
|
|
3
3
|
|
|
4
4
|
/** /new command: start a new session in a fresh slot.
|
|
@@ -6,16 +6,21 @@ import { C } from "./ansi.mjs"
|
|
|
6
6
|
export async function handleNewCommand(ctx) {
|
|
7
7
|
const { agent, state, pushLine, showPicker, render } = ctx
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// F-MI7:newSession = async(探测束)——doNewSession 随之为 async,调用点 await
|
|
10
|
+
const doNewSession = async () => {
|
|
11
|
+
const slot = await newSession(agent.cwd)
|
|
11
12
|
// 2026-08-31 会诊 F3:resetSessionState 清全量会话态(_fullHistory/title/_sessionStart/
|
|
12
13
|
// 多槽设计凭证(_engDesignTokens——单值镜像 _engDesignToken 已退役 D3)/压缩与验证计数
|
|
13
14
|
// 等)——原实现只清 agent.history,新会话首次落盘把旧会话完整人类线 + 旧标题写进新
|
|
14
15
|
// slot(实锤 .19/.3 双副本)。_slot 更新为粘性新槽位。
|
|
15
16
|
resetSessionState(agent)
|
|
16
17
|
agent._slot = slot
|
|
18
|
+
// §14.3.4:/new 绑定新槽记录存储(baseHistory 空——identity 此时为 null = 待固化;
|
|
19
|
+
// 首次保存/首条追加时补写现场身份)
|
|
20
|
+
bindRecordStore(agent, { slotFile: slotPath(agent.cwd, slot), identity: agent._sessionStart ?? null, baseHistory: [] })
|
|
17
21
|
state.tasks = []
|
|
18
22
|
state.lines = []
|
|
23
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):行集清空 → 字符账同步归零
|
|
19
24
|
state.streaming = ""
|
|
20
25
|
render()
|
|
21
26
|
pushLine(`New session started (slot ${slot}; /session to switch back)`, C.dim)
|
|
@@ -26,8 +31,8 @@ export async function handleNewCommand(ctx) {
|
|
|
26
31
|
{ type: "item", text: "Yes, start new session in a new slot", action: "yes" },
|
|
27
32
|
{ type: "item", text: "Cancel", action: "no" },
|
|
28
33
|
], { defaultIndex: 1 })
|
|
29
|
-
if (e?.action === "yes") doNewSession()
|
|
34
|
+
if (e?.action === "yes") await doNewSession()
|
|
30
35
|
return
|
|
31
36
|
}
|
|
32
|
-
doNewSession()
|
|
37
|
+
await doNewSession()
|
|
33
38
|
}
|
package/src/tui/cmd-reindex.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { C } from "./ansi.mjs"
|
|
|
4
4
|
* ctx: { agent, distillOpts, pushLine } */
|
|
5
5
|
export async function handleReindexCommand(ctx) {
|
|
6
6
|
const { agent, distillOpts, pushLine } = ctx
|
|
7
|
-
const { syncDir, codeSync, docSync } = await import("
|
|
7
|
+
const { syncDir, codeSync, docSync } = await import("@thincoder/core/memory.mjs")
|
|
8
8
|
pushLine("[reindex] Rebuilding index...", C.tool)
|
|
9
9
|
agent.memory.db.prepare("DELETE FROM files").run()
|
|
10
10
|
agent.memory.db.prepare("DELETE FROM code_chunks").run()
|
package/src/tui/cmd-restore.mjs
CHANGED
|
@@ -5,14 +5,14 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
5
5
|
* (CHECKPOINT.md D8). ctx: { agent, showPicker, pushLine, pushLabel } */
|
|
6
6
|
export async function handleRestoreCommand(ctx) {
|
|
7
7
|
const { agent, showPicker, pushLine, pushLabel } = ctx
|
|
8
|
-
const { listCheckpoints, rewind, isGitRepo } = await import("
|
|
8
|
+
const { listCheckpoints, rewind, isGitRepo } = await import("@thincoder/core/git/checkpoint.mjs")
|
|
9
9
|
if (!isGitRepo(agent.cwd)) {
|
|
10
10
|
pushLine("[rewind] not a git repository, checkpoints unavailable", C.error)
|
|
11
11
|
return
|
|
12
12
|
}
|
|
13
13
|
// F6 lazy fallback——与 git 工具 checkpoint list/create 入口一致(git-checkpoint.mjs):外部
|
|
14
14
|
// git commit 后(HEAD 时间 > 最新快照)先清空过期快照,/restore 不列出 commit 前状态。
|
|
15
|
-
const { lazyClearIfCommitted } = await import("
|
|
15
|
+
const { lazyClearIfCommitted } = await import("@thincoder/core/tools/git-checkpoint.mjs")
|
|
16
16
|
await lazyClearIfCommitted(agent.cwd)
|
|
17
17
|
const cps = await listCheckpoints(agent.cwd)
|
|
18
18
|
if (cps.length === 0) {
|
package/src/tui/cmd-session.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker } from "
|
|
1
|
+
import { listSlots, switchToSlot, applySession, renameSlot, activeSlot, slotOccupancy, readEndMarker, sessionDescriptor, loadSlotFile } from "@thincoder/core/session.mjs"
|
|
2
|
+
import { resolveEngineeringManifest } from "@thincoder/core/manifest.mjs"
|
|
2
3
|
import { ansi, C } from "./ansi.mjs"
|
|
3
4
|
import { restoreLines } from "./startup.mjs"
|
|
4
5
|
import { stringWidth, sliceByWidth } from "./render.mjs"
|
|
@@ -81,6 +82,26 @@ export async function handleSessionCommand(ctx) {
|
|
|
81
82
|
// 2026-08-31 会诊 deepseek 🔴:目标槽被另一活进程占用时 switchToSlot 不认领(避免
|
|
82
83
|
// 劫持对方 active),本次会话仍读到数据,但下次保存会 fork 到新槽——提前提示。
|
|
83
84
|
const occ = slotOccupancy(agent.cwd, e.slot)
|
|
85
|
+
// #41 先判后切(MANIFEST.md §2.8 F3 · 父侧裁定 3):目标槽**合值**(§2.2 会话权威值
|
|
86
|
+
// 「槽带 `engineering` 字段 ? 槽值 : `config.agent.engineering`」)=== true 时,判据前置于
|
|
87
|
+
// `switchToSlot`——判据落其后会留下「指针已切、会话未换」的半态(下次 `saveSession` 把旧
|
|
88
|
+
// 会话写回目标槽 = 跨槽覆盖)。预读 = `loadSlotFile`(**纯读**——无认领副作用)。
|
|
89
|
+
// 触发条件 = 合值(遗留槽(无 `engineering` 字段)自工程会话切入时合值 = true ⇒ 预判据
|
|
90
|
+
// 照触发——F3 触发条件口径);槽不可读(preview null)不判:该次切换本就以「Slot not
|
|
91
|
+
// found」收场(switchToSlot 返回 null),无半态面。拒 ⇒ 切槽整体不发生(switchToSlot /
|
|
92
|
+
// applySession 均不调——零副作用)+ 原因行;准 ⇒ 既有切换 + applySession 之后一行附着。
|
|
93
|
+
const preview = loadSlotFile(agent.cwd, e.slot)
|
|
94
|
+
const slotEng = preview?.engineering
|
|
95
|
+
const targetEng = preview != null && (slotEng !== undefined ? slotEng === true : agent.config?.agent?.engineering === true)
|
|
96
|
+
let manifestAfterSwitch = null
|
|
97
|
+
if (targetEng) {
|
|
98
|
+
const r = resolveEngineeringManifest(agent.cwd, { writer: "main" })
|
|
99
|
+
if (!r.ok) {
|
|
100
|
+
pushLine(`Cannot switch to slot ${e.slot} — ${r.message} (slot unchanged)`, C.warn)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
manifestAfterSwitch = r.manifest
|
|
104
|
+
}
|
|
84
105
|
const data = switchToSlot(agent.cwd, e.slot)
|
|
85
106
|
if (!data) {
|
|
86
107
|
pushLine(`Slot ${e.slot} not found`, C.dim)
|
|
@@ -89,14 +110,20 @@ export async function handleSessionCommand(ctx) {
|
|
|
89
110
|
if (occ.occupied) {
|
|
90
111
|
pushLine(`⚠ Slot ${e.slot} is being used by another live process (${occ.owner}) — continuing here will create a new copy on the next save`, C.warn)
|
|
91
112
|
}
|
|
92
|
-
applySession(agent, data)
|
|
113
|
+
applySession(agent, data, occ.occupied ? {} : { slot: e.slot })
|
|
114
|
+
// #41 准翻列:附着动作 = 翻转面自己赋值(`agent.manifest` ← 判据结果——相位行当回合起活)。
|
|
115
|
+
if (manifestAfterSwitch) agent.manifest = manifestAfterSwitch
|
|
93
116
|
// Rebuild from history (lazy) — the display snapshot is deprecated.
|
|
117
|
+
// §14.3.6:描述符 { history(尾窗+±1), total, base }——标签口径 = total(非窗口长度);
|
|
118
|
+
// 未绑定(占用分支——模式 F)回退全量数组。
|
|
119
|
+
const desc = sessionDescriptor(agent, data)
|
|
94
120
|
state.lines = []
|
|
95
|
-
|
|
121
|
+
state._linesChars = 0 // TUI-OOM-ROOTCAUSE(§15.3.2):切槽重建行集 → 字符账归零,restoreLines 重新入账
|
|
122
|
+
restoreLines(state, desc)
|
|
96
123
|
state.tasks = agent.tasks ?? []
|
|
97
124
|
if (state.tasks.length > 0 && state.tasks.every((t) => t.status === "done")) {
|
|
98
125
|
state.tasks = []
|
|
99
126
|
}
|
|
100
|
-
pushLabel(`── Switched to slot ${e.slot} (${
|
|
127
|
+
pushLabel(`── Switched to slot ${e.slot} (${desc.total} messages) ──`, C.warn)
|
|
101
128
|
render()
|
|
102
129
|
}
|
package/src/tui/cmd-skills.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
4
4
|
* ctx: { agent, pushLine, pushLabel } */
|
|
5
5
|
export async function handleSkillsCommand(ctx) {
|
|
6
6
|
const { agent, pushLine, pushLabel } = ctx
|
|
7
|
-
const { loadSkills } = await import("
|
|
7
|
+
const { loadSkills } = await import("@thincoder/core/skills.mjs")
|
|
8
8
|
const skills = await loadSkills(agent.cwd)
|
|
9
9
|
pushLabel(`❯ Skills`, ansi.bold + C.tool)
|
|
10
10
|
if (skills.length === 0) {
|
package/src/tui/cmd-think.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { ansi, C } from "./ansi.mjs"
|
|
|
5
5
|
|
|
6
6
|
export async function handleThinkCommand(ctx, args = []) {
|
|
7
7
|
const { agent, showPicker, syncProviderField, pushLine, pushLabel } = ctx
|
|
8
|
-
const { specForModel } = await import("
|
|
8
|
+
const { specForModel } = await import("@thincoder/core/config.mjs")
|
|
9
9
|
|
|
10
10
|
// Fast path: direct args — exit immediately
|
|
11
11
|
const cur = agent.provider
|
|
@@ -33,7 +33,8 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
33
33
|
if (autoThinkEnabled) { pushLine("Auto-think is ON — manual settings are overridden each turn; turn Auto off first via /think", C.error); return }
|
|
34
34
|
await applyThink({ action: "effort", level }, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue)
|
|
35
35
|
pushLabel("❯ Think", ansi.bold + C.tool)
|
|
36
|
-
|
|
36
|
+
// none = 关思考档(applyThink 内归一到 off 路径)⇒ 回执同 off 文案,不比照档位报“…none”
|
|
37
|
+
pushLine(level === "none" ? "Thinking: OFF" : `Thinking effort: ${level}`, C.tool)
|
|
37
38
|
return
|
|
38
39
|
}
|
|
39
40
|
if (sub) { pushLine("Usage: /think [on|off|effort <level>]", C.error); return }
|
|
@@ -78,18 +79,29 @@ export async function handleThinkCommand(ctx, args = []) {
|
|
|
78
79
|
pushLine(`Auto-think: ${newAuto ? "ON" : "OFF"}`, C.tool)
|
|
79
80
|
return // exit loop — no useful actions remain when auto mode just changed (T8b 锁定——toggle 即退是已批准语义——2026-09-05 曾误改 OFF 回菜单——测试抓回归——已撤销)
|
|
80
81
|
} else if (e.action === "effort") {
|
|
81
|
-
pushLine(`Reasoning effort: ${e.level}`, C.tool)
|
|
82
|
+
pushLine(e.level === "none" ? "Thinking: OFF" : `Reasoning effort: ${e.level}`, C.tool)
|
|
82
83
|
} else {
|
|
84
|
+
// 与菜单头(:47-48)同式:`thinking:null`(NF1 显式 off)必须落 OFF——本式缺 `!== null` 守卫时
|
|
85
|
+
// effort 型 off 后回执误报 ON(同一状态、两处公式——单一口径 = 面板式)。
|
|
83
86
|
const nowEnabled = cur.thinking?.type === thinkOnValue
|
|
84
|
-
|| (cur.thinking?.type === undefined && !isCustomThink)
|
|
87
|
+
|| (cur.thinking !== null && cur.thinking?.type === undefined && !isCustomThink)
|
|
85
88
|
pushLine(`Thinking: ${nowEnabled ? "ON" : "OFF"}`, C.tool)
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop
|
|
91
|
-
|
|
93
|
+
/** Shared apply logic — extracted from handleThinkCommand for reuse in both fast path and loop.
|
|
94
|
+
* Named export (2026-09-20): the on-default-effort rule (§2.8) is unit-tested by driving this
|
|
95
|
+
* directly with a synthetic spec — behavior otherwise unchanged.
|
|
96
|
+
* Effort 档位 `"none"` = 关思考 ⇒ 入口归一到 off 路径(与菜单 off 项同形——见内注释)。 */
|
|
97
|
+
export async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCustomThink, thinkOnValue) {
|
|
92
98
|
const cur = agent.provider
|
|
99
|
+
// `effort` 档位取 `"none"` = **关思考**(非“强度零”):与本面 off 动作同语义 ⇒ 归一到 off 路径
|
|
100
|
+
// (各族取其原生 off 形:effort 型 = `thinking:null`+删 effort;type 型 = `{type:"disabled"}`)。
|
|
101
|
+
// 不归一的两处违约:① 菜单头同拍显示「Thinking: ON | Effort: none」自相矛盾(off 标记缺失);
|
|
102
|
+
// ② 残留 `reasoningEffort:"none"` 会被载荷层当强度档送(`provider/core.mjs:197-204`),与百炼
|
|
103
|
+
// qwen 侧 off 判据(`config.mjs:133-139` 的 `thinking === null ⇒ false`)错位 ⇒ 服务端仍思考。
|
|
104
|
+
if (e.action === "effort" && e.level === "none") e = { action: "off" }
|
|
93
105
|
if (e.action === "auto") {
|
|
94
106
|
const cfg = agent.config.agent ??= {}
|
|
95
107
|
cfg.autoThink = !cfg.autoThink
|
|
@@ -113,9 +125,10 @@ async function applyThink(e, agent, syncProviderField, spec, isEffortOnly, isCus
|
|
|
113
125
|
// Explicit off persists thinking:null (NF1 convention — distinguishable from autoThink's
|
|
114
126
|
// delete/undefined); "on" deletes the marker so enable_thinking maps from effort again.
|
|
115
127
|
if (!enable) { cur.thinking = null; delete cur.reasoningEffort }
|
|
116
|
-
// "on" 默认 effort 取 spec
|
|
117
|
-
// (enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400
|
|
118
|
-
|
|
128
|
+
// "on" 默认 effort 取 spec 枚举的**首个非 "none"** 档(批次档 §1.8-② / 设计 §2.8 D-9):硬编码 "high"
|
|
129
|
+
// 对 qwen3.8-max(enum xhigh/medium/low)无效,会被 core.mjs 枚举校验 throw(400 前置);而直接取枚举
|
|
130
|
+
// 首项在新档(首项 "none")等于把思考关掉 ⇒ 跳过 "none";枚举全 "none" 时与无枚举同型 ⇒ 回退 "high"
|
|
131
|
+
else { delete cur.thinking; if (!cur.reasoningEffort) cur.reasoningEffort = spec.reasoningEffortEnum?.find((v) => v !== "none") ?? "high" }
|
|
119
132
|
await syncProviderField(agent.activeProvider, "thinking", cur.thinking)
|
|
120
133
|
if (!enable) await syncProviderField(agent.activeProvider, "reasoningEffort", undefined)
|
|
121
134
|
else await syncProviderField(agent.activeProvider, "reasoningEffort", cur.reasoningEffort)
|
|
@@ -11,7 +11,7 @@ export function createConfigHelpers(agent, opts = {}) {
|
|
|
11
11
|
/** 磁盘新鲜读 → mutate(单操作)→ mtime 门控写;冲突时 throw(提示语见下——TUI
|
|
12
12
|
* slash 分发层统一 [error] 行展示;调用方若自带 try/catch 展示 Save failed 同文案)。 */
|
|
13
13
|
async function persistRaw(mutate) {
|
|
14
|
-
const { writeConfigAtomic, configPath } = await import("
|
|
14
|
+
const { writeConfigAtomic, configPath } = await import("@thincoder/core/config.mjs")
|
|
15
15
|
const r = writeConfigAtomic(cfgPath ?? configPath, mutate)
|
|
16
16
|
if (!r.ok) throw new Error("config changed on disk concurrently — retry")
|
|
17
17
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display-budget.mjs — TUI 显示层额度(TUI-OOM-ROOTCAUSE 批——TUI.md §15.3)。
|
|
3
|
+
*
|
|
4
|
+
* 职责(§15.3.2):常量单源(D-TB4)+ 行/载体计长(`lineChars`)+ `state.lines` 总量
|
|
5
|
+
* 对账(`syncLineBudget`)。纯函数本体(`capText` / `appendCappedText`)住
|
|
6
|
+
* `thincoder-core/text-budget.mjs`(零依赖)——与 agent 侧捕获共用(AGENT-LOOP.md §23.3.1,D2 单源);
|
|
7
|
+
* 本模块只承载 TUI 面常量与对账。
|
|
8
|
+
*
|
|
9
|
+
* 依赖方向:本模块只引 `./ansi.mjs` + `@thincoder/core/text-budget.mjs`(叶子——不引 subagent 族,
|
|
10
|
+
* 冻结锚点平移由调用方经 `onTrim` 注入,避环)。
|
|
11
|
+
*
|
|
12
|
+
* 计量口径 = UTF-16 码元(`String.length`——TUI.md §15.2 表 1);行数维(5000 行环 /
|
|
13
|
+
* 500 行块环 / 200 条目环)**原样保留**,字符维为第二维(D-TB5——既有 N5/N6 语义不动)。
|
|
14
|
+
*/
|
|
15
|
+
import { C } from "./ansi.mjs"
|
|
16
|
+
import { capText, appendCappedText, fillMarker } from "@thincoder/core/text-budget.mjs"
|
|
17
|
+
|
|
18
|
+
export { capText }
|
|
19
|
+
|
|
20
|
+
// ─── 常量(§15.3.1——单源;数值改动一处生效)────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** 单行(pushLine/pushLabel/恢复行/流式 flush 行)字符额度。 */
|
|
23
|
+
export const LINE_MAX_CHARS = 64_000
|
|
24
|
+
/** 工具块 `argsJson` 总量。 */
|
|
25
|
+
export const ARGS_JSON_MAX_CHARS = 24_000
|
|
26
|
+
/** 工具块 `result` 行数组总量(与既有 400 行双维)。 */
|
|
27
|
+
export const TOOL_RESULT_MAX_CHARS = 64_000
|
|
28
|
+
/** 输出环单条目(超出:尾截断 + 标记)。 */
|
|
29
|
+
export const TOOL_OUTPUT_ENTRY_MAX_CHARS = 8_000
|
|
30
|
+
/** 输出环总量(超出:与既有 200 条目环同款丢最旧)。 */
|
|
31
|
+
export const TOOL_OUTPUT_TOTAL_MAX_CHARS = 128_000
|
|
32
|
+
/** 子代理块单环(与 500 显示行双维;裁最旧——省略标记 N6 语义不变)。 */
|
|
33
|
+
export const SUB_BLOCK_CHAR_LIMIT = 128_000
|
|
34
|
+
/** 评审载体(`_advisorBlocks` 累积 + `_frozenAdvisor`):头 32K + 尾 96K + 中段标记。 */
|
|
35
|
+
export const ADVISOR_TEXT_MAX_CHARS = 128_000
|
|
36
|
+
/** 流式缓冲(`state.streaming` / `state.reasoning`)累积额度。 */
|
|
37
|
+
export const STREAM_MAX_CHARS = 256_000
|
|
38
|
+
/** `state.lines` 全部行与载体文本总量(超出:最小步进裁头 + 冻结锚点平移 + 收据行——保底见下行)。 */
|
|
39
|
+
export const LINES_CHAR_BUDGET = 2_000_000
|
|
40
|
+
/** 字符超额裁剪保底行数——裁剪不得使 `state.lines` 低于该值;触底仍未脱额即停(接受超额——上界 = 保底 × 单行上限)。 */
|
|
41
|
+
export const LINES_TRIM_FLOOR = 200
|
|
42
|
+
/** 搜索匹配计数上限(超出截断 + 提示行)。 */
|
|
43
|
+
export const SEARCH_MATCH_CAP = 10_000
|
|
44
|
+
|
|
45
|
+
// ─── 标记形态(逐字——§15.3.2 进测试断言)──────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export const LINE_TRUNC_MARKER = "… [line truncated: N chars omitted]"
|
|
48
|
+
export const MIDDLE_TRUNC_MARKER = "… [middle truncated: N chars omitted]"
|
|
49
|
+
|
|
50
|
+
/** 评审/流式额度头尾分配(头 32K + 尾 96K——保裁决尾部;与常量同基数)。 */
|
|
51
|
+
export const ADVISOR_CAP_OPTS = {
|
|
52
|
+
hard: ADVISOR_TEXT_MAX_CHARS,
|
|
53
|
+
head: 32_000,
|
|
54
|
+
tail: 96_000,
|
|
55
|
+
marker: MIDDLE_TRUNC_MARKER,
|
|
56
|
+
}
|
|
57
|
+
/** 流式累积额度(头/尾各半——flush 行再受 LINE_MAX_CHARS)。 */
|
|
58
|
+
export const STREAM_CAP_OPTS = {
|
|
59
|
+
hard: STREAM_MAX_CHARS,
|
|
60
|
+
head: Math.floor(STREAM_MAX_CHARS / 4),
|
|
61
|
+
tail: Math.floor(STREAM_MAX_CHARS / 2),
|
|
62
|
+
marker: MIDDLE_TRUNC_MARKER,
|
|
63
|
+
}
|
|
64
|
+
/** 行截断(单行巨内容场景):保留头部至 `LINE_MAX_CHARS` + 尾标记(行尾截断语义)。 */
|
|
65
|
+
export function capLine(text) {
|
|
66
|
+
return capText(text, {
|
|
67
|
+
max: LINE_MAX_CHARS,
|
|
68
|
+
keepHead: LINE_MAX_CHARS,
|
|
69
|
+
keepTail: 0,
|
|
70
|
+
marker: LINE_TRUNC_MARKER,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** TUI 侧流式累积(alias——本体 text-budget.appendCappedText,D2 单源)。 */
|
|
75
|
+
export function appendCapped(prev, add, opts) {
|
|
76
|
+
return appendCappedText(prev, add, opts)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 评审文本单次截断(§15.3.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——与累积侧同源)。 */
|
|
80
|
+
export function capAdvisorText(text) {
|
|
81
|
+
return capText(text, { max: ADVISOR_TEXT_MAX_CHARS, keepHead: 32_000, keepTail: 96_000, marker: MIDDLE_TRUNC_MARKER })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 行数组总量额度(尾截断 + 标记;首行保真——ARGS_JSON/TOOL_RESULT 双维用)。
|
|
86
|
+
* 行放不下时:可放部分 → 本行头截断;无空间 → 直接以标记行收尾。
|
|
87
|
+
* @returns {string[]} 原数组或(截断 + 标记行)新数组
|
|
88
|
+
*/
|
|
89
|
+
export function capLines(lines, max, marker = MIDDLE_TRUNC_MARKER) {
|
|
90
|
+
const arr = Array.isArray(lines) ? lines : []
|
|
91
|
+
if (arr.length === 0) return arr
|
|
92
|
+
let total = 0
|
|
93
|
+
for (const s of arr) total += typeof s === "string" ? s.length : 0
|
|
94
|
+
if (total <= max) return arr
|
|
95
|
+
const out = []
|
|
96
|
+
let used = 0
|
|
97
|
+
for (const s of arr) {
|
|
98
|
+
const str = typeof s === "string" ? s : String(s ?? "")
|
|
99
|
+
const remain = max - used
|
|
100
|
+
if (str.length <= remain) { out.push(str); used += str.length; continue }
|
|
101
|
+
if (remain > 0) {
|
|
102
|
+
const kept = str.slice(0, remain)
|
|
103
|
+
out.push(kept)
|
|
104
|
+
used += kept.length
|
|
105
|
+
}
|
|
106
|
+
break
|
|
107
|
+
}
|
|
108
|
+
out.push(fillMarker(marker, total - used))
|
|
109
|
+
return out
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── 计长与总量对账(§15.3.2 / §15.3.4)───────────────────────────────────
|
|
113
|
+
|
|
114
|
+
const arrChars = (a) => {
|
|
115
|
+
if (!Array.isArray(a)) return 0
|
|
116
|
+
let n = 0
|
|
117
|
+
for (const s of a) n += typeof s === "string" ? s.length : 0
|
|
118
|
+
return n
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 一行 + 其 `_toolBlock`/`_frozenSubTask`/`_frozenAdvisor` 字段计长(单一口径)。 */
|
|
122
|
+
export function lineChars(l) {
|
|
123
|
+
if (!l) return 0
|
|
124
|
+
let n = typeof l.text === "string" ? l.text.length : 0
|
|
125
|
+
const b = l._toolBlock
|
|
126
|
+
if (b) {
|
|
127
|
+
n += typeof b.argsSummary === "string" ? b.argsSummary.length : 0
|
|
128
|
+
n += typeof b.summary === "string" ? b.summary.length : 0
|
|
129
|
+
n += arrChars(b.argsJson) + arrChars(b.output) + arrChars(b.result)
|
|
130
|
+
}
|
|
131
|
+
if (l._frozenSubTask) n += l._frozenSubTask._charCount ?? 0
|
|
132
|
+
if (typeof l._frozenAdvisor === "string") n += l._frozenAdvisor.length
|
|
133
|
+
return n
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 行增量记账(幂等——行对象上存上次计入值 `_budgetChars`;载体原地变更后重调即自愈)。 */
|
|
137
|
+
export function accountLine(state, l) {
|
|
138
|
+
if (!l) return
|
|
139
|
+
const prev = Number.isFinite(l._budgetChars) ? l._budgetChars : 0
|
|
140
|
+
const next = lineChars(l)
|
|
141
|
+
state._linesChars = Math.max(0, (state._linesChars ?? 0) + (next - prev))
|
|
142
|
+
l._budgetChars = next
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 行移除负向出账(§5.5 记账口径——增删均须过账;不变式 `_linesChars === Σ lineChars`):
|
|
146
|
+
* `state._linesChars -= lineChars(l)`,钳 0。与 `accountLine` 不可互替——后者以行对象
|
|
147
|
+
* `_budgetChars` 幂等(行从 `state.lines` 脱落后重调增量恒 0 ⇒ 账不降),移除需本导出。 */
|
|
148
|
+
export function releaseLine(state, l) {
|
|
149
|
+
if (!l) return
|
|
150
|
+
state._linesChars = Math.max(0, (state._linesChars ?? 0) - lineChars(l))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 直算对账(双算法对照 / 初始化 / 漂移自愈——测试直算同源)。 */
|
|
154
|
+
export function accountAll(state) {
|
|
155
|
+
let total = 0
|
|
156
|
+
for (const l of state.lines ?? []) {
|
|
157
|
+
const n = lineChars(l)
|
|
158
|
+
l._budgetChars = n
|
|
159
|
+
total += n
|
|
160
|
+
}
|
|
161
|
+
state._linesChars = total
|
|
162
|
+
return total
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 收据行长度预留(`syncLineBudget` 单轮 need 计入的收据余量:前缀 32 + N 数字位 + 尾 17 < 96——覆 7 位数余量)。 */
|
|
166
|
+
const RECEIPT_RESERVE_CHARS = 96
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* `state.lines` 总量对账(§15.3.3 落点表末行):超 `LINES_CHAR_BUDGET` → **最小步进**裁头 +
|
|
170
|
+
* 收据行,且不使 `lines.length` 低于 `LINES_TRIM_FLOOR`(触底 = 内层 break——接受超额,
|
|
171
|
+
* 不靠守卫退出:FLOOR+1 且残余超额时守卫仍为真)。每轮裁「至额度内所需最少行数」
|
|
172
|
+
* (need = 超额 + 收据上界预留;上限 = 可裁至保底的行数);`onTrim(state, removedCount)` 由
|
|
173
|
+
* 调用方注入冻结锚点平移(display-budget 不引 subagent 族——避环)。`pushLineLike` 注入面(无生产调用):
|
|
174
|
+
* 仅**头插**语义下自洽——尾插会使收据位置、收据 N 口径与「净位移 = 丢行数 − 1」约定失真。
|
|
175
|
+
* 幂等(未超限 / 行数 ≤ 保底)。
|
|
176
|
+
* 旧颗粒 `min(1000, lines.length-1)` 在行数 ≤1001 且超额时一轮裁到 1 行(用户实测收据
|
|
177
|
+
* 「1 lines remaining」——TUI-HISTORY-TRIM 批修复:保底 200 + 最小步进)。
|
|
178
|
+
* @returns {number} 本轮裁掉的行数
|
|
179
|
+
*/
|
|
180
|
+
export function syncLineBudget(state, { pushLineLike = null, onTrim = null } = {}) {
|
|
181
|
+
const lines = state.lines
|
|
182
|
+
if (!Array.isArray(lines)) return 0
|
|
183
|
+
if (!Number.isFinite(state._linesChars)) accountAll(state)
|
|
184
|
+
let removed = 0
|
|
185
|
+
while (state._linesChars > LINES_CHAR_BUDGET && lines.length > LINES_TRIM_FLOOR) {
|
|
186
|
+
const limit = lines.length - LINES_TRIM_FLOOR
|
|
187
|
+
const need = state._linesChars - LINES_CHAR_BUDGET + RECEIPT_RESERVE_CHARS
|
|
188
|
+
let take = 0
|
|
189
|
+
let freed = 0
|
|
190
|
+
while (take < limit && freed < need) { freed += lineChars(lines[take]); take++ }
|
|
191
|
+
lines.splice(0, take)
|
|
192
|
+
state._linesChars = Math.max(0, state._linesChars - freed)
|
|
193
|
+
removed += take
|
|
194
|
+
const receipt = `... [earlier messages trimmed — ${lines.length} lines remaining]`
|
|
195
|
+
if (typeof pushLineLike === "function") pushLineLike(receipt)
|
|
196
|
+
else {
|
|
197
|
+
const line = { text: receipt, color: C.dim }
|
|
198
|
+
line._budgetChars = receipt.length
|
|
199
|
+
lines.unshift(line)
|
|
200
|
+
state._linesChars += receipt.length
|
|
201
|
+
}
|
|
202
|
+
if (typeof onTrim === "function") onTrim(state, take)
|
|
203
|
+
if (take >= limit) break // 触底(可裁上限用尽)——接受超额;收敛靠本 break,不靠守卫退出
|
|
204
|
+
}
|
|
205
|
+
return removed
|
|
206
|
+
}
|