@x-otto/cli 0.0.1-alpha.0
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/README.md +120 -0
- package/bin/otto.js +61 -0
- package/dist/auth-command-CXAB3_hs.js +11 -0
- package/dist/auth-command-CXAB3_hs.js.map +1 -0
- package/dist/build-id-BX4QHzM0.js +3 -0
- package/dist/build-id-BX4QHzM0.js.map +1 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/otto-plugin.json +8 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +60023 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin.ts +64 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-messages.ts +916 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-usage.ts +81 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-completions.ts +158 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +575 -0
- package/dist/bundled-extensions/plugin-skill-inductor/otto-plugin.json +8 -0
- package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/meta.json +1 -0
- package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/plugin.cjs +52781 -0
- package/dist/bundled-extensions/plugin-skill-inductor/plugin.ts +77 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-detector.ts +210 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-digest.ts +146 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-store.ts +87 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/episode-store.ts +122 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/episode.ts +125 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/extraction-prompt.ts +97 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/inductor-service.ts +183 -0
- package/dist/bundled-extensions/plugin-skill-inductor/src/similarity.ts +58 -0
- package/dist/command-dispatcher-BgStVcCF.js +13 -0
- package/dist/command-dispatcher-BgStVcCF.js.map +1 -0
- package/dist/config-migration-DatflIxs.js +21 -0
- package/dist/config-migration-DatflIxs.js.map +1 -0
- package/dist/daemon-COESl-zU.js +21 -0
- package/dist/daemon-COESl-zU.js.map +1 -0
- package/dist/daemon-base-YOWgYYN7.js +3 -0
- package/dist/daemon-base-YOWgYYN7.js.map +1 -0
- package/dist/debug-CiosFwme.js +34 -0
- package/dist/debug-CiosFwme.js.map +1 -0
- package/dist/discover-with-bundled-CznLjkkw.js +3 -0
- package/dist/discover-with-bundled-CznLjkkw.js.map +1 -0
- package/dist/doctor-C84-hhEr.js +30 -0
- package/dist/doctor-C84-hhEr.js.map +1 -0
- package/dist/doctor-attach-_zeIJewx.js +42 -0
- package/dist/doctor-attach-_zeIJewx.js.map +1 -0
- package/dist/doctor-cdp-K_mIFRfA.js +5 -0
- package/dist/doctor-cdp-K_mIFRfA.js.map +1 -0
- package/dist/doctor-heap-Rr2nNvcw.js +11 -0
- package/dist/doctor-heap-Rr2nNvcw.js.map +1 -0
- package/dist/doctor-ps-OL3FGQYu.js +8 -0
- package/dist/doctor-ps-OL3FGQYu.js.map +1 -0
- package/dist/extension-Bhzoel6s.js +169 -0
- package/dist/extension-Bhzoel6s.js.map +1 -0
- package/dist/extension-plugin-CnF6O_zK.js +7 -0
- package/dist/extension-plugin-CnF6O_zK.js.map +1 -0
- package/dist/extension-plugin-DWcakwRx.js +2 -0
- package/dist/extension-v-liRnUX.js +2 -0
- package/dist/feedback-BES_6ebi.js +21 -0
- package/dist/feedback-BES_6ebi.js.map +1 -0
- package/dist/feedback-BRbLUn9u.js +2 -0
- package/dist/feishu-setup-D6vnuNLw.js +6 -0
- package/dist/feishu-setup-D6vnuNLw.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/install-DlVJRHRT.js +11 -0
- package/dist/install-DlVJRHRT.js.map +1 -0
- package/dist/install-WCcQ9k9M.js +2 -0
- package/dist/mcp-DbTYT7hB.js +12 -0
- package/dist/mcp-DbTYT7hB.js.map +1 -0
- package/dist/migrate-boDXhEyV.js +14 -0
- package/dist/migrate-boDXhEyV.js.map +1 -0
- package/dist/model-auth-C27iG6Jq.js +3 -0
- package/dist/model-auth-C27iG6Jq.js.map +1 -0
- package/dist/model-auth-C4bsWvvn.js +2 -0
- package/dist/observe-CVg-MRM2.js +32 -0
- package/dist/observe-CVg-MRM2.js.map +1 -0
- package/dist/parse-cli-tFDOeVU1.js +3 -0
- package/dist/parse-cli-tFDOeVU1.js.map +1 -0
- package/dist/persistenced-CCwHu1Ww.js +36 -0
- package/dist/persistenced-CCwHu1Ww.js.map +1 -0
- package/dist/proxy-BvdBsB1O.js +2 -0
- package/dist/proxy-evg7ZvoD.js +3 -0
- package/dist/proxy-evg7ZvoD.js.map +1 -0
- package/dist/ps-DKrluEuy.js +9 -0
- package/dist/ps-DKrluEuy.js.map +1 -0
- package/dist/registry-B3nfN62q.js +2 -0
- package/dist/registry-D0NONjLv.js +6 -0
- package/dist/registry-D0NONjLv.js.map +1 -0
- package/dist/remote-persistence-server-B_hONoVR.js +35 -0
- package/dist/remote-persistence-server-B_hONoVR.js.map +1 -0
- package/dist/run-attach-Ci5jYyCa.js +3 -0
- package/dist/run-attach-Ci5jYyCa.js.map +1 -0
- package/dist/serve-pVR6UIT9.js +21 -0
- package/dist/serve-pVR6UIT9.js.map +1 -0
- package/dist/service-client-BLNeugM9.js +3 -0
- package/dist/service-client-BLNeugM9.js.map +1 -0
- package/dist/time-travel-ZozuPPI-.js +21 -0
- package/dist/time-travel-ZozuPPI-.js.map +1 -0
- package/dist/version-C0qM-qJM.js +3 -0
- package/dist/version-C0qM-qJM.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["globalAgentJobRegistry","detectLanguage"],"sources":["../src/terminal-clear-on-exit.ts","../src/modes/turn-continuation.ts","../src/submit-busy.ts","../src/command-parser.ts","../src/commands/interactive-commands.ts","../src/live-token-estimate.ts","../src/commands/interactive-events.ts","../src/history-pager.ts","../src/context-pressure-reminder.ts","../src/commands/interactive-persistence.ts","../src/commands/interactive-background.ts","../src/commands/interactive-model.ts","../src/theme-preset-wiring.ts","../src/commands/schedule-backend-methods.ts","../src/commands/interactive-schedule.ts","../src/commands/interactive-context.ts","../src/commands/interactive-setup.ts","../src/stale-build-detector.ts","../src/commands/interactive-submit-helpers.ts","../src/commands/interactive-stop.ts","../src/commands/interactive-session-setup.ts","../src/commands/input-history-store.ts","../src/commands/shell-history-loader.ts","../src/commands/interactive-history-setup.ts","../src/commands/paste-session-binding.ts","../src/commands/interactive-agent-toasts.ts","../src/commands/recent-mentions.ts","../src/commands/interactive-memory-governor.ts","../src/commands/interactive-tui-callbacks.ts","../src/commands/interactive-error-retry.ts","../src/commands/interactive.ts","../src/commands/workspace-trust.ts","../src/commands/run-modes.ts","../src/theme-markdown-wiring.ts","../src/cli.ts","../src/index.ts"],"sourcesContent":["/**\n * terminal-clear-on-exit.ts — RFC-291 legacy fallback: clear only Otto-owned TUI output.\n *\n * 默认终局是 TUI scoped viewport(alternate screen):退出时 TUI 自己释放备用屏幕,终端回到\n * 启动 otto 前的内容。本模块只服务 `OTTO_TUI_SCOPED_VIEWPORT=0` 的旧原生 scrollback 路径。\n *\n * scoped viewport 关闭时,TUI 仍会把对话流写入终端原生 scrollback;终端没有“只删除\n * 本程序写入的历史区间”的 API,只能用 3J 全局清 scrollback。因此该旧路径保留原逃生口\n * `OTTO_TUI_CLEAR_ON_EXIT=0`,但默认 scoped viewport 下本函数是 no-op。\n */\nconst ESC = String.fromCharCode(27)\nconst CLEAR_ALL = `${ESC}[2J${ESC}[3J${ESC}[H`\n\n/**\n * 退出前清空终端可见屏 + scrollback(`2J` 清可见屏 + `3J` 清 scrollback + `H` 归位)。\n * 仅当目标是 TTY 且未被逃生口禁用时才写出;非 TTY(管道/重定向)或禁用时是 no-op。\n * best-effort:写失败(如流已关闭)静默吞掉,不阻塞退出流程。\n */\nexport function clearTerminalOnExit(stream: NodeJS.WriteStream = process.stdout): void {\n if (process.env['OTTO_TUI_SCOPED_VIEWPORT'] !== '0') return\n if (process.env['OTTO_TUI_CLEAR_ON_EXIT'] === '0') return\n if (!stream.isTTY) return\n try {\n stream.write(CLEAR_ALL)\n } catch { /* best-effort,流可能已关闭 */ }\n}\n","/**\n * 回合续跑状态机:runTurn / onContinuePrompt / markStopReason.\n */\nimport type { InteractiveResult } from './run-modes'\n\n/**\n * 续跑提示(策略)——最小 well-formed user 消息。\n * 详细指令由 system prompt 携带,此处只需最小消息(减少历史污染,SUSP-1)。\n */\nexport const BUDGET_CONTINUE_PROMPT = 'Continue with the remaining work until the task is complete.'\n\nexport interface ContinuationDriver {\n runTurn(prompt: string): Promise<InteractiveResult>\n /**\n * 宿主渲染一个续轮结果。返回 'exit' 停止所有循环(result.type==='exit' → 宿主跑 handleExit)。\n * 镜像 cli 续轮 push:跳过 noop/help,未流式去重过的文本 push 到对话流。\n */\n onTurnResult(result: InteractiveResult): 'continue' | 'exit'\n askBudgetContinue(): Promise<boolean>\n /** 用户停下:宿主把「为什么停」留一条进转录。 */\n onBudgetStopped(): void\n beforeContinuationTurn(): void\n}\n\n/**\n * 多轮续跑编排主循环(budget-continue only;todo 续轮已由引擎闸 RFC-094 接管)。\n * @param first 首轮结果(已由宿主渲染 + 处理 exit/help;本函数只处理其后的续跑)。\n */\nexport async function driveContinuation(first: InteractiveResult, d: ContinuationDriver): Promise<void> {\n let cur = first\n while (cur.type === 'response' && cur.endReason === 'max-tool-turns' && cur.notice) {\n if (!(await d.askBudgetContinue())) {\n d.onBudgetStopped()\n return\n }\n d.beforeContinuationTurn()\n const cont = await d.runTurn(BUDGET_CONTINUE_PROMPT)\n if (d.onTurnResult(cont) === 'exit') return\n cur = cont\n }\n}\n","/**\n * busy 状态计算:工具执行/流式/暂停等状态的单一真源判据。\n */\nexport function effectiveBusy(engineBusy: boolean, cliProcessing: boolean): boolean {\n return engineBusy || cliProcessing\n}\n","/**\n * 指令解析:/command args[0] args[1] ... → { name, args, rawCommand }.\n */\n\n/** 指令解析成功。 */\nexport interface CommandMatch {\n isCommand: true\n command: string\n args: string[]\n}\n\n/** 非指令(普通对话文本)。 */\nexport interface NotCommand {\n isCommand: false\n /** raw.trim() when raw itself is not a command. */\n text: string\n}\n\nexport type ParseResult = CommandMatch | NotCommand\n\n/**\n * 分割一个以 / 开头(不含 /)的参数字符串为 token 数组。\n *\n * 分词规则:\n * - 空格/Tab 分割 token,连续空格折叠\n * - 双引号 \"…\" 内空格保留为单 token;\\\" 转义引号自身\n * - 单引号 '…' 内空格保留为单 token;不含转义(字面量)\n * - 反斜杠 \\ 转义下一个字符\n * - 未闭合引号延伸到行尾\n */\nfunction tokenize(s: string): { tokens: string[] } {\n const tokens: string[] = []\n let i = 0\n let current = ''\n\n const push = () => {\n if (current) {\n tokens.push(current)\n current = ''\n }\n }\n\n while (i < s.length) {\n const ch = s[i]!\n\n if (ch === ' ' || ch === '\\t') {\n push()\n i++\n continue\n }\n\n if (ch === '\\\\') {\n i++\n if (i < s.length) current += s[i]\n i++\n continue\n }\n\n if (ch === '\"') {\n i++\n const seg = readUntil(s, i, '\"')\n current += seg.value\n i = seg.next\n continue\n }\n\n if (ch === \"'\") {\n i++\n const seg = readUntil(s, i, \"'\", false)\n current += seg.value\n i = seg.next\n continue\n }\n\n current += ch\n i++\n }\n\n push()\n return { tokens }\n}\n\n/**\n * 从 pos 开始读取直到 next 字符(处理了 next 前的 \\ 转义)。\n * 返回读取的内容 + 扫描停止的位置。\n */\nfunction readUntil(s: string, start: number, delimiter: string, escape = true): { value: string; next: number } {\n let out = ''\n let i = start\n while (i < s.length) {\n const ch = s[i]!\n if (escape && ch === '\\\\') {\n i++\n if (i < s.length) out += s[i]\n i++\n continue\n }\n if (ch === delimiter) {\n i++\n return { value: out, next: i }\n }\n out += ch\n i++\n }\n return { value: out, next: i }\n}\n\n/**\n * 解析用户原始输入。\n *\n * @param raw 用户输入的原始文本(可能含前导/尾随空格)。\n * @returns CommandMatch(是指令)或 NotCommand(非指令)。\n */\nexport function parseCommand(raw: string): ParseResult {\n const text = raw.trim()\n\n if (!text.startsWith('/')) return { isCommand: false, text }\n\n if (text === '/') return { isCommand: false, text }\n if (text.startsWith('//')) return { isCommand: false, text }\n\n const rest = text.slice(1)\n const { tokens } = tokenize(rest)\n\n if (tokens.length === 0) return { isCommand: false, text }\n\n const [command, ...args] = tokens\n\n return { isCommand: true, command: command!, args }\n}\n","/**\n * interactive-commands.ts — TUI 内置命令注册(全部从单一命令目录 SLASH_COMMANDS 派生)。\n *\n * RFC-074 D2:命令名真相只有一处(command-dispatcher 的 SLASH_COMMANDS)。本文件不再手抄\n * 命令名集合/注册表,只从该目录投影:安全命令白名单、engine-builtin 集、TUI 注册。\n */\n\nimport type { CommandRegistry } from '@x-otto/tui'\nimport { SLASH_COMMANDS, SAFE_COMMAND_NAMES, ENGINE_BUILTIN_COMMANDS } from '../command-dispatcher'\n\n// 投影集从 command-dispatcher 派生;此处 re-export 保持既有 import 站点不变(单源不双声明)。\nexport { SAFE_COMMAND_NAMES, ENGINE_BUILTIN_COMMANDS }\n\n/**\n * `pin` 指令的动态展示名解析(RFC-158 D5):唯一真相源,`buildSlashCommands`(typeahead\n * 面板)与 `/help` 渲染(interactive.ts)两处独立对接方式必须都调用它,保证展示一致——\n * 当前会话已置顶时 `/pin` 展示为 `/unpin`,其余指令名原样返回。\n */\nexport function resolvePinCommandLabel(name: string, currentSessionPinned: boolean): string {\n return name === 'pin' && currentSessionPinned ? 'unpin' : name\n}\n\n/**\n * `/pause` 指令的动态展示名解析(RFC-186 D5,同构 resolvePinCommandLabel)。\n * 暂停中或等待暂停时,`/pause` 展示为 `/continue`;其余指令名原样返回。\n */\nexport function resolvePauseCommandLabel(name: string, isPausedOrPending: boolean): string {\n return name === 'pause' && isPausedOrPending ? 'continue' : name\n}\n\n/**\n * `/help` 面板正文行组装(RFC-148 M6 续推,2026-07-21:从 interactive.ts onSubmit 的\n * `result.type === 'help'` 分支精确切分——纯展示逻辑,无自引用闭包/晚绑定依赖,onSubmit\n * 主体(handleSubmit 自引用 + retryMode/errorRetryAttempt 三态耦合)本身不在本次切分范围)。\n *\n * RFC-158 D5:`/help` 与 typeahead 面板走不同渲染路径(本函数直接迭代 `reg.list()`,不经过\n * `buildSlashCommands`),但共享同一个 `resolvePinCommandLabel` 判断函数,保证 `/pin` 指令在\n * 两处的展示名一致(当前会话已置顶时都显示 `/unpin`)。排序仍用原始 `c.name`(与 typeahead 的\n * sortKey 语义一致:展示名变化不影响列表顺序)。\n */\nexport function buildHelpLines(\n reg: CommandRegistry,\n currentSessionPinned: boolean,\n cmdDesc: Record<string, string>,\n): string[] {\n const cmds = reg.list().sort((a, b) => a.name.localeCompare(b.name))\n const names = cmds.map((c) => {\n const displayName = resolvePinCommandLabel(c.name, currentSessionPinned)\n const aliases = c.aliases?.length ? ` (${c.aliases.join(', ')})` : ''\n return ` /${displayName}${aliases}`\n })\n const maxNameLen = Math.max(...names.map((n) => n.length))\n const lines: string[] = []\n for (let i = 0; i < cmds.length; i++) {\n const c = cmds[i]!\n const displayName = resolvePinCommandLabel(c.name, currentSessionPinned)\n const desc = cmdDesc[displayName] ?? cmdDesc[c.name] ?? c.description\n lines.push(`${names[i]!.padEnd(maxNameLen)} — ${desc}`)\n }\n lines.push('', 'Prefixes: !<cmd> run shell | @<file> read file')\n return lines\n}\n\nexport function buildSlashCommands(\n reg: CommandRegistry,\n agentProcessing: boolean,\n safeCommandNames: ReadonlySet<string>,\n cmdDesc?: Record<string, string>,\n currentSessionPinned = false,\n isPausedOrPending = false,\n): Array<{ name: string; description?: string; disabled: boolean; sortKey?: string }> {\n return reg.list()\n .filter(c => {\n // RFC-186:闲时(无运行、未暂停)隐藏 /pause /continue——挂着一条点下去永远弹 toast\n // 的指令是死 UI。仅在运行中(展示 /pause)或暂停中(展示 /continue)时显示。\n if (c.name === 'pause') return agentProcessing || isPausedOrPending\n return true\n })\n .map(c => {\n const displayName = resolvePauseCommandLabel(resolvePinCommandLabel(c.name, currentSessionPinned), isPausedOrPending)\n return {\n name: displayName,\n description: cmdDesc?.[displayName] ?? cmdDesc?.[c.name] ?? c.description,\n disabled: agentProcessing && !safeCommandNames.has(c.name) && c.name !== 'abort',\n // 排序键固定用原始指令 id(如 'pin'),与展示名('unpin')解耦——避免 pin→unpin 的\n // 字母序变化导致面板列表位置跳动(RFC-158 D5,CommandEntry.sortKey)。\n sortKey: c.name,\n }\n })\n}\n\n/**\n * 注册内置 TUI 命令到 CommandRegistry——从 SLASH_COMMANDS 目录派生(RFC-074 D2)。\n *\n * 注册条目 = 目录中 `!tuiHidden`(排除 exit/abort/copy,由 TUI 核解析)且通过 `envGate` 的项;\n * 名称、aliases、英文兜底描述全取自目录条目(execute 为 no-op,真实执行走 dispatchSlashCommand)。\n */\nexport function registerBuiltinTuiCommands(reg: CommandRegistry): void {\n for (const cmd of SLASH_COMMANDS) {\n if (cmd.tuiHidden) continue\n if (cmd.envGate && process.env[cmd.envGate] !== '1') continue\n if (reg.has(cmd.name)) reg.unregister(cmd.name)\n reg.register({\n name: cmd.name,\n description: cmd.description ?? cmd.name,\n aliases: cmd.aliases,\n type: 'action',\n source: 'builtin',\n execute: () => '',\n })\n }\n}\n","/**\n * live-token-estimate.ts — 流式输出的**节流** token 估算(状态栏上下文实时占用用)。\n *\n * 背景:`text_delta` 是 100+/s 的高频事件。逐 delta 调用 `estimateTokens`(tiktoken +\n * 病理输入护栏,见 `@x-otto/memory` token-estimator.ts)会把 BPE 编码放进流式热路径,\n * 是已知的 TUI 卡顿来源;但一直用 `chars/4` 又对中文/代码/JSON 偏差很大(CJK 实际\n * 接近 1 token/字),导致状态栏 live 占用与回合结算值明显不一致。\n *\n * 折中口径:**低频精算 + 高频线性外推**——每 `intervalMs` 用真实 estimator 精算一次\n * 累计文本,两次精算之间按最近一次实测的「每字符 token 比率」线性外推新增字符。\n * 比率来自真实文本而非常数,故中文/代码场景也能贴近实际,而 CPU 成本与 delta 频率解耦。\n */\n\nexport interface LiveTokenEstimatorOptions {\n /** 真实 token 估算器(默认由调用方注入 `@x-otto/memory` 的 estimateTokens)。 */\n estimate: (text: string) => number\n /** 精算最小间隔(毫秒)。默认 250ms。 */\n intervalMs?: number\n /** 时间源(可注入便于确定性测试)。 */\n now?: () => number\n}\n\n/** 尚无实测比率时的保守回退(对齐历史 `chars/4` 口径)。 */\nconst FALLBACK_TOKENS_PER_CHAR = 0.25\n\nexport class LiveTokenEstimator {\n private readonly estimate: (text: string) => number\n private readonly intervalMs: number\n private readonly now: () => number\n\n private text = ''\n /** 最近一次精算的结果与其覆盖的字符数。 */\n private measuredTokens = 0\n private measuredChars = 0\n private measuredAt = -Infinity\n\n constructor(options: LiveTokenEstimatorOptions) {\n this.estimate = options.estimate\n this.intervalMs = options.intervalMs ?? 250\n this.now = options.now ?? (() => Date.now())\n }\n\n /** 清空累计文本与实测基线(回合边界调用)。 */\n reset(): void {\n this.text = ''\n this.measuredTokens = 0\n this.measuredChars = 0\n this.measuredAt = -Infinity\n }\n\n /** 追加一段流式增量;必要时触发一次精算(受 intervalMs 节流)。 */\n push(delta: string): void {\n if (!delta) return\n this.text += delta\n const at = this.now()\n if (at - this.measuredAt >= this.intervalMs) this.measure(at)\n }\n\n /** 强制精算一次(回合边界/需要最准值时调用)。 */\n flush(): void {\n this.measure(this.now())\n }\n\n /** 当前 token 估算:最近一次精算值 + 之后新增字符按实测比率外推。 */\n tokens(): number {\n const pendingChars = this.text.length - this.measuredChars\n if (pendingChars <= 0) return this.measuredTokens\n const ratio =\n this.measuredChars > 0 ? this.measuredTokens / this.measuredChars : FALLBACK_TOKENS_PER_CHAR\n return this.measuredTokens + Math.ceil(pendingChars * ratio)\n }\n\n private measure(at: number): void {\n this.measuredTokens = this.text ? this.estimate(this.text) : 0\n this.measuredChars = this.text.length\n this.measuredAt = at\n }\n}\n","/**\n * interactive-events.ts — session 事件 → TUI 投影 + 后台任务面板接线。\n *\n * `setupInteractiveEvents` 工厂\n * 接收所需 locals(session/tui/app/投影 state + 回合持久化回调)作为依赖,内部持有事件投影\n * 私有状态(redCtx/压缩与裁剪一次性标记),返回已接线的 bgUnsubs + attachSessionHandler +\n * 当前 sessionUnsub 取值器(供 cliCtx facade 读取)。\n */\nimport {\n SANDBOX_ESCALATION_MARKER,\n globalProcessRegistry,\n globalAgentJobRegistry,\n globalAgentObservability,\n resolveSessionTitle,\n} from '@x-otto/coding'\n// RFC-161 M3a:STR 改从 cli i18n 层导入;其余渲染类符号属 M3b 处置范围,暂保留。\nimport { observationRow, observationLiveFields, COMPACTING_SENTINEL, isUsingOverage } from '@x-otto/tui'\nimport type { ChatMessage, PermissionDecision } from '@x-otto/tui'\nimport type { GrillAnswer, GrillQuestion } from '@x-otto/interchange'\nimport { i18next } from '@x-otto/tui'\nimport {\n projectSessionEvent,\n resolveChoiceAnswer,\n resolveTextAnswer,\n sandboxBypassCommand,\n type ProjectCtx,\n} from '@x-otto/session-contract'\nimport { shouldShowPulseSurvey, type PulseSurveyTriggerState } from '../pulse-survey-trigger'\nimport type { PulseSurveyRating } from '@x-otto/coding'\nimport { SDK_VERSION } from '@x-otto/env'\nimport { reduceToTui, buildTurnSummary } from '../session-contract/reduce'\nimport { recordPauseState } from './model-effort-record'\nimport { estimateTokens } from '../token-estimation'\nimport { LiveTokenEstimator } from '../live-token-estimate'\n\n/** TUI 实例类型(startTui 返回值)——与 interactive-model.ts 同款写法。 */\ntype TuiInstance = Awaited<ReturnType<typeof import('@x-otto/tui').startTui>>\n\n/** bash 类工具的 approval input:提取命令文本;非 bash 类仍返回 JSON。 */\nfunction extractCommandInput(toolName: string, args: Record<string, unknown> | undefined): string {\n if (/^(bash|shell|run_command|execute|exec)$/i.test(toolName)) {\n const cmd = (args as { command?: string } | undefined)?.command\n if (cmd) return cmd\n }\n return JSON.stringify(args ?? {}).slice(0, 300)\n}\n\n\nexport async function renderGrillQuestion(tui: TuiInstance, q: GrillQuestion): Promise<GrillAnswer> {\n const ctx = [\n `● ${q.header} — ${q.askedBy} asks:`,\n q.background ? `\\n${q.background}` : '',\n i18next.t('cmd.grillRecommendation', { rec: q.recommendation }),\n q.evidence?.length ? i18next.t('cmd.grillEvidence', { joined: q.evidence.map(e => ` · ${e}`).join('\\n') }) : '',\n ].join('')\n tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text', text: ctx }] })\n\n const options = q.options ?? []\n if (options.length > 0) {\n if (options.length <= 2 && !q.multiSelect && q.allowFreeform === false) {\n const picked = await new Promise<boolean>((resolve) => {\n tui.modal.openModal({\n kind: 'confirm',\n title: q.question,\n message: options.map(o => `${o.label}${o.recommended ? ' (recommended)' : ''}: ${o.rationale}`).join('\\n'),\n confirmLabel: options[0]?.label ?? i18next.t('cmd.grillOptionYes'),\n cancelLabel: options[1]?.label ?? i18next.t('cmd.grillOptionNo'),\n onConfirm: () => resolve(true),\n onCancel: () => resolve(false),\n })\n })\n const chosen = picked ? options[0]?.label : options[1]?.label\n const selected = chosen ? [chosen] : []\n return resolveChoiceAnswer(q, selected)\n }\n\n const result = await tui.modal.openModal({\n kind: 'questions',\n title: q.question,\n questions: [{\n header: q.header,\n question: q.question,\n multiSelect: !!q.multiSelect,\n // grill_me 统一附带自由文本框:列出的选项都不合适时,真人可直接录入自己的选项。\n // 仅当 agent 显式 allowFreeform:false 时关闭。\n freeformInput: q.allowFreeform !== false,\n freeformPlaceholder: i18next.t('cmd.grillFreeformPlaceholder'),\n options: options.map(o => ({\n label: o.recommended ? `${o.label} ★` : o.label,\n description: o.rationale,\n })),\n }],\n })\n const picked = result?.answers?.[q.header] ?? []\n const selected = picked.map(l => l.replace(/\\s*★$/, ''))\n return resolveChoiceAnswer(q, selected, result?.freeformText)\n }\n\n // 问题正文放 description(独立内容栏,常驻不消失),placeholder 只承担通用\n // 输入提示——避免此前把整段问题文本塞进 placeholder,一开始打字问题就消失、\n // 且视觉上易与「二选一确认」弹窗混淆的问题。\n const text = await tui.modal.openModal({\n kind: 'input',\n title: q.header,\n description: [q.question],\n placeholder: i18next.t('cmd.grillAnswerPlaceholder'),\n })\n return resolveTextAnswer(q, text)\n}\n\nexport interface InteractiveEventsDeps {\n /**\n * B6:取**当前** session 的 live 取值器(非按值捕获)。/resume 切会话后 interactive.ts 的\n * `let session` 被重指,attachSessionHandler 重新 subscribe 时必须订到新会话——按值捕获会冻结到旧会话,\n * 致切换后无流式/工具/任务渲染、占用条与面板写错会话。\n */\n getSession: () => import('@x-otto/coding').AgentSession\n app: import('@x-otto/coding').App\n tui: TuiInstance\n projState: ReturnType<typeof import('@x-otto/session-contract').createProjectorState>\n redState: ReturnType<typeof import('../session-contract/reduce').createReducerState>\n recomputeBudget: (livePendingTokens?: number, compactionAdjustment?: { before: number; after: number }) => void\n persistEditedFiles: () => void\n persistSubagents: () => void\n persistTurnCount: () => void\n /** RFC-156:压缩完成后调用,清空上下文占用提醒的区间计数。 */\n onContextCompacted: () => void\n /** 收到 text_delta 时置位(interactive.ts 的 streamedText 去重标记)。 */\n markStreamed: () => void\n}\n\nexport interface InteractiveEvents {\n /** 后台进程/作业/观测订阅句柄(runInteractiveCommand finally 中 clearBgSubs 消费)。 */\n bgUnsubs: Array<() => void>\n /** (重新)挂载 session.subscribe 事件处理器。 */\n attachSessionHandler: () => void\n /** 当前 sessionUnsub(供 cliCtx facade 的 `() => sessionUnsub` 读取)。 */\n getSessionUnsub: () => (() => void) | undefined\n /**\n * 最近一次已确认展示(authSource==='oauth' 且非 rejected)的供应商 usage 快照——\n * /usage 命令的数据源(见 handlers/usage.ts)。null=本会话尚无快照或非 OAuth 订阅计划。\n */\n getLastProviderUsage: () => import('@x-otto/interchange').ProviderUsageSnapshot | null\n /**\n * 2026-07-14 修复:当前会话是否处于写租约只读降级态(供 /status 面板展示,见\n * handlers/inspect-context.ts)。true=最近一次收到 session.write-lease-denied 且尚未\n * 收到对称的 session.write-lease-restored;false=正常/已恢复。\n */\n isWriteLeaseReadOnly: () => boolean\n /**\n * RFC-180 D7:当前会话是否处于 turn 边界暂停态(供 /status 面板展示)。镜像最近一次\n * run.paused/run.resumed 事件方向,与 isWriteLeaseReadOnly 同款模式。会话本身的真源\n * 是 AgentSession.isPaused(interactive.ts 已注入 isPaused probe 给 startTui),本\n * 字段是 interactive-events 侧独立维护的镜像,用于 /status 命令展示(不依赖 TUI\n * 内部 store,保持该模块跨 tui/headless 场景通用)。\n */\n isRunPaused: () => boolean\n}\n\nexport function setupInteractiveEvents(deps: InteractiveEventsDeps): InteractiveEvents {\n const {\n getSession,\n app,\n tui,\n projState,\n redState,\n recomputeBudget,\n persistEditedFiles,\n persistSubagents,\n persistTurnCount,\n onContextCompacted,\n markStreamed,\n } = deps\n\n // /usage 命令数据源——与 tui.setProviderUsage 的两个调用点同步更新(见下方 done 事件处理),\n // 保存的是\"已确认展示\"的快照(authSource==='oauth' 且非 rejected),与状态栏徽标语义一致。\n let lastProviderUsage: import('@x-otto/interchange').ProviderUsageSnapshot | null = null\n // 2026-07-14 修复:当前会话写租约只读态(/status 面板数据源)。随 denied/restored\n // 事件翻转——事件本身已在 @x-otto/runtime persistence-sync 侧做状态转换去重(只在真实\n // 发生转换时才 publish),此处只需镜像最近一次收到的事件方向。\n let writeLeaseReadOnly = false\n // RFC-180 D7:暂停态镜像(/status 面板与状态栏数据源)。随 run.paused/run.resumed\n // 事件翻转,与 writeLeaseReadOnly 同款\"镜像最近一次事件方向\"模式。\n let runPausedState = false\n // RFC-186 D5:暂停等待态追踪——pauseRun 请求后到 turn 边界生效前的工具计数与等待秒数。\n let pauseRequestedTracker: { at: number; toolCount: number } | null = null\n // 状态栏倒计时定时器句柄\n let pausePendingTimer: ReturnType<typeof setInterval> | null = null\n\n // 流程暂停动作审计(终局 2026-07-31):run.pause_requested/paused/cancelled/resumed\n // 此前只走 EventBus→TUI 浮层,零 trace、零持久化——重启后「暂停过/恢复过」不可审计。\n // 事件驱动单点记录:所有触发入口(/pause、Esc 弹窗、/continue、提交隐式恢复)最终都\n // publish 同一组事件,在此统一落 trace(含 turn 号)+ L1 system 条目(resume 后转录\n // 可见;role:'system' 被 defaultConvertToLLM 过滤,不进 LLM 上下文)。\n const recordPauseTrace = (state: 'requested' | 'paused' | 'cancelled' | 'resumed'): void => {\n const session = getSession()\n const turn = session.session.getTurnCount()\n recordPauseState(\n {\n traceStore: app.storage.traceStore,\n sessionId: session.id,\n ...(turn !== undefined ? { turn } : {}),\n },\n state,\n 'session-event',\n )\n }\n const appendPauseL1 = (subtype: string): void => {\n getSession().session.append({\n role: 'system',\n subtype,\n content: subtype,\n level: 'info',\n timestamp: Date.now(),\n })\n }\n // RFC-181 D6/M3:视觉委托 toast 会话内去重——同一会话内只提示一次(首次委托发生时),\n // 避免多轮反复触发委托时刷屏(区别于 session.history-capped 的\"每次都是新信息\"语义,\n // 委托能力本身是会话级恒定事实,重复提示无增量价值)。\n let visionDelegateToastShown = false\n\n // 残影修复(2026-07):此处此前有 lastToolResultText()——从 session.messages() 倒查\n // \"最后一条 tool_result\"取文本,完全不按 call.id 匹配。工具结果要等整个 prompt 结束才\n // 批量落树(persistence.persistNew),回合中期读到的永远是上一轮遗留的旧结果;并行/\n // 异步工具场景下每个工具行都会显示同一条陈旧文本(残影事故根因)。现在 resultText 由\n // AgentSession.onToolCallEnd 在产生处按 call 精确构造、随 tool.call.end 事件下发\n // (project.ts 优先消费 event.resultText),此处不再需要猜测式回退。\n\n // Per-turn token delta tracker: snapshot total at turn start, return delta at turn end.\n let lastTokenTotal: number | undefined\n \n const redCtx: ProjectCtx = {\n currentModel: () => getSession().model.id,\n cacheHitRatio: () => getSession().getLastCacheHitRatio(),\n turnTokens: () => {\n const msgs = getSession().session.messages() as ReadonlyArray<{\n role?: string\n usage?: {\n inputTokens?: number\n outputTokens?: number\n cacheReadTokens?: number\n cacheWriteTokens?: number\n }\n }>\n let cur = 0\n for (const m of msgs) {\n if (m.role === 'assistant' && m.usage) {\n // 此前只累加 inputTokens+outputTokens,遗漏 cacheRead/cacheWrite——高命中率会话下\n // 真实 input 大头计入 cache_read,累计增量常年非正,导致本回合 tokens(连带\n // cacheHitRatio 显示,见 message-lines.ts/transcript-text.ts 的解耦修复)被误隐藏。\n cur +=\n (m.usage.inputTokens ?? 0) +\n (m.usage.outputTokens ?? 0) +\n (m.usage.cacheReadTokens ?? 0) +\n (m.usage.cacheWriteTokens ?? 0)\n }\n }\n const prev = lastTokenTotal\n lastTokenTotal = cur\n if (prev == null || cur <= prev) return undefined\n return cur - prev\n },\n }\n let lastCompactBefore: number | undefined\n // 裁剪本会话首次才提示——prune 每轮可触发,只在转录里标记一次「裁剪已启用」,避免刷屏。\n let pruneNoticeShown = false\n // RFC-097:本轮已输出字符数累加器(text_delta 驱动,turn.start 时重置)。\n let outputCharsThisTurn = 0\n // 状态栏上下文 live 预算:当前回合尚未被 provider usage 权威值覆盖的本地预估 token。\n // prompt/tool result 在 session.messages() 与 usage 落盘前就会真实占用下一轮上下文,必须随\n // agent 工作实时推动占用条,而不是等 turn.end/prompt.end 才跳变。\n let promptTokensThisTurn = 0\n let toolResultTokensThisTurn = 0\n // 流式输出估算走节流精算器(逐 delta 调 estimateTokens 会把 BPE 放进热路径,见\n // live-token-estimate.ts 头注释;纯 chars/4 又对中文/代码严重偏低)。\n const streamTokenEstimator = new LiveTokenEstimator({ estimate: estimateTokens })\n const resetLiveEstimates = (): void => {\n promptTokensThisTurn = 0\n toolResultTokensThisTurn = 0\n outputCharsThisTurn = 0\n streamTokenEstimator.reset()\n }\n const recomputeLiveBudget = (): void => {\n recomputeBudget(promptTokensThisTurn + toolResultTokensThisTurn + streamTokenEstimator.tokens())\n }\n\n // ContextPanel 回合跟踪:prompt.start 时记录起始消息索引,prompt.end 时据此计算本回合消息数。\n let turnStartMsgIdx = 0\n\n // RFC-208:满意度脉冲调查触发状态(进程内存态,不持久化跨会话——见 pulse-survey-trigger.ts 头注释)。\n const pulseSurveyTriggerState: PulseSurveyTriggerState = {}\n // RFC-209 M2:数据落地不再直接实例化 sink——经 app.feedbackRegistry 间接调用插件实现\n // (核心包禁止 import 插件代码,见 RFC-209 重要事项规则1)。插件未装载时 get() 返回\n // undefined,可选链静默跳过(RFC-209 §2.6 降级行为)。\n\n const shortStat = (diff: string): string => {\n let a = 0, r = 0\n for (const l of diff.split('\\n')) {\n if (l.startsWith('+') && !l.startsWith('+++')) a++\n else if (l.startsWith('-') && !l.startsWith('---')) r++\n }\n return `+${a}/-${r}`\n }\n const runBackgroundTasks = (): void => {\n const procs = globalProcessRegistry.listForSession(getSession().id).map((p) => ({\n id: p.id,\n command: p.command,\n type: 'process' as const,\n origin: 'main' as const,\n status:\n p.status === 'running' ? ('running' as const) : p.status === 'exited' ? ('exited' as const) : ('killed' as const),\n port: p.port,\n startedAt: p.startedAt,\n exitCode: p.exitCode,\n // RFC-223:运行时输出识别到的服务信息(url/port/protocol),供状态栏「服务」指示器消费。\n detectedService: p.detectedService,\n }))\n const obsById = new Map(globalAgentObservability.list().map((o) => [o.id, o]))\n const liveOf = (id: string) => {\n const o = obsById.get(id)\n return o ? observationLiveFields(o) : {}\n }\n const jobs = globalAgentJobRegistry.listForSession(getSession().id).map((j) => ({\n id: j.id,\n command: j.title,\n type: 'agent-session' as const,\n origin: j.origin,\n status:\n j.status === 'running' || j.status === 'ready' || j.status === 'needs_input'\n ? ('running' as const)\n : j.status === 'applied'\n ? ('exited' as const)\n : j.status === 'failed'\n ? ('failed' as const)\n : ('killed' as const),\n agentStatus: j.status,\n diffStat: j.diff ? shortStat(j.diff) : undefined,\n startedAt: j.startedAt,\n ...liveOf(j.id),\n }))\n const jobIds = new Set(jobs.map((j) => j.id))\n const swarmRows = globalAgentObservability\n .list()\n .filter((o) => !jobIds.has(o.id))\n .map(observationRow)\n const all = [...procs, ...jobs, ...swarmRows]\n // 分组顺序:人手委派(user)在前,非人手(main = 模型自主,plugin = 插件经\n // agent.dispatch 自主发起,RFC-287 D2)在后。用\"取反\"而非枚举 main——\n // 枚举写法会让未来新增的 origin 取值静默从面板消失(本行曾是 origin === 'user' 与\n // === 'main' 两段拼接,JobOrigin 扩到三值时 plugin 作业就会看不见)。\n tui.stores.tasks.setBackgroundTasks([\n ...all.filter((t) => t.origin === 'user'),\n ...all.filter((t) => t.origin !== 'user'),\n ])\n }\n\n // RFC-089 M2: N 个 registry 订阅高频触发时,帧内只跑一次重建(CPU + 分配)\n const COALESCE_MS = 30\n let bgCoalesceTimer: ReturnType<typeof setTimeout> | null = null\n\n const pushBackgroundTasks = (): void => {\n if (bgCoalesceTimer !== null) return // 已有挂起的合帧\n bgCoalesceTimer = setTimeout(() => {\n bgCoalesceTimer = null\n runBackgroundTasks()\n }, COALESCE_MS)\n }\n\n const flushBackgroundTasks = (): void => {\n if (bgCoalesceTimer !== null) {\n clearTimeout(bgCoalesceTimer)\n bgCoalesceTimer = null\n }\n runBackgroundTasks()\n }\n\n const bgUnsubs: Array<() => void> = [\n globalProcessRegistry.onSpawn(pushBackgroundTasks),\n globalProcessRegistry.onExit(flushBackgroundTasks),\n globalAgentJobRegistry.onSpawn(pushBackgroundTasks),\n globalAgentJobRegistry.onUpdate(pushBackgroundTasks),\n globalAgentJobRegistry.onExit(flushBackgroundTasks),\n globalAgentJobRegistry.onRemove(flushBackgroundTasks),\n globalAgentObservability.subscribe(pushBackgroundTasks),\n ]\n runBackgroundTasks()\n\n let sessionUnsub: (() => void) | undefined\n const attachSessionHandler = () => {\n sessionUnsub?.()\n // 2026-07-14 修复:切会话(/resume)时重置写租约只读镜像——它反映的是\"当前订阅的\n // 这一个会话\"的最近状态,不能带着旧会话的只读态污染新会话(新会话大概率并未被\n // 拒绝过写租约)。真实状态会在新会话首次 denied/restored 事件到达时被正确设置。\n writeLeaseReadOnly = false\n sessionUnsub = getSession().subscribe((event) => {\n for (const se of projectSessionEvent(event, projState, redCtx)) {\n // 回合完成摘要(TUI 底部\"✓ 完成 · N 个工具 · 耗时 · tokens · 模型\"行)持久化——\n // reduceToTui 之前算好摘要对象(buildTurnSummary 与其内部渲染路径共用同一份构造\n // 逻辑,见 reduce.ts 头注释),追加进 session 的 turnSummaries 面板态(RFC-108 D3\n // 同款独立存储,不进 session.messages()/发给模型的真实历史,见 TurnSummaryEntry\n // 头注释)。turn 用 projState.currentTurn(prompt.start 已自增,此刻正是刚完成的\n // 回合号)。同一 turn 覆盖写(last-write-wins)而非盲 append:/exit -r / 重连订阅等\n // 异常路径可能让同一 prompt.end 被处理两次,若追加会在 resume 后插回两个\"✓ 完成\"。\n if (se.kind === 'prompt.end') {\n const summary = buildTurnSummary(se, redState)\n const currentTurnSummary = { turn: projState.currentTurn, completedAt: Date.now(), ...summary }\n const existing = getSession().session.getTurnSummaries() ?? []\n getSession().session.setTurnSummaries([\n ...existing.filter((x) => x.turn !== currentTurnSummary.turn),\n currentTurnSummary,\n ])\n }\n // RFC-333 M2-B:setTasks/addTurnBoundary 已从 TuiHandle 迁到 store action,\n // 但 TuiSink 契约仍以这两个方法名表达(reduce.ts 的领域语义)。用薄 adapter 把\n // sink 的这两个成员路由到 tui.stores.tasks,其余成员(pushMessage/pushEngineEvent\n // /echoUserInput/getNotifyStats)仍走 tui——sink 不直接依赖 store shape。\n reduceToTui(\n se,\n {\n pushMessage: tui.messages.pushMessage,\n echoUserInput: tui.messages.echoUserInput,\n pushEngineEvent: tui.engine.pushEngineEvent,\n getNotifyStats: tui.info.getNotifyStats,\n setTasks: (tasks, title) => tui.stores.tasks.setTasks(tasks, title),\n addTurnBoundary: (messageIndex) => tui.stores.tasks.addTurnBoundary(messageIndex),\n },\n redState,\n )\n // write_todos 更新后立即写回会话 metadata + write-through 落盘(RFC-295 M1)。\n // 此前只写内存、等下次 prompt.end/turn.end 节流落盘(30s/5turn 窗口)——进程中途\n // 崩溃则本次 todo 更新永久丢失。改为复刻 draft 先例(interactive-tui-callbacks.ts):\n // setter 后立即 sessionManager.save() 落盘,fail-soft 吞错(含写租约拒写,与 draft\n // 语义一致,不制造\"主快照旧+面板态新\"的新不一致)。save 经 enqueue 串行链、doSave\n // 执行时读 live getter,无并发旧覆新竞态(RFC-295 §3 D2)。\n if (se.kind === 'tasks.update') {\n getSession().session.setTodoList(projState.todoList)\n void app.sessionManager.save(getSession().id).catch(() => {})\n }\n }\n if (event.type === 'stream.event' && (event as { event?: { type?: string } }).event?.type === 'text_delta') {\n markStreamed()\n // RFC-097:累加本轮输出 token 估算(字符数/4,与 streaming-context.ts 的既有估算惯例一致),\n // 驱动 progressPercent(responding 阶段的输出上限占比)。\n const delta = (event as { event?: { delta?: string } }).event?.delta ?? ''\n outputCharsThisTurn += delta.length\n streamTokenEstimator.push(delta)\n tui.engine.pushEngineEvent({ kind: 'outputTokensThisTurn', n: Math.floor(outputCharsThisTurn / 4) })\n // 每 delta 刷新上下文占用条(prompt + tool result + 流式输出预估 token),进度条随输出实时右移。\n recomputeLiveBudget()\n }\n if (event.type === 'stream.event' && (event as { event?: { type?: string } }).event?.type === 'done') {\n // 状态栏「供应商 usage」徽标——仅 Anthropic OAuth 订阅计划展示。API key 走标准 RPM/TPM\n // 不带 unified 头族,providerUsage 会是 null(provider 层已判定,见 @x-otto/ai\n // extractProviderUsageFromHeaders);此处再叠加 authSource 判断做双重保险(防止未来\n // 其它 provider 误填充同名字段)。status='rejected' 时不展示徽标——让位给\n // showRateLimitCountdown 覆盖层,避免状态栏和覆盖层同时展示重复信息。\n const providerUsage = (\n event as { event?: { providerUsage?: import('@x-otto/interchange').ProviderUsageSnapshot | null } }\n ).event?.providerUsage\n // undefined(非 Anthropic provider,字段未携带)→ 不改动状态栏现状,保留上一次的徽标。\n if (providerUsage !== undefined) {\n // status='rejected' 时默认不展示徽标(让位给 RateLimitCountdown 覆盖层),\n // 但如果 overage 可用(status=rejected + overage=allowed/warning,见 @x-otto/tui\n // isUsingOverage),用户仍在通过额外用量池正常使用——此时保持徽标展示 +overage 标识,\n // 不也不触发 RateLimitCountdown(因为\"有 overage = 不会真正被阻断\")。\n const hasOverage =\n providerUsage !== null &&\n isUsingOverage(providerUsage.status, providerUsage.overageStatus)\n if (providerUsage === null || (providerUsage.status === 'rejected' && !hasOverage)) {\n tui.stores.notification.setProviderUsage(null)\n lastProviderUsage = null\n } else {\n // 捕获本次 done 事件对应的 provider——getAuthSource 是异步调用,resolve 前用户可能\n // 已 /model 切换到另一 provider;若不核对,会把新 provider 的徽标误标成本次\n // (旧 provider)的 authSource 判断结果。resolve 后用当前 provider 复核,不一致则丢弃\n // (不写入陈旧数据,宁可本次不刷新也不显示错误来源的徽标)。\n const providerAtEmit = getSession().model.provider\n void app.authStore\n .getAuthSource(providerAtEmit)\n .then((source) => {\n if (getSession().model.provider !== providerAtEmit) return\n const usage = source === 'oauth' ? providerUsage : null\n tui.stores.notification.setProviderUsage(usage)\n lastProviderUsage = usage\n })\n // review P1:getAuthSource 可能抛错(store 损坏/IO 异常)——不 catch 会产生\n // UnhandledPromiseRejection 且状态栏/usage 命令永久卡在旧快照。静默吞错,与\n // usage.ts:28 的 `.catch(() => null)` 兜底设计一致(失效不阻塞主流程)。\n .catch(() => {})\n }\n }\n }\n switch (event.type) {\n case 'turn.start':\n // RFC-097:turn 真正发起 LLM 请求(runTurn 调用前 emit)——requesting 阶段起点,\n // 对应 SpinnerMode 'requesting'(↑ 箭头 + 快速 shimmer,等待首字节)。\n // 只清输出增量:prompt/tool result 仍是当前 prompt 的 live 上下文占用,直到 turn.end\n // 用 provider usage 权威值校准。\n outputCharsThisTurn = 0\n streamTokenEstimator.reset()\n recomputeLiveBudget()\n tui.engine.pushEngineEvent({ kind: 'spinnerMode', mode: 'requesting' })\n break\n case 'turn.end':\n // 多轮工具调用的长回合(一次 prompt.end 前可能有 N 个 turn.end):assistantMessage.usage\n // 在 turn.end 时已写入会话消息,此时刷新占用条——避免占用条在整个提交跑完前\"冻结\",\n // 只在最终 prompt.end 才跳变一次给用户\"假死\"感(占用条应渐进反映真实消耗)。\n resetLiveEstimates()\n recomputeBudget()\n break\n case 'streaming.start':\n // RFC-097:provider 收到 SDK 首个真实事件(message_start)后才 yield type:'start',\n // 经 agent-session 透出为 streaming.start——精确对应\"首字节到达\",切到 responding\n // 阶段(↓ 箭头 + 慢速 shimmer,对应 'responding')。\n tui.engine.pushEngineEvent({ kind: 'spinnerMode', mode: 'responding' })\n // ESC 撤回窗口关闭——模型已接起首字节,pending 消息转正。\n tui.pending.clearPendingEcho()\n break\n case 'streaming.end':\n // RFC-097:本次流式输出结束——清除阶段箭头(后续若有工具调用循环,走 activeTool\n // 自己的视觉通道,不复用 spinner mode;下一轮 turn.start 会重新置为 requesting)。\n tui.engine.pushEngineEvent({ kind: 'spinnerMode', mode: null })\n // 兜底清 pending——若 streaming.start 未能触发(provider 不 emit),回合结束关闭窗口。\n tui.pending.clearPendingEcho()\n break\n case 'prompt.start':\n resetLiveEstimates()\n promptTokensThisTurn = estimateTokens(event.text)\n recomputeLiveBudget()\n // 首次 prompt 开始时把路由从 Home 切到会话视图(此后不再显示 Home)。\n tui.info.setTopView({ session: getSession().id })\n // editedFiles/subagents 是\n // 「本回合活动记录」——新回合开始清上一回合。持久化语义随之变为「最后回合」:\n // 每回合 persist 当前回合、--continue 恢复显示最后完成回合、下回合即清。\n // Tasks 不在此清:它累积直到【全完成】,由提交处全完成清理(interactive.ts 上方\n // `todoList.every(done/skipped/failed)` 分支)——这是既有行为,符合 todo 跨回合本质。\n tui.panes.clearEditedFiles()\n tui.plugins.clearSubagents()\n // ContextPanel:记录本回合起始消息索引(prompt.end 时据此计算消息数 + 追加回合行)。\n turnStartMsgIdx = redState.messageIndex\n // 在回合开始时预添加一行(消息数 0、tokens 0),提示\"此回合进行中\";\n // prompt.end 时通过 markContextTurns 回填真实数据。\n tui.info.addContextTurn({\n turnIndex: projState.currentTurn,\n role: 'assistant',\n messageCount: 0,\n estimatedTokens: 0,\n compacted: false,\n truncated: false,\n pruned: false,\n })\n break\n case 'compaction.start':\n lastCompactBefore = event.messageCount\n // RFC-097 M2:压缩期间零反馈是真实缺口(子agent评审F10)——复用 activeTool 状态机,\n // 哨兵值驱动 Spinner 显示\"压缩中\"。压缩与工具调用时序互斥(context-pipeline.ts 的\n // applyMemoryTransform 在 turn.start 之前跑,见 M2 前置检查),不会与真实 activeTool 冲突。\n tui.engine.pushEngineEvent({ kind: 'activeTool', name: COMPACTING_SENTINEL })\n break\n case 'compaction.end': {\n const before = lastCompactBefore ?? event.retainedCount\n const after = event.retainedCount\n tui.engine.pushEngineEvent({ kind: 'activeTool', name: null })\n if (after < before) {\n // Gap B:推一条对话流内系统通知取代瞬时 toast——自动压缩此前零 UI,现在在\n // 转录里留下可滚动的压缩边界标记(footer 指向 /compact log 查看完整摘要)。\n tui.messages.pushMessage({ role: 'compact_boundary', before, after })\n // /compact log 详情日志:记完整摘要文本(供事后核实压缩没有丢失关键信息)。\n tui.engine.pushEngineEvent({ kind: 'compactionLog', entryKind: 'compact', before, after, summary: event.summary })\n // RFC-156:真实发生了压缩——清空占用提醒的区间计数,进入新一轮对话生命周期。\n onContextCompacted()\n }\n lastCompactBefore = undefined\n // 压缩后上下文真实缩小,立即刷新占比条——不等下一轮 prompt.end。\n // RFC-227 fix:传 before/after 给 recomputeBudget 按比例下调 API usage 快照\n // (否则 lastIn 来自旧 2000 条数据,setContextBudget 传同一值 → React 不渲染)。\n recomputeBudget(undefined, { before, after })\n break\n }\n case 'compaction.reactive.notice': {\n // 终局 review 补齐(2026-07-21):上下文溢出(413)触发的反应式压缩——与\n // compaction.start/end(token 阈值驱动)是独立触发路径,此前零 UI(用户遇到\n // 上下文溢出被自动挽救时界面悄无声息)。复用同一套 compact_boundary/\n // compactionLog 呈现(无 summary 时 compactionLog 面板显示\"无可用摘要\",\n // 见 compactLogNoSummary 文案,非本次新增缺口)。不复用 COMPACTING_SENTINEL\n // spinner 过渡——反应式压缩是 stream 失败恢复路径的同步分支,很快会进入\n // 下一轮 turn.start 触发新 spinner,不需要额外过渡态。\n const { messagesBefore: before, messagesAfter: after } = event\n if (after < before) {\n tui.messages.pushMessage({ role: 'compact_boundary', before, after })\n tui.engine.pushEngineEvent({\n kind: 'compactionLog',\n entryKind: 'compact',\n before,\n after,\n summary: event.summary,\n })\n onContextCompacted()\n }\n // RFC-227 fix:同 compaction.end——按比例下调 API usage 快照让占比条实时变化。\n recomputeBudget(undefined, { before, after })\n break\n }\n case 'image.degradation.notice':\n // 终局 review 补齐(2026-07-21):图片降级兜底(413/400 image-constraint 触发\n // 丢弃部分历史图片)——此前零 UI(engine-nodes.ts 曾注释\"全仓无代码显式监听\")。\n // 与 image.vision-delegate 同款一次性 toast(非阻断提醒),trigger 区分文案。\n if (event.degradedCount > 0) {\n tui.info.showToast(\n i18next.t('cmd.titleInfo'),\n i18next.t('cmd.imageDegradationNotice', { count: event.degradedCount, context: event.trigger === 'image-constraint' ? 'image-constraint' : 'other' }),\n )\n }\n break\n // 修复(历史裁剪可观测性,2026-07-12;语义澄清 RFC-159 T6,2026-07-13):会话内存态\n // 消息数超上限(capStoredHistory)从内存视图移除最早消息——DB 侧 append-forever,\n // 被移出条目仍保留在存储中。与 memory.pruned 不同,本事件每次触发都代表一次新的、\n // 真实的内存视图收窄(非同一处 token 裁剪的重复提示),故不做\"仅首次\"去重,每次都推标记。\n case 'session.history-capped':\n // RFC-178 D6:透传 trigger 分型(count/bytes/memory-pressure),TUI 据此分型文案。\n // RFC-321 R12:额外透传 lossy——有损裁剪(压缩连续失败后的兜底)意味着这部分\n // 上下文**没有摘要覆盖、模型不再可见**,与常规无损裁剪必须给出不同提示,\n // 否则既有\"存储中仍完整保留\"的措辞会误导用户以为上下文没损失。\n tui.messages.pushMessage({\n role: 'history_capped',\n removedCount: event.removedCount,\n trigger: event.trigger,\n ...(event.lossy ? { lossy: true } : {}),\n })\n tui.engine.pushEngineEvent({ kind: 'compactionLog', entryKind: 'history_capped', removedCount: event.removedCount })\n break\n // RFC-159 D3 + 2026-07-14 修复:另一存活进程持有本会话写租约——本进程改动不会落盘\n // (只读降级)。三层信息呈现(终局 review 确认,避免阻塞 dialog——对话本身不受影响,\n // 只是持久化被拒,阻塞式确认框会打断用户当前仍在正常进行的操作却解决不了任何问题):\n // ① toast 首次详细提示(原因 + 两条可执行解决路径);② 对话流单行持久标记(toast\n // 易错过/瞬时,标记不会消失,用户事后滚动历史能看清具体哪几轮丢失);③ /status 面板\n // 补充详情(inspect-context.ts)。leaseDeniedState 已在 @x-otto/runtime persistence-sync\n // 侧做 denied↔restored 转换去重(每次真实发生状态转换才 publish 一次),此处收到即\n // 代表\"新发生的、值得告知用户\"的转换,不需要再叠加一层\"仅首次\"判断。\n case 'session.write-lease-denied':\n writeLeaseReadOnly = true\n tui.messages.pushMessage({ role: 'write_lease_denied' })\n tui.info.showToast(i18next.t('lease.deniedToastTitle'), i18next.t('lease.deniedToastBody'))\n break\n // 与 denied 对称:写租约恢复可写——对话流留痕(首尾呼应只读区间边界)+ 轻量 toast\n // 确认(用户此前收到的\"只读\"提示需要一个\"已解决\"的对称信号,否则只能自行猜测)。\n case 'session.write-lease-restored':\n writeLeaseReadOnly = false\n tui.messages.pushMessage({ role: 'write_lease_restored' })\n tui.info.showToast(i18next.t('lease.restoredToastTitle'), i18next.t('lease.restoredToastBody'))\n break\n // RFC-305 D3:restore 时 trace↔DB 对账检出上次异常终止的落库缺口——已丢失、\n // 不可恢复(trace 是有损骨架),toast 明确告知丢失规模,用户可据此决定重述。\n case 'session.crash-gap-detected':\n tui.info.showToast(\n i18next.t('crashGap.toastTitle'),\n i18next.t('crashGap.toastBody', { lostTurns: event.lostTurns }),\n )\n break\n // RFC-186 D2/D7:暂停请求已发出(pauseRun())——设置浮层 + 状态栏倒计时。\n // 区别于 run.paused:本事件在请求时发布,run.paused 在 turn 边界生效后发布。\n case 'run.pause_requested':\n runPausedState = true\n pauseRequestedTracker = { at: Date.now(), toolCount: 0 }\n recordPauseTrace('requested')\n tui.engine.setPauseDialog?.({\n phase: 'requested',\n requestedAt: Date.now(),\n toolCount: 0,\n onCancel: () => { /* injected by handler */ },\n onDismiss: () => { tui.engine.setPauseDialog?.(null) },\n })\n // 每秒推送状态栏倒计时\n if (pausePendingTimer) clearInterval(pausePendingTimer)\n pausePendingTimer = setInterval(() => {\n if (!pauseRequestedTracker) { clearInterval(pausePendingTimer!); pausePendingTimer = null; return }\n const elapsed = Math.floor((Date.now() - pauseRequestedTracker.at) / 1000)\n tui.stores.notification.setPausePending({ elapsedSec: elapsed, toolCount: pauseRequestedTracker.toolCount })\n }, 1000)\n break\n case 'tool.call.end': {\n const resultTokens =\n typeof event.resultTokensEstimate === 'number'\n ? event.resultTokensEstimate\n : event.resultText\n ? estimateTokens(event.resultText)\n : 0\n if (resultTokens > 0) {\n toolResultTokensThisTurn += resultTokens\n recomputeLiveBudget()\n }\n // RFC-186 D5:暂停等待期间追踪工具执行计数\n if (pauseRequestedTracker) {\n pauseRequestedTracker.toolCount++\n tui.engine.setPauseDialog?.({\n phase: 'requested',\n requestedAt: pauseRequestedTracker.at,\n toolCount: pauseRequestedTracker.toolCount,\n onCancel: () => { /* injected by handler */ },\n onDismiss: () => { tui.engine.setPauseDialog?.(null) },\n })\n }\n // RFC-210 D5(M4):跨端渲染 toPayload——插件 a2ui.renderer 命中该工具结果时,\n // 把产出的 a2ui 组件树作为独立消息推入转录(renderMessageLines 的既有 a2ui\n // block 渲染路径消费,Ink catalog)。fire-and-forget + fail-soft:无插件/\n // 无命中/超时 → 静默跳过,默认工具结果渲染不受影响。与 service EventBridge\n // 的 maybeSendA2uiRendererPayload 同构(TUI 宿主进程内直调,不走 WS)。\n void (async () => {\n try {\n const payloadMessage = {\n role: 'tool_result',\n name: event.toolCall.name,\n toolCallId: event.toolCall.id,\n content: [{ type: 'text', text: event.resultText ?? '' }],\n // 修复 P0-B1:携带 details 字段,使 a2ui 渲染器 toPayload 可读取工具结果\n // 的结构化数据(如 plugin-deploy-kit 的 deployStatus 对象),而不仅是文本预览。\n details: event.details,\n }\n const components = await app.resolveA2uiRendererPayload(payloadMessage)\n if (components && components.length > 0) {\n tui.messages.pushMessage({\n role: 'assistant',\n content: [{ type: 'a2ui', components }],\n } as Parameters<typeof tui.messages.pushMessage>[0])\n }\n } catch {\n /* fail-soft:payload 管线绝不影响转录主流 */\n }\n })()\n break\n }\n case 'run.paused':\n // RFC-186 D2/D7:暂停已生效——浮层切换为 active + pushMessage\n recordPauseTrace('paused')\n if (pauseRequestedTracker) {\n tui.engine.setPauseDialog?.({\n phase: 'active',\n requestedAt: pauseRequestedTracker.at,\n toolCount: pauseRequestedTracker.toolCount,\n totalToolCount: pauseRequestedTracker.toolCount,\n })\n tui.messages.pushMessage({ role: 'run_paused', toolCount: pauseRequestedTracker.toolCount })\n appendPauseL1('run_paused')\n if (pausePendingTimer) { clearInterval(pausePendingTimer); pausePendingTimer = null }\n tui.stores.notification.setPausePending(null)\n pauseRequestedTracker = null\n } else {\n // 第三方路径(如 Esc 弹窗直接触发但没有 tracker)→ 仍 push 消息\n tui.messages.pushMessage({ role: 'run_paused' })\n appendPauseL1('run_paused')\n }\n tui.stores.notification.setPaused(true)\n break\n case 'run.pause_cancelled':\n // RFC-186 D7:取消等待——清除浮层 + pushMessage\n recordPauseTrace('cancelled')\n tui.engine.setPauseDialog?.(null)\n tui.messages.pushMessage({ role: 'run.pause_cancelled' as const } as unknown as ChatMessage)\n appendPauseL1('run_pause_cancelled')\n if (pausePendingTimer) { clearInterval(pausePendingTimer); pausePendingTimer = null }\n tui.stores.notification.setPausePending(null)\n pauseRequestedTracker = null\n runPausedState = false\n break\n case 'run.resumed':\n // RFC-186 D7:继续运行——清除浮层 + pushMessage + 清除暂停态\n recordPauseTrace('resumed')\n tui.engine.setPauseDialog?.(null)\n tui.messages.pushMessage({ role: 'run_resumed' })\n appendPauseL1('run_resumed')\n if (pausePendingTimer) { clearInterval(pausePendingTimer); pausePendingTimer = null }\n tui.stores.notification.setPausePending(null)\n pauseRequestedTracker = null\n runPausedState = false\n tui.stores.notification.setPaused(false)\n break\n case 'steer.consumed':\n // RFC-337 D3:一条 steer 追加被模型消费进本回合上下文 → 转正 commit 到 scrollback\n // (此后不再可撤回;ESC 撤回退化为中止整回合)。commitPendingSteer 内部判空幂等。\n tui.pending.commitPendingSteer(event.id)\n break\n // RFC-181 D6/M3:视觉委托结果——仅当会话主模型不支持视觉且真实发生了委托描述时\n // 触发(未注入 describeImages 端口或模型本身支持视觉时零事件,见 vision-gate.ts)。\n // 会话内只提示一次(首次委托,见 visionDelegateToastShown 声明处的理由);描述\n // 失败(failedCount>0)不单独提示——占位文本已让模型知晓,用户侧无需额外关注。\n case 'image.vision-delegate':\n if (!visionDelegateToastShown && event.describedCount > 0) {\n visionDelegateToastShown = true\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('model.visionDelegateToast', { count: event.describedCount }))\n }\n break\n case 'memory.pruned': {\n // 裁剪每轮可触发,只在本会话首次推一条对话流标记(「裁剪已启用」),之后静默。\n if (!pruneNoticeShown) {\n pruneNoticeShown = true\n tui.messages.pushMessage({ role: 'prune_boundary' })\n }\n // /compact log 详情日志:每次裁剪都记(非\"仅首次\"——与 scrollback 提示的一次性\n // 去重语义不同,日志面板要能看到会话内全部裁剪事件的明细)。\n tui.engine.pushEngineEvent({\n kind: 'compactionLog',\n entryKind: 'prune',\n tokensSaved: event.tokensSaved,\n prunedCount: event.prunedCount,\n })\n // 裁剪后上下文真实缩小,立即刷新占比条。\n recomputeBudget()\n break\n }\n case 'error':\n // 异常回合也持久化【当前回合】面板——已跑过的子代理/已编辑文件是事实,不随回合失败丢失。\n persistEditedFiles()\n persistSubagents()\n // RFC-283 D5:error 事件先于 catch 块到达(emit 同步)——如果 pendingEcho 仍非空\n // (streaming 从未开始 = LLM 从未响应),撤回 pending echo。\n // pendingEcho 已空(streaming 已开始后失败)= 消息已转正,不撤回。\n tui.pending.withdrawPendingEcho()\n break\n case 'prompt.continued': {\n // RFC-094 D3:引擎 todo 闸门自动续跑进度指示——toast 显示\"续跑中 r/n…\"\n tui.info.showToast(\n i18next.t('cmd.titleInfo'),\n i18next.t('cmd.todoGateContinuing', { round: event.round, cap: event.maxRounds, left: event.incompleteCount }),\n )\n break\n }\n case 'prompt.budget.notice': {\n // RFC-104 D3/D5:token/时间预算超额 toast(仅 steer 路径;硬停由边界层通知,防双渲)。\n if (event.steered) {\n tui.info.showToast(\n i18next.t('cmd.titleInfo'),\n i18next.t('cmd.promptBudgetExceeded', { context: event.dimension, used: event.used, budget: event.budget }),\n )\n }\n break\n }\n case 'budget.notice': {\n // RFC-104 D5:预算触顶三态 toast——扩展续跑 / 无进展被拒收尾 / 额度耗尽收尾。\n // 硬停路径(extended=false 且未 steer)不 toast:MaxToolTurnsError 边界层已产\n // describeMaxTurns 可见通知,避免双重提示。\n if (event.extended) {\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('cmd.budgetExtended', { maxTurns: event.maxTurns }))\n } else if (event.steered) {\n tui.info.showToast(\n i18next.t('cmd.titleInfo'),\n event.progressDenied\n ? i18next.t('cmd.budgetProgressDenied')\n : i18next.t('cmd.budgetSteered', { maxTurns: event.maxTurns }),\n )\n }\n break\n }\n case 'progress.stall.notice': {\n // RFC-203 D2:触顶前早期停滞检出——与 budget.notice 独立文案(非\"已达上限\",\n // 是\"检测到打转\"),见 agent-session-event-forwarding.ts 的接线语义澄清注释。\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('cmd.progressStallDetected', { maxRepeat: event.maxRepeat }))\n break\n }\n case 'tool.call.mismatch.notice': {\n // RFC-232 D4/G4:工具调用协议违约——按 outcome 区分文案,避免无声中断。\n if (event.outcome === 'retry') {\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('cmd.toolCallMismatchRetry', { retryCount: event.retryCount }))\n } else {\n tui.info.showToast(i18next.t('cmd.titleError'), i18next.t('cmd.toolCallMismatchTerminated'))\n }\n break\n }\n case 'session.cost-budget-exceeded': {\n // RFC-204 D1/D2:会话累计成本超软预算——一次性提醒,不阻断。\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('cmd.sessionCostBudgetExceeded', { used: (event.totalCostUSD).toFixed(2), budget: (event.budgetUSD).toFixed(2) }))\n break\n }\n case 'prompt.end': {\n resetLiveEstimates()\n // RFC-337 D3:回合结束时把仍待定的 steer 批量**转正**(不是丢弃)——引擎\n // steeringQueue 跨 prompt 存续(Agent.reset 无生产调用方),未消费的 steer 会在\n // 下一轮 prompt 被 drain 进模型上下文;丢弃回显会制造「模型看得见、用户看不见」\n // 的不一致。正常路径它们已在 steer.consumed 时逐条转正,此处只兜底残余。\n tui.pending.clearPendingSteers()\n // 分段估算(共享 recomputeBudget 单一真源——读会话 usage + 当前窗口/模型)\n recomputeBudget()\n // ContextPanel:回填本回合消息数(prompt.start 已预加占位行,此处更新为真实值)。\n tui.info.markContextTurns({\n turnIndex: projState.currentTurn,\n messageCount: Math.max(0, redState.messageIndex - turnStartMsgIdx),\n })\n // 回合结束持久化【当前回合】editedFiles + subagents 权威快照(无活动回合写 [],\n // 两面板对称——editedFiles 须随回合落盘,避免无编辑回合残留上一回合)。\n persistEditedFiles()\n persistSubagents()\n // 持久化绝对回合总数——resume 时据此末尾对齐派生 turnBoundaries。\n persistTurnCount()\n // 新会话标题在首轮后由首条消息自动派生——回合结束刷新 SessionHeader/终端标题\n // (与 wireModelSuggesterAndTitle 同一 resolveSessionTitle 真源回落链)。\n {\n const s = getSession()\n const title = resolveSessionTitle(s.session.metadata().title, s.session.messages(), s.id)\n tui.stores.tasks.setSessionTitle(title)\n tui.lifecycle.setTerminalTitle(title)\n }\n // 回合结束上报 notify 合并率(→ stderr,供离线 profiling 收集)。\n // RFC-116 M4:默认关闭——该数据现由 /perf 面板可视化承载,日常用户不需要\n // 再看到这条 stderr 噪音;env 开关保留给需要每回合数字的离线 profiling 场景,\n // 不删除输出内容本身,只加开关(纯粹的默认行为改善,非能力削减)。\n if (process.env['OTTO_TUI_DEBUG_NOTIFY_STATS']) {\n const ns = tui.info.getNotifyStats()\n if (ns.coalescedCalls > 0) {\n const ratio = (ns.coalescedCalls / Math.max(1, ns.coalescedFires)).toFixed(1)\n console.error(`\\x1b[90m\\x1b[2m[perf] notify merge ${ns.coalescedCalls}→${ns.coalescedFires} (${ratio}x) direct ${ns.immediateCalls} flush ${ns.flushFires}\\x1b[0m`)\n }\n }\n // RFC-208 §3.1/§3.5:满意度脉冲调查触发判定——放在 recomputeBudget() 之后(回合真正\n // 结束的权威时点)。前置检查 forced-upgrade 在场时恒不展示(§5 风险段\"并发\"缓解措施)。\n // RFC-209 §2.6/M4 T13:feedback 插件被禁用(disabled_plugins 含 plugin-feedback)时\n // feedbackRegistry.get() 恒 undefined——此时不应触发判定/弹窗(否则用户看到一个提交\n // 必然被静默丢弃的弹层,UI 层面的降级不完整,只做到\"不崩溃\"不够)。\n {\n const pulseSurveyConfig = app.settings.get('pulse_survey')\n const shouldShow =\n app.feedbackRegistry.get() !== undefined &&\n shouldShowPulseSurvey(\n projState.currentTurn,\n {\n enabled: pulseSurveyConfig?.enabled ?? true,\n probability: pulseSurveyConfig?.probability ?? 0.05,\n minTurnGap: pulseSurveyConfig?.min_turn_gap ?? 20,\n forcedUpgradeActive: tui.info.isForcedUpgradeActive(),\n },\n pulseSurveyTriggerState,\n )\n if (shouldShow) {\n pulseSurveyTriggerState.lastShownTurn = projState.currentTurn\n const submit = (rating: PulseSurveyRating, comment: string | undefined) => {\n app.feedbackRegistry.get()?.recordPulseSurvey?.({\n ts: Date.now(),\n rating,\n comment,\n sdkVersion: SDK_VERSION,\n modelId: getSession().model?.id,\n })\n }\n tui.stores.notification.setPulseSurvey({\n phase: 'rating',\n rating: null,\n selectedIndex: 0,\n commentText: '',\n commentOffset: 0,\n onSubmit: submit,\n onClose: () => tui.stores.notification.setPulseSurvey(null),\n })\n }\n }\n break\n }\n case 'ask.user.required': {\n const q = event.question\n const reqId = event.requestId\n void (async () => {\n // RFC-344 M2-02:会话问询也必须入队。\n //\n // 不是为插件让路,而是修既有漏洞:TUI 的 `resolvers.questions` 是单槽位\n // (`handle-modal.ts:324`),后开覆盖先开——同会话连续 grill 时第二个会\n // 覆盖第一个的 resolver,被覆盖的 Promise **永远不 resolve**(agent 挂死)。\n // 队列保证\"同一时刻只有一个问询在飞\",两类问询共用同一不变式。\n const center = app.inquiryCenter\n const admitted = center.enqueue({\n id: reqId,\n origin: { kind: 'session', sessionId: event.sessionId },\n question: q,\n })\n if (!admitted.accepted) {\n // 队列满/超限——按 fail-closed 收敛,不静默丢弃(否则 agent 永久等待)。\n getSession().resolveAskUser(reqId, {\n answer: '',\n acceptedRecommendation: false,\n resolvedBy: 'human',\n })\n return\n }\n // 等到轮到自己再渲染。会话问询优先级最高,通常立即返回。\n // false = 已被 TTL 清理/shutdown 收敛,不再渲染(中心已 resolve 其 Promise)。\n if (!(await center.waitForTurn(reqId))) {\n getSession().resolveAskUser(reqId, {\n answer: '',\n acceptedRecommendation: false,\n resolvedBy: 'human',\n })\n return\n }\n\n // finally 释放槽位:渲染路径有 3 个出口(confirm/questions/input)且都可能抛,\n // 漏释放会让队列永久阻塞——后续所有问询(含 agent 的)都卡死。\n // settle 幂等,重复调用无副作用。\n try {\n const answer = await renderGrillQuestion(tui, q)\n getSession().resolveAskUser(reqId, answer)\n } finally {\n // 用 release 而非 settle:会话问询的答案经 `resolveAskUser` 走 grill gate,\n // 中心的 answer Promise 无人消费——传一个假答案只会误导读代码的人。\n center.release(reqId)\n }\n })()\n break\n }\n case 'approval.required': {\n // 「始终允许」对 bash 持久化两段命令前缀(git status 而非整个 bash)+ 弹窗显式呈现拟授权。\n const bashCmd =\n event.toolName === 'bash' && typeof event.arguments?.command === 'string'\n ? (event.arguments.command as string)\n : ''\n const alwaysPrefix = bashCmd ? app.bashAlwaysAllowPrefix(bashCmd) : null\n void tui.modal.requestPermission({\n tool: event.toolName,\n input: extractCommandInput(event.toolName, event.arguments),\n risk: event.risk ?? 'medium',\n description: event.description,\n alwaysAllowLabel: alwaysPrefix ?? undefined,\n })\n .then((decision: PermissionDecision) => {\n const bypassCmd = sandboxBypassCommand(event.description, event.arguments as Record<string, unknown> | undefined, SANDBOX_ESCALATION_MARKER)\n if (decision.approved && bypassCmd) {\n app.approveSandboxBypass(bypassCmd)\n }\n getSession().resolveApproval(event.id, decision.approved)\n if (decision.approved && decision.mode === 'always') {\n if (event.toolName === 'bash') {\n // RFC-073 P2/OQ-1(A):bash 只在可前缀化时记前缀条目。不可前缀化(alwaysPrefix\n // 为 null)的复合/混淆命令——UI 已不提供 always——这里防御性地不回退为裸 `bash`\n // 整工具放行(盲授权)。\n if (alwaysPrefix) void app.allowToolAlways(`bash(${alwaysPrefix})`, getSession().id)\n } else {\n // 非 bash 工具:always = 整工具放行(通用 PermissionRequest 的既定语义)。\n void app.allowToolAlways(event.toolName, getSession().id)\n }\n }\n })\n break\n }\n }\n })\n }\n\n return {\n bgUnsubs,\n attachSessionHandler,\n getSessionUnsub: () => sessionUnsub,\n getLastProviderUsage: () => lastProviderUsage,\n isWriteLeaseReadOnly: () => writeLeaseReadOnly,\n isRunPaused: () => runPausedState,\n }\n}\n","/**\n * history-pager.ts — RFC-300 M2/M3b:更早历史补页的 CLI 侧数据通道(有状态)。\n *\n * 职责:把「内存顶部向上翻页」翻译成 repository 反向分页查询 + 转 TUI ChatMessage。\n *\n * 设计(避免逐条 seq 对齐的脆弱性):\n * - **锚由 CLI 闭包维护**,TUI 只发 `{limit}` 请求——TUI 不需要知道 DB seq。\n * - resume 会话:锚 = restore 尾窗口的最小 seq(与 restore 同窗口口径),\n * loadedSeqSet = 尾窗口全部 message 条目 seq(去重兜底)。\n * - 实时会话:锚 = 启动时 entryCount(= next_seq,启动后新消息 seq ≥ 此值)。\n * - 转换:user/assistant 文本经 agentMessagesToChatMessages(与 resume 同款);\n * 工具消息(tool_call/tool_result)由本模块**自组装**(复用 argPreview/joinMessageText),\n * 以支持 M3b 跨页配对(见下)。每条赋稳定 id `hist-<seq>` 与 seq。\n * - 停止:到达 clear 边界(RFC-029 可见转录语义,/clear 前不回拉)或 seq 0 →\n * 返回 null(TUI 侧 earlierEnded 优雅结束)。\n *\n * M3b 跨页 tool 配对(A4 修复):\n * 拉取从最新往更老——result(seq 大)先于 call(seq 小)到达。单页转换会:\n * - 较新页 result 配不到更老页的 call → 渲染成退化 tool(name='tool',无 args);\n * - 更老页 call 的 result 在较新页已渲染 → 渲染成 pending tool(无 result)。\n * 修复:**result 延迟**——result 的 call 不在已见集合时**暂存 pendingResults 不渲染**,\n * 更老页的 call 到达时配对渲染完整 tool 消息(call 的 name/args + 暂存 result)。\n * 渲染位置 = call 所在页(seq 序),信息完整无退化无重复。\n */\nimport type { ChatMessage } from '@x-otto/tui'\nimport type { SqliteSessionRepository } from '@x-otto/session'\nimport { SESSION_RESUME_WINDOW_MESSAGES } from '@x-otto/session-contract'\nimport type { Message, ToolCall } from '@x-otto/ai'\nimport { agentMessagesToChatMessages, argPreview, joinMessageText } from './view-model-adapter'\n\n/** 单次补页窗口大小(500 条 ≈ 内存 cap 的 1/10,平衡响应与行索引重建成本)。 */\nexport const HISTORY_PAGE_LIMIT = 500\n\nexport interface HistoryPager {\n /** 拉取更早一页;无更早(clear 边界/seq 0/重复过滤后为空)→ null。 */\n loadEarlier(limit?: number): Promise<ChatMessage[] | null>\n}\n\n/** 暂存的未配对 result(其 call 在更老页,未拉到)。 */\ninterface PendingResult {\n toolName?: string\n content: unknown\n isError?: boolean\n}\n\n/** 从 assistant 消息提取 tool_call blocks。 */\nfunction collectCalls(msg: Message): Map<string, ToolCall> {\n const calls = new Map<string, ToolCall>()\n if (msg?.role === 'assistant' && Array.isArray(msg.content)) {\n for (const block of msg.content) {\n if (block.type === 'tool_call') calls.set(block.id, block)\n }\n }\n return calls\n}\n\n/**\n * 创建补页器。\n *\n * @param repo session repository(app.sessionRepoRef.current)。\n * @param sessionId 目标会话。\n * @param resumed 是否 resume/continue 会话(true = 以 restore 尾窗口为锚与去重集合;\n * false = 实时会话,以启动时 entryCount 为锚)。\n */\nexport async function createHistoryPager(\n repo: SqliteSessionRepository,\n sessionId: string,\n resumed: boolean,\n): Promise<HistoryPager> {\n let anchor: number\n const loadedSeqSet = new Set<number>()\n // M3b 跨页配对状态。\n const seenToolCalls = new Map<string, ToolCall>() // 已见 call(供较新页 result 配对)\n const seenRenderedIds = new Set<string>() // 已渲染完整 tool 的 call id(call 位置抑制)\n const pendingResults = new Map<string, PendingResult>() // 未配对 result(等更老页 call)\n\n if (resumed) {\n // 与 restore 同窗口口径:尾窗口 message 条目 seq 全部记入去重集合,锚 = 窗口最小 seq。\n // RFC-321 D7:与 restore 同窗口口径 → 跟随 SESSION_RESUME_WINDOW_MESSAGES(非 cap)。\n const tail = await repo.loadTailEntries(sessionId, SESSION_RESUME_WINDOW_MESSAGES)\n anchor = tail.length > 0 ? tail[0]!.seq : 0\n for (const row of tail) {\n if (row.type === 'message') loadedSeqSet.add(row.seq)\n }\n } else {\n anchor = await repo.getEntryCount(sessionId) // = next_seq:启动后新消息 seq ≥ 此值\n }\n\n return {\n async loadEarlier(limit = HISTORY_PAGE_LIMIT): Promise<ChatMessage[] | null> {\n if (anchor <= 0) return null\n const rows = await repo.loadEntriesBefore(sessionId, anchor, limit)\n if (rows.length === 0) return null\n\n // 预扫描本页:pageCalls(本页 call,供本页 result 配对)、pageResultIds(本页 result)。\n const pageCalls = new Map<string, ToolCall>()\n const pageResultIds = new Set<string>()\n for (const row of rows) {\n if (row.type !== 'message') continue\n const msg = row.data as Message\n if (msg?.role === 'assistant') {\n for (const [id, block] of collectCalls(msg)) pageCalls.set(id, block)\n } else if (msg?.role === 'tool_result' && typeof msg.toolCallId === 'string') {\n pageResultIds.add(msg.toolCallId)\n }\n }\n\n const out: ChatMessage[] = []\n const stamp = (m: ChatMessage, seq: number): ChatMessage => ({ ...m, id: `hist-${seq}`, seq } as ChatMessage & { seq: number })\n\n for (const row of rows) {\n if (row.type === 'clear') {\n // RFC-029:/clear 切可见转录——clear 即补页终点:丢弃 clear 之前已收集的\n // (它们不属于当前可见转录),锚置 0 停止(防死循环:不再向前拉)。\n anchor = 0\n out.length = 0\n continue\n }\n if (row.type !== 'message') continue // compaction 边界条目不承载可见消息\n if (loadedSeqSet.has(row.seq)) continue // 去重兜底(resume 锚偏旧时)\n\n const msg = row.data as Message\n if (!msg || typeof msg.role !== 'string') continue\n if ((msg as { internal?: boolean }).internal) continue\n\n if (msg.role === 'tool_result') {\n const call = seenToolCalls.get(msg.toolCallId ?? '') ?? pageCalls.get(msg.toolCallId ?? '')\n if (call) {\n // 完整渲染(配 call)——工具消息自组装。\n const argsObj = call.arguments ?? {}\n out.push(stamp({\n role: 'tool',\n name: msg.toolName || call.name || 'tool',\n args: argPreview(argsObj),\n argsObj,\n result: joinMessageText(msg.content as Parameters<typeof joinMessageText>[0]),\n isError: msg.isError,\n } as ChatMessage, row.seq))\n seenRenderedIds.add(msg.toolCallId!)\n } else {\n // call 在更老页(未拉到)→ 暂存延迟(M3b),不渲染退化消息。\n pendingResults.set(msg.toolCallId!, { toolName: msg.toolName, content: msg.content, isError: msg.isError })\n }\n continue\n }\n\n if (msg.role === 'assistant') {\n const calls = collectCalls(msg)\n if (calls.size > 0) {\n // 抑制集合:本页 result + 已渲染 + 已暂存——这些 call 不渲染 pending,\n // 由下方手动配对渲染。\n const suppressed = new Set([...pageResultIds, ...seenRenderedIds, ...pendingResults.keys()])\n const textOnly = agentMessagesToChatMessages([msg], { resolvedIds: suppressed })\n for (const m of textOnly) out.push(stamp(m, row.seq))\n // 手动配对 calls(文本已由上面渲染)。\n for (const [id, block] of calls) {\n if (pageResultIds.has(id)) continue // 本页 result 稍后渲染(配本页 call)\n const pending = pendingResults.get(id)\n if (pending) {\n // 跨页配对:call(本页)+ 暂存 result → 完整 tool。\n pendingResults.delete(id)\n const argsObj = block.arguments ?? {}\n out.push(stamp({\n role: 'tool',\n name: block.name,\n args: argPreview(argsObj),\n argsObj,\n result: joinMessageText(pending.content as Parameters<typeof joinMessageText>[0]),\n isError: pending.isError ?? false,\n } as ChatMessage, row.seq))\n }\n // 无 else-if 兜底(审核 3 修复):孤儿 call(DB 无对应 result,回合中断)的\n // pending 已由上方 agentMessagesToChatMessages 转换渲染(suppressed 不含\n // 孤儿 id)——再渲染会重复同 id 消息。\n }\n continue\n }\n }\n\n // user / 纯文本 assistant / 其他 role:委托标准转换(无配对需求)。\n const converted = agentMessagesToChatMessages([msg])\n for (const m of converted) out.push(stamp(m, row.seq))\n }\n\n // 跨页累积:本页 call/result 供更老页使用。\n for (const [id, block] of pageCalls) seenToolCalls.set(id, block)\n\n // 锚前移到本次窗口最小 seq(下次补页从更早继续)。\n if (anchor > 0) anchor = rows[0]!.seq\n return out.length > 0 ? out : null\n },\n }\n}\n","/**\n * 上下文压感提醒状态机:approaching/nearing/critical 三带离散阶梯 + 免打扰退避。\n */\n\nexport type PressureZone = 'approaching' | 'nearing' | 'critical'\n\nconst ZONES: readonly PressureZone[] = ['approaching', 'nearing', 'critical']\n\nexport interface PressureReminderConfig {\n /** 各区间提醒次数上限。 */\n maxReminders: Record<PressureZone, number>\n /** 指数退避基准轮次(第 N 次提醒需等待 backoffBaseTurns * 2^(N-2) 轮,N>=2)。 */\n backoffBaseTurns: number\n /** 回落多少百分点后重置该区间计数(防边界抖动重复触发)。 */\n resetMarginPct: number\n /** 「接近」区间在 pruneLine 前多少百分点开始。 */\n approachingMarginPct: number\n}\n\nexport const DEFAULT_PRESSURE_REMINDER_CONFIG: PressureReminderConfig = {\n maxReminders: { approaching: 1, nearing: 3, critical: 1 },\n backoffBaseTurns: 4,\n resetMarginPct: 10,\n approachingMarginPct: 10,\n}\n\nexport interface PressureCheckInput {\n /** 当前上下文占用百分比(0-100)。 */\n contextPercent: number\n /** prune 触发线百分比(随 contextWindow 动态派生)。 */\n pruneLinePct: number\n /** compact 触发线百分比(随 contextWindow 动态派生)。 */\n compactLinePct: number\n /** 当前对话轮次(退避节奏基准)。 */\n currentTurn: number\n}\n\nexport interface PressureReminder {\n zone: PressureZone\n /** 触发时刻的占用百分比(供文案插值,如 STR.contextPressureNearing(pct))。 */\n pct: number\n}\n\ninterface ZoneState {\n /** 已提醒次数。 */\n count: number\n /** 上次提醒的轮次;undefined = 从未在本区间提醒过。 */\n lastRemindedTurn: number | undefined\n /** 本次进入该区间以来的峰值占用(用于回落重置判定)。 */\n peakPct: number\n /** 是否已被判定「已回落,等待重新进入」(防止刚回落又原地反弹被误判为持续停留)。 */\n active: boolean\n}\n\nfunction initZoneState(): ZoneState {\n return { count: 0, lastRemindedTurn: undefined, peakPct: 0, active: false }\n}\n\n/** 由占用百分比 + 触发线,判定当前所属区间(不在任何提醒区间则返回 null)。 */\nfunction resolveZone(\n pct: number,\n pruneLinePct: number,\n compactLinePct: number,\n approachingMarginPct: number,\n): PressureZone | null {\n if (pct >= compactLinePct) return 'critical'\n if (pct >= pruneLinePct) return 'nearing'\n if (pct >= pruneLinePct - approachingMarginPct) return 'approaching'\n return null\n}\n\n/** 区间下界(供回落重置判定)。 */\nfunction zoneLowerBound(\n zone: PressureZone,\n pruneLinePct: number,\n compactLinePct: number,\n approachingMarginPct: number,\n): number {\n if (zone === 'critical') return compactLinePct\n if (zone === 'nearing') return pruneLinePct\n return pruneLinePct - approachingMarginPct\n}\n\n/** 第 N 次提醒(N>=2)需等待的轮次间隔:backoffBaseTurns * 2^(N-2)。 */\nfunction backoffTurns(config: PressureReminderConfig, nextCount: number): number {\n if (nextCount <= 1) return 0\n return config.backoffBaseTurns * 2 ** (nextCount - 2)\n}\n\nexport class ContextPressureReminder {\n private readonly config: PressureReminderConfig\n private zones: Record<PressureZone, ZoneState>\n private suppressed = false\n\n constructor(config: Partial<PressureReminderConfig> = {}) {\n this.config = { ...DEFAULT_PRESSURE_REMINDER_CONFIG, ...config }\n this.zones = {\n approaching: initZoneState(),\n nearing: initZoneState(),\n critical: initZoneState(),\n }\n }\n\n /**\n * 每轮 recomputeBudget() 后调用一次。返回本轮应触发的提醒(如有),否则 null。\n * 副作用:更新命中区间的计数/峰值/回落状态;未命中区间按回落规则清空。\n */\n check(input: PressureCheckInput): PressureReminder | null {\n if (this.suppressed) return null\n\n const { contextPercent: pct, pruneLinePct, compactLinePct, currentTurn } = input\n const currentZone = resolveZone(pct, pruneLinePct, compactLinePct, this.config.approachingMarginPct)\n\n // 先处理未命中/回落的区间:占用低于该区间下界减 resetMarginPct → 清零计数。\n for (const zone of ZONES) {\n if (zone === currentZone) continue\n const state = this.zones[zone]\n if (!state.active) continue\n const lowerBound = zoneLowerBound(zone, pruneLinePct, compactLinePct, this.config.approachingMarginPct)\n if (pct < lowerBound - this.config.resetMarginPct) {\n this.zones[zone] = initZoneState()\n }\n }\n\n if (currentZone === null) return null\n\n // 防御:currentTurn 非单调递增场景(如 /clear、/resume 切会话后 currentTurn 归零,\n // 而调用方未及时调用 onCompacted() 重置——理论上不应发生,因为两条路径均已显式\n // 接线 onCompacted,但状态机自身不依赖调用方纪律,独立对此免疫)。检测到 currentTurn\n // 倒退到早于该区间上次提醒的轮次,视为全新周期,整个区间状态重置后按首次进入处理。\n const existing = this.zones[currentZone]\n if (existing.lastRemindedTurn !== undefined && currentTurn < existing.lastRemindedTurn) {\n this.zones[currentZone] = initZoneState()\n }\n\n const state = this.zones[currentZone]\n state.active = true\n state.peakPct = Math.max(state.peakPct, pct)\n\n const maxCount = this.config.maxReminders[currentZone]\n if (state.count >= maxCount) return null\n\n const nextCount = state.count + 1\n if (state.lastRemindedTurn !== undefined) {\n const requiredGap = backoffTurns(this.config, nextCount)\n if (currentTurn - state.lastRemindedTurn < requiredGap) return null\n }\n\n state.count = nextCount\n state.lastRemindedTurn = currentTurn\n return { zone: currentZone, pct: Math.round(pct) }\n }\n\n /** compaction.end(after < before)或 /compact 成功后调用,清空全部区间状态。 */\n onCompacted(): void {\n this.zones = {\n approaching: initZoneState(),\n nearing: initZoneState(),\n critical: initZoneState(),\n }\n }\n\n /** 「本次会话不再提醒」——彻底关闭,直到状态机被重新构造(新会话)。 */\n suppress(): void {\n this.suppressed = true\n }\n\n /** 供测试/可观测查询当前各区间计数状态。 */\n snapshot(): Readonly<Record<PressureZone, { count: number; lastTurn: number | undefined }>> {\n return {\n approaching: { count: this.zones.approaching.count, lastTurn: this.zones.approaching.lastRemindedTurn },\n nearing: { count: this.zones.nearing.count, lastTurn: this.zones.nearing.lastRemindedTurn },\n critical: { count: this.zones.critical.count, lastTurn: this.zones.critical.lastRemindedTurn },\n }\n }\n}\n","/**\n * interactive-persistence.ts — 占用条重算 + 回合面板持久化(editedFiles/subagents/turnCount)。\n *\n * `createInteractivePersistence`\n * 工厂接收 session/app/tui/projState + 模型可变状态 ref(窗口/当前模型 id 随 /model 切换变动),\n * 返回 recomputeBudget + 三个 fail-soft 持久化闭包 + editFileCallback(每次编辑即时快照)。\n */\n// RFC-161 M3a:STR 改从 cli i18n 层导入;subagentEntryStatus 属 M3b 处置范围,暂保留。\nimport { subagentEntryStatus } from '@x-otto/tui'\nimport { i18next } from '@x-otto/tui'\nimport { computeContextBudget } from '../context-budget'\nimport { ContextPressureReminder, type PressureZone } from '../context-pressure-reminder'\n\n/** TUI 实例类型(startTui 返回值)。 */\ntype TuiInstance = Awaited<ReturnType<typeof import('@x-otto/tui').startTui>>\n/** app.editFileCallback 的入参类型(编辑文件信息)。 */\ntype EditFileInfo = Parameters<NonNullable<import('@x-otto/coding').App['editFileCallback']>>[0]\n\nexport interface InteractivePersistenceDeps {\n /** B6:live session 取值器——/resume 切会话后 recomputeBudget/persist* 必须读新会话,不可按值冻结。 */\n getSession: () => import('@x-otto/coding').AgentSession\n app: import('@x-otto/coding').App\n tui: TuiInstance\n projState: ReturnType<typeof import('@x-otto/session-contract').createProjectorState>\n /** 占用条分母——随 /model 切换更新(不同模型窗口不同)。 */\n contextWindow: { value: number }\n /** cost 速率随当前模型;/model 切换时更新,避免陈旧速率。 */\n currentModelId: { value: string | undefined }\n}\n\nexport interface InteractivePersistence {\n /**\n * 重算上下文占用条。可传 `livePendingTokens`(当前 prompt/流式输出/tool result 等尚未被\n * provider usage 权威值覆盖的预估 token 数)用于实时刷新——不传时使用 API usage 的精确值\n * (回合边界)。\n */\n recomputeBudget: (livePendingTokens?: number, compactionAdjustment?: { before: number; after: number }) => void\n persistEditedFiles: () => void\n persistSubagents: () => void\n persistTurnCount: () => void\n editFileCallback: (info: EditFileInfo) => void\n /** RFC-156:压缩完成后调用(compaction.end 且 after<before,或 /compact 成功),清空全部提醒区间计数。 */\n onContextCompacted: () => void\n}\n\nexport function createInteractivePersistence(deps: InteractivePersistenceDeps): InteractivePersistence {\n const { getSession, app, tui, projState, contextWindow, currentModelId } = deps\n\n /**\n * backlog T7(2026-07-11 全局分析):usage 扫描缓存。recomputeBudget 被每个\n * text_delta 驱动(`interactive-events.ts` 的 stream.event 处理,100+/s),而\n * `session.messages()` 是 O(N) walkBranch + 倒序全量 usage 扫描——流式期间消息树\n * 与 usage 不变(assistant usage 在回合结束才写入),per-delta 重扫是纯浪费。\n * 规则:无参调用(权威边界)全量重算并刷新缓存;带 livePendingTokens 的\n * live 刷新复用缓存(sessionId 不匹配时兜底重算——/resume 切会话场景)。\n */\n interface UsageScan {\n sessionId: string\n lastIn: number\n lastOut: number\n lastCacheRead: number\n lastCacheWrite: number\n totIn: number\n totOut: number\n messageCount: number\n }\n let usageScanCache: UsageScan | undefined\n\n // RFC-156:上下文占用弹性提醒——会话内瞬态状态机(不持久化,理由见 RFC-156 §4.3)。\n const pressureReminder = new ContextPressureReminder()\n const PRESSURE_ZONE_MESSAGE: Record<PressureZone, (pct: number) => string> = {\n approaching: (pct) => i18next.t('cmd.contextPressureApproaching', { pct }),\n nearing: (pct) => i18next.t('cmd.contextPressureNearing', { pct }),\n critical: (pct) => i18next.t('cmd.contextPressureCritical', { pct }),\n }\n\n const scanUsage = (): UsageScan => {\n const session = getSession()\n const msgs = session.session.messages() as ReadonlyArray<{\n role?: string\n usage?: { inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number }\n }>\n let lastIn = 0, lastOut = 0, lastCacheRead = 0, lastCacheWrite = 0, gotLast = false\n let totIn = 0, totOut = 0\n for (let i = msgs.length - 1; i >= 0; i--) {\n const u = msgs[i]?.usage\n if (!u) continue\n if (!gotLast) {\n lastIn = u.inputTokens ?? 0\n lastOut = u.outputTokens ?? 0\n lastCacheRead = u.cacheReadTokens ?? 0\n lastCacheWrite = u.cacheWriteTokens ?? 0\n gotLast = true\n }\n if (msgs[i]?.role === 'assistant') {\n totIn += u.inputTokens ?? 0\n totOut += u.outputTokens ?? 0\n }\n }\n return {\n sessionId: session.session.id,\n lastIn, lastOut, lastCacheRead, lastCacheWrite,\n totIn, totOut,\n messageCount: msgs.length,\n }\n }\n\n const recomputeBudget = (livePendingTokens?: number, compactionAdjustment?: { before: number; after: number }): void => {\n const isStreamingRefresh = livePendingTokens !== undefined\n const scan =\n isStreamingRefresh && usageScanCache && usageScanCache.sessionId === getSession().session.id\n ? usageScanCache\n : scanUsage()\n usageScanCache = scan\n\n // RFC-227 fix:compaction 后 API usage(lastIn/lastOut)仍是旧上下文的快照,\n // 须按消息数比例下调——否则 status bar / /context 面板的百分比不会变化(\n // setContextBudget 传同一个值 → React 判定无变化 → 不渲染)。\n const divisor = compactionAdjustment ? compactionAdjustment.before : 1\n const ratio = compactionAdjustment ? compactionAdjustment.after / divisor : 1\n const adj = (v: number) => (ratio < 1 ? Math.round(v * ratio) : v)\n\n // cost 速率随当前模型(currentModelId 会在 /model 切换时更新,避免用陈旧速率)。\n const rate = app.modelRegistry.find(currentModelId.value ?? '')?.cost\n const cost = rate ? scan.totIn * (rate.input ?? 0) + scan.totOut * (rate.output ?? 0) : 0\n const budget = computeContextBudget({\n systemPrompt: getSession().systemPrompt,\n tools: getSession().tools,\n lastIn: adj(scan.lastIn),\n lastOut: adj(scan.lastOut),\n lastCacheRead: adj(scan.lastCacheRead),\n lastCacheWrite: adj(scan.lastCacheWrite),\n contextWindow: contextWindow.value,\n modelName: getSession().model?.name ?? '',\n totIn: scan.totIn,\n totOut: scan.totOut,\n cost: cost > 0 ? cost : undefined,\n messageCount: scan.messageCount,\n streamingOutputTokens: livePendingTokens,\n })\n tui.stores.budget.setBudget(budget)\n tui.stores.context.setBudget(budget)\n\n // RFC-156:占用弹性提醒——只在回合边界(非流式高频刷新)检查,避免同一轮内\n // 因 text_delta 高频调用被反复判定(isStreamingRefresh 时 budget 仅做实时展示刷新)。\n if (!isStreamingRefresh && budget.contextWindow > 0) {\n const pruneLinePct = (budget.pruneLine / budget.contextWindow) * 100\n const compactLinePct = (budget.compactLine / budget.contextWindow) * 100\n const reminder = pressureReminder.check({\n contextPercent: budget.contextPercent,\n pruneLinePct,\n compactLinePct,\n currentTurn: projState.currentTurn,\n })\n if (reminder) {\n tui.info.pushNotification(\n i18next.t('cmd.titleContextPressure'),\n PRESSURE_ZONE_MESSAGE[reminder.zone](reminder.pct),\n 'system',\n reminder.zone === 'critical' ? 'warning' : 'info',\n )\n }\n }\n }\n\n // M72:编辑文件面板——每次 edit/write 工具执行后将修改记录推送到 TUI EditedFilesPane,\n // 并同步持久化到会话 metadata(不存 oldContent/newContent,仅保留 stat 行)。\n // fail-soft:持久化是旁路,任何失败都吞错记日志、绝不冒泡(否则经 emit()[无 try/catch]→ runtime\n // prompt.end publish[在 try 块内]→ 把成功回合转成失败回合)。把\"设计意图的 fail-soft\"变成\"代码强制\"。\n // 回合制:editedFiles 是「本回合活动」。editFileCallback 每次编辑即写一份当前快照\n // (兜中途崩溃),prompt.end/error 再写一份当前回合权威快照——【无编辑回合写 []】,与 persistSubagents\n // 对称,避免 BUG-1(subagents 写空而 editedFiles 残留上一回合 → resume 两面板不一致)。\n const persistEditedFiles = (): void => {\n try {\n getSession().session.setEditedFiles(\n tui.messages.getEditedFiles().map(f => ({\n path: f.path,\n operation: f.operation,\n addedLines: f.addedLines,\n removedLines: f.removedLines,\n timestamp: f.timestamp,\n toolCallId: f.toolCallId,\n })),\n )\n // RFC-295 M1:write-through 落盘——此前只写内存、等下次 turn.end/prompt.end 节流落盘\n // (30s/5turn 窗口),中途崩溃丢失。复刻 draft 先例:setter 后立即 save,fail-soft 吞错。\n // editFileCallback 每次编辑即调本闭包 → 每次编辑即落盘(覆盖 mid-prompt 窗口)。\n void app.sessionManager.save(getSession().id).catch(() => {})\n } catch (e) {\n console.error(`\\x1b[90m\\x1b[2m[persist] editedFiles skipped (non-fatal): ${e instanceof Error ? e.message : String(e)}\\x1b[0m`)\n }\n }\n\n const editFileCallback = (info: EditFileInfo): void => {\n try {\n tui.stores.tasks.addEditedFile({\n path: info.path,\n operation: info.operation,\n oldContent: info.oldContent,\n newContent: info.newContent,\n addedLines: info.addedLines,\n removedLines: info.removedLines,\n timestamp: info.timestamp,\n toolCallId: info.toolCallId,\n })\n } catch (e) {\n console.error(`\\x1b[90m\\x1b[2m[panel] addEditedFile skipped (non-fatal): ${e instanceof Error ? e.message : String(e)}\\x1b[0m`)\n }\n persistEditedFiles() // 即时快照(自带 fail-soft)\n }\n\n // 回合制:把【当前回合】子代理面板(stat-only)写回 metadata。prompt.end + error 各调一次\n // (异常回合已跑过的子代理是事实,保留)。prompt.start 已清上一回合 → 无子代理回合写 []。映射\n // BackgroundTaskSummary→SubagentEntry:status 取 agentStatus(真源) 归一 running/done/error/killed。\n const persistSubagents = (): void => {\n try {\n const entries = [...tui.plugins.getSubagents().values()].map(t => ({\n id: t.id,\n name: t.command || t.id,\n status: subagentEntryStatus(t),\n startedAt: t.startedAt,\n }))\n getSession().session.setSubagents(entries)\n // RFC-295 M1:write-through 落盘(同 persistEditedFiles)。setter 后立即 save,fail-soft。\n void app.sessionManager.save(getSession().id).catch(() => {})\n } catch (e) {\n console.error(`\\x1b[90m\\x1b[2m[persist] subagents skipped (non-fatal): ${e instanceof Error ? e.message : String(e)}\\x1b[0m`)\n }\n }\n // 同上 fail-soft:turnCount 持久化失败不得影响回合。\n const persistTurnCount = (): void => {\n try {\n getSession().session.setTurnCount(projState.currentTurn)\n } catch (e) {\n console.error(`\\x1b[90m\\x1b[2m[persist] turnCount skipped (non-fatal): ${e instanceof Error ? e.message : String(e)}\\x1b[0m`)\n }\n }\n\n // 注:草稿(drafts)持久化不走这里——Ctrl+S/O 回调在 startTui({...}) 内联定义(早于本\n // persistence 对象存在,因其依赖 startTui 返回的 tui 实例),逻辑内联在 interactive.ts +\n // handlers/draft.ts,命名/排序/上限裁剪单一真源见 draft-entries.ts。\n\n const onContextCompacted = (): void => {\n pressureReminder.onCompacted()\n }\n\n return {\n recomputeBudget,\n persistEditedFiles,\n persistSubagents,\n persistTurnCount,\n editFileCallback,\n onContextCompacted,\n }\n}\n","/**\n * 后台任务观测 TUI 接线层(命令注册/事件订阅/状态同步)。\n */\nimport { readFile } from 'node:fs/promises'\n\nimport {\n globalProcessRegistry,\n globalAgentJobRegistry,\n globalAgentObservability,\n readTaskOutputTail,\n} from '@x-otto/coding'\n// RFC-161 M3a:STR 改从 cli i18n 层导入(M3a 首轮多行 import 语句漏抓,本次补齐)。\n// formatTranscriptText/formatTurnTimeline/formatAgentDiagnosticsText/observationRow 属\n// M3b 独立观察项(与 tui SubagentsPane/DelegatesPane 深度共享 BackgroundTaskSummary 类型,\n// 体量大且拆分有双份维护语义分裂风险,暂不纳入本轮,见 RFC-161 M3b 里程碑记录)。\nimport {\n formatTranscriptText,\n formatTurnTimeline,\n formatAgentDiagnosticsText,\n observationRow,\n} from '@x-otto/tui'\nimport { i18next } from '@x-otto/tui'\nimport { agentMessagesToChatMessages } from '../view-model-adapter'\n\n/** TUI 实例类型(startTui 返回值)。 */\ntype TuiInstance = Awaited<ReturnType<typeof import('@x-otto/tui').startTui>>\n\nexport interface BackgroundControllerDeps {\n app: import('@x-otto/coding').App\n session: import('@x-otto/coding').AgentSession\n /** 惰性取 tui(控制器先于 startTui 创建)。 */\n getTui: () => TuiInstance\n}\n\nexport interface BackgroundController {\n /** 取消所有后台订阅句柄(finally 清理)。 */\n clearBgSubs: () => void\n /** 停止详情面板轮询 + 释放 retain(finally / 面板关闭 / 作业终态)。 */\n stopBgOutputPoll: () => void\n /** 停止子代理对话流轮询 + unpin/retain 释放(finally / 退出子代理流)。 */\n stopBgSubagentPoll: () => void\n /** 接收 events 模块产出的订阅句柄。 */\n setBgUnsubs: (unsubs: Array<() => void>) => void\n /** startTui config 的后台面板回调(整体展开)。 */\n handlers: {\n onBackgroundKill: (id: string) => void\n onBackgroundApply: (id: string) => void\n onDelegateSupply: (id: string, text: string) => void\n onBackgroundViewOutput: (id: string) => void\n onBackgroundOutputClose: () => void\n onBackgroundSwitch: (id: string) => void\n onBackgroundDelete: (id: string) => void\n onSubagentExit: () => void\n }\n}\n\nexport function createBackgroundController(deps: BackgroundControllerDeps): BackgroundController {\n const { app, getTui } = deps\n\n let bgUnsubs: Array<() => void> = []\n const clearBgSubs = (): void => {\n for (const u of bgUnsubs) u()\n bgUnsubs = []\n }\n let bgOutputTimer: ReturnType<typeof setInterval> | undefined\n let bgFollowedJobId: string | undefined\n let bgSubagentTimer: ReturnType<typeof setInterval> | undefined // M72\n let bgSubagentId: string | undefined // M72\n // 详情面板去抖:500ms 轮询每帧都回灌会让运行中 live 区无谓重绘→闪动。只在内容签名变化时才回灌。\n let lastBgOutputSig: string | undefined\n const stopBgOutputPoll = (): void => {\n if (bgOutputTimer) { clearInterval(bgOutputTimer); bgOutputTimer = undefined }\n if (bgFollowedJobId) { globalAgentJobRegistry.retain(bgFollowedJobId, false); bgFollowedJobId = undefined }\n lastBgOutputSig = undefined\n }\n // M72:sub-agent 对话流模式轮询清理。\n const stopBgSubagentPoll = (): void => {\n if (bgSubagentTimer) { clearInterval(bgSubagentTimer); bgSubagentTimer = undefined }\n if (bgSubagentId) {\n globalAgentObservability?.unpin(bgSubagentId)\n if (globalAgentJobRegistry.get(bgSubagentId)) globalAgentJobRegistry.retain(bgSubagentId, false)\n bgSubagentId = undefined\n }\n }\n\n const onBackgroundKill = (id: string): void => {\n if (globalAgentJobRegistry.get(id)) { globalAgentJobRegistry.cancel(id); return }\n if (globalAgentObservability.abort(id)) { return }\n void globalProcessRegistry.kill(id)\n }\n\n /**\n * d 键:从面板删除一项已终结的委派。按 id 归属分派到对应 registry.remove——三类注册表\n * 互斥存在(同一 id 只会在其中一处),依次尝试即可,不需要额外的类型标记。\n * DelegatesPane 侧已用 isTaskActive 门控只对终态项渲染 d 提示,这里不重复校验终态\n * (registry.remove 本身对运行中项也安全——只影响 UI 可见性,不误杀真实进程/作业)。\n */\n const onBackgroundDelete = (id: string): void => {\n if (globalAgentJobRegistry.get(id)) { globalAgentJobRegistry.remove(id); return }\n if (globalProcessRegistry.get(id)) { globalProcessRegistry.remove(id); return }\n globalAgentObservability.remove(id)\n }\n\n const onBackgroundApply = (id: string): void => {\n const tui = getTui()\n const say = (msg: string) => tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text', text: msg }] })\n const outcome = app.agentJobs.apply(id)\n if (outcome.applied) {\n const zone = outcome.forbiddenZone?.length\n ? i18next.t('cmd.bgForbiddenZone', { count: outcome.forbiddenZone.length, files: outcome.forbiddenZone.join('\\n '), id: id })\n : ''\n say(i18next.t('cmd.bgJobApplied', { id: id, message: outcome.message, zone: zone }))\n if (outcome.capabilityTouched) {\n void app.reloadCapabilities().then(\n (r) => {\n tui.plugins.setPluginRegistrations(app.getPluginRegistrations())\n tui.info.showToast(i18next.t('cmd.titleReload'), i18next.t('cmd.reloadDone', { mcpTools: r.mcpTools, models: r.models, agents: r.agents, teams: r.teams }))\n },\n (e) => say(i18next.t('cmd.bgAutoReloadFailed', { msg: e instanceof Error ? e.message : String(e) })),\n )\n }\n } else {\n const conflicts = outcome.conflictPaths.length ? i18next.t('cmd.bgConflictFiles', { joined: outcome.conflictPaths.join('\\n ') }) : ''\n say(i18next.t('cmd.bgJobIncomplete', { id: id, status: outcome.status, message: outcome.message, conflicts: conflicts }))\n }\n }\n\n const onDelegateSupply = (id: string, text: string): void => {\n app.agentJobs.supply(id, text)\n }\n\n const onBackgroundViewOutput = (id: string): void => {\n const tui = getTui()\n const readFrame = async (): Promise<void> => {\n // 去抖:内容签名(状态+正文+诊断)未变则跳过回灌,避免运行态 live 区无谓重绘闪动。\n const emit = (view: NonNullable<Parameters<typeof tui.stores.tasks.setBackgroundOutput>[0]>): void => {\n const sig = `${view.status}|${view.text}|${view.responseText ?? ''}|${view.diff ?? ''}`\n if (sig === lastBgOutputSig) return\n lastBgOutputSig = sig\n tui.stores.tasks.setBackgroundOutput(view)\n }\n const job = globalAgentJobRegistry.get(id)\n if (job) {\n const status =\n job.status === 'running' || job.status === 'needs_input' ? 'running' as const\n : job.status === 'applied' ? 'exited' as const\n : job.status === 'failed' ? 'failed' as const\n : 'killed' as const\n // 人审 diff 入口:ready(apply 前审)/ applied(apply 后回看)时把完整 diff 交给\n // 详情视图渲染(Diff tab)。此前 diff 全程不可见,用户只能凭 +N/-M stat 按 a。\n const diff = (job.status === 'ready' || job.status === 'applied') && job.diff?.trim()\n ? job.diff\n : undefined\n emit({ id, command: job.title, status, text: readTaskOutputTail(job.outputPath, 20), prompt: globalAgentObservability.get(id)?.prompt, diff })\n if (status !== 'running') stopBgOutputPoll()\n return\n }\n const obs = globalAgentObservability.get(id)\n if (obs) {\n const msgs = globalAgentObservability.getMessages(id) ?? []\n const chatMsgs = agentMessagesToChatMessages(msgs)\n const streamText = globalAgentObservability.getStreamingText(id)\n const transcript = formatTranscriptText(chatMsgs, streamText)\n const now = Date.now()\n const diag = formatAgentDiagnosticsText(observationRow(obs), now)\n const timeline = formatTurnTimeline(globalAgentObservability.getTurns(id), now)\n const responseText = [\n diag.length ? diag.join('\\n') : '',\n timeline.length ? timeline.join('\\n') : '',\n ].filter(Boolean).join(`\\n${'─'.repeat(40)}\\n\\n`)\n const status =\n obs.status === 'done' ? 'exited' as const\n : obs.status === 'error' || obs.status === 'aborted' ? 'failed' as const\n : 'running' as const\n emit({ id, command: `@${obs.agentName}`, status, text: transcript, messages: chatMsgs, streamText, responseText, prompt: obs.prompt })\n if (status !== 'running') stopBgOutputPoll()\n return\n }\n const p = globalProcessRegistry.get(id)\n if (!p) { stopBgOutputPoll(); tui.stores.tasks.setBackgroundOutput(null); return }\n const status = p.status === 'running' ? 'running' : p.status === 'exited' ? 'exited' : 'killed'\n let text = ''\n if (p.logPath) {\n try { text = (await readFile(p.logPath, 'utf-8')).split('\\n').slice(-20).join('\\n').trimEnd() } catch { text = '' }\n }\n emit({ id, command: p.command, status, text })\n if (status !== 'running') stopBgOutputPoll()\n }\n stopBgOutputPoll()\n if (globalAgentJobRegistry.get(id)) { globalAgentJobRegistry.retain(id, true); bgFollowedJobId = id }\n void readFrame()\n bgOutputTimer = setInterval(() => { void readFrame() }, 500)\n }\n\n const onBackgroundOutputClose = (): void => { stopBgOutputPoll() }\n\n // M72:s 键切换到 sub-agent 对话流。\n const onBackgroundSwitch = (id: string): void => {\n const tui = getTui()\n stopBgSubagentPoll()\n const genContext = (): void => {\n const msgs = globalAgentObservability?.getMessages(id)\n const obs = globalAgentObservability?.get(id)\n // 构造 SubagentContext(分叉:swarm 成员仅 observability-only)\n let title = obs?.agentName ?? ''\n const job = globalAgentJobRegistry.get(id)\n if (job) title = job.title || title\n const chatMsgs = msgs ? agentMessagesToChatMessages(msgs) : []\n const turns = globalAgentObservability?.getTurns(id)\n const streamText = globalAgentObservability?.getStreamingText(id)\n const now = Date.now()\n const ctx: import('@x-otto/tui').SubagentContext = {\n id,\n title,\n messages: chatMsgs,\n status: job?.status ?? 'running',\n diagnostics: obs ? formatAgentDiagnosticsText(observationRow(obs), now) : [],\n timeline: turns ? formatTurnTimeline(turns, now) : [],\n streamText,\n }\n tui.panes.switchToSubagentContext(ctx)\n }\n // 消息保护\n globalAgentObservability?.pin(id)\n if (globalAgentJobRegistry.get(id)) {\n globalAgentJobRegistry.retain(id, true)\n }\n bgSubagentId = id\n genContext()\n // 运行中 agent:500ms 轮询刷新\n bgSubagentTimer = setInterval(() => { genContext() }, 500)\n }\n\n // M72:esc 退出 sub-agent 对话流(停轮询 + unpin + retain(false))。\n const onSubagentExit = (): void => {\n stopBgSubagentPoll()\n getTui().panes.switchToSubagentContext(null)\n }\n\n return {\n clearBgSubs,\n stopBgOutputPoll,\n stopBgSubagentPoll,\n setBgUnsubs: (unsubs) => { bgUnsubs = unsubs },\n handlers: {\n onBackgroundKill,\n onBackgroundApply,\n onDelegateSupply,\n onBackgroundViewOutput,\n onBackgroundOutputClose,\n onBackgroundSwitch,\n onBackgroundDelete,\n onSubagentExit,\n },\n }\n}\n","/**\n * 模型管理 UI 管线(model/refresh/list 命令的 TUI 交互)。\n */\nimport { createModelUsabilityGate } from '@x-otto/ai'\nimport { i18next } from '@x-otto/tui'\nimport type { TuiHandle } from '@x-otto/tui'\nimport { formatEffortLabel } from './interactive-utils'\nimport { recordModelSwitch } from './model-effort-record'\n\nconst fmtStatusModel = (id: string, cw?: number, effort?: string): string => {\n const effortLabel = formatEffortLabel(effort)\n const name = effortLabel ? `${id}·${effortLabel}` : id\n if (!cw) return name\n const win = cw >= 1_000_000 ? `${(cw / 1_000_000).toFixed(1)}M` : `${Math.round(cw / 1000)}K`\n return `${name} (${win})`\n}\n\n/** /model 选择器「最近使用」分组的最大保留条数(MRU)。 */\nexport const MAX_RECENT_MODELS = 5\n\n/**\n * 把 modelId 推到 MRU 列表最前(纯函数,供 applyModel 与单测复用)。\n * 已存在则去重后移到最前(不产生重复项),并裁剪到 MAX_RECENT_MODELS 条。\n */\nexport function pushRecentModel(recent: readonly string[], modelId: string): string[] {\n return [modelId, ...recent.filter((id) => id !== modelId)].slice(0, MAX_RECENT_MODELS)\n}\n\n/**\n * 从提交文本中剥离全部 `[@model:id]` chip(RFC-036 @model sigil)。\n *\n * 返回剥离后的文本 + 提取的模型 id 列表(保持出现顺序;`\\]` 转义还原)。\n * 纯函数,供 onSubmit 与单测复用。语义(终局修复):@model chip = **真实切换当前\n * 会话模型**(与 `/model` 命令同一 applyModel 路径),不再是提示词前缀注入——\n * 后者对当前回合的执行模型零作用力(LLM 无法切换自己),是\"@model 无效\"的根因。\n */\nexport function extractModelChips(text: string): { text: string; modelIds: string[] } {\n const modelChipRe = /\\[@model:((?:\\\\\\]|[^\\]])+)\\]/g\n const modelIds: string[] = []\n const out = text\n .replace(modelChipRe, (_m, id: string) => {\n modelIds.push(id.replace(/\\\\\\]/g, ']'))\n return ''\n })\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n return { text: out, modelIds }\n}\n\n/** applyModel 闭包所需依赖(可变状态通过 wrapper 对象传递,避免解构值的值拷贝问题)。 */\nexport interface ApplyModelDeps {\n modelReady: { value: boolean }\n currentModelId: { value: string | undefined }\n contextWindow: { value: number }\n tuiRef: { value: Awaited<ReturnType<typeof import('@x-otto/tui').startTui>> | undefined }\n pushBudgetNow: { value: (() => void) | undefined }\n app: import('@x-otto/coding').App\n /** 惰性取当前 session(非按值捕获——session 变量可能被 sessionRef setter 重新赋值)。 */\n getSession: () => import('@x-otto/coding').AgentSession\n options: { value: { model?: string; continueSession?: string; empty?: boolean } | undefined }\n fmtDisplayModel: (id: string | undefined) => string\n /** RFC-091 M3:模型变更后通知 engine 进程(setModel / recordCompaction) */\n onEngineModelAction?: (cmd: import('@x-otto/interchange').EngineCommand) => void\n}\n\nexport function createApplyModel(deps: ApplyModelDeps) {\n const { modelReady, currentModelId, contextWindow, tuiRef, pushBudgetNow, app, getSession, options, fmtDisplayModel, onEngineModelAction } = deps\n\n return async (modelId: string, opts?: { persist?: boolean; silent?: boolean }): Promise<string> => {\n const session = getSession()\n const oldModelId = session.model?.id\n if (options.value) options.value.model = modelId\n currentModelId.value = modelId\n\n const model = app.modelRegistry.find(modelId)\n // 占位模型拒绝:__otto_setup__ 仅用于无模型启动桥接,不可被用户选中为活跃模型。\n if (model && model.id === '__otto_setup__') {\n return `Model \"${modelId}\" is a setup placeholder — pick a real model with /model first.`\n }\n\n if (opts?.persist !== false) {\n try {\n // RFC-090 M2/D3:/model 选择必须写「新会话冷启读取的最高优先级键」。冷启默认模型 =\n // session-config-resolver 的 `model_slots?.default ?? model`——model_slots.default 优先级更高。\n // 故有 model_slots 时同步 merge 写 default(保留 slots/compact/subagent_default_slot 等其它字段),\n // 并保留顶层 model 供 resolveEffectiveModel/ensureModel 读者;二者指向同一选择、跨重启 sticky。\n // 不改 resolver 的优先级/defaultModel 基准(RFC-088 槽位契约,见 RFC-090 §6 规则4)。\n const patch: { model: string; model_slots?: unknown; recent_models?: string[] } = { model: modelId }\n const slots = app.settings.get('model_slots')\n if (slots) patch.model_slots = { ...slots, default: modelId }\n // 「最近使用」MRU(最多 MAX_RECENT_MODELS 条)——仅记录注册表命中的真实切换\n // (registry 未命中走下方 fail-loud 分支,不写入 recent_models,避免污染历史)。\n if (model) {\n const recent = app.settings.get('recent_models') ?? []\n patch.recent_models = pushRecentModel(recent, modelId)\n }\n await app.settings.persist(patch as Parameters<typeof app.settings.persist>[0])\n } catch {\n /* 持久化失败不阻断本轮切换 */\n }\n }\n\n if (model) {\n session.model = model\n // RFC-326 前置清障:`modelReady` 只在模型真正被应用到 session 后才置位。\n //\n // 此前本行写在函数入口(`currentModelId` 赋值旁),意味着两条 fail-loud 分支\n // ——占位模型 `__otto_setup__` 与 registry 未命中——**也会**把 ready 翻成 true\n // 并原样返回。后果是 `setup-wizard.ts` 里那段专门为「凭据无效」写的防护\n // `await applyModel(id); if (!ctx.state.modelReady) throw ...`\n // 恒不成立(两者共享同一个 ref,见 interactive.ts 的 modelReadyRef),成为一行\n // 永不执行的死代码——注释写着\"applyModel 成功不代表 modelReady 翻为 true\",\n // 实现却保证了它总是 true。向导因此会在模型并未真正装上时宣布 All set,\n // 把失败推迟到用户敲下第一条 prompt 时才由 ensureModelUsableForPrompt 暴露。\n //\n // 注意边界:此处只保证\"模型存在且已装载到 session\",**不代表凭据有效**。\n // 凭据/订阅风险仍由 `createModelUsabilityGate` 判定(见 ensureModelUsableForPrompt)。\n // 收敛成单一可失效真源是 RFC-326 M1 的范围,本次只消除\"未应用却报 ready\"的撒谎态。\n modelReady.value = true\n // RFC-130 M131-2:`/model` 只改 session.model,tools 仍是 createSession 快照——\n // 切到 supportsTools===false(Cursor)须清空,切回须恢复 MCP/内置工具集。\n if (modelId !== oldModelId) {\n app.syncSessionToolsFor(session.id)\n // 三层一致记录(终局 2026-07-31):对话流标记 + L1 会话 entries 持久化\n // (resume 后转录可见)+ trace 审计。仅真实切换时记录(oldModelId 不同),\n // 与页脚刷新/工具集同步同一门控。\n recordModelSwitch(\n {\n tui: tuiRef.value ?? ({ messages: { pushMessage: () => {} } } as unknown as Pick<TuiHandle, 'messages'>),\n session,\n traceStore: app.storage.traceStore,\n },\n {\n from: oldModelId,\n to: model.id,\n label: fmtStatusModel(model.id, model.contextWindow, session.thinkingLevel),\n },\n )\n }\n // RFC-124 P0-C(RFC-105 D7 懒激活):`onProvider:<id>` 此前只有 schema 允许声明,\n // 无任何真实触发点——声明了 `activationEvents:[\"onProvider:<api>\"]` 的插件,即使用户\n // 切换到了该插件贡献的 model,其代码轴仍不会被激活。model.api 对插件贡献的 model 是\n // 命名空间化的 `<pluginId>:<name>`(RFC-105 D2),切换模型即是该 provider 首次/再次\n // 被选用的天然事件时机,fire-and-forget(不阻塞模型切换主流程)。\n //\n // 复审发现修复(RFC-124 P0-C finding C-2):内置 api(`anthropic-messages`/\n // `openai-responses` 等)是平键、不含冒号;插件 api 强制 `<pluginId>:<name>` 命名空间\n // 前缀(`pluginProviderApi`,RFC-105 D3)。此前无条件对所有模型切换都 fire,意味着\n // 切到**内置**模型也会 fire `onProvider:anthropic-messages` 这类系统级事件字符串——\n // 插件可声明 `activationEvents:[\"onProvider:anthropic-messages\"]` 蹭用户\"切到内置模型\"\n // 这个必然会发生的操作,绕过\"该插件自己的 provider 被使用\"这一懒激活语义、零用户感知地\n // 提前激活自己的代码轴(若只声明非高危能力如 hooks,甚至不需要过任何能力确认)。\n // 只对命名空间化的插件 api(含冒号)触发,内置 api 不触发。\n if (model.api.includes(':')) {\n void app.firePluginActivationEvent?.(`onProvider:${model.api}`)\n }\n onEngineModelAction?.({ kind: 'setModel', modelId: modelId })\n // RFC-090 M1/D1:applyModel 是页脚的唯一权威——提交成功后把页脚刷成「已提交的执行模型」。\n // 覆盖 model-auth.ts 抢先写的乐观值,也覆盖 ensureModel 自愈/无凭证回退这类之前不刷页脚的\n // caller(消除反向 stale:冷启 deepseek→首 prompt 自愈 fable-5 后页脚仍显 deepseek 的问题)。\n // silent 只压 toast,不压页脚刷新——回退后的真实模型仍须如实显示。\n tuiRef.value?.stores.tasks.setModelName(fmtStatusModel(model.id, model.contextWindow, session.thinkingLevel))\n // RFC-097:切模型时同步刷新输出上限,供 Spinner 进度百分比分母使用。\n tuiRef.value?.engine.pushEngineEvent({ kind: 'maxOutputTokens', n: model.maxOutputTokens })\n // 切换模型时按新 provider 的凭据来源决定是否清除 usage 徽标:\n // - 切换到 OAuth provider → 保留旧徽标(同一 provider 额度有效,下一轮 done 事件刷新)\n // - 切换到 API key / env provider → 清除(这些 provider 不产生 unified 头族,\n // 若不清除,从 Anthropic OAuth 切到 DeepSeek API key 后状态栏残留旧额度数据)。\n if (app.authStore?.getAuthSource) {\n void app.authStore\n .getAuthSource(model.provider)\n .then((source) => {\n if (source !== 'oauth') tuiRef.value?.stores.notification.setProviderUsage(null)\n })\n .catch(() => {\n tuiRef.value?.stores.notification.setProviderUsage(null)\n })\n } else {\n tuiRef.value?.stores.notification.setProviderUsage(null)\n }\n\n if (modelId !== oldModelId && app.storage.memory?.reconfigure) {\n app.storage.memory.reconfigure({ contextWindow: model.contextWindow, maxOutput: model.maxOutputTokens })\n try {\n const ctx = session.session.buildContext()\n // 真收窄边界:会话存 AgentMessage[](含 SystemAgentMessage),memory 端口在 Message[] 上做\n // token 适配——此处显式收窄(系统消息在 convertToLLM 边界已被过滤,运行期实为 Message)。\n const result = await app.storage.memory.ensureFitsWindow?.(\n ctx.messages as unknown as import('@x-otto/ai').Message[],\n session.id,\n )\n if (result?.compacted && result.summary && result.replacement) {\n // Message ⊆ AgentMessage:直接拓宽,无需 cast。\n session.session.recordCompaction(result.summary as string, result.replacement as never)\n // RFC-303 D8:字段类型已修正为 `unknown[]`(真实语义),不再需要 `as unknown as string`\n // 双重断言掩盖类型声明与实际值的不一致。\n onEngineModelAction?.({ kind: 'recordCompaction', summary: result.summary as string, replacement: result.replacement as unknown[] })\n const win = model.contextWindow >= 1_000_000\n ? `${(model.contextWindow / 1_000_000).toFixed(1)}M`\n : `${Math.round(model.contextWindow / 1000)}k`\n tuiRef.value?.info.showToast(i18next.t('cmd.titleModel'), i18next.t('cmd.modelCompactedFor', { window: win }))\n }\n } catch (err) {\n console.error(`\\x1b[3m\\x1b[2m[model-switch] compaction skipped: ${err instanceof Error ? err.message : String(err)}\\x1b[0m`)\n }\n }\n\n contextWindow.value = model.contextWindow\n pushBudgetNow.value?.()\n\n if (modelId !== oldModelId && !opts?.silent) {\n tuiRef.value?.info.showToast(i18next.t('cmd.titleModel'), i18next.t('model.switchedTo', { label: fmtDisplayModel(modelId) }))\n }\n\n return `Model → ${modelId} (active next turn).`\n }\n // RFC-090 M1/D2:注册表未命中 = 切换失败,fail-loud。把页脚重置回「实际执行模型」,撤销\n // model-auth.ts 抢先写的乐观值,绝不留「页脚显示新模型 + 执行走旧模型」的撒谎态。\n if (session.model) {\n tuiRef.value?.stores.tasks.setModelName(fmtStatusModel(session.model.id, session.model.contextWindow, session.thinkingLevel))\n tuiRef.value?.engine.pushEngineEvent({ kind: 'maxOutputTokens', n: session.model.maxOutputTokens })\n }\n return `Model \"${modelId}\" not in registry — switch NOT applied (still ${session.model?.id ?? 'unset'}). Check the id or /auth the provider.`\n }\n}\n\nexport interface EnsureModelDeps {\n /** 惰性取当前 session(非按值捕获——session 变量可能被 sessionRef setter 重新赋值)。 */\n getSession: () => import('@x-otto/coding').AgentSession\n app: import('@x-otto/coding').App\n tuiRef: { value: Awaited<ReturnType<typeof import('@x-otto/tui').startTui>> | undefined }\n options: { value: { model?: string; continueSession?: string } | undefined }\n fmtDisplayModel: (id: string | undefined) => string\n applyModel: (modelId: string, opts?: { persist?: boolean; silent?: boolean }) => Promise<string>\n /** 「退出」按钮回调——取消当前对话,退出交互模式。 */\n onExitSession?: () => void\n /** 「去登录」按钮回调——直接打开 provider 登录窗口。 */\n onLogin?: (provider: string) => void\n}\n\nexport function createEnsureModelUsableForPrompt(deps: EnsureModelDeps) {\n const { getSession, app, tuiRef, options, fmtDisplayModel, applyModel } = deps\n let modelValidated = false\n // 终局 review 修复:fallback 选型此前只查「有没有凭证」(hasCredential),选中的\n // 替代模型可能持有订阅绑定的 OAuth token(如 Anthropic Claude Code 的 sk-ant-oat)——\n // 这类凭证专供官方客户端自身 system prompt 使用,subagent/普通请求会被 429 拒绝。\n // 用户会看到\"已切换到 X\"却发现 X 一用就限流,体验上是\"看起来修好了、实际没修好\"。\n // 改用与 packages/ai 委派选型同一套 usability gate(认证 + OAuth 429 风险双重判定),\n // 与 ModelResolver.pickUsableFallback 保持一致的可用性标准(见 model-resolver.ts)。\n const isModelUsable = createModelUsabilityGate(app.authStore)\n\n return async (): Promise<void> => {\n if (modelValidated) return\n modelValidated = true\n\n const session = getSession()\n const current = session.model\n if (!current) return\n\n const usable = async (model: { provider: string; api: string }): Promise<boolean> => {\n try {\n return (await isModelUsable(model)).usable\n } catch {\n return false\n }\n }\n\n // 修复(2026-07-21 回归,同日 96a854650 引入):`current` 是**主会话已激活、正在\n // 使用的模型**——SessionConfigResolver 创建会话时经 `ModelResolver.resolveModel\n // (gated=false)` 解析,该路径明确「主会话首个候选原样胜出,不过 usability 闸」\n // (见 model-resolver.ts 同名方法注释)。本函数却用**更严格**的完整闸(含\n // oauth-429-risk 订阅风险判定)重新校验 `current`,标准前后矛盾——原本\"不过闸即\n // 采信\"的模型,一开口发第一个 prompt 就被判定\"不可用\",静默切换到别的模型,\n // 对话流不停顿,用户毫无察觉(catch 场景:Claude 明明在正常工作,却被强制换掉)。\n //\n // 根因:Anthropic 订阅 OAuth token(sk-ant-oat)在主会话场景下经\n // `plugin-anthropic` 的 `customizeRequest`(`prependAnthropicOAuthIdentity` 等\n // 官方 CLI 客户端伪装)已经规避了 429——`oauth-429-risk` 这个判据是为**子代理\n // 委派**场景设计的(RFC-061,子代理有自己的 system prompt,伪装未必生效/涉及\n // 用户订阅额度归属问题),不适用于主会话「已经在正常工作的当前模型」。\n //\n // 修复:`current` 只检查是否有凭证(`hasCredential`),不做订阅风险二次判定——\n // 与其被创建时采用的\"主会话不过闸\"标准保持一致。`found`(下方自动挑选一个从未\n // 使用过的候选模型)仍走完整 `usable()`(含 429 风险规避),避免静默选中一个\n // 有已知限流风险的陌生模型作为\"安全回退\"。\n const hasCredential = async (model: { provider: string; api: string }): Promise<boolean> => {\n try {\n const source = await app.authStore.getAuthSource(model.provider)\n return source !== null\n } catch {\n return false\n }\n }\n\n const configuredId = options.value?.model ?? app.settings.get('model')\n if (typeof configuredId !== 'string' || !configuredId) return\n\n const desired = app.modelRegistry.find(configuredId)\n if (desired && (await hasCredential(desired))) {\n if (desired.id !== current.id) await applyModel(desired.id)\n return\n }\n\n // 终局 review 修复:向用户解释「为何」配置模型不可用,而非只说\"不可用\"——\n // ModelResolution.rejected 的溯源此前只进 logger.debug(session-config-resolver.ts/\n // task-runner.ts 注释明确记录\"无 UI 消费方\"),用户看到 fallback 弹窗却猜不出该\n // /auth 还是模型名打错了。本函数已经掌握判断所需的全部信息(desired 是否存在于\n // registry、其可用性判定结果),无需引入新依赖即可补上这行原因文案。\n const fallbackReason = !desired\n ? i18next.t('model.reasonNotFound')\n : i18next.t('model.reasonUnauthenticated', { provider: desired.provider })\n\n let active = current\n // current(当前激活模型)同上:只查凭证,不查订阅 429 风险——它已经在正常工作,\n // 不该被子代理场景专用的严格闸拦下来。找**新**候选替代它时才用完整 usable()\n // (含 429 风险规避),避免自动选中一个已知会限流的陌生模型。\n if (!(await hasCredential(current))) {\n const all = app.modelRegistry.getAll().filter(m => m.id !== '__otto_setup__')\n let found: typeof current | undefined\n for (const m of all) {\n if (await usable(m)) {\n found = m\n break\n }\n }\n found ??= all[0]\n // 修复(#2/4 静默切换时序错位):候选模型先找到,但只在用户点「确认」后才\n // 真正执行 applyModel——此前 silent 切换发生在 openModal 之前,用户点「取消」\n // 退出时已经被换掉的模型无法恢复。现在 active 只更新为候选值供弹窗文案显示。\n if (found) {\n active = found\n }\n }\n\n const fallbackModelId = active.id\n tuiRef.value?.modal.openModal({\n kind: 'confirm',\n title: i18next.t('model.title'),\n message: [\n `${fmtDisplayModel(configuredId)} → ${fmtDisplayModel(active.id)}`,\n '',\n fallbackReason,\n '',\n i18next.t('model.hint'),\n ],\n confirmLabel: i18next.t('model.permanent'),\n extraLabel: i18next.t('model.login'),\n cancelLabel: i18next.t('model.dismiss'),\n onConfirm: () => {\n // confirm 后才真正切换模型 + 持久化(修复静默切换时序错位)。silent 不必设——\n // 弹窗本身已是用户可见的确认动作,applyModel 自带的 toast 给出最终确认反馈。\n void applyModel(fallbackModelId, { persist: true })\n },\n onExtra: () => {\n if (deps.onLogin && desired?.provider) {\n deps.onLogin(desired.provider)\n } else {\n tuiRef.value?.info.showToast(\n i18next.t('model.login'),\n i18next.t('model.fallbackLoginToast', { provider: desired?.provider ?? '' }),\n )\n }\n },\n onCancel: () => {\n deps.onExitSession?.()\n },\n })\n }\n}","/**\n * theme-preset-wiring.ts — RFC-277 M5 spike + M6 生产接线:把插件贡献的主题预设\n * (`@x-otto/coding` `collectPluginThemePresets` 的纯数据产出)适配进 `@x-otto/tui` 的\n * `ThemeRegistry`,并在 M6 起接入 CLI 启动主路径。\n *\n * 为什么单独一个文件:`@x-otto/coding` 不依赖 `@x-otto/tui`(架构边界,装载层产出纯数据形状),\n * `@x-otto/cli` 同时依赖两者,是唯一能做这个适配的包(同 `theme-markdown-wiring.ts`/\n * `theme-color-role-drift.test.ts` 的既有跨包桥接模式)。\n *\n * M5 阶段只验证 register/unregister/fallback 生命周期是否可行(不做 marketplace/\n * distribution)。M6 新增 `buildThemeRegistry`——在 CLI 启动时装配一次真实 registry(内置\n * dark/light + 全部活跃插件贡献),供 `startTui`/`app-props` 传给 `ThemeProvider`。\n */\nimport { createLogger } from '@x-otto/shared'\nimport { createBuiltinThemeRegistry, ThemeRegistry } from '@x-otto/tui'\nimport type { ThemePreset, ThemePresetSource } from '@x-otto/tui'\nimport { collectPluginThemePresets } from '@x-otto/coding'\nimport type { App, PluginThemePresetEntry } from '@x-otto/coding'\n\nconst logger = createLogger('@x-otto/cli:theme-preset-wiring')\n\n/**\n * 把单个插件贡献条目注册进 registry。effective id 由 `ThemeRegistry.register()` 内部\n * 按 `${extensionId}/${localId}` 生成(M1 既有行为),本函数只负责数据形状适配——\n * `colors`/`markdown` 是插件声明时已过 manifest schema 校验的纯数据(装载层不重复校验,\n * 契约同 `wireMarkdownThemeOverrides`)。\n */\nexport function registerPluginThemePreset(registry: ThemeRegistry, entry: PluginThemePresetEntry): ThemePreset {\n const source: ThemePresetSource = { kind: 'extension', extensionId: entry.pluginId }\n return registry.register(source, {\n localId: entry.localId,\n label: entry.label,\n appearance: entry.appearance,\n // 插件声明的完整调色板已过 manifest schema 校验(27 字段齐全,hex 格式合法),\n // 但 ThemeRegistry.ThemePreset['colors'] 类型是 tui 侧 ThemeColors——两者结构一致,\n // 只是分属两个包各自的 zod schema 推导类型,这里做跨包边界的显式收口转换。\n colors: entry.colors as unknown as ThemePreset['colors'],\n markdown: entry.markdown as ThemePreset['markdown'],\n })\n}\n\n/**\n * 批量注册一个插件贡献的全部主题预设,返回成功注册的 preset 列表(跳过冲突条目,不中止\n * 整批——同一插件内一条 preset id 冲突不该拖累其余条目,见突变测试钉死此行为)。\n */\nexport function registerPluginThemePresets(\n registry: ThemeRegistry,\n entries: readonly PluginThemePresetEntry[],\n): ThemePreset[] {\n const registered: ThemePreset[] = []\n for (const entry of entries) {\n try {\n registered.push(registerPluginThemePreset(registry, entry))\n } catch (err) {\n // fail-soft:单条注册失败(如与已注册 preset id 冲突)不阻断其余条目,同\n // collectPluginThemePresets 系列 loader 的既有 fail-soft 纪律。独立审核 finding:\n // 此前静默吞错零日志——加 warn 以便区分\"预期的 id 冲突\"与\"非预期的类型/运行时错误\"。\n logger.warn(\n { pluginId: entry.pluginId, localId: entry.localId, err: String(err) },\n 'theme preset registration failed, skipped',\n )\n }\n }\n return registered\n}\n\n/** 卸载某插件贡献的全部主题预设(幂等)。返回移除条目数。 */\nexport function unregisterPluginThemePresets(registry: ThemeRegistry, pluginId: string): number {\n return registry.unregisterBySource({ kind: 'extension', extensionId: pluginId })\n}\n\n/**\n * RFC-277 M6:CLI 启动期装配一次完整 `ThemeRegistry`——内置 dark/light + 全部活跃插件\n * (`app.activePlugins()`)贡献的主题预设。装配是\"一次性快照\"(启动时求值),不是热重载\n * 订阅——插件在运行期新增/卸载 theme preset contribution 需要重启才能反映(同 M5 spike\n * 边界:不做完整插件热重载生命周期管理,只验证/落地\"启动时装配一次\"这条路径)。\n */\nexport function buildThemeRegistry(app: Pick<App, 'activePlugins'>): ThemeRegistry {\n const registry = createBuiltinThemeRegistry()\n const entries = collectPluginThemePresets(app.activePlugins())\n registerPluginThemePresets(registry, entries)\n return registry\n}\n","/**\n * backend-methods.ts —— RFC-349:schedule 面板 in-process backend methods。\n *\n * 与 browser-use/mobile-use 的区别:那些插件的 backend methods 跑在 MCP server 子进程里。\n * schedule 的 SchedulerService 依赖 `AgentJobService`/`sessionRef`/`notifySubscriber`——\n * 这些只能存活在 App 主进程,无法搬到子进程。故本文件注册为 **in-process backend handler**\n * (经 `PanelBackendController.registerInProcessBackend`),面板 `usePanelBackend().call()`\n * 走同一条路径,但执行发生在 App 主进程内。\n *\n * 面板侧调用经 `toBackendWireMethod` 统一加 `backend/` 前缀,本文件接收时剥离前缀。\n */\nimport type { SchedulerService } from '@x-otto/schedule'\nimport { parseCron } from '@x-otto/schedule'\nimport type { ScheduledTask } from '@x-otto/schedule'\n\n/** 剥离 `backend/` 前缀(panel-backend-runtime 的 toBackendWireMethod 加的)。 */\nfunction stripBackendPrefix(method: string): string {\n return method.startsWith('backend/') ? method.slice('backend/'.length) : method\n}\n\n/** 面板状态快照(经 pushState 推送或 call 返回)。 */\nexport interface SchedulePanelState {\n tasks: ScheduledTask[]\n runningTaskIds: string[]\n isFireOwner: boolean\n fireOwnerFailureReason: string | undefined\n}\n\nexport function createScheduleBackendHandler(scheduler: SchedulerService): {\n call: (method: string, params: Record<string, unknown>) => Promise<unknown>\n getSnapshot: () => SchedulePanelState\n} {\n const getSnapshot = (): SchedulePanelState => ({\n tasks: scheduler.list(),\n runningTaskIds: [...scheduler.getRunningTaskIds()],\n isFireOwner: scheduler.isFireOwner(),\n fireOwnerFailureReason: scheduler.fireOwnerFailureReason(),\n })\n\n const call = async (\n method: string,\n params: Record<string, unknown>,\n ): Promise<unknown> => {\n const name = stripBackendPrefix(method)\n\n switch (name) {\n case 'schedule.list':\n return getSnapshot()\n\n case 'schedule.add': {\n const input = params as unknown as {\n name: string\n prompt: string\n cronExpression: string\n recurring: boolean\n }\n const task = scheduler.add({\n name: input.name,\n prompt: input.prompt,\n cronExpression: input.cronExpression,\n recurring: input.recurring,\n origin: 'user' as const,\n })\n return { ok: true, taskId: task.id, name: task.name }\n }\n\n case 'schedule.update': {\n const input = params as unknown as {\n taskId: string\n name?: string\n prompt?: string\n cronExpression?: string\n enabled?: boolean\n }\n const patch: { name?: string; prompt?: string; cronExpression?: string; enabled?: boolean } = {}\n if (input.name !== undefined) patch.name = input.name\n if (input.prompt !== undefined) patch.prompt = input.prompt\n if (input.cronExpression !== undefined) patch.cronExpression = input.cronExpression\n if (input.enabled !== undefined) patch.enabled = input.enabled\n const ok = scheduler.update(input.taskId, patch)\n return { ok }\n }\n\n case 'schedule.runNow': {\n const { taskId } = params as unknown as { taskId: string }\n scheduler.runNow(taskId)\n return { ok: true }\n }\n\n case 'schedule.toggle': {\n const { taskId, enabled } = params as unknown as { taskId: string; enabled: boolean }\n const ok = scheduler.update(taskId, { enabled })\n return { ok }\n }\n\n case 'schedule.cancel': {\n const { taskId } = params as unknown as { taskId: string }\n const result = scheduler.cancel(taskId)\n return result\n }\n\n case 'schedule.remove': {\n const { taskId } = params as unknown as { taskId: string }\n const removed = scheduler.remove(taskId)\n return { ok: removed }\n }\n\n case 'schedule.readLogs': {\n const { taskId, limit } = params as unknown as { taskId: string; limit?: number }\n const entries = scheduler.readFireLogs(taskId, limit ?? 20)\n return { entries }\n }\n\n case 'schedule.parseCron': {\n const { expr } = params as unknown as { expr: string }\n try {\n const next = parseCron(expr)\n return { ok: true, next: next.toISOString() }\n } catch (err) {\n return { ok: false, error: err instanceof Error ? err.message : String(err) }\n }\n }\n\n default:\n throw new Error(`unknown schedule backend method: ${name}`)\n }\n }\n\n return { call, getSnapshot }\n}\n","/**\n * 定时任务 TUI 交互管线(状态栏/事件/toast 接线)。\n */\n// RFC-161 M2:SchedulerService 等实现已迁至 @x-otto/schedule,直连新真源\n// (coding 的 re-export shim 仅供尚未迁移的其余消费方使用)。\nimport { SchedulerService, ScheduleRegistry, createScheduleStore, createLocalFireOwnership, createRemoteScheduleStore, createRemoteFireOwnership } from '@x-otto/schedule'\nimport { globalAgentJobRegistry } from '@x-otto/runtime'\nimport { setScheduleService } from './handlers/schedule'\nimport { createScheduleBackendHandler } from './schedule-backend-methods'\n\nimport type { App, AgentSession } from '@x-otto/coding'\nimport type { ScheduleStore, FireOwnership } from '@x-otto/schedule'\nimport type { TuiHandle } from '@x-otto/tui'\n\n/** Remote connection details, threaded from CLIOptions.sessionUrl/sessionToken. */\nexport interface ScheduleRemoteConfig {\n sessionUrl: string\n sessionToken: string\n}\n\nexport function setupScheduleService(\n app: App,\n sessionRef: { readonly value: AgentSession },\n tui: TuiHandle,\n remoteConfig?: ScheduleRemoteConfig,\n): { pollInterval: ReturnType<typeof setInterval>; scheduler: SchedulerService } {\n // Wire schedule service for /schedule command + cron tools\n const workspaceKey = app.workspaceRef?.key ?? 'default'\n const scheduleRegistry = new ScheduleRegistry()\n\n let scheduleStore: ScheduleStore\n let scheduleOwnership: FireOwnership\n if (app.storage.remoteSessionConfigured && remoteConfig) {\n const getAuth = async (): Promise<{ token: string }> => ({ token: remoteConfig.sessionToken })\n scheduleStore = createRemoteScheduleStore({\n sessionUrl: remoteConfig.sessionUrl,\n getAuth,\n wsKey: workspaceKey,\n })\n scheduleOwnership = createRemoteFireOwnership({\n sessionUrl: remoteConfig.sessionUrl,\n wsKey: workspaceKey,\n getAuth,\n })\n } else {\n scheduleStore = createScheduleStore(workspaceKey)\n scheduleOwnership = createLocalFireOwnership(workspaceKey)\n }\n\n const scheduler = new SchedulerService({\n registry: scheduleRegistry,\n store: scheduleStore,\n ownership: scheduleOwnership,\n jobRegistry: globalAgentJobRegistry,\n startJob: (input) => ({ id: app.agentJobs.start({ title: input.title, prompt: input.prompt, sessionId: input.sessionId, origin: input.origin }).id }),\n getSessionId: () => sessionRef.value.id,\n // RFC-327 D2:订阅者到点通知的窄端口——scheduler 只交出纯数据标识,\n // 由 App 决定投给哪个运行中的 service(并强制\"插件仍激活\"这一 revoke 语义)。\n // 该路径**不开会话授权 epoch**(schedule.tick 无 sessionId),见 App 侧注释。\n notifySubscriber: (subscriber, tick) =>\n app.notifyPluginServiceSchedule(\n { pluginId: subscriber.pluginId, serviceId: subscriber.serviceId },\n tick,\n ),\n // RFC-230:本函数在 TUI 启动流程内被调用,晚于 app.start()(settings 已 load),\n // getResilienceConfig() 调用安全。真实构造点(非 packages/coding App 内部),\n // 见 RFC-230 §3 D6 Scheduler 小节二轮评审 P2 修正。\n retryBackoffMs: app.getResilienceConfig().schedule.retryBackoffMs,\n })\n // Non-critical background feature — a bootstrap failure here must not take down\n // the whole interactive session (fire-and-forget start() would otherwise surface\n // as an unhandled rejection and crash the process).\n void scheduler.start().catch((err) => {\n console.error(`\\x1b[3m\\x1b[2m[schedule] scheduler failed to start: ${err instanceof Error ? err.message : String(err)}\\x1b[0m`)\n })\n setScheduleService(scheduler)\n app.setScheduleService(scheduler) // A1: let cron_* model tools reach this scheduler\n\n // RFC-349 M1:注册 in-process backend handler——面板经 usePanelBackend().call() 访问 scheduler\n const scheduleBackend = createScheduleBackendHandler(scheduler)\n app.panelBackendController.registerInProcessBackend('plugin-schedule', scheduleBackend.call)\n\n // Push schedule tasks to TUI store every 5 seconds (status bar / task list).\n // RFC-349 M2: panel data is now fetched via backend.call('schedule.list') —\n // this interval only updates the TUI taskStore for status bar display.\n const pollInterval = setInterval(() => {\n if (!tui) return\n const tasks = scheduler.list()\n const taskSummaries = tasks.map((t) => ({\n id: t.id,\n name: t.name,\n prompt: t.prompt,\n cronExpression: t.cronExpression,\n enabled: t.enabled,\n recurring: t.recurring,\n nextFireAt: t.nextFireAt,\n lastFiredAt: t.lastFiredAt,\n lastFireFailed: t.lastFireFailed,\n lastFireError: t.lastFireError,\n lastFireCancelled: t.lastFireCancelled,\n }))\n tui.stores.tasks.setScheduleTasks(taskSummaries)\n }, 5000)\n pollInterval.unref()\n\n return { pollInterval, scheduler }\n}\n","/**\n * interactive-context.ts — CliContext 构建器。\n *\n * `buildCliContext` 工厂接收所有需要的依赖(tui/app/可变状态 ref + 回调函数),\n * 返回 CliContext 对象供 command-dispatcher 使用。\n */\nimport { createReducerState } from '../session-contract/reduce'\nimport { restoreSessionPanels } from './interactive-resume'\nimport { registerBuiltinTuiCommands } from './interactive-commands'\n\nimport type { CliContext } from '../command-dispatcher'\nimport type { App, AgentSession } from '@x-otto/coding'\nimport type { InteractiveMode } from '../modes/interactive-mode'\nimport type { CommandRegistry, CommandEntry, ChatMessage, TuiHandle } from '@x-otto/tui'\nimport type { Lang } from '@x-otto/tui'\nimport type { createProjectorState } from '@x-otto/session-contract'\n\nexport function buildCliContext(p: {\n tui: TuiHandle\n app: App\n /** RFC-277 M6:启动时装配的主题预设 registry(内置 + 插件贡献),/preference theme 消费。 */\n themeRegistry: import('@x-otto/tui').ThemeRegistry\n // ── mutable state (ref boxes for scalars that are reassigned; objects passed by reference) ──\n sessionRef: { value: AgentSession }\n interactiveRef: { value: InteractiveMode }\n regRef: { value: CommandRegistry }\n slashCommandsRef: { value: CommandEntry[] }\n modelReadyRef: { value: boolean }\n currentModelIdRef: { value: string | undefined }\n currentLangRef: { value: Lang }\n projState: ReturnType<typeof createProjectorState>\n agentProcessingRef: { value: boolean }\n modelsRef: { value: Array<{ id: string; name: string; provider: string; api?: string; strengths: string[]; contextWindow?: number }> }\n availableModelIds: Set<string>\n restartRequestedRef: { value: boolean }\n // ── callbacks ──\n applyModel: (modelId: string) => Promise<string>\n persistAvailable: () => void\n refreshModelsNow: (say?: (msg: string) => void) => Promise<void>\n refreshSlashCommands: () => CommandEntry[]\n events: {\n attachSessionHandler: () => void\n getSessionUnsub: () => (() => void) | undefined\n getLastProviderUsage: () => import('@x-otto/interchange').ProviderUsageSnapshot | null\n isWriteLeaseReadOnly: () => boolean\n isRunPaused: () => boolean\n }\n /** RFC-156:/compact 手动压缩成功后调用,清空上下文占用提醒的区间计数。 */\n onContextCompacted: () => void\n redState: ReturnType<typeof createReducerState>\n agentMessagesToChatMessages: (msgs: readonly unknown[]) => ChatMessage[]\n PROVIDER_DISPLAY_NAMES: Record<string, string>\n apiCredentialId: (api: string, provider: string) => string\n}): CliContext {\n const {\n tui, app, themeRegistry,\n sessionRef, interactiveRef, regRef, slashCommandsRef, modelReadyRef, currentModelIdRef,\n currentLangRef, projState, agentProcessingRef, modelsRef,\n availableModelIds, restartRequestedRef,\n applyModel, persistAvailable, refreshModelsNow, refreshSlashCommands, events, redState,\n agentMessagesToChatMessages, PROVIDER_DISPLAY_NAMES,\n apiCredentialId, onContextCompacted,\n } = p\n\n return {\n // cast-free:CliContext['tui'] 是 Pick<TuiHandle,…>,真 TuiHandle 恒可赋值;\n // 删 `as` 让编译器强制校验 TuiHandle ⊇ TuiBridge(漂移即编译错,对齐下方 app)。\n tui,\n // 直接传真 App(由 AppBridge 端口契约约束),取代旧手抄字面量镜像。\n // 旧字面量是纯 pass-through(每项 = app.X),却要与 App + AppBridge 三方手工保持同步——\n // 漏同步即运行时 undefined 且 typecheck/单测抓不到(只真机暴露)。直接传 app 后,App 是否满足\n // AppBridge 由编译器强制校验,footgun 消除。\n app,\n state: {\n get session() { return sessionRef.value },\n set session(v) { sessionRef.value = v },\n get interactive() { return interactiveRef.value },\n set interactive(v) { interactiveRef.value = v },\n get reg() { return regRef.value },\n set reg(v) { regRef.value = v },\n get slashCommands() { return slashCommandsRef.value },\n set slashCommands(v) { slashCommandsRef.value = v },\n get modelReady() { return modelReadyRef.value },\n set modelReady(v) { modelReadyRef.value = v },\n get currentModelId() { return currentModelIdRef.value },\n set currentModelId(v) { currentModelIdRef.value = v },\n get currentLang() { return currentLangRef.value },\n set currentLang(v) { currentLangRef.value = v },\n get todoList() { return projState.todoList },\n set todoList(v) { projState.todoList = v },\n get agentProcessing() { return agentProcessingRef.value },\n set agentProcessing(v) { agentProcessingRef.value = v },\n get models() { return modelsRef.value },\n set models(v) { modelsRef.value = v },\n get availableModelIds() { return availableModelIds },\n get restartRequested() { return restartRequestedRef.value },\n set restartRequested(v) { restartRequestedRef.value = Boolean(v) },\n themeRegistry,\n },\n callbacks: {\n applyModel,\n persistAvailable,\n refreshModelsNow,\n registerAllTuiCommands: () => { registerBuiltinTuiCommands(regRef.value) },\n refreshSlashCommands,\n attachSessionHandler: () => events.attachSessionHandler(),\n // B6:/resume 切会话后原地重置 projState/redState(工厂按引用持有,不可重建)+ 恢复目标会话面板态。\n restoreSwitchedSession: () => {\n projState.delegated.clear()\n projState.todoList = []\n projState.currentTurn = 0\n Object.assign(redState, createReducerState())\n tui.stores.tasks.setTasks([], undefined)\n tui.panes.clearEditedFiles()\n tui.plugins.clearSubagents()\n restoreSessionPanels({ session: sessionRef.value, projState, tui })\n // RFC-156:/resume 切到另一会话——旧会话的占用提醒区间计数(含 lastRemindedTurn)\n // 对新会话的 currentTurn 基准完全无意义,显式重置(不依赖回落 fallback,新会话\n // 占用可能恰好落在与旧会话相同的区间,fallback 不会触发,见 RFC-156 §9)。\n onContextCompacted()\n },\n // 独立回调——仅 /clear 用;restoreSwitchedSession 的 currentTurn 重置属另一语义域\n // (/resume 切会话的整体面板重建),两处巧合同值但不可合并/互相调用。\n resetCurrentTurn: () => { projState.currentTurn = 0 },\n onContextCompacted,\n sessionUnsub: () => events.getSessionUnsub(),\n getLastProviderUsage: () => events.getLastProviderUsage(),\n isWriteLeaseReadOnly: () => events.isWriteLeaseReadOnly(),\n isRunPaused: () => events.isRunPaused(),\n agentMessagesToChatMessages,\n PROVIDER_DISPLAY_NAMES,\n apiCredentialId,\n },\n }\n}\n","/**\n * interactive-setup.ts — startTui 之后的启动设置。\n *\n * `runPostTuiSetup` 执行断点接线、help/resume/version/trace 初始化和 session 事件订阅。\n * 在 buildCliContext 之后调用,接收 events 和 bg 等 late-bound 依赖。\n */\nimport { i18next } from '@x-otto/tui'\nimport { homedir } from 'node:os'\n\nimport { restoreResumedSession } from './interactive-resume'\nimport { makeRegistryClient } from './handlers/registry'\nimport { setDetectedEnvProxy } from './handlers/proxy'\n\nimport { rankCandidates } from '@x-otto/shared'\nimport { resolveSessionTitle } from '@x-otto/coding'\nimport { discoverPluginsWithBundled } from '../discover-with-bundled'\n\nimport type { App, AgentSession } from '@x-otto/coding'\nimport type { SchedulerService } from '@x-otto/schedule'\nimport type { TuiHandle } from '@x-otto/tui'\nimport type { createProjectorState } from '@x-otto/session-contract'\nimport type { BackgroundController } from './interactive-background'\nimport { onBackgroundStatusChange, removeBackgroundSession } from './background-session-store'\n\n/**\n * `AgentSession.session.entries()` 返回项的最小结构切面(避免为此新增 `@x-otto/session`\n * 包依赖——`cli` 包目前只依赖 `@x-otto/coding`/`@x-otto/session-contract`,`SessionEntry`\n * 判别联合的完整定义在 `@x-otto/session`,此处只需要三个字段,内联结构类型足够且解耦)。\n */\ninterface MinimalSessionEntry {\n type: string\n parentId?: string\n message?: { role?: string }\n}\n\n/**\n * MCP 非阻塞连接完成通知 + 代理环境变量检测(RFC-199 D3 自 interactive.ts 提取,行为零变化)。\n *\n * RFC-150 D1:app.start() 内 MCP 连接与 TUI 首帧渲染并行,TUI 就绪后晚绑定回调以便连接\n * 完成/失败时弹 toast(若在绑定前已完成,App 侧回调存于闭包引用,下一次 tick 仍触发)。\n * 代理检测保持在 MCP 回调内部执行(含 early-return 之后)——与提取前的控制流逐字一致。\n */\n/**\n * RFC-287 M4:把生态检索接进 `capability_gap` 工具(自迭代回路的认知入口)。\n *\n * 接线在 cli 层而非 coding 层——registry 客户端住在 cli(`makeRegistryClient` 需要\n * `OTTO_USER_CONFIG_DIR`/官方源等 cli 侧配置),coding 不反向依赖 cli。\n *\n * **只注入检索(读),不注入安装(写)**:R1 零豁免要求模型生成/发现的插件与人类第三方\n * 插件走完全相同的信任门,故模型侧结构上就不存在自助安装通路——工具只能返回引导文本,\n * 由用户自己执行 `otto extension install`(走既有高危能力确认弹窗)。\n *\n * registry 不可达时返回 `undefined`(而非空命中)——让工具能如实区分\"生态里没有\"与\n * \"没查成\",不会把网络故障说成\"该能力不存在\"。\n */\nexport function wireEcosystemSearch(app: App): void {\n app.setEcosystemSearch(async (searchTerms: string[]) => {\n try {\n const client = makeRegistryClient(app)\n const { registries, warnings } = await client.loadAll()\n const candidates = registries.flatMap((registry) =>\n registry.entries.map((entry) => ({ entry, registry })),\n )\n const ranked = rankCandidates(\n candidates.map((hit) => ({\n text: hit.entry.id,\n aliases: [hit.entry.description, ...(hit.entry.keywords ?? [])],\n payload: hit,\n })),\n searchTerms.join(' '),\n { maxResults: 8 },\n )\n return {\n hits: ranked.map((r) => ({\n id: r.item.payload.entry.id,\n description: r.item.payload.entry.description,\n capabilities: r.item.payload.entry.capabilities,\n sourceName: r.item.payload.registry.sourceName,\n stale: r.item.payload.registry.stale,\n })),\n warnings,\n }\n } catch {\n // 不可达 ≠ 空结果(见函数头注)。异常不外抛——工具调用不应因生态检索失败中断模型回合。\n return undefined\n }\n })\n}\n\nexport function wireStartupNotices(p: {\n app: App\n tui: Pick<TuiHandle, 'info'>\n}): void {\n const { app, tui } = p\n app.setMcpStartupCallback((_result) => {\n // MCP 连接结果静默处理——启动期已有多层 toast(信任门/onboarding/向导),\n // 再加 MCP 连接通知会叠成 toast 风暴,用户不知该看哪个。\n // 代理环境变量检测移至独立回调(与 MCP 连接通知解耦,见下方 setMcpConnectCallback 复用处)。\n\n // 代理环境变量检测:若系统有 HTTP_PROXY/HTTPS_PROXY 但 otto 未配置代理,弹 toast 提示,\n // **并把检测结果交给 /proxy 面板**(RFC-288 D5)——此前只弹 toast 就丢弃,面板完全\n // 不知情,用户环境里明明有可用地址却仍需在面板里手打一遍。面板据此提供\"一键采用\",\n // 但仍需用户显式确认才落盘(不自动消费环境变量,沿用前身 RFC 规则 5)。\n const ENV_PROXY_VARS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'] as const\n const hitVar = ENV_PROXY_VARS.find((name) => process.env[name])\n const envProxy = hitVar ? process.env[hitVar] : undefined\n const proxyCfg = app.settings.get('proxy') as { url?: string } | undefined\n if (envProxy && hitVar && !proxyCfg?.url) {\n setDetectedEnvProxy(envProxy, hitVar)\n tui.info.showToast(i18next.t('proxy.title'), i18next.t('proxy.envDetectedToast', { source: hitVar, value: envProxy }))\n }\n })\n}\n\n/**\n * /model 参数补全注册 + 终端标题设置(RFC-199 D3 自 interactive.ts 提取,行为零变化)。\n * 终端标题走 RFC-078 M133-03 单一真源回落链:meta.title → deriveSessionTitle(首条消息)\n * → id 前 8 位,与 service/resume 同源。\n */\nexport function wireModelSuggesterAndTitle(p: {\n app: App\n tui: Pick<TuiHandle, 'plugins' | 'lifecycle' | 'stores'>\n session: AgentSession\n}): void {\n const { app, tui, session } = p\n tui.plugins.registerArgumentSuggester('model', (prefix: string, maxResults = 20) => {\n const models = app.modelRegistry.getAll().filter(m => m.id !== '__otto_setup__')\n return rankCandidates(\n models.map((m) => ({\n text: m.id,\n aliases: [m.name, m.provider],\n payload: m,\n })),\n prefix,\n { maxResults },\n ).map((r) => ({ name: r.item.payload.id, description: r.item.payload.name }))\n })\n\n const meta = session.session.metadata()\n const title = resolveSessionTitle(meta.title, session.session.messages(), session.id)\n tui.lifecycle.setTerminalTitle(title)\n // 对话中 SessionHeader 同步展示会话标题(与终端标题同一真源回落链)。\n tui.stores.tasks.setSessionTitle(title)\n}\n\n/**\n * 补齐\"真空区\"参数补全(有已知候选集、但无参数时不会弹出 select picker 兜底的命令)——\n * 复刻 `wireModelSuggesterAndTitle` 的 RFC-125 D3 模式,接线到 `/schedule remove|run|\n * enable|disable`、`/replay`、`/registry remove`、`/trust plugin`、`/session unlock`。\n *\n * 与 `/resume`/`/pin`/`/archive` 等已有 select picker 兜底的命令不同——\n * 这五个命令此前必须手敲候选 id/name,敲错静默失败(`/schedule list` 之外无发现路径)。\n *\n * `getScheduler` 用惰性 getter 而非直接传值——scheduler 由 `setupScheduleService` 稍后\n * (同一启动序列内)才构造完成,调用点若早于该构造会拿到 undefined;用闭包读最新值。\n */\nexport function wireExtraArgumentSuggesters(p: {\n app: App\n tui: Pick<TuiHandle, 'plugins'>\n getScheduler: () => SchedulerService | undefined\n}): void {\n const { app, tui, getScheduler } = p\n\n tui.plugins.registerArgumentSuggester('schedule', (prefix: string, maxResults = 20) => {\n // /schedule remove|run|enable|disable <id> 是两段式子命令(同 /session unlock <id>)——\n // list/add/logs 不需要 id 参数,故子命令候选只列前 4 个(remove/run/enable/disable)。\n const SUBCOMMANDS_WITH_ID = ['remove', 'run', 'enable', 'disable'] as const\n const matchedSub = SUBCOMMANDS_WITH_ID.find((s) => prefix.toLowerCase().startsWith(`${s} `))\n if (!matchedSub) {\n return rankCandidates(\n [...SUBCOMMANDS_WITH_ID, 'list', 'add', 'logs'].map((s) => ({ text: s, aliases: [], payload: s })),\n prefix,\n { maxResults },\n ).map((r) => ({ name: r.item.payload }))\n }\n const scheduler = getScheduler()\n if (!scheduler) return []\n const idPrefix = prefix.slice(matchedSub.length + 1)\n const tasks = scheduler.list()\n return rankCandidates(\n tasks.map((t) => ({ text: t.id, aliases: [t.name], payload: t })),\n idPrefix,\n { maxResults },\n ).map((r) => ({ name: `${matchedSub} ${r.item.payload.id}`, description: r.item.payload.name }))\n })\n\n tui.plugins.registerArgumentSuggester('replay', (prefix: string, maxResults = 20) => {\n // 同步快照:sessionManager 内存池已加载的会话(异步 listAllSessions 会牵出磁盘 I/O,\n // 补全场景对\"尚未加载进内存的历史会话\"不敏感,取舍同 `/model` suggester 的同步取数)。\n const sessions = app.listSessions()\n return rankCandidates(\n sessions.map((s) => ({ text: s.id, aliases: s.title ? [s.title] : [], payload: s })),\n prefix,\n { maxResults },\n ).map((r) => ({ name: r.item.payload.id, description: r.item.payload.title ?? r.item.payload.id.slice(0, 8) }))\n })\n\n tui.plugins.registerArgumentSuggester('session', (prefix: string, maxResults = 20) => {\n // `/session unlock <id>`——子命令词 'unlock' 本身不参与候选(suggester 只对 spaceIdx\n // 之后的整个剩余串生效,见 useTypeahead.ts getCommandSuggestions),故此处对 'unlock '\n // 前缀做剥离,让候选仍按 session id 过滤。\n const argPrefix = prefix.toLowerCase().startsWith('unlock ') ? prefix.slice(7) : prefix\n if (argPrefix !== prefix) {\n const sessions = app.listSessions()\n return rankCandidates(\n sessions.map((s) => ({ text: s.id, aliases: s.title ? [s.title] : [], payload: s })),\n argPrefix,\n { maxResults },\n ).map((r) => ({ name: `unlock ${r.item.payload.id}`, description: r.item.payload.title ?? r.item.payload.id.slice(0, 8) }))\n }\n return rankCandidates([{ text: 'unlock', aliases: [], payload: 'unlock' }], prefix, { maxResults })\n .map((r) => ({ name: r.item.payload, description: 'Write-lease diagnostics/force-unlock' }))\n })\n\n tui.plugins.registerArgumentSuggester('registry', (prefix: string, maxResults = 20) => {\n const argPrefix = prefix.toLowerCase().startsWith('remove ') ? prefix.slice(7) : prefix\n if (argPrefix === prefix) {\n // 'list'/'add'/'remove' 子命令词本身的候选(真正的 remove <name> 值在下方分支)。\n return rankCandidates(\n ['list', 'add', 'remove'].map((s) => ({ text: s, aliases: [], payload: s })),\n prefix,\n { maxResults },\n ).map((r) => ({ name: r.item.payload }))\n }\n const sources = makeRegistryClient(app).listSources().filter((s) => !s.builtin)\n return rankCandidates(\n sources.map((s) => ({ text: s.name, aliases: [s.url], payload: s })),\n argPrefix,\n { maxResults },\n ).map((r) => ({ name: `remove ${r.item.payload.name}`, description: r.item.payload.url }))\n })\n\n tui.plugins.registerArgumentSuggester('trust', (prefix: string, maxResults = 20) => {\n const argPrefix = prefix.toLowerCase().startsWith('plugin ') ? prefix.slice(7) : prefix\n if (argPrefix === prefix) {\n return rankCandidates(\n ['status', 'trust', 'untrust', 'plugin'].map((s) => ({ text: s, aliases: [], payload: s })),\n prefix,\n { maxResults },\n ).map((r) => ({ name: r.item.payload }))\n }\n const ws = app.workspaceDir ?? process.cwd()\n const plugins = discoverPluginsWithBundled({ cwd: ws, homedir: homedir() })\n return rankCandidates(\n plugins.map((pl) => ({ text: pl.id, aliases: [], payload: pl })),\n argPrefix,\n { maxResults },\n ).map((r) => ({ name: `plugin ${r.item.payload.id}`, description: r.item.payload.scope }))\n })\n}\n\n/**\n * RFC-115 M4:判定会话树最后一条 entry 是否为\"悬挂用户消息\"(已 append 但从未获得\n * 助手响应——重启前 abort 中断的那一轮)。纯函数,供 `runPostTuiSetup` 消费与独立单测。\n *\n * 只检查最后一条 entry(不回溯)——防御性设计:若最后一条不是用户消息(非预期状态,\n * 如已被别的路径处理过、或树为空),返回 `undefined` 表示\"不做任何撤回/续跑\",\n * 宁可放弃自动续跑,也不误撤已配对完整的对话。\n *\n * 已知窄窗口限制(经独立评审 P2-1 记录):若 abort 恰好发生在\"用户消息已 append、\n * 压缩(compaction)边界已写回\"之间的极窄时序(内存压力触发的压缩与本轮 abort 竞态),\n * 最后一条 entry 会是 compaction 而非 message,本函数返回 `undefined`——自动续跑被\n * 跳过(非错误,只是不生效),用户手动重发即可,不会误撤或损坏树结构。\n */\nexport function findDanglingUserMessageParent(\n entries: ReadonlyArray<MinimalSessionEntry>,\n): { parentId: string | undefined } | undefined {\n const last = entries[entries.length - 1]\n if (last?.type !== 'message') return undefined\n if (last.message?.role !== 'user') return undefined\n return { parentId: last.parentId }\n}\n\n/**\n * 版本检查结果应用(RFC-208 §5 风险段\"forced-upgrade 并发\"):从 runPostTuiSetup 的\n * updateCheckPromise.then 回调抽出为独立可测函数(纯函数式副作用应用,无分支复杂度)。\n *\n * forced=true 时除常驻横幅外,必须主动清除脉冲调查浮层(tui.setPulseSurvey(null))——\n * 不视为提交,不写入日志,防止 Band A 抢占后 Band B 残留重新出现造成用户困惑。\n */\nexport function applyUpdateCheckResult(\n tui: TuiHandle,\n upd: { newer: boolean; forced: boolean; latest: string; current: string } | null,\n): void {\n if (!upd || !upd.newer) return\n if (upd.forced) {\n // 强制升级:常驻横幅 + 禁用输入\n tui.info.pushNotification(i18next.t('cmd.titleUpdate'), i18next.t('cmd.updateNotifForced', { latest: upd.latest, current: upd.current }), 'version', 'error')\n tui.stores.tasks.setForcedUpgrade(i18next.t('cmd.updateForcedBanner', { latest: upd.latest, current: upd.current }))\n tui.stores.notification.setPulseSurvey(null)\n } else {\n // 普通更新:toast 自动消失 + 通知铃铛\n tui.info.pushNotification(i18next.t('cmd.titleUpdate'), i18next.t('cmd.updateNotifAvailable', { latest: upd.latest, current: upd.current }), 'version', 'info')\n tui.info.showToast(i18next.t('cmd.titleUpdate'), i18next.t('cmd.updateToast', { latest: upd.latest }))\n }\n}\n\nexport async function runPostTuiSetup(p: {\n tui: TuiHandle\n options: { resumePrompt?: string }\n app: App\n session: AgentSession\n projState: ReturnType<typeof createProjectorState>\n resumedNote: string | undefined\n resumed: AgentSession | undefined\n updateCheckPromise: Promise<{ newer: boolean; forced: boolean; latest: string; current: string } | null>\n bg: BackgroundController\n events: { bgUnsubs: Array<() => void>; attachSessionHandler: () => void }\n}): Promise<void> {\n const {\n tui, options, app, session, projState,\n resumedNote, resumed,\n updateCheckPromise,\n bg, events,\n } = p\n\n // 注:`--help`/`-h` 已在 `cli.ts` 顶层直接打印纯文本帮助并退出(不再进入 interactive\n // 模式,见该处注释——otto 命令与 TUI 渲染层应保持独立,help 不应绑死 TUI 启动路径),\n // 故本函数不会在 `options.showHelp` 为真时被调用,此处不再保留 TUI InfoDialog 分支。\n\n // --continue 转录回放 + 回合面板状态恢复(见 interactive-resume.ts)\n if (resumedNote) {\n restoreResumedSession({ resumedNote, isFound: Boolean(resumed), session, projState, tui })\n }\n\n // RFC-115 M4:内存治理触发重启后,若重启前会话正忙,--resume-prompt 携带最后一条\n // 用户消息原文——自动续跑,不需要用户手动重新输入。\n //\n // 正确性关键点:那条用户消息在 abort 前已经被 `packages/agent/src/pipeline.ts` 的\n // `prompt()`(`persistence.append(userMessage)`)写入了会话树(这正是它能被 M3\n // 的 onConfirm 捕获到的前提)——若直接重新 `session.prompt(text)`,等价于向会话树\n // 追加第二条内容相同的用户消息,用户会在转录里看到自己说了两遍同一句话。\n //\n // 修复:先用 `Session.branch()` 把当前分支 leaf 撤回到该悬挂用户消息的 parentId\n // (逻辑撤销,不删除底层 entry——树上数据仍在,只是当前分支视图不再包含它),\n // 再干净地重新 `prompt()` 一次,转录里只会出现一条。若该消息本身是分支根(无\n // parentId,理论上不可能——正常对话至少有 system 隐式起点,但防御性处理),\n // 放弃撤回、直接 prompt(宁可接受重复也不破坏树结构)。\n //\n // 预检(D5 残留风险缓解,RFC §7 风险 2):若子进程启动即已处于 critical(根因可能\n // 是单条巨大工具输出,重启后立刻重现同样的内存峰值),放弃自动续跑,改为 toast\n // 告警保留原文,交还用户手动决定,不静默重复同样的失败路径。\n if (options.resumePrompt && resumed) {\n const sample = app.memoryGovernor.sample()\n if (sample.level === 'critical') {\n tui.info.showToast(\n i18next.t('cmd.titleMemoryPressure'),\n i18next.t('cmd.memoryPressureResumeAbortedMessage'),\n )\n } else {\n // 经独立评审 P1-2 修正:用 branchEntries()(当前分支视图,walkBranch() 未做类型过滤)\n // 而非 entries()(全量扁平数组,含侧分支/其他分支的 compaction 边界)——与 onConfirm\n // 捕获逻辑用的 messages()(= walkBranch().filter(type==='message'))同源,避免两处\n // 判定口径不一致导致的漏判(如侧分支残留的 compaction entry 干扰当前分支判定)。\n const dangling = findDanglingUserMessageParent(\n session.session.branchEntries() as ReadonlyArray<MinimalSessionEntry>,\n )\n if (dangling) {\n if (dangling.parentId) {\n session.session.branch(dangling.parentId)\n }\n // internal 未标记(非 RFC-094 续跑指令消息)——这是用户本人的原话重新提交,\n // 应该像普通对话一样在转录里正常呈现(对齐 D3 时序注释:不污染用户可见转录\n // 指的是\"不额外插入机器生成的续跑指令\",用户原话本身不属于这类)。\n //\n // .catch() 必需(经独立评审 P1-1 修正):prompt() 是 async,模型不可用/网络故障等\n // 失败会产生 rejected promise;Node ≥15 默认 --unhandled-rejections=throw,未捕获\n // 会终止刚重启的进程——比原地不续跑更糟(用户連 Dialog 都看不到就又崩了一次)。\n void session.prompt(options.resumePrompt).catch((err) => {\n app.logger.warn(\n { err: err instanceof Error ? err.message : String(err) },\n '[memory-governor] auto-resume prompt failed',\n )\n tui.info.showToast(i18next.t('cmd.titleError'), i18next.t('cmd.memoryPressureResumeFailedMessage'))\n })\n }\n }\n }\n\n // 异步版本检查(不阻塞 TUI)\n void updateCheckPromise.then((upd) => applyUpdateCheckResult(tui, upd))\n\n // ── session 事件 → TUI 投影 + 后台任务面板(见 interactive-events.ts)──\n bg.setBgUnsubs(events.bgUnsubs)\n events.attachSessionHandler()\n\n // ── RFC-234 M4 HITL 冒泡:后台会话 gate 通知 ──\n wireBackgroundSessionNotifications(tui, app)\n}\n\n/**\n * RFC-234 M4:注册后台会话状态变更监听器。\n * 当后台会话进入 needs-input(approval/grill/plan gate)时,推通知中心。\n * 通知去抖由 onBackgroundStatusChange 内置的 300ms per-session 去抖保证。\n */\nfunction wireBackgroundSessionNotifications(\n tui: TuiHandle,\n app: App,\n): void {\n // ── HITL 冒泡:后台会话 gate → 通知中心 ──\n onBackgroundStatusChange((state) => {\n if (state.status !== 'needs-input') return\n\n const shortId = state.id.slice(0, 8)\n const sessions = app.listSessions()\n const info = sessions.find((s) => s.id === state.id)\n const title = info?.title ?? shortId\n\n tui.info.pushNotification(\n 'session needs input',\n `Background session '${title}' (${shortId}) requires your input (/resume ${shortId})`,\n 'system',\n 'warning',\n )\n })\n\n // ── 驱逐清理:SessionPool idle/LRU 驱逐后,移除对应的后台快照(对齐 app-hooks.ts 的 rfc047-notification-detach 模式)──\n app.hookRegistry.on('session.deleted', 'rfc234-background-cleanup', (input) => {\n removeBackgroundSession(input.sessionId)\n })\n}\n","/**\n * 检测 dist 是否陈旧(src 修改时间 > dist 修改时间),提示重建。\n */\nimport { existsSync } from 'node:fs'\nimport { execFile, type ChildProcess } from 'node:child_process'\nimport { dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { Logger } from '@x-otto/shared'\n\n/** 默认检查间隔:10 分钟(RFC D4)。 */\nexport const DEFAULT_CHECK_INTERVAL_MS = 600_000\n/** git 子进程超时:5 秒(评审 F3 防挂起)。 */\nconst GIT_TIMEOUT_MS = 5_000\n\n/**\n * 从 import.meta.url 逐级向上查找第一个包含 .git 子目录/文件的祖先目录,\n * 找不到则返回 undefined。\n *\n * 不硬编码路径层数(评审 F5):dev 时 url 指向 src/、构建后指向 dist/,\n * 两者到仓库根的层数不同,算法必须是\"逐级向上找 .git\"而非固定层数。\n *\n * 测试可通过可选 startPath 参数注入起始路径覆盖 import.meta.url。\n */\nexport function resolveOttoRepoRoot(startPath?: string): string | undefined {\n let current = startPath ?? fileURLToPath(import.meta.url)\n // 从文件路径开始向上遍历(文件自身的目录开始)\n current = dirname(current)\n const root = current === '/' ? current : '/'\n while (current !== root) {\n try {\n if (existsSync(current + '/.git')) return current\n } catch { /* permissions / broken links → skip */ }\n current = dirname(current)\n }\n // 也检查文件系统根目录本身\n try {\n if (existsSync(root + '/.git')) return root\n } catch { /* ignore */ }\n return undefined\n}\n\nexport interface StaleBuildDetectorOptions {\n /** otto 自身源码仓库根目录(resolveOttoRepoRoot() 的返回值)。 */\n repoRoot: string\n /** 本进程加载时的 BUILD_ID。 */\n currentBuildId: number\n /** 检查间隔(毫秒),默认 600_000(10 分钟)。 */\n checkIntervalMs?: number\n /** 检测到落后时触发回调。 */\n onStale?: (newBuildId: number) => void\n /** 错误日志记录器(来自 @x-otto/shared)。 */\n logger?: Logger\n}\n\nexport class StaleBuildDetector {\n private readonly repoRoot: string\n private readonly currentBuildId: number\n private readonly checkIntervalMs: number\n private readonly onStale?: (newBuildId: number) => void\n private readonly logger?: Logger\n\n /**\n * 上次通知的 buildId(评审 F2 必须调整)。\n * 使用数字而非 boolean——同一进程存活期内磁盘可能多次产生新构建,\n * 每次新的落后都应该重新通知(而不是只通知一次就永久静默)。\n */\n private lastNotifiedBuildId: number | undefined = undefined\n\n /** dispose 竞态防护(对齐 M2 MemoryGovernor 模式)。 */\n private disposed = false\n private timer: ReturnType<typeof setTimeout> | undefined\n /** 进行中的 git 子进程句柄(dispose 时显式 kill 防残留)。 */\n private activeChild: ChildProcess | undefined\n\n constructor(options: StaleBuildDetectorOptions) {\n this.repoRoot = options.repoRoot\n this.currentBuildId = options.currentBuildId\n this.checkIntervalMs = options.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS\n this.onStale = options.onStale\n this.logger = options.logger\n }\n\n /** 启动周期性检测(unref 定时器,不阻止进程退出)。 */\n start(): void {\n if (this.disposed) return\n // 首次延迟用 checkIntervalMs,之后每次自调度\n this.scheduleNext()\n }\n\n /** 停止检测,清理定时器 + kill 进行中的子进程。幂等。 */\n dispose(): void {\n // RFC-133 D8:先设 disposed 标志位再 clearTimeout,防止 tick 尾部\n // 继续调度新 setTimeout 造成定时器泄漏(对齐 M2 的 F1 竞态修正)。\n this.disposed = true\n if (this.timer) {\n clearTimeout(this.timer)\n this.timer = undefined\n }\n if (this.activeChild) {\n try { this.activeChild.kill() } catch { /* child may have already exited */ }\n this.activeChild = undefined\n }\n this.lastNotifiedBuildId = undefined\n }\n\n /** 安排下一次检查。 */\n private scheduleNext(): void {\n if (this.disposed) return\n this.timer = setTimeout(() => {\n this.timer = undefined\n void this.performCheck().then(() => {\n this.scheduleNext()\n })\n }, this.checkIntervalMs)\n this.timer.unref()\n }\n\n /** 执行一次 git rev-list --count HEAD 检查。 */\n private performCheck(): Promise<void> {\n return new Promise<void>((resolve) => {\n if (this.disposed) {\n resolve()\n return\n }\n\n const child = execFile(\n 'git',\n ['rev-list', '--count', 'HEAD'],\n { cwd: this.repoRoot, timeout: GIT_TIMEOUT_MS },\n (err, stdout) => {\n this.activeChild = undefined\n if (err) {\n // 失败场景:超时/git 不存在/非零退出码——warn 一次,不重试风暴\n this.logger?.warn(\n { err: err instanceof Error ? err.message : String(err) },\n '[stale-build-detector] git rev-list failed',\n )\n resolve()\n return\n }\n\n const raw = stdout.trim()\n const newBuildId = parseInt(raw, 10)\n if (!Number.isFinite(newBuildId)) {\n this.logger?.warn(\n { raw },\n '[stale-build-detector] failed to parse git output as number',\n )\n resolve()\n return\n }\n\n if (\n newBuildId > this.currentBuildId &&\n newBuildId !== this.lastNotifiedBuildId\n ) {\n this.lastNotifiedBuildId = newBuildId\n try {\n this.onStale?.(newBuildId)\n } catch (cbErr) {\n this.logger?.warn(\n { err: cbErr instanceof Error ? cbErr.message : String(cbErr) },\n '[stale-build-detector] onStale callback threw',\n )\n }\n }\n resolve()\n },\n )\n\n this.activeChild = child\n\n child.on('error', () => {\n // execFile 的 'error' 事件(如 git 不存在)——回调也会收到 err,\n // 这里只清理引用,避免回调中重复 warn。\n this.activeChild = undefined\n })\n })\n }\n}\n","/**\n * interactive-submit-helpers.ts — onSubmit 闭包中抽取的自包含子处理器。\n *\n * `runAsyncBash` / `handleAbortIntercept` / `runMemoryCapture` 是从 onSubmit god 闭包\n * 抽出的独立工厂函数,保留原有逻辑不变量,零行为变化。\n */\nimport { i18next } from '@x-otto/tui'\nimport { spawn } from 'node:child_process'\nimport { globalProcessRuntime } from '@x-otto/runtime'\nimport { buildAllowedEnv, checkShellCommand } from '@x-otto/shared'\n\nimport { parseCommand } from '../command-parser'\nimport { shapeNoteSmart } from './shape-note'\n\nimport type { App } from '@x-otto/coding'\nimport type { SubmitMode, TuiHandle } from '@x-otto/tui'\n\n/** `submitState` 中 runAsyncBash / handleAbortIntercept 需读写的字段。 */\nexport interface SubmitStateSlice {\n shellChild: import('node:child_process').ChildProcess | null\n userAborted: boolean\n}\n\n/**\n * 空内容提交判定(真实用户反馈:空内容不该进对话流)。\n *\n * `true` = 应当静默丢弃本次提交(不 echo 用户气泡、不起回合、不发 prompt 给模型)。\n *\n * 两条产生空内容的路径都由本判定收口:\n * ① `@model` chip 单独提交——`extractModelChips` 剥离 chip 后文本恒为空(其内部\n * `.trim()`,见 `interactive-model.ts`)。此时模型**已切换完成**,语义是纯 action\n * 而非对话,绝不能再 echo 一条空 user 气泡 + 发空 prompt。\n * ② 纯空白输入直接按 Enter——`PromptInput` 与 `text-input-logic` 均不 gate 空文本\n * (Enter 恒返回 `submit` action,见 text-input-logic.ts:114-115),空串会一路\n * 走到 `echoUserInput` + `handleInput`。\n *\n * 调用点约束(重要):必须在 `@model` chip 处理**之后**调用——否则 chip 的切模型副作用\n * 会被一并跳过(用户点了 @model 却什么也没发生)。\n */\nexport function isEmptySubmit(text: string): boolean {\n return text.trim().length === 0\n}\n\n/**\n * `!`/`!!` 异步 shell 命令处理器工厂(RFC-189 D3:`!`(单感叹号)与 `!!`(双感叹号)在\n * TUI 交互模式下合并为同一异步执行语义,消除单 `!` 此前走 `execSync` 同步阻塞整个 Ink\n * 渲染进程的架构缺陷)。\n *\n * `echoPrefix` 由调用方按实际输入前缀传入(`'!'` 或 `'!!'`)——对抗评审 F1 修正:此前硬\n * 编码 `'!! '` 前缀,导致单 `!` 触发时用户会看到输入被回显成 `!!`,产生\"打一个感叹号却\n * 显示两个\"的困惑。回显仍在危险命令检查**之后**触发(保留原行为:被拦截的危险命令不\n * 回显到对话流,只展示拦截提示)。\n */\nexport function createAsyncBashRunner(p: {\n tui: TuiHandle\n app: App\n submitState: SubmitStateSlice\n}): (rawCmd: string, echoPrefix?: '!' | '!!') => void {\n const { tui, app, submitState } = p\n return (rawCmd: string, echoPrefix: '!' | '!!' = '!!'): void => {\n const cmd = rawCmd.trim()\n if (!cmd) return\n // 拒绝危险命令,引导用户走 bash tool\n const check = checkShellCommand(cmd)\n if (!check.safe) {\n tui.info.showToast(i18next.t('cmd.titleShell'), i18next.t('cmd.dangerousBlocked', { reason: check.reason }))\n tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text',\n text: i18next.t('cmd.dangerousBlockedDetail', { reason: check.reason, cmd: cmd }) }] })\n return\n }\n tui.messages.echoUserInput(`${echoPrefix} ${cmd}`)\n tui.shell.startShellExecution(cmd)\n submitState.shellChild = spawn(cmd, {\n shell: true,\n cwd: app.workspaceDir,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: buildAllowedEnv(),\n })\n // RFC-085 M4: 纳入 ProcessRuntime 统一追踪(cli `!!` shell,evictable)。\n // 非 detached——kill 走 signalGroup 单 pid 回退(C1);onInterrupt 仍就地 SIGTERM/SIGKILL。\n globalProcessRuntime.registerChild(\n {\n command: cmd,\n args: [],\n owner: { type: 'cli-shell', id: '!!' },\n category: 'shell',\n lifecycle: 'evictable',\n cwd: app.workspaceDir,\n },\n submitState.shellChild,\n )\n submitState.shellChild.stdout!.on('data', (data: Buffer) => {\n tui.shell.appendShellOutput(data.toString('utf-8'))\n })\n submitState.shellChild.stderr!.on('data', (data: Buffer) => {\n tui.shell.appendShellOutput(data.toString('utf-8'))\n })\n submitState.shellChild.on('close', (code, signal) => {\n submitState.shellChild = null\n // ShellPane 常驻面板已展示完成态(图标 + 状态文字 + 颜色,3s 后自动消失)——\n // 此处 toast 与面板信息完全重复,故不再弹(面板是权威展示,toast 是冗余通知)。\n if (signal) {\n tui.shell.finishShellExecution(null, undefined, true)\n } else {\n tui.shell.finishShellExecution(code)\n }\n })\n submitState.shellChild.on('error', (err) => {\n submitState.shellChild = null\n tui.shell.finishShellExecution(null, `\\nError: ${err.message}`)\n })\n }\n}\n\n/**\n * `/abort` 拦截处理器工厂。\n *\n * RFC-289 D0/D1:本处**不再自行 openModal**——停止确认框由 `createStopController()`\n * 统一持有,`/abort` 与 Ctrl+C / `/exit` 共用同一实例。此前三个入口各自开框,两者并发时\n * 会在路由栈里叠成多个 confirm 条目,触发\"弹窗永久关不掉、agent 再也中止不了\"的死锁\n * (见 RFC-289 §2.1)。\n */\nexport function createAbortInterceptor(p: {\n agentProcessingRef: { value: boolean }\n tui: TuiHandle\n /** RFC-289 D1:停止意图统一入口(由 interactive.ts 构造并注入)。 */\n requestStop: (intent: 'abort' | 'exit' | 'exit-restart') => void\n}): (text: string, mode: SubmitMode) => boolean {\n const { agentProcessingRef, tui, requestStop } = p\n return (text: string, mode: SubmitMode): boolean => {\n const early = parseCommand(text)\n if (!(early.isCommand && early.command.toLowerCase() === 'abort' && mode === 'send')) return false\n if (agentProcessingRef.value) {\n // 框已在场时 requestStop 内部只 replace intent,不新开框(D0 不变式)。\n requestStop('abort')\n } else {\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('cmd.noRunningTask'))\n }\n return true\n }\n}\n\n/** `#` 手动记忆捕获处理器工厂。 */\nexport function createMemoryCapture(p: {\n app: App\n tui: TuiHandle\n}): (text: string) => Promise<void> {\n const { app, tui } = p\n return async (text: string): Promise<void> => {\n const note = text.trim()\n if (!note) return\n if (!app.storage.autoMemory) { tui.info.showToast(i18next.t('cmd.titleMemory'), i18next.t('cmd.memoryDisabled')); return }\n // §14.3-C:opt-in 模型命名(OTTO_MEMORY_LLM_NAMING=1);默认/失败回退 slugify。\n const naming = process.env.OTTO_MEMORY_LLM_NAMING === '1' ? app.completeText : undefined\n const shaped = await shapeNoteSmart(note, naming)\n await app.storage.autoMemory.record(shaped)\n tui.messages.echoUserInput(`# ${note}`)\n tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text',\n text: i18next.t('cmd.memorySaved', { name: shaped.name }) }] })\n }\n}\n\n/** RFC-225 D5:output 硬截断上限(约 5,000 行常规文本)——见 RFC \"output 硬截断上限\"说明。 */\nexport const SHELL_ASK_AI_MAX_OUTPUT_CHARS = 50_000\n\n/**\n * RFC-225 M2:把 shell 执行快照组装为\"问 AI\"消息文本(纯函数,供 `interactive.ts` 与单测\n * 复用)。errorLabel 互斥优先级(RFC D5):`terminated` 优先于 `exitCode`,两者皆缺失时\n * (防御性分支,正常不应触达)兜底\"未知错误\"。output 超 `SHELL_ASK_AI_MAX_OUTPUT_CHARS`\n * 字符时截断并追加提示行。\n */\nexport function buildShellAskAiPrompt(\n snapshot: { command: string; output: string; exitCode: number | null; terminated?: boolean },\n): string {\n const errorLabel =\n snapshot.terminated === true\n ? i18next.t('shell.askAiTerminated')\n : snapshot.exitCode !== null\n ? i18next.t('shell.exitCode', { code: snapshot.exitCode })\n : i18next.t('shell.unknownError')\n const output =\n snapshot.output.length > SHELL_ASK_AI_MAX_OUTPUT_CHARS\n ? `${snapshot.output.slice(0, SHELL_ASK_AI_MAX_OUTPUT_CHARS)}\\n${i18next.t('shell.truncated')}`\n : snapshot.output\n return i18next.t('shell.prompt', { command: snapshot.command, errorLabel, output })\n}\n","/**\n * interactive-stop.ts — 停止意图统一入口(RFC-289 D0/D1)。\n *\n * ## 统领性不变式(D0)\n *\n * > **任一时刻,「停止」语义的确认框在路由栈中至多存在一个实例。**\n * > 新的停止入口只能**替换**该实例的意图(intent),或在其上**升级**(escalate),\n * > **绝不允许新开第二个框**。\n *\n * 本模块是**唯一**允许打开停止确认框的地方。Ctrl+C / `/abort` / `/exit` 三个入口全部\n * 收敛到 `createStopController().requestStop(intent)`,禁止任何调用方直接\n * `openModal({kind:'confirm'})` 表达停止语义(由 `stop-dialog-single-entry-gate` 静态门禁强制)。\n *\n * ## 为何提升为不变式而非局部修复\n *\n * 事故表层是「Ctrl+C 连按堆两个框 → 弹窗永久卡死、agent 再也中止不了」,但同样的机制在\n * `/abort` 与 Ctrl+C 并发时同样成立,在未来任何新增停止入口上也会重演。逐个入口打补丁只能\n * 修掉已知的三条路径,第四条路径出现时缺陷原样复发——把纪律变成结构才能阻断复发链。\n *\n * (反面教材:本 RFC 初稿曾设计「`/exit` 运行中弹一个**独立的**退出确认框」,那正好制造了\n * 第二个停止框,与 Ctrl+C 的框可同时在场,把本要修的堆叠问题在新入口上原样复刻。)\n *\n * ## Ctrl+C 升级阶梯(D2)\n *\n * ```\n * 【agent 运行中】\n * Ctrl+C ① → requestStop('abort') 弹框:中止 / 暂停 / 取消(焦点默认在安全的「取消」)\n * Ctrl+C ② → 升级为 exit 并立即执行 终止当前回合 + 退出 otto\n *\n * 【agent 空闲】\n * Ctrl+C ① → toast「再按一次退出」\n * Ctrl+C ② → 退出 otto\n * ```\n *\n * 忙闲两态同构(都是「① 提示确认 → ② 真正退出」),用户只需记一套规则。\n */\nimport { i18next } from '@x-otto/tui'\n\nimport type { AgentSession } from '@x-otto/coding'\nimport type { TuiHandle } from '@x-otto/tui'\n\n/** 停止意图。`abort` = 只停当前回合;`exit`/`exit-restart` = 停回合并结束/重启进程。 */\nexport type StopIntent = 'abort' | 'exit' | 'exit-restart'\n\n/** Ctrl+C 一次按键的解算结果(`resolveCtrlCAction` 输出)。 */\nexport type CtrlCAction =\n /** 同一物理按键的重复信号 → 静默丢弃,**不产生任何反馈**。 */\n | 'dedup'\n /** shell 异步命令在跑 → 终止 shell。 */\n | 'kill-shell'\n /** agent 在跑、停止框未开 → 弹停止确认框(阶梯一级)。 */\n | 'open-stop-dialog'\n /** agent 在跑、停止框已开 → 就地升级为「终止并退出」(阶梯二级)。 */\n | 'escalate-exit'\n /** 空闲首次 → 提示「再按一次退出」。 */\n | 'hint-exit'\n /** 空闲二次 → 退出进程。 */\n | 'exit-process'\n\n/**\n * Ctrl+C 单次按键的动作解算(纯函数,供 `onInterrupt` 与单测共用)。\n *\n * 抽为纯函数的动机(RFC-289 用户实测回归):`onInterrupt` 是一串顺序敏感的守卫,\n * 曾出现两处顺序/判据缺陷,且因逻辑内嵌在 500 行闭包里而无法单测:\n *\n * ① **去重路径误发空闲态 toast**——Ctrl+C 经 Ink `useInput` 与 raw stdin `\\x03`\n * **两路**送达(PromptInput.tsx / lifecycle.ts),微秒级重复。此前去重分支无条件\n * 弹「再按一次 Ctrl+C 退出」(空闲态专属语义),于是 agent 运行中按一次 Ctrl+C\n * 会**先闪\"退出\"提示、再出现「中止 Agent?」**——正是用户报告的现象。\n * 修正:去重是同一次按键的重复信号,**不产生任何独立反馈**。\n *\n * ② **判忙用裸 `agentProcessing` 而非 `isBusyNow()`**——前者只在 `!isEngineBuiltin`\n * 分支置位、且晚于 `await ensureModelUsableForPrompt()`,回合刚起跑的窗口内为\n * false,Ctrl+C 会漏进空闲分支弹\"再按一次退出\"而非中止框。\n *\n * @param nowMs 当前时刻\n * @param lastCtrlCMs 上次受理 Ctrl+C 的时刻(去重基准)\n * @param s 当前状态快照\n */\nexport function resolveCtrlCAction(\n nowMs: number,\n lastCtrlCMs: number,\n s: {\n /** shell 异步命令是否在跑。 */\n shellRunning: boolean\n /** 判忙真源——须为 `isBusyNow()`(引擎 OR cli 处理周期),不可传裸 agentProcessing。 */\n busy: boolean\n /** 停止确认框是否已在场。 */\n stopDialogOpen: boolean\n /** 空闲态连按计数(本次按键**之前**的值)。 */\n idleCtrlCCount: number\n },\n): CtrlCAction {\n // 50ms 只防三路信号撞车;人类双按 100-200ms 不会被挡(放行是设计意图)。\n if (nowMs - lastCtrlCMs < CTRL_C_DEDUP_MS) return 'dedup'\n if (s.shellRunning) return 'kill-shell'\n if (s.busy) return s.stopDialogOpen ? 'escalate-exit' : 'open-stop-dialog'\n return s.idleCtrlCCount + 1 >= 2 ? 'exit-process' : 'hint-exit'\n}\n\n/** 三路信号(Ink useInput / raw stdin \\x03 / SIGINT)撞车去重窗口。 */\nexport const CTRL_C_DEDUP_MS = 50\n\n/** 停止确认框的路由路径——D0 单实例判定的锚点。 */\nconst CONFIRM_PATH = '/dialog/confirm'\n\nexport interface StopControllerDeps {\n /** TUI 句柄活引用——启动期为 null,raw stdin/SIGINT 可能早于 TUI 就绪触发。 */\n getTui: () => TuiHandle | null\n /** 会话活引用(`/resume` 切会话后会重新赋值,必须按引用取最新)。 */\n getSession: () => AgentSession\n /** 引擎命令通道(abort 经此下发,与既有 dispatchToEngine 同一通道)。 */\n dispatchAbort: () => void\n /** 中止时清空三层 todo(L1 会话 / L2 projState / L3 TUI)。 */\n clearTodoLayers: () => void\n /** 标记本次停止由用户主动发起(抑制错误上报路径)。 */\n markUserAborted: () => void\n /** 重置 Ctrl+C 计数(阶梯复位)。 */\n resetCtrlCCount: () => void\n /** 请求重启(`/exit --restart`)——由 cli.ts 在 app.stop() 完成后 spawn 新进程。 */\n markRestartRequested: () => void\n /** 拆 TUI(内含 throw/hang 双兜底,见 interactive.ts stopTuiGuarded)。 */\n stopTui: () => void\n /** agent 是否正在跑(引擎真源 OR cli 处理周期)。 */\n isBusy: () => boolean\n}\n\nexport interface StopController {\n /**\n * 请求停止(D1 唯一入口)。\n *\n * - 停止框**不在场** → 开框(按 intent 决定文案与按钮)。\n * - 停止框**已在场** → **不开新框**,替换 intent 与文案(handle-modal 的 D3 replace\n * 保证栈不增长)。\n * - agent 空闲且 intent 是 exit 类 → 零摩擦直接退出(脚本/自动化场景不加确认摩擦)。\n */\n requestStop: (intent: StopIntent) => void\n /**\n * Ctrl+C 专用:停止框是否已在场。为真时调用方应走**升级**分支(D2 二级 = 终止并退出),\n * 而不是再次 `requestStop('abort')`(那只会 replace 成同样的框,用户看不到任何反馈)。\n */\n isStopDialogOpen: () => boolean\n /**\n * D2 二级:把在场的停止框**就地升级**为「终止并退出」并立即执行。\n *\n * 必须经 TUI 的 confirm resolver 通道(`resolveConfirm`)settle 掉当前框——不能绕过它\n * 直接 abort + stopTui,否则 resolver 不清空、路由不 goBack,等于人为制造孤儿条目\n * (正是本次卡死事故 A3 的成因)。\n */\n escalateToExit: () => void\n /**\n * RFC-303:不经确认框、直接中止当前 agent 回合——供用户在明确操作某个具体控件\n * (非全局裸键)时触发的停止意图使用,如 RetryDialog 的「停止」按钮/Esc。\n *\n * 与 `requestStop('abort')` 的区别:后者弹二次确认框(D0 统一停止入口的默认路径,\n * 适配 Ctrl+C 这类容易误触的全局裸键);本方法不弹框——用户点击具体控件本身已是\n * 明确、低误触风险的意图表达,再套一层确认框只会制造\"点两次才能真正停下来\"的摩擦\n * (grill 定稿,RFC-303)。不涉及路由栈(不开/不关任何 confirm 路由),故不受 D0\n * 「停止确认框单实例」不变式约束——D0 管的是确认框不重复,不要求停止必须经确认框,\n * `escalateToExit` 的直接执行分支即为先例。\n */\n abortNow: () => void\n}\n\nexport function createStopController(deps: StopControllerDeps): StopController {\n const {\n getTui,\n getSession,\n dispatchAbort,\n clearTodoLayers,\n markUserAborted,\n resetCtrlCCount,\n markRestartRequested,\n stopTui,\n isBusy,\n } = deps\n\n /** 当前在场停止框的意图——供 escalate 与 resolver 分流;无框时为 null。 */\n let activeIntent: StopIntent | null = null\n\n const isStopDialogOpen = (): boolean =>\n activeIntent !== null && getTui()?.info.getCurrentPath() === CONFIRM_PATH\n\n /** 真正执行中止(无 UI,供确认后与无 TUI 降级路径共用)。 */\n const performAbort = (): void => {\n markUserAborted()\n resetCtrlCCount()\n dispatchAbort()\n clearTodoLayers()\n }\n\n /** 真正执行退出(先 abort 让引擎优雅收尾,再拆 TUI)。 */\n const performExit = (restart: boolean): void => {\n if (restart) markRestartRequested()\n markUserAborted()\n resetCtrlCCount()\n // 与 Ctrl+C 中止路径对齐:先 abort 让引擎停下,再拆 TUI(避免 in-flight 写入被\n // TUI 拆除打断)。idle 时 abort 是 no-op,故无条件调用即可。\n try { dispatchAbort() } catch { /* ignore if idle */ }\n stopTui()\n }\n\n /** 按 intent 解算弹窗呈现与确认后的行为。 */\n const renderFor = (intent: StopIntent) => {\n if (intent === 'abort') {\n return {\n title: i18next.t('confirm.abortConfirmTitle'),\n message: i18next.t('confirm.abortConfirmMessage'),\n confirmLabel: i18next.t('common.abortButton'),\n // RFC-180 §4:第三按钮「暂停」——turn 边界软停止,区别于中止的硬打断。\n // exit 类意图不带此按钮:暂停语义是「停下来待命」,与退出(进程结束)逻辑互斥。\n extraLabel: i18next.t('common.pauseButton'),\n onConfirm: () => {\n performAbort()\n getTui()?.info.showToast(i18next.t('cmd.titleAborted'), i18next.t('cmd.runAborted'))\n },\n }\n }\n const restart = intent === 'exit-restart'\n return {\n title: restart\n ? i18next.t('confirm.exitRestartConfirmTitle')\n : i18next.t('confirm.exitConfirmTitle'),\n message: i18next.t('confirm.exitConfirmMessage'),\n confirmLabel: restart\n ? i18next.t('common.abortAndRestartButton')\n : i18next.t('common.abortAndExitButton'),\n extraLabel: undefined,\n onConfirm: () => { performExit(restart) },\n }\n }\n\n const openOrReplace = (intent: StopIntent): void => {\n const tui = getTui()\n if (!tui) return\n const view = renderFor(intent)\n activeIntent = intent\n tui.modal.openModal({\n kind: 'confirm',\n title: view.title,\n message: view.message,\n confirmLabel: view.confirmLabel,\n confirmColor: 'amber',\n extraLabel: view.extraLabel,\n extraColor: 'accent',\n // 破坏性确认:焦点默认落在安全的「取消」,防 Enter 误触。\n dangerous: true,\n onConfirm: () => {\n activeIntent = null\n view.onConfirm()\n },\n onCancel: () => { activeIntent = null },\n onExtra: view.extraLabel\n ? () => {\n activeIntent = null\n // RFC-180 D4:直接调用 session.pauseRun() 取返回值分流 toast 文案——\n // dispatchToEngine 是 void 通道(EngineCommand 协议不回传结果),\n // isBusy=false 时 pauseRun() 静默 no-op,若不感知返回值会让用户以为按钮没反应。\n const paused = getSession().pauseRun()\n if (paused) {\n getTui()?.info.showToast(i18next.t('cmd.titlePaused'), i18next.t('cmd.runWillPauseAfterTurn'))\n } else {\n getTui()?.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('cmd.noRunningTaskToPause'))\n }\n }\n : undefined,\n })\n }\n\n const requestStop = (intent: StopIntent): void => {\n const tui = getTui()\n\n // TUI 未就绪(raw stdin / SIGINT 早于首帧):无对话框可弹,直接执行。\n if (!tui) {\n if (intent === 'abort') performAbort()\n else performExit(intent === 'exit-restart')\n return\n }\n\n // 空闲态的 exit 类意图零摩擦——脚本/自动化场景占绝大多数,且无 in-flight 工作可丢。\n // abort 意图在空闲时无事可做(调用方各自 toast 提示),此处不越权处理。\n if (intent !== 'abort' && !isBusy()) {\n performExit(intent === 'exit-restart')\n return\n }\n\n // D0/D1:框已在场时**不新开**,只替换 intent(handle-modal D3 的 replace 保证栈不增长)。\n openOrReplace(intent)\n }\n\n const escalateToExit = (): void => {\n const tui = getTui()\n if (!tui || !isStopDialogOpen()) return\n // 就地升级:先把在场框的 intent 换成 exit(含文案),再经 confirm resolver 通道 settle。\n // 走 resolveConfirm 而非直接 performExit,是为了让 resolver 清空 + 路由 goBack 都发生\n // ——绕过它会留下孤儿条目(本次卡死事故 A3 的成因)。\n openOrReplace('exit')\n tui.modal.resolveConfirm(true)\n }\n\n return { requestStop, isStopDialogOpen, escalateToExit, abortNow: performAbort }\n}\n","/**\n * interactive-session-setup.ts — 会话启动配置(模型恢复 + thinking 档位载入)。\n *\n * 从 interactive.ts 拆分出来以控制文件行数。零行为变更。\n */\nimport type { App, AgentSession } from '@x-otto/coding'\n\n/**\n * 首次启动 / --continue 恢复后的会话配置:\n * — 恢复会话将 model 拉回当前 config(避免沿用旧配置模型)\n * — 将 config.thinking_level 套入会话\n *\n * 降级(config 模型不可用)留给 prompt 入口 ensureModelUsableForPrompt 带 Dialog 提示。\n */\nexport async function applySessionStartupConfig(\n session: AgentSession,\n app: App,\n isResumed: boolean,\n): Promise<void> {\n // 恢复会话(--continue)沿用其**持久化的 model**——若该会话创建于旧 config 时期,会一直用旧模型\n // (用户报告「--continue 后还是 deepseek,明明 config 已是 haiku」的根因)。model 是全局配置而非\n // 会话固有属性,恢复应跟随当前 config(与新建会话一致)。仅 recover 方向:config 模型已注册\n // (hydrate 后才有日期版 id)+ provider 已认证才切,并随之 reconfigure 窗口(避免按旧窗口判压缩);\n // 降级(config 模型不可用)留给 prompt 入口 ensureModelUsableForPrompt 带 Dialog 提示。\n if (isResumed) {\n const cfgId = app.settings.get('model')\n if (typeof cfgId === 'string' && cfgId && session.model?.id !== cfgId) {\n const m = app.modelRegistry.find(cfgId)\n if (m && (await app.providerRegistry.hasCredential(m.provider))) {\n session.model = m\n app.storage.memory?.reconfigure?.({ contextWindow: m.contextWindow, maxOutput: m.maxOutputTokens })\n }\n }\n }\n\n // /effort 持久化档载入:config.thinking_level 有效则套到会话(下一回合 prompt 读取)。\n // 非全局热切换——仅当前会话;恢复会话沿用此 config(与 model 跟随 config 一致)。\n // RFC-235:档位对齐 Claude Code(去 minimal 加 max)。存量配置存了 'minimal' 的迁移为 'low'\n // (Claude Code 最低档);其余非法值忽略(保持会话默认档)。\n {\n const lvl = app.settings.get('thinking_level')\n const VALID = ['low', 'medium', 'high', 'xhigh', 'max']\n const migrated = lvl === 'minimal' ? 'low' : lvl\n if (typeof migrated === 'string' && VALID.includes(migrated)) {\n session.thinkingLevel = migrated as typeof session.thinkingLevel\n }\n }\n}\n","/**\n * InputHistoryStore — cli 内部的输入历史持久化(RFC-088 M4b)。\n *\n * 与 session 快照解耦——inputHistory 是纯 UI 态(↑/↓ 导航 / 历史搜索面板(/history · Ctrl+R)),\n * 移出 session 纯域、独立落盘到 `~/.otto/sessions/<sessionId>.history.json`。\n *\n * 落盘复用 write-temp→rename 原子写模式(对齐 @x-otto/persistence atomic-write.ts)。\n */\nimport { OTTO_HOME } from '@x-otto/env'\nimport { join, dirname } from 'node:path'\nimport { mkdir, writeFile, readFile, rename, unlink } from 'node:fs/promises'\nimport { isShadowModeEnabled } from '@x-otto/env'\n\nconst MAX_ENTRIES = 100\n\nexport class InputHistoryStore {\n private entries: string[] = []\n private readonly filePath: string\n\n constructor(sessionId: string, rootDir: string = join(OTTO_HOME, 'sessions')) {\n // RFC-345:路径默认走 OTTO_HOME(M9——原为 homedir() 硬编码,探针/真实隔离不对应的孤儿目录)。\n // 保留覆盖参数供测试注入临时根。\n this.filePath = join(rootDir, `${sessionId}.history.json`)\n }\n\n list(max?: number): string[] {\n return this.entries.slice(-(max ?? MAX_ENTRIES))\n }\n\n append(entry: string): void {\n if (!entry.trim()) return\n this.entries.push(entry)\n if (this.entries.length > MAX_ENTRIES) {\n this.entries = this.entries.slice(-MAX_ENTRIES)\n }\n }\n\n hydrate(entries: string[]): void {\n this.entries = entries.slice(-MAX_ENTRIES)\n }\n\n async persist(): Promise<void> {\n // RFC-345 §D8:影子态输入历史只活在内存(entries 数组为真源,list/↑↓ 导航照常),\n // 不落盘。load() 仍读磁盘继承既有历史(overlay:读照常、写只进内存)。\n if (isShadowModeEnabled()) return\n const data = JSON.stringify(this.entries, null, 2)\n const tmpPath = `${this.filePath}.tmp`\n try {\n await mkdir(dirname(this.filePath), { recursive: true })\n await writeFile(tmpPath, data, 'utf-8')\n await rename(tmpPath, this.filePath)\n } catch (error) {\n await unlink(tmpPath).catch(() => {})\n throw error\n }\n }\n\n async load(): Promise<string[]> {\n try {\n const data = await readFile(this.filePath, 'utf-8')\n const parsed: unknown = JSON.parse(data)\n if (Array.isArray(parsed) && parsed.every((e): e is string => typeof e === 'string')) {\n this.entries = parsed.slice(-MAX_ENTRIES)\n return this.entries\n }\n return []\n } catch {\n return []\n }\n }\n}\n","/**\n * RFC-129 M129-2:外部 shell 历史文件只读加载(cli 侧 IO + 路径发现)。\n */\nimport { access, readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport {\n parseBashHistory,\n parseFishHistory,\n parseZshHistory,\n BASH_HISTFILE_SOURCE,\n FISH_HISTFILE_SOURCE,\n ZSH_HISTFILE_SOURCE,\n} from '@x-otto/tui'\n\nimport type { ExternalHistoryConfig } from '@x-otto/setting'\nimport type { HistoryEntry } from '@x-otto/tui'\n\nconst DEFAULT_MAX_ENTRIES = 2000\n\nconst PARSER_BY_SOURCE: Record<\n string,\n (content: string) => HistoryEntry[]\n> = {\n [ZSH_HISTFILE_SOURCE]: (content) => parseZshHistory(content),\n [BASH_HISTFILE_SOURCE]: (content) => parseBashHistory(content),\n [FISH_HISTFILE_SOURCE]: (content) => parseFishHistory(content),\n}\n\nexport interface ResolvedHistorySource {\n id: string\n path: string\n}\n\nexport function resolveDefaultShellHistorySources(): ResolvedHistorySource[] {\n const shell = process.env.SHELL ?? ''\n const home = homedir()\n const histfile = process.env.HISTFILE\n\n if (shell.includes('fish')) {\n return [\n { id: FISH_HISTFILE_SOURCE, path: join(home, '.local/share/fish/fish_history') },\n { id: FISH_HISTFILE_SOURCE, path: join(home, '.config/fish/fish_history') },\n ]\n }\n\n if (shell.includes('bash')) {\n return [{\n id: BASH_HISTFILE_SOURCE,\n path: histfile?.startsWith('~')\n ? join(home, histfile.slice(2))\n : histfile ?? join(home, '.bash_history'),\n }]\n }\n\n if (shell.includes('zsh') || !shell) {\n return [{\n id: ZSH_HISTFILE_SOURCE,\n path: histfile?.startsWith('~')\n ? join(home, histfile.slice(2))\n : histfile ?? join(home, '.zsh_history'),\n }]\n }\n\n return []\n}\n\nexport function resolveConfiguredSources(\n config: ExternalHistoryConfig | undefined,\n): ResolvedHistorySource[] {\n const configured = config?.sources?.filter(\n (s): s is { id: string; path: string; enabled?: boolean } =>\n s?.enabled !== false && Boolean(s?.id) && Boolean(s?.path),\n )\n if (configured && configured.length > 0) {\n const home = homedir()\n return configured.map((source) => ({\n id: source.id,\n path: expandHome(source.path, home),\n }))\n }\n return resolveDefaultShellHistorySources()\n}\n\nfunction expandHome(path: string, home: string): string {\n if (path.startsWith('~/')) return join(home, path.slice(2))\n if (path === '~') return home\n return path\n}\n\nasync function isReadable(path: string): Promise<boolean> {\n try {\n await access(path)\n return true\n } catch {\n return false\n }\n}\n\nasync function loadSourceFile(\n source: ResolvedHistorySource,\n maxEntries: number,\n): Promise<HistoryEntry[]> {\n const parser = PARSER_BY_SOURCE[source.id]\n if (!parser) return []\n if (!(await isReadable(source.path))) return []\n\n try {\n const content = await readFile(source.path, 'utf8')\n const entries = parser(content)\n return entries.slice(-maxEntries)\n } catch {\n return []\n }\n}\n\n/** 加载外部 shell 历史;`enabled !== true` 时立即返回空数组(RFC-129 §7-3)。 */\nexport async function loadExternalShellHistory(\n config: ExternalHistoryConfig | undefined,\n): Promise<HistoryEntry[]> {\n if (config?.enabled !== true) return []\n\n const maxEntries = config.maxEntries ?? DEFAULT_MAX_ENTRIES\n const sources = resolveConfiguredSources(config)\n const seenPaths = new Set<string>()\n const merged: HistoryEntry[] = []\n\n for (const source of sources) {\n if (seenPaths.has(source.path)) continue\n seenPaths.add(source.path)\n const entries = await loadSourceFile(source, maxEntries)\n merged.push(...entries)\n }\n\n return merged.slice(-maxEntries)\n}\n","import { InputHistoryStore } from './input-history-store'\nimport { loadExternalShellHistory } from './shell-history-loader'\n\nimport { entriesToCommands } from '@x-otto/tui'\n\nimport type { AgentSession } from '@x-otto/runtime'\nimport type { ExternalHistoryConfig } from '@x-otto/setting'\n\n/**\n * 交互模式输入历史初始化(RFC-199 D3 自 interactive.ts 提取,行为零变化):\n * ① 持久化输入历史(RFC-088 M4b)——独立文件恢复,首次迁移从 session 旧快照回灌\n * (hydrate + persist + 删除旧字段,确保下次 snapshot 不再含 inputHistory);\n * ② 外部 shell 历史(RFC-129)——仅 config.history.external.enabled 时加载。\n */\nexport async function setupInputHistory(\n session: AgentSession,\n externalConfig: ExternalHistoryConfig | undefined,\n): Promise<{\n historyStore: InputHistoryStore\n inputHistory: string[]\n externalHistoryCommands: string[] | undefined\n}> {\n const historyStore = new InputHistoryStore(session.id)\n const inputHistory = await historyStore.load().then((entries) => {\n if (entries.length > 0) return entries\n // 一次性迁移:旧快照有 inputHistory 但无 history 文件 → hydrate + persist + 删除旧字段\n const legacy = session.session.getInputHistory()\n if (legacy && legacy.length > 0) {\n historyStore.hydrate(legacy)\n historyStore.persist().catch(() => {})\n // RFC-088 M4b Q3:显式删除旧字段,确保下次 snapshot 不再含 inputHistory\n session.session.setInputHistory([])\n return legacy\n }\n return []\n })\n\n const externalHistoryCommands = externalConfig?.enabled === true\n ? entriesToCommands(await loadExternalShellHistory(externalConfig))\n : undefined\n\n return { historyStore, inputHistory, externalHistoryCommands }\n}\n","/**\n * paste-session-binding.ts — RFC-338 D7/D9:粘贴缓存的会话归属绑定。\n *\n * ## 为什么需要这一层\n *\n * 粘贴缓存(`PasteStore`)住在 `PromptInput` 内部的 `useRef` 单例里,**整个 TUI\n * 生命周期只创建一次,且组件不接收 `sessionId`**。而 `/resume` 与 `/fork` 都是\n * **同进程内**切换会话(不重挂载组件),若不干预,A 会话的粘贴条目会被当作 B\n * 会话的内容继续使用、并写进 B 的分区。\n *\n * 故绑定必须由 cli 侧驱动:切会话时把旧会话的快照落盘、把新会话的快照 hydrate 进去。\n * 数据通路是 `TuiHandle.exportPaste()/hydratePaste()`(RFC-338 D7a 选定的 ref 桥,\n * 动词命名以避开 `TuiHandle` 的 set* 增长门禁)。\n *\n * ## 为什么不放在 PersistenceSync 的回合级落盘里\n *\n * 粘贴/提交是**用户交互事件**,不在 agent 回合边界上——用户可能粘贴后长时间不提交,\n * 也可能提交后立刻退出。挂在回合节奏上会丢数据,故本模块提供显式落盘入口。\n */\nimport type { PasteSnapshot, TuiHandle } from '@x-otto/tui'\n\n/** 面板态门店的最小读取面(避免 cli 直接依赖 @x-otto/runtime 的具体类)。 */\nexport interface PasteStateGateway {\n getPaste(sessionId: string): Promise<{ entries: PasteSnapshot['entries']; nextId: number }>\n setPaste(\n sessionId: string,\n snapshot: { sessionId: string; entries: PasteSnapshot['entries']; nextId: number },\n ): Promise<boolean>\n}\n\n/** TUI 侧粘贴桥的最小操作面。 */\nexport type PasteBridge = Pick<TuiHandle['pending'], 'hydratePaste' | 'exportPaste'>\n\nexport interface PasteSessionBinder {\n /** 启动/切入某会话:把该会话的持久化快照恢复进输入框 store。 */\n load(sessionId: string): Promise<void>\n /** 把当前输入框 store 的快照落盘到指定会话分区。 */\n save(sessionId: string): Promise<void>\n /**\n * 切换会话(`/resume`):先把**旧**会话的状态落盘,再 hydrate **新**会话的。\n * 顺序不可颠倒——先 hydrate 会让旧会话的未落盘条目被覆盖丢失。\n */\n switchTo(fromSessionId: string | undefined, toSessionId: string): Promise<void>\n /**\n * 分裂会话(`/fork`,RFC-338 D9):把源会话的快照**复制**到目标分区,\n * 并让 `nextId` 一并继承。\n *\n * 为何必须继承 `nextId`:fork 后两条时间线共享同一段历史,若目标会话的游标从 1\n * 重新开始,新粘贴会分配到源会话历史中已存在的 id —— 历史里的 `[@paste:#4]`\n * 就会展开成一份完全不同的内容(重要事项规则 1b,静默数据损坏级)。\n */\n fork(fromSessionId: string, toSessionId: string): Promise<void>\n}\n\n/**\n * 全链路 fail-soft:粘贴缓存是**便利性**功能,任何一步失败最多让 chip 失效\n * (此时 UI 已有\"粘贴内容已失效\"的 toast 与提交阻断,是安全的失败),\n * 绝不允许因为它让切会话/fork/启动这些主流程报错。\n */\nexport function createPasteSessionBinder(deps: {\n gateway: () => PasteStateGateway | undefined\n bridge: () => PasteBridge | undefined\n onError?: (stage: string, error: unknown) => void\n}): PasteSessionBinder {\n const fail = (stage: string, error: unknown): void => {\n deps.onError?.(stage, error)\n }\n\n const load = async (sessionId: string): Promise<void> => {\n const gateway = deps.gateway()\n const bridge = deps.bridge()\n if (!gateway || !bridge) return\n try {\n const snapshot = await gateway.getPaste(sessionId)\n bridge.hydratePaste({ entries: snapshot.entries, nextId: snapshot.nextId })\n } catch (error) {\n fail('load', error)\n // 恢复失败时显式清空——绝不能让上一会话的条目留在 store 里被当作本会话内容\n // (会话归属错配比\"缓存丢失\"严重得多)。\n try {\n bridge.hydratePaste(null)\n } catch {\n /* 桥不可用,忽略 */\n }\n }\n }\n\n const save = async (sessionId: string): Promise<void> => {\n const gateway = deps.gateway()\n const bridge = deps.bridge()\n if (!gateway || !bridge) return\n try {\n const snapshot = bridge.exportPaste()\n if (!snapshot) return // TUI 尚未挂载(桥为空),无可落盘内容\n await gateway.setPaste(sessionId, {\n sessionId,\n entries: snapshot.entries,\n nextId: snapshot.nextId,\n })\n } catch (error) {\n fail('save', error)\n }\n }\n\n return {\n load,\n save,\n\n async switchTo(fromSessionId: string | undefined, toSessionId: string): Promise<void> {\n if (fromSessionId === toSessionId) return\n if (fromSessionId !== undefined) await save(fromSessionId)\n await load(toSessionId)\n },\n\n async fork(fromSessionId: string, toSessionId: string): Promise<void> {\n const gateway = deps.gateway()\n const bridge = deps.bridge()\n if (!gateway || !bridge) return\n try {\n // 以**内存中的当前状态**为准(用户可能刚粘贴还没落盘),回退到磁盘快照。\n const live = bridge.exportPaste()\n const source = live ?? (await gateway.getPaste(fromSessionId))\n await gateway.setPaste(toSessionId, {\n sessionId: toSessionId,\n entries: source.entries,\n // 规则 1b:游标随分裂一起继承,两条时间线的 id 空间不重叠。\n nextId: source.nextId,\n })\n // fork 后 TUI 停留在新会话上,store 内容与新分区一致,无需 re-hydrate。\n } catch (error) {\n fail('fork', error)\n }\n },\n }\n}\n","import { i18next } from '@x-otto/tui'\nimport type { AgentSession } from '@x-otto/runtime'\nimport type { RateLimitType, TuiHandle } from '@x-otto/tui'\n\n\n/** startTui 句柄的最小消费面(结构化类型,避免引入完整 TuiHandle)。 */\ninterface ToastTui {\n engine: Pick<TuiHandle['engine'], 'showRetryDialog' | 'clearRetryDialog'>\n info: Pick<TuiHandle['info'], 'showToast'>\n}\n\n/**\n * coreAgent 事件 → TUI 即时反馈的三路订阅(RFC-199 D3 自 interactive.ts 提取,行为零变化):\n * ① stream.retry → 重试倒计时条(H1,用户按键加速跳过退避;RFC-303:新增停止按钮/Esc\n * 直接中止当前回合,不二次确认——此前该浮层唯一出路是隐藏快捷键 Ctrl+C 且会\n * 额外弹二次确认框,用户反馈\"无法关闭、自主终止\");\n * ② rate.limit.exhausted → 清倒计时 + 捕获 resetsAt/limitType(H7,UI 弹出交给 catch 块——\n * 同步 emit 早于 catch 的 throw,见 TypedEventEmitter.emit 与 handleStreamError 顺序);\n * ③ lifecycle.tool.completed.crossBoundary → toast(RFC-144 M6:跨 prompt 边界完成通知\n * 绝不作为对话消息出现,只以 toast 呈现,避免静默丢失)。\n */\nexport function wireAgentToasts(deps: {\n session: AgentSession\n tui: ToastTui\n retryAccelerate: { skip: boolean }\n setRateLimitInfo: (info: { limitType?: RateLimitType; resetsAt?: number }) => void\n /** RFC-303:重试倒计时浮层的停止按钮/Esc——直接中止当前回合(不二次确认)。 */\n abortNow: () => void\n}): void {\n const { session, tui, retryAccelerate, setRateLimitInfo, abortNow } = deps\n\n session.coreAgent.on('stream.retry', ({ attempt, maxRetries, delayMs, reason }) => {\n tui.engine.showRetryDialog(attempt, maxRetries, delayMs, retryAccelerate, () => {\n // onExpired:倒计时自然到期或用户按键加速后清除\n tui.engine.clearRetryDialog()\n }, reason, () => {\n // onStop:用户主动停止——立即中止当前回合,浮层由 showRetryDialog 内部包装清除。\n abortNow()\n })\n })\n\n session.coreAgent.on('rate.limit.exhausted', ({ limitType, resetsAt }) => {\n tui.engine.clearRetryDialog()\n setRateLimitInfo({ limitType, resetsAt })\n })\n\n session.coreAgent.on('lifecycle.tool.completed.crossBoundary', ({ toolName, result }) => {\n const title = i18next.t('cmd.titleBackgroundTask')\n const body = result.isError\n ? i18next.t('cmd.lifecycleCrossBoundaryFailed', { toolName: toolName })\n : i18next.t('cmd.lifecycleCrossBoundaryDone', { toolName: toolName })\n tui.info.showToast(title, body)\n })\n}\n","/**\n * ⚠ 提及(@mentions)历史:最近使用的@文件/符号/chip 建议。\n */\nimport type { RecentMentionEntry } from '@x-otto/setting'\nimport type { SigilEntry } from '@x-otto/tui'\n\n/** @ 面板「最近使用」分组的最大保留条数(MRU),与 SigilPane.SIGIL_RECENT_MAX 对齐。 */\nexport const MAX_RECENT_MENTIONS = 5\n\n/**\n * 把选中的 sigil 条目推到 MRU 列表最前(纯函数)。\n * 去重键为 `(kind, label)` 复合键(对齐 PluginInputRegistry 的条目复合键约定)——\n * 同 label 不同 kind 视为不同条目;已存在则去重后移到最前,不产生重复项,\n * 并裁剪到 MAX_RECENT_MENTIONS 条。\n */\nexport function pushRecentMention(\n recent: readonly RecentMentionEntry[],\n entry: RecentMentionEntry,\n): RecentMentionEntry[] {\n const keyOf = (e: Pick<RecentMentionEntry, 'kind' | 'label'>) => `${e.kind}\\u0000${e.label}`\n const key = keyOf(entry)\n return [entry, ...recent.filter((e) => keyOf(e) !== key)].slice(0, MAX_RECENT_MENTIONS)\n}\n\n/** SigilEntry → RecentMentionEntry(只保留持久化最小字段,丢弃 `preview`/`source`)。 */\nexport function toRecentMentionEntry(entry: SigilEntry): RecentMentionEntry {\n return {\n label: entry.label,\n value: entry.value,\n kind: entry.kind,\n description: entry.description,\n }\n}\n\n/** RecentMentionEntry[] → SigilEntry[](供 SigilPane recentEntries 消费;source 固定 'recent')。 */\nexport function toSigilEntries(recent: readonly RecentMentionEntry[]): SigilEntry[] {\n return recent.map((e) => ({\n label: e.label,\n value: e.value,\n kind: e.kind,\n description: e.description,\n source: 'recent',\n }))\n}\n","/**\n * 内存治理 TUI 交互管线(onWarning/onCritical 回调 → toast/compact/提示)。\n */\nimport type { App } from '@x-otto/coding'\nimport type { TuiHandle } from '@x-otto/tui'\nimport type { AgentSession } from '@x-otto/runtime'\nimport type { ContentPart } from '@x-otto/ai'\nimport { i18next } from '@x-otto/tui'\nimport { joinMessageText } from '../view-model-adapter'\n\n/** RFC-167 D4:D4 持续通知的重复间隔(ms)。默认 30 分钟,经 OTTO_MEMORY_CRITICAL_NOTIFY_INTERVAL_MS 覆盖。 */\nconst DEFAULT_CRITICAL_NOTIFY_INTERVAL_MS = 30 * 60 * 1000\n/** RFC-167 D4:二级兜底触发所需的通知周期数。默认 4(= 2 小时 @ 30min/周期),经\n * OTTO_MEMORY_CRITICAL_NOTIFY_ESCALATION_CYCLES 覆盖。 */\nconst DEFAULT_CRITICAL_NOTIFY_ESCALATION_CYCLES = 4\n\nfunction resolveMsEnv(envVar: string, fallback: number): number {\n const raw = process.env[envVar]\n if (!raw) return fallback\n const parsed = Number(raw)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback\n}\n\nfunction resolveIntEnv(envVar: string, fallback: number): number {\n const raw = process.env[envVar]\n if (!raw) return fallback\n const parsed = Number(raw)\n return Number.isFinite(parsed) && parsed > 0 && Number.isInteger(parsed) ? parsed : fallback\n}\n\nexport interface MemoryGovernorCallbacksDeps {\n app: App\n tui: TuiHandle\n getSession: () => AgentSession\n /** RFC-115 M4:重启前会话正忙时暂存的最后一条用户消息原文,供 cli.ts 附加 --resume-prompt。 */\n setRestartResumePrompt: (text: string) => void\n /** RFC-115 M3:本次重启由内存治理 critical Dialog 触发——供 cli.ts 附加 --memory-restart-grace。 */\n setRestartMemoryTriggered: (value: boolean) => void\n handleExit: (result: { type: 'exit'; restart?: boolean }) => void\n /** 2026-07-19:onWarning 自动压缩成功路径 UI 反馈修复——复用手动 /compact 与 token 驱动\n * 自动压缩同款收尾(清空占用提醒区间计数 + 立即刷新占比条),不等下一轮 prompt.end。 */\n onContextCompacted: () => void\n recomputeBudget: () => void\n}\n\nexport function registerMemoryGovernorCallbacks(deps: MemoryGovernorCallbacksDeps): void {\n const {\n app,\n tui,\n getSession,\n setRestartResumePrompt,\n setRestartMemoryTriggered,\n handleExit,\n onContextCompacted,\n recomputeBudget,\n } = deps\n\n // RFC-115 M3:硬阈值抢先重启——critical 分级弹 Dialog 征得用户确认后走既有\n // restart 陪跑路径(handleExit 复用,D1-D2)。memoryCriticalSuppressed 是\n // 进程级瞬态 flag(onExtra \"本次会话不再提醒\"回调设置,非持久化,经 D2/P2-2 明确)。\n let memoryCriticalSuppressed = false\n // RFC-133 M1:全屏面板在场时跳过弹窗的升级路径状态。criticalSkipCount 是连续\n // 因面板在场被跳过的次数(每次 onCritical 触发且面板 active 时递增,弹窗后重置)。\n // criticalSkippedSinceMs 是首次因面板跳过的 Unix 毫秒时间戳(跨 Dialog 累积,\n // 只在分级降回非 critical 或面板不再 active 时清零——评审 F4 防止\"弹窗→稍后提醒\n // →计时器归零\"无限延期)。\n let criticalSkipCount = 0\n let criticalSkippedSinceMs: number | undefined = undefined\n\n // RFC-167 D2/D3:critical Dialog 打开态标记——onCriticalTimeout 触发时据此判断\"用户\n // 是否从未响应过弹窗\"。invariant(强制门禁,RFC-167 §5):`criticalDialogOpen === true`\n // 当且仅当 critical Dialog 实际处于用户可见的打开状态,清零路径仅限于\n // onConfirm/onCancel/onExtra 三个回调——不得新增其他清零路径。\n //\n // ESC 关闭路径核实(RFC-167 M2 设计闸门前置检查结论):`ConfirmDialog.tsx` 的\n // `escapeIndex={maxIdx}` 使 ESC 触发 `onPick(maxIdx)`,maxIdx 恒映射到 `onCancel()`\n // 分支(`Choices.tsx:59` `if (escapeIndex !== undefined) onPick(escapeIndex)` +\n // `ConfirmDialog.tsx` 的 `else onCancel()`);`startTui-handlers.ts` 的\n // `onConfirmResolve()` 是 `confirmRequest` 清空的唯一入口,且必然三选一调用\n // `resolve()`/`cancel()`/`extraFn()` 之一——不存在\"Dialog 被清空但三个回调都未触发\"\n // 的第四条路径。因此 ESC **就是** `onCancel`,不需要额外的 ESC 特判或超时点二次校验\n // (RFC §4.2 预留的\"若无 ESC 生命周期回调则走二次校验\"分支在本仓不适用,此处记录\n // 核实结论而非直接选择该分支)。\n let criticalDialogOpen = false\n // RFC-167 D4:D4 持续通知的周期计数(用户已明确\"稍后提醒\"后,每个通知周期递增;\n // 达到 escalation 周期数后触发一次性二级兜底降级)。分级恢复健康或用户后续确认/\n // 重启时清零。注意:escalation 是\"到达第 N 个周期时额外多做一次降级动作\",不是\n // 通知循环的终点——RFC-167 D4 原文\"每隔一个更长的提醒间隔重复一次,直到用户手动\n // /exit 或分级恢复健康\"要求通知本身持续到用户退出或恢复健康,escalation 只触发\n // 一次(`criticalEscalationFired` 标记),之后通知仍按周期继续(不再重复触发\n // compactNow/trace,避免每 30 分钟都重复降级)。\n let criticalNotifyCycle = 0\n let criticalEscalationFired = false\n const criticalNotifyIntervalMs = resolveMsEnv(\n 'OTTO_MEMORY_CRITICAL_NOTIFY_INTERVAL_MS',\n DEFAULT_CRITICAL_NOTIFY_INTERVAL_MS,\n )\n const criticalNotifyEscalationCycles = resolveIntEnv(\n 'OTTO_MEMORY_CRITICAL_NOTIFY_ESCALATION_CYCLES',\n DEFAULT_CRITICAL_NOTIFY_ESCALATION_CYCLES,\n )\n let criticalNotifyTimer: ReturnType<typeof setInterval> | undefined\n\n /** D3 时序共享执行体:无论是用户点击\"立即重启\"确认,还是 onCriticalTimeout 判定用户从未\n * 响应弹窗,都走同一套重启流程(①暂存续跑消息 → ②restartMemoryTriggered → ③abort → ④stop)。\n * 提取为共享函数,避免 onConfirm 与 onCriticalTimeout 两处重复实现同一时序(RFC-167 §5)。 */\n const executeConfirmedRestart = (): void => {\n const session = getSession()\n if (session.isBusy) {\n const messages = session.session.messages()\n const last = messages[messages.length - 1] as\n | { role?: string; content?: unknown; internal?: boolean }\n | undefined\n if (last?.role === 'user' && !last.internal) {\n const text = joinMessageText(last.content as string | ReadonlyArray<ContentPart>)\n if (text) setRestartResumePrompt(text)\n }\n }\n setRestartMemoryTriggered(true)\n handleExit({ type: 'exit', restart: true })\n }\n\n const stopCriticalNotifyTimer = (): void => {\n if (criticalNotifyTimer) {\n clearInterval(criticalNotifyTimer)\n criticalNotifyTimer = undefined\n }\n criticalNotifyCycle = 0\n criticalEscalationFired = false\n }\n\n app.memoryGovernor.setCallbacks({\n onWarning: () => {\n void getSession()\n .compactNow()\n .then((result) => {\n // 成功路径可观测性修复(2026-07-19,用户反馈\"裁剪前没看到压缩提示\"排查发现):\n // compactNow() 底层只 emit compaction.before/after 内部 hook(仅供日志),不\n // publish compaction.start/end——不像 token 驱动的自动压缩(agent-session.compact())\n // 或手动 /compact 那样天然有 UI。此前只有失败路径有 toast(RFC-147 M3 D-C2),\n // 成功路径完全静默,用户对\"70% 内存压力触发的自动压缩\"毫无感知。\n // result.compacted=false(消息太少/memory 未启用)时静默——与手动 /compact\n // 的 compactNoop 语义一致,不是错误,不值得打断用户。\n if (result.compacted) {\n tui.info.showToast(i18next.t('cmd.titleAutoCompact'), i18next.t('compact.edResult', { before: result.before, after: result.after }))\n // RFC-156:真实发生了压缩——清空占用提醒的区间计数,进入新一轮对话生命周期\n // (对齐 token 驱动自动压缩 compaction.end 分支、手动 /compact 的同款收尾)。\n onContextCompacted()\n recomputeBudget()\n }\n })\n .catch((err) => {\n const msg = err instanceof Error ? err.message : String(err)\n app.logger.warn({ err: msg }, '[memory-governor] compactNow (soft degrade) failed')\n // RFC-147 M3 D-C2:自动软压缩失败此前完全静默(仅记日志),用户无从感知——\n // 升级为 toast 告警。不进 ErrorRetryCard(无\"原消息\"可重投,卡片语义专属\n // prompt 回合终止场景);不做自动倒计时重试——下一次内存阈值命中会自然\n // 再触发一次 compactNow,天然具备重试语义。\n tui.info.showToast(i18next.t('cmd.titleAutoCompact'), i18next.t('cmd.autoCompactFailed', { msg: msg }))\n })\n },\n onCritical: (sample) => {\n if (memoryCriticalSuppressed) {\n // P2-3 修正:suppress 后不完全静默——降级为低优先级通知,用户在通知中心\n // 仍可查(不弹窗打断,但不是\"用户毫无感知地逼近 OOM\")。\n tui.info.pushNotification(\n i18next.t('cmd.titleMemoryPressure'),\n i18next.t('cmd.memoryPressureMessage', { usedPct: (sample.usedPct).toFixed(0) }),\n 'system',\n 'info',\n )\n return\n }\n // RFC-133 M1:全屏面板在场时不再无限静默跳过——引入次数/时长升级路径。\n // 面板 active 时递增跳过计数并记录首次时间戳;跳过次数 <3 且累计时长 <5min\n // 才静默跳过(不打扰用户正在看的面板);任一条件命中即无视面板状态强制弹窗,\n // 防止面板挂着导致 critical 信号被无限吃掉直到物理 OOM。\n if (tui.info.isFullScreenPaneActive()) {\n criticalSkipCount++\n criticalSkippedSinceMs ??= Date.now()\n if (criticalSkipCount < 3 && (Date.now() - criticalSkippedSinceMs) < 5 * 60 * 1000) {\n return\n }\n // 命中升级条件:无视面板状态,强制弹窗(不 return,继续往下走到 openConfirm)\n }\n // 弹窗前重置跳过计数(criticalSkippedSinceMs 不重置——评审 F4:跨 Dialog 累积,\n // 防止\"弹窗→稍后提醒→面板依然 active→计时器归零→再等 5 分钟\"无限延期。只在\n // 分级降回非 critical 时由 onLevelChange 清零,或面板退出时自然不再进入此分支)。\n criticalSkipCount = 0\n // RFC-167 D2:弹窗打开——标记 criticalDialogOpen(onCriticalTimeout 据此判断\n // 用户是否从未响应)。invariant:只在 onConfirm/onCancel/onExtra 三个回调内清零。\n criticalDialogOpen = true\n tui.modal.openModal({\n kind: 'confirm',\n title: i18next.t('cmd.titleMemoryPressure'),\n message: i18next.t('cmd.memoryPressureMessage', { usedPct: (sample.usedPct).toFixed(0) }),\n confirmLabel: i18next.t('cmd.memoryPressureRestartNow'),\n cancelLabel: i18next.t('cmd.memoryPressureRemindLater'),\n confirmColor: 'amber',\n extraLabel: i18next.t('cmd.memoryPressureSuppress'),\n onConfirm: () => {\n criticalDialogOpen = false\n stopCriticalNotifyTimer()\n // D3 时序①-④:① 若会话正忙,取最后一条用户消息原文暂存供重启后续跑\n // (有 todo 列表场景走既有 RFC-094 TodoGateRunner 路径,此处只覆盖无 todo\n // 结构化跟踪的单轮长任务)→ ② restartRequested=true(handleExit 内部设置)\n // → ③ dispatchToEngine(abort) → ④ stopTuiGuarded()/app.stop()(含 saveAll)。\n // ①必须先于③(abort 后 isBusy 变为 false,无法再判断是否需要续跑)。提取为\n // executeConfirmedRestart 共享函数(RFC-167 M2:与 onCriticalTimeout 的\n // \"从未响应\"分支共用同一套重启时序,不重复实现)。\n executeConfirmedRestart()\n },\n onCancel: () => {\n // 稍后提醒:criticalDialogOpen 清零(用户已明确表态,onCriticalTimeout 后续\n // 触发时不应再判定为\"从未响应\",改走 D4 持续通知路径)。\n criticalDialogOpen = false\n },\n onExtra: () => {\n criticalDialogOpen = false\n memoryCriticalSuppressed = true\n },\n })\n },\n // RFC-115 D5:重启后宽限期内命中 critical——降级为常驻通知而非再次弹窗,\n // 把决策权交还用户(用户可主动 /exit 重启),避免连续打断。\n onCriticalDuringGrace: (sample) => {\n tui.info.pushNotification(\n i18next.t('cmd.titleMemoryPressure'),\n i18next.t('cmd.memoryPressureMessage', { usedPct: (sample.usedPct).toFixed(0) }),\n 'system',\n 'warning',\n )\n },\n // RFC-167 D1-D4:critical 分级持续超过 criticalTimeoutMs(默认 15 分钟)时触发——\n // 与上方 onCritical(状态跃迁触发)完全独立、互补。两分支路由:\n // ① criticalDialogOpen === true(用户从未响应过弹窗)→ 代替用户执行其已经看到过的\n // 默认建议(onConfirm 同款重启流程,经 executeConfirmedRestart 共享);\n // ② criticalDialogOpen === false(用户已点过 confirm/cancel/extra 任一按钮,明确\n // 表过态)→ 走 D4 持续非阻塞通知 + 二级兜底超时,不覆盖用户已做出的选择。\n onCriticalTimeout: (sample) => {\n if (criticalDialogOpen) {\n // 用户从未关闭过弹窗——本次超时视为\"确认默认建议\",直接执行重启(不弹二次\n // 确认,因为弹窗内容本身已经告知过用户\"检测到内存压力,建议重启\",这不是\n // \"完全静默的自动重启\",是对已展示内容的默认执行)。\n criticalDialogOpen = false\n executeConfirmedRestart()\n return\n }\n // D4:用户已经表过态(无论确认/取消/抑制)——启动/维持持续通知定时器,每\n // criticalNotifyIntervalMs 提醒一次,直到用户手动 /exit 或分级恢复健康(不会\n // 自行停止,RFC-167 D4 原文明确要求\"直到…\");累计到 criticalNotifyEscalationCycles\n // 个周期时额外触发一次性二级兜底(compactNow 降级 + trace 事件)——escalation\n // 只是\"在通知循环内额外做一次降级动作\",不是通知循环的终点,之后通知仍按周期\n // 继续(不重复触发 compactNow/trace,criticalEscalationFired 保证只降级一次)。\n const notifyOnce = (): void => {\n criticalNotifyCycle++\n tui.info.pushNotification(\n i18next.t('cmd.titleMemoryPressure'),\n i18next.t('cmd.memoryPressureMessage', { usedPct: app.memoryGovernor.sample().usedPct.toFixed(0) }),\n 'system',\n 'warning',\n )\n if (!criticalEscalationFired && criticalNotifyCycle >= criticalNotifyEscalationCycles) {\n criticalEscalationFired = true\n void getSession().compactNow().catch((err) => {\n const msg = err instanceof Error ? err.message : String(err)\n app.logger.warn(\n { err: msg },\n '[memory-governor] critical-unattended-escalation compactNow failed',\n )\n })\n void app.storage.traceStore\n ?.append(getSession().id, {\n ts: Date.now(),\n sessionId: getSession().id,\n kind: 'lifecycle',\n node: 'critical-unattended-escalation',\n payload: { usedPct: sample.usedPct, cycles: criticalNotifyCycle },\n })\n .catch((err: unknown) => {\n app.logger.warn(\n { err: err instanceof Error ? err.message : String(err) },\n '[memory-governor] critical-unattended-escalation trace append failed',\n )\n })\n }\n }\n if (!criticalNotifyTimer) {\n notifyOnce()\n criticalNotifyTimer = setInterval(notifyOnce, criticalNotifyIntervalMs)\n criticalNotifyTimer.unref?.()\n }\n },\n // RFC-116 M3:footer 常驻指示器——覆盖全部分级变化方向(含恢复到 healthy),\n // 不受冷却期/宽限期约束,纯粹的状态展示同步,与上面几个回调驱动的降级/重启\n // 动作正交(同一次 tick() 内 onLevelChange 与 onWarning/onCritical 可能都触发,\n // 互不冲突)。\n onLevelChange: (level) => {\n // RFC-133 M1:分级降回非 critical 时清零面板跳过累计状态(恢复健康后\n // 再次进入 critical+面板 active 场景应该是全新的计时周期)。\n if (level !== 'critical') {\n criticalSkipCount = 0\n criticalSkippedSinceMs = undefined\n // RFC-167 D4:分级恢复健康(或降至 warning)——停止持续通知定时器 + 清零周期计数。\n // 内存压力已经不再持续,不需要再走 D4 的定期提醒/二级兜底路径;下次重新进入\n // critical 会是全新的 criticalEnteredAt 计时周期(MemoryGovernor 侧已保证),\n // 本层的通知计时器同步归零,避免用陈旧的周期计数误判升级触发点。\n stopCriticalNotifyTimer()\n }\n // footer 常驻指示器新增百分比展示:顺手带上本次采样的 usedPct(sample() 是纯读取,\n // 零额外开销),使指示器不必等用户手动打开 /perf 面板才能看到具体数字。\n tui.engine.pushEngineEvent({ kind: 'perf', indicatorLevel: level, memory: app.memoryGovernor.sample() })\n },\n })\n\n // RFC-196 M2:进程群预算超限 → TUI 通知(app-lifecycle 已接 onSample 落 trace +\n // FleetMonitor 内部 logger.warn;此处逐字段合并只追加 onBudgetExceeded,不覆盖 onSample)。\n // 通知中心(非弹窗)——预算超限是趋势告警不是紧急事件,不打断当前操作;FleetMonitor\n // 自带区间去重(连续超限只发一次,恢复后可再触发),此处无需再去重。\n app.fleetMonitor.setCallbacks({\n onBudgetExceeded: (sample) => {\n tui.info.pushNotification(\n i18next.t('cmd.titleFleetBudget'),\n i18next.t('cmd.fleetBudgetExceeded', { processCount: sample.processCount, totalRssMb: sample.totalRssMb }),\n 'system',\n 'warning',\n )\n },\n })\n}\n","/**\n * TUI 回调集中装配工厂(createTuiCallbacks):openModel/followUp/queuedPush 等。\n */\nimport type { App } from '@x-otto/coding'\nimport type { TuiHandle } from '@x-otto/tui'\nimport type { AgentSession } from '@x-otto/runtime'\nimport type { CliContext } from '../command-dispatcher'\nimport { dispatchSlashCommand } from '../command-dispatcher'\nimport { i18next } from '@x-otto/tui'\nimport { nextPermissionMode } from './interactive-utils'\nimport { sortDrafts, makeDraftItem, capDrafts } from './draft-entries'\n\nexport interface TuiCallbacksDeps {\n app: App\n getTui: () => TuiHandle\n getTuiRef: () => TuiHandle | undefined\n getSession: () => AgentSession\n getCliCtx: () => CliContext\n historyStore: { hydrate: (entries: string[]) => void; persist: () => Promise<void> }\n /** RFC-338:粘贴缓存绑定器 getter(装配晚于本工厂,故用 getter 惰性取)。 */\n getPasteBinder?: () => { save: (sessionId: string) => Promise<void> } | undefined\n}\n\nexport interface TuiCallbacks {\n onCyclePermissionMode: () => void\n onHistoryPush: (entries: readonly string[]) => void\n onDraftSave: (text: string) => void\n onDraftDelete: (id: string) => void\n onDraftDeleteMany: (ids: string[]) => void\n onA2uiAction: (componentId: string, action: string, payload?: Record<string, unknown>) => void\n onPerfRefresh: () => void\n onProxySubmitUrl: (url: string) => void\n onProxyToggleEnabled: () => void\n onProxyClear: () => void\n onProxyTest: () => void\n}\n\nexport function createTuiCallbacks(deps: TuiCallbacksDeps): TuiCallbacks {\n const { app, getTui, getTuiRef, getSession, getCliCtx, historyStore } = deps\n\n return {\n onCyclePermissionMode: () => {\n const tui = getTui()\n // 循环次序(UI 策略)宿主决定 → 立即更新显示 → 异步持久化机制(App.setPermissionMode)。\n const next = nextPermissionMode(app.getPermissionMode())\n tui.stores.tasks.setPermissionMode(next)\n tui.info.showToast(i18next.t('cmd.titlePermission'), i18next.t('pref.permModeSwitched', { label: i18next.t('pref.permModeLabel', { context: next }) }))\n void app.setPermissionMode(next)\n },\n onHistoryPush: (entries) => {\n try {\n historyStore.hydrate([...entries])\n historyStore.persist().catch(() => {})\n } catch { /* best-effort */ }\n // RFC-338:提交即落盘粘贴缓存——与输入历史同一时机(历史里存的是含\n // [@paste:#N] 的原文,两者必须同步落盘,否则重启后历史在、原文丢)。\n // fire-and-forget + fail-soft:落盘失败最多让 chip 跨重启失效,不阻断提交。\n void deps.getPasteBinder?.()?.save(getSession().id)\n },\n // 草稿持久化:随会话快照走本地/远端存储(@x-otto/session PersistedSessionConfig.drafts)——\n // 读写用**当前活跃**会话(session 是 let、/resume 切会话后重指向,此处每次调用时取\n // 最新值,对齐本文件其余闭包如 persistAvailable 的写法)。保存/删除是随时触发的用户\n // 动作,不挂在回合边界上,故显式调 app.sessionManager.save() 立即落盘(fail-soft,\n // 吞错不阻断 UI)。命名/排序逻辑单一真源见 draft-entries.ts(与 handlers/draft.ts 共用)。\n onDraftSave: (text: string) => {\n const session = getSession()\n const existing = session.session.getDrafts() ?? []\n const item = makeDraftItem(undefined, text, existing)\n const next = capDrafts([...existing, item])\n session.session.setDrafts(next)\n void app.sessionManager.save(session.id).catch(() => {})\n getTuiRef()?.stores.tasks.setDrafts(sortDrafts(next))\n },\n onDraftDelete: (id: string) => {\n const session = getSession()\n const next = (session.session.getDrafts() ?? []).filter((e) => e.id !== id)\n session.session.setDrafts(next)\n void app.sessionManager.save(session.id).catch(() => {})\n getTuiRef()?.stores.tasks.setDrafts(sortDrafts(next))\n },\n // 批量删除(DraftPane 多选 + d):单次 setDrafts + 单次 save,避免逐条触发 N 次并发落盘。\n onDraftDeleteMany: (ids: string[]) => {\n const session = getSession()\n const idSet = new Set(ids)\n const next = (session.session.getDrafts() ?? []).filter((e) => !idSet.has(e.id))\n session.session.setDrafts(next)\n void app.sessionManager.save(session.id).catch(() => {})\n getTuiRef()?.stores.tasks.setDrafts(sortDrafts(next))\n },\n // M-N3:A2UI Button 真交互——数字键 1-9 触发 a2ui action,绕过手动输入 /a2ui-action 命令。\n onA2uiAction: (componentId, action, payload) => {\n const args = payload ? [componentId, action, JSON.stringify(payload)] : [componentId, action]\n void dispatchSlashCommand('a2ui-action', args, getCliCtx())\n },\n // RFC-116 M5:`/perf` 面板内手动刷新键(r)——复用既有 /perf 命令分发路径\n // (非重复实现 handlePerf 的采集逻辑),对齐 onA2uiAction 的 dispatchSlashCommand 模式。\n // 同款闭包延迟绑定:cliCtx 在此处定义时尚未赋值,但回调体只在用户实际按键时才执行,\n // 届时 cliCtx 已就绪(与 onA2uiAction 相同的既有安全模式)。\n onPerfRefresh: () => {\n void dispatchSlashCommand('perf', [], getCliCtx())\n },\n // Proxy 面板可编辑化:三个操作复用既有 `/proxy <url>|on|off|clear` 命令分发路径\n // (handleProxy 已有完整 persist + setProxyView 回推 + toast 逻辑),不重复实现——\n // 面板内触发与命令行敲字符效果完全一致,同一条真源。\n onProxySubmitUrl: (url: string) => {\n void dispatchSlashCommand('proxy', [url], getCliCtx())\n },\n onProxyToggleEnabled: () => {\n const proxy = app.settings.get('proxy') as { url?: string; enabled?: boolean } | undefined\n const next = proxy?.enabled !== false ? 'off' : 'on'\n void dispatchSlashCommand('proxy', [next], getCliCtx())\n },\n onProxyClear: () => {\n void dispatchSlashCommand('proxy', ['clear'], getCliCtx())\n },\n onProxyTest: () => {\n void dispatchSlashCommand('proxy', ['test'], getCliCtx())\n },\n }\n}\n","/**\n * 错误重试 TUI 交互层(流式错误 → rate-limit/connection/context-overflow 分支 tip 提示 + 重試/换模型操作)。\n */\nimport type { TuiHandle } from '@x-otto/tui'\nimport { i18next } from '@x-otto/tui'\nimport type { AgentSession } from '@x-otto/runtime'\nimport type { TraceEvent } from '@x-otto/agent'\nimport type { AppendLog } from '@x-otto/persistence'\nimport { classifySubmitError } from './interactive-utils'\nimport { recordRetryQueued, recordRetryCancelled, recordRetryReset } from './model-effort-record'\n\nimport type { CliContext } from '../command-dispatcher'\n\ntype ShowErrorRetryItem = Parameters<TuiHandle['engine']['showErrorRetry']>[0]\n\nexport interface ErrorRetryPresentationDeps {\n msg: string\n provider: string | undefined\n authError: boolean\n isRefreshFailed: boolean\n unsupported: boolean\n rateLimit: boolean\n rateLimitInfo: { limitType?: ShowErrorRetryItem['limitType']; resetsAt?: number } | undefined\n /** RFC-153 D6:错误来源插件 id;undefined=非插件来源,`ErrorRetryCard` 不展示来源行。\n * 只服务展示,不参与本函数任何分支选择判定(规则 8)。 */\n pluginSource: string | undefined\n /** 当前 per-prompt 重试计数(自增前)。仅通用错误分支(非 rateLimit/authError/unsupported)\n * 会调用 `incErrorRetryAttempt()` 递增后再读取——与原实现\"只在 else 分支 `errorRetryAttempt++`\"\n * 的行为完全对齐,其余三个分支读取的都是未自增的值。 */\n getErrorRetryAttempt: () => number\n incErrorRetryAttempt: () => number\n errorRetryMax: number\n /** RFC-230:mode='auto' 自动倒计时秒数(默认 30,来自 ResilienceConfig.errorRetry.intervalMs/1000)。\n * 纯展示字段,同 pluginSource 处理方式——不参与任何分支选择判定。 */\n errorRetryIntervalSeconds: number\n retryQueued: () => void\n resetAndRetry: () => void\n cancelRetry: () => void\n switchModelThenRetry: () => void\n goToAuth: () => void\n}\n\n/**\n * 构建本次错误应展示的 ErrorRetryCard 参数(RFC-147 M3 D-C2 场景矩阵,纯函数)。\n * 调用方(interactive.ts onSubmit catch 块)直接把返回值传给 `tui.engine.showErrorRetry(...)`。\n */\nexport function buildErrorRetryPresentation(deps: ErrorRetryPresentationDeps): ShowErrorRetryItem {\n // RFC-153 D6:pluginSource 是纯展示字段,不参与下方任何分支选择判定(规则 8)——统一在\n // 出口处补上,四个分支体本身逐字保留原实现,不逐一改写(避免 4 处散落维护同一份追加逻辑)。\n // RFC-230:intervalSeconds 同理纯展示字段。\n return {\n ...buildBase(deps),\n pluginSource: deps.pluginSource,\n intervalSeconds: deps.errorRetryIntervalSeconds,\n }\n}\n\nfunction buildBase(deps: ErrorRetryPresentationDeps): ShowErrorRetryItem {\n const {\n msg,\n provider,\n authError,\n isRefreshFailed,\n unsupported,\n rateLimit,\n rateLimitInfo,\n getErrorRetryAttempt,\n incErrorRetryAttempt,\n errorRetryMax,\n retryQueued,\n resetAndRetry,\n cancelRetry,\n switchModelThenRetry,\n goToAuth,\n } = deps\n\n if (rateLimitInfo) {\n // 硬额度耗尽(Anthropic unified-status:rejected):resetsAt 已知走官方倒计时\n // 自动重试;缺失时不做伪重试,只给切换模型(既有裁定:RateLimitCountdown.tsx\n // 原注释,见 D-C2 矩阵最后一行),并入统一卡片视觉。\n if (rateLimitInfo.resetsAt !== undefined) {\n return {\n reason: msg,\n attempt: 0,\n max: errorRetryMax,\n mode: 'resetsAt',\n resetsAt: rateLimitInfo.resetsAt,\n limitType: rateLimitInfo.limitType,\n actions: [\n { kind: 'retryNow', label: i18next.t('error.retryNow'), run: retryQueued },\n { kind: 'switchModel', label: i18next.t('error.switchModel'), run: switchModelThenRetry },\n ],\n onExpired: retryQueued,\n onCancel: cancelRetry,\n }\n }\n return {\n reason: msg,\n attempt: 0,\n max: errorRetryMax,\n mode: 'manual',\n limitType: rateLimitInfo.limitType,\n actions: [\n { kind: 'switchModel', label: i18next.t('error.switchModel'), run: switchModelThenRetry },\n { kind: 'retryNow', label: i18next.t('retry.nowButton'), run: retryQueued },\n ],\n onExpired: () => {},\n onCancel: cancelRetry,\n }\n }\n\n if (authError) {\n // 认证失效不自动重试——凭证不变则重投必然复现同一 401,5 次自动重试是对\n // 服务端的无意义轰炸。给 [去登录] 主动作(登录完成后 afterSwitch 模式自动\n // 重投),追加 [切换模型](换一个凭证有效的模型也能立即解锁,不必等修好当前\n // provider 的凭证——与 rateLimit/unsupported 两分支保持同款修复动作一致),\n // 保留 [重试] 手动兜底(用户可能已在别处完成登录)。\n return {\n reason: isRefreshFailed ? i18next.t('cmd.authRefreshFailed', { ps: provider ? ` (${provider})` : '', ls: provider ? ` ${provider}` : '' }) : i18next.t('cmd.authFailed', { ps: provider ? ` (${provider})` : '', ls: provider ? ` ${provider}` : '' }),\n attempt: getErrorRetryAttempt(),\n max: errorRetryMax,\n mode: 'manual',\n actions: [\n { kind: 'login', label: i18next.t('model.goToLoginButton'), run: goToAuth },\n { kind: 'switchModel', label: i18next.t('error.switchModel'), run: switchModelThenRetry },\n { kind: 'retryNow', label: i18next.t('retry.nowButton'), run: retryQueued },\n ],\n onExpired: () => {},\n onCancel: cancelRetry,\n }\n }\n\n if (unsupported) {\n // 模型不支持同样不自动重试——重发只会复现同一拒绝。给 [切换模型] 主动作。\n return {\n reason: i18next.t('cmd.modelNotSupported'),\n attempt: getErrorRetryAttempt(),\n max: errorRetryMax,\n mode: 'manual',\n actions: [\n { kind: 'switchModel', label: i18next.t('error.switchModel'), run: switchModelThenRetry },\n { kind: 'retryNow', label: i18next.t('retry.nowButton'), run: retryQueued },\n ],\n onExpired: () => {},\n onCancel: cancelRetry,\n }\n }\n\n // 通用错误 / 瞬时限流(自动退避已耗尽后仍 429,非硬配额)——per-prompt\n // 30 秒×5 自动重投。第 5 次仍失败后转终态:不再自动,仅保留手动重试(重置配额)。\n // 与原实现一致:只有本分支会递增 errorRetryAttempt(rateLimit/authError/unsupported\n // 三个分支只读取,不自增)。\n const errorRetryAttempt = incErrorRetryAttempt()\n const exhausted = errorRetryAttempt > errorRetryMax\n return {\n reason: rateLimit ? i18next.t('cmd.rateLimitedRetriesExhausted') : i18next.t('cmd.requestFailed', { msg: msg }),\n attempt: Math.min(errorRetryAttempt, errorRetryMax),\n max: errorRetryMax,\n mode: 'auto',\n exhausted,\n actions: [{ kind: 'retryNow', label: i18next.t('retry.nowButton'), run: exhausted ? resetAndRetry : retryQueued }],\n onExpired: exhausted ? () => {} : retryQueued,\n onCancel: cancelRetry,\n }\n}\n\n// ── RFC-183 O-2:onSubmit catch 块纯搬迁 ──────────────────────────────────\n\n/**\n * onSubmit catch 块错误处理——分类、回调构造、showErrorRetry 组装(RFC-183 O-2)。\n * 从 interactive.ts runInteractiveCommand 的 onSubmit 闭包内纯搬迁,零行为变化。\n */\nexport function handleSubmitCatchError(params: {\n err: unknown\n submitState: { userAborted: boolean }\n provider: string | undefined\n rateLimitInfoRef: { current: { limitType?: ShowErrorRetryItem['limitType']; resetsAt?: number } | undefined }\n retryModeRef: { current: boolean }\n errorRetryAttemptRef: { current: number }\n ERROR_RETRY_MAX: number\n /** RFC-230:自动倒计时秒数(来自 ResilienceConfig.errorRetry.intervalMs/1000)。 */\n ERROR_RETRY_INTERVAL_SECONDS: number\n handleSubmit: ((text: string, mode: string, intent: string) => void) | undefined\n latestSubmittedPromptRef?: { current: { text: string; mode: string } | undefined }\n text: string\n mode: string\n handleModel: (args: string[], ctx: CliContext, opts?: { afterSwitch?: () => void }) => Promise<void>\n handleProviderAuth: (args: string[], ctx: CliContext) => Promise<void>\n cliCtx: CliContext\n tui: TuiHandle\n}): void {\n const { err } = params\n const msg = err instanceof Error ? err.message : String(err)\n const isAbort = (err as { code?: string } | undefined)?.code === 'AGENT_ABORTED'\n if (params.submitState.userAborted || isAbort) {\n // silent — already cleaned up in onInterrupt\n } else {\n const provider = params.provider\n const errCode = (err as { code?: string } | undefined)?.code\n const pluginSource = (err as { pluginSource?: string } | undefined)?.pluginSource\n const { authError, isRefreshFailed, unsupported, rateLimit, pluginSource: classifiedPluginSource } =\n classifySubmitError(msg, errCode, pluginSource)\n const rateLimitInfo = params.rateLimitInfoRef.current\n params.rateLimitInfoRef.current = undefined\n\n // 动作审计(终局 2026-07-31):ErrorRetryCard 的回合级重投/取消/重置此前零 trace\n // (引擎侧 stream.retry 只覆盖引擎内自动退避)。经 cliCtx 取 traceStore——CLI 交互\n // 动作层与引擎层事件在 trace 流同源可审计。\n const traceDeps = (() => {\n const ctx = params.cliCtx as { app?: { storage?: { traceStore?: unknown } }; state?: { session?: AgentSession } } | undefined\n const traceStore = ctx?.app?.storage?.traceStore\n const session = ctx?.state?.session\n if (!traceStore || !session) return null\n const turn = session.session.getTurnCount()\n return {\n traceStore: traceStore as AppendLog<TraceEvent>,\n sessionId: session.id,\n ...(turn !== undefined ? { turn } : {}),\n }\n })()\n\n const retryQueued = () => {\n if (traceDeps) recordRetryQueued(traceDeps, {\n attempt: params.errorRetryAttemptRef.current + 1,\n max: params.ERROR_RETRY_MAX,\n mode: rateLimitInfo ? 'resetsAt' : authError ? 'manual' : unsupported ? 'manual' : 'auto',\n reason: msg.slice(0, 120),\n })\n params.retryModeRef.current = true\n const latestSubmitted = params.latestSubmittedPromptRef?.current\n const latestFollowUp = params.tui.engine.takeLastQueuedFollowUp?.()\n const shouldUseLatestSubmitted = latestSubmitted != null && latestSubmitted.text !== params.text\n const retryText = shouldUseLatestSubmitted ? latestSubmitted.text : latestFollowUp?.text ?? params.text\n const retryMode = shouldUseLatestSubmitted ? latestSubmitted.mode : latestFollowUp?.mode ?? params.mode\n queueMicrotask(() => params.handleSubmit?.(retryText, retryMode, 'send'))\n }\n const resetAndRetry = () => { if (traceDeps) recordRetryReset(traceDeps); params.errorRetryAttemptRef.current = 0; retryQueued() }\n const cancelRetry = () => { if (traceDeps) recordRetryCancelled(traceDeps, { attempt: params.errorRetryAttemptRef.current }); params.errorRetryAttemptRef.current = 0 }\n const switchModelThenRetry = () => { void params.handleModel([], params.cliCtx, { afterSwitch: retryQueued }) }\n\n params.tui.engine.showErrorRetry(buildErrorRetryPresentation({\n msg,\n provider,\n authError,\n isRefreshFailed,\n unsupported,\n rateLimit,\n rateLimitInfo,\n pluginSource: classifiedPluginSource,\n getErrorRetryAttempt: () => params.errorRetryAttemptRef.current,\n incErrorRetryAttempt: () => ++params.errorRetryAttemptRef.current,\n errorRetryMax: params.ERROR_RETRY_MAX,\n errorRetryIntervalSeconds: params.ERROR_RETRY_INTERVAL_SECONDS,\n retryQueued,\n resetAndRetry,\n cancelRetry,\n switchModelThenRetry,\n goToAuth: () => { void params.handleProviderAuth([provider ?? ''].filter(Boolean), params.cliCtx) },\n }))\n }\n}\n","import { i18next } from '@x-otto/tui'\nimport { resolve } from 'node:path'\n\nimport { apiCredentialId, App, expandCommandBody } from '@x-otto/coding'\nimport { InteractiveMode } from '../modes/interactive-mode'\nimport { driveContinuation } from '../modes/turn-continuation'\nimport type { Lang, LangMode, SubmitMode, RateLimitType } from '@x-otto/tui'\n// RFC-161 M3a/M3b:STR/detectLanguage 从 cli 自建 i18n 层导入;effectiveBusy 从 cli 自建\n// (tui/cli 共享同一份 fzf-lite 实现,避免自建导致搜索排序结果分叉)。\nimport { effectiveBusy } from '../submit-busy'\nimport { detectLanguage, setLanguage as setCliLanguage } from '../i18n'\nimport type { EngineCommand } from '@x-otto/interchange'\nimport { BUILD_ID, SDK_VERSION } from '@x-otto/env'\nimport { markStartupPhase } from '@x-otto/shared'\nimport { checkForUpdate } from '../hot-upgrade'\nimport { getVersion } from '../version'\nimport { parseCommand } from '../command-parser'\nimport { dispatchSlashCommand, DISPATCHABLE_COMMAND_NAMES } from '../command-dispatcher'\nimport type { CliContext } from '../command-dispatcher'\nimport { createProjectorState } from '@x-otto/session-contract'\nimport { createReducerState } from '../session-contract/reduce'\nimport { registerBuiltinTuiCommands, SAFE_COMMAND_NAMES, ENGINE_BUILTIN_COMMANDS, buildSlashCommands, buildHelpLines } from './interactive-commands'\nimport { renderGrillQuestion, setupInteractiveEvents } from './interactive-events'\nimport { createHistoryPager } from '../history-pager'\nimport { createInteractivePersistence } from './interactive-persistence'\nimport { registerSigilEntries, registerModelSigil, refreshShortcutSigilEntries } from './interactive-input-setup'\nimport { createBackgroundController, type BackgroundController } from './interactive-background'\nimport { createApplyModel, createEnsureModelUsableForPrompt, extractModelChips } from './interactive-model'\nimport { FtueController, buildFtueDeps } from './ftue/ftue-controller'\nimport { isFtueReady } from './ftue/ftue-snapshot'\nimport { createModelUsabilityGate } from '@x-otto/ai'\nimport { buildThemeRegistry } from '../theme-preset-wiring'\nimport { handleProviderAuth, handleModel } from './handlers/model-auth'\nimport { agentMessagesToChatMessages } from '../view-model-adapter'\n\nimport { setupScheduleService } from './interactive-schedule'\nimport { buildCliContext } from './interactive-context'\nimport { runPostTuiSetup, wireStartupNotices, wireModelSuggesterAndTitle, wireExtraArgumentSuggesters, wireEcosystemSearch } from './interactive-setup'\nimport { resolveOttoRepoRoot, StaleBuildDetector } from '../stale-build-detector'\nimport { createAsyncBashRunner, createAbortInterceptor, createMemoryCapture, buildShellAskAiPrompt, isEmptySubmit } from './interactive-submit-helpers'\nimport { createStopController, resolveCtrlCAction } from './interactive-stop'\nimport { PROVIDER_DISPLAY_NAMES, notificationLevel, formatDisplayModel, registerDiscoveredCommands } from './interactive-utils'\nimport { applySessionStartupConfig } from './interactive-session-setup'\nimport { setupInputHistory } from './interactive-history-setup'\nimport { createPasteSessionBinder } from './paste-session-binding'\nimport { wireAgentToasts } from './interactive-agent-toasts'\nimport { sortDrafts } from './draft-entries'\nimport { pushRecentMention, toRecentMentionEntry, toSigilEntries } from './recent-mentions'\nimport { registerMemoryGovernorCallbacks } from './interactive-memory-governor'\nimport { gitFallbackEditedFileContentAsync } from '../edited-file-content'\nimport { createTuiCallbacks } from './interactive-tui-callbacks'\nimport { handleSubmitCatchError } from './interactive-error-retry'\n\nimport type { CLIOptions } from '../types'\n\n/**\n * 启动交互式 TUI 模式(`otto` / `otto --interactive` / `otto -i`)。\n *\n * 之前 inline 在 runCli() 的 `case 'interactive':` 分支中(~800 行),\n * 现抽取为独立函数,降低 cli.ts 体积。\n */\n/** runInteractiveCommand 返回值。 */\nexport interface InteractiveResult {\n /** /exit --restart 或 /upgrade --force 请求重启时非空。 */\n restart?: {\n sessionId: string\n /** RFC-115 M3:本次重启由内存治理(critical Dialog 确认)触发——cli.ts 据此在\n * spawn 子进程时附加 --memory-restart-grace,供子进程起算 D5 宽限期。 */\n memoryTriggered?: boolean\n /** RFC-115 M4:重启前会话正忙时暂存的最后一条用户消息原文,供 cli.ts 在 spawn\n * 子进程时附加 --resume-prompt,子进程启动后自动续跑。瞬态值,不持久化。 */\n resumePrompt?: string\n }\n}\n\nexport async function runInteractiveCommand(\n options: CLIOptions,\n app: App,\n /** RFC-164 M164-2 D6:memory conflict 回调 ref——runInteractiveCommand 在 startTui 返回后设置\n * `ref.current`,使 HttpMemoryStore 的 onConflict 回调能触发 TUI toast。ref 由 cli.ts 创建\n * 并传递给 createApp 的 onMemoryConflict 选项(两者共享同一 ref 对象)。 */\n memoryConflictRef?: { current?: (path: string, conflictPath: string) => void },\n): Promise<InteractiveResult> {\n // ── restart 标记:由 handleExit 设置,cli.ts 在 app.stop() 后消费。 ──\n let restartRequested = false\n /** RFC-115 M3:本次 exit/restart 是否由内存治理 critical Dialog 触发(供 cli.ts 附加\n * --memory-restart-grace)。区别于用户 /exit --restart 或 /upgrade --force。 */\n let restartMemoryTriggered = false\n /** RFC-115 M4:D3 时序①——重启前若会话正忙,暂存最后一条用户消息原文,供重启后\n * 子进程自动续跑。瞬态值,不写入 SessionMetadata/持久化(经 M3 P0-3 一致的设计取向)。 */\n let restartResumePrompt: string | undefined\n\n // ── 后台任务面板控制器(订阅句柄 + 两路轮询 + 面板回调;见 interactive-background.ts)。\n // 在 startTui 之前创建(handlers 是其入参),经 getTui 惰性取 tui。 ──\n let bgController: BackgroundController | undefined\n\n try {\n // CC 式权限:交互式 TUI 默认写操作需用户确认\n const curMode = app.settings.get('permission_mode')\n if (curMode === undefined || curMode === 'auto') {\n await app.settings.update({ permission_mode: 'confirm' })\n }\n\n // --continue <id>:恢复已有会话\n const resumed = options.continueSession\n ? (app.getSession(options.continueSession) ?? (await app.sessionManager.restore(options.continueSession)) ?? undefined)\n : undefined\n const convoCount = resumed\n ? resumed.session.buildContext().messages.filter((m) => { const r = (m as { role?: string }).role; return r === 'user' || r === 'assistant' }).length\n : 0\n // RFC-305 D3:restore 检出的崩溃缺口在订阅建立前已产生——事件通道无人消费,\n // 此处主动消费返回值,待 runPostTuiSetup 订阅就绪后 toast(startupCrashGapRef)。\n const startupCrashGap = resumed ? app.sessionManager.consumeCrashGap(resumed.id) : undefined\n const startupCrashGapRef = { value: startupCrashGap }\n const resumedNote = !options.continueSession\n ? undefined\n : resumed\n ? i18next.t('cmd.resumeRestored', { idShort: options.continueSession.slice(0, 8), count: convoCount })\n : i18next.t('cmd.resumeNotFound', { id: options.continueSession })\n\n const continuedExisting = Boolean(options.continueSession && resumed)\n const modelExplicit = Boolean(\n options.model || app.settings.get('model') || app.settings.get('model_slots')?.default,\n )\n // H1:手动重试加速标志——TUI 里 Enter/r 置位 skip,agent 轮询跳过退避等待。\n const retryAccelerate = { skip: false }\n\n // 模型解析根因修复:先 hydrate 磁盘 catalog 缓存,**再** createSession。否则 resolveModel 在\n // registry 仅含静态种子时运行——日期版 id(如 anthropic/claude-haiku-4-5-20251001)只存在于\n // 磁盘缓存 / live 拉取结果里,静态种子只有无日期的 anthropic/claude-haiku-4-5——配置模型 find\n // 不中 → 启动即回退到别的可用模型(用户报告的「还是回退了 deepseek」根因)。hydrate 是本地文件\n // 读 + merge,开销极小、可阻塞启动;提前后 resolveModel 能直接解析到配置模型,状态栏从启动即显示\n // 配置模型、无 deepseek 闪现。prompt 入口校验(ensureModelUsableForPrompt)保留为安全网。\n const cachedAvailable = await app.hydrateModelCache().catch(() => new Set<string>())\n\n // 首次启动无模型:注册临时占位模型,使 createSession 不抛异常,TUI 正常启动。\n // 用户可在 TUI 内使用 /setup、/extension、/auth、/model 完成三步引导\n // (Provider → Login → Model),无需额外弹窗。当真实模型就绪后占位模型被替换。\n // --continue 恢复已有会话时跳过(session 已有模型 track)。\n const registryEmpty = !resumed && app.modelRegistry.getAll().length === 0\n const SETUP_PLACEHOLDER_ID = '__otto_setup__'\n if (registryEmpty) {\n app.modelRegistry.register({\n id: SETUP_PLACEHOLDER_ID,\n name: 'Setup required',\n provider: 'otto',\n api: 'otto',\n baseUrl: '',\n reasoning: false,\n input: ['text'],\n contextWindow: 32_000,\n maxOutputTokens: 4_000,\n })\n }\n\n // 占位模型防泄漏:settings 里有 model/model_slots 但 registry 只有占位模型时,\n // modelExplicit 不应为 true——否则 modelReady 跳过整个三步引导。\n const effectiveModelExplicit = registryEmpty ? false : modelExplicit\n\n let session = resumed ?? await app.createSession({\n interactive: true,\n retryAccelerate,\n ...(effectiveModelExplicit ? {} : { model: app.modelRegistry.getAll()[0] }),\n })\n\n await applySessionStartupConfig(session, app, !!resumed)\n\n // RFC-091 结案注:TUI 保持同进程 session(与业界 CLI 实现的 TUI 同型——单进程 channel 通信)。\n // engine-main + StdioEngineTransport 重定位为 app-server 模式(非 TUI 前端专用,\n // 如 web-ui/headless),不在交互式 TUI 中 spawn 影子进程。见 RFC-091 §结案。\n\n let modelReady = effectiveModelExplicit || continuedExisting\n const modelReadyRef = { get value() { return modelReady }, set value(v) { modelReady = v } }\n let models: Array<{ id: string; name: string; provider: string; api: string; strengths: string[]; contextWindow: number }> = app.modelRegistry.getAll().filter(m => m.id !== SETUP_PLACEHOLDER_ID).map(m => ({\n id: m.id,\n name: m.name,\n provider: m.provider,\n api: m.api,\n strengths: m.strengths ?? [],\n contextWindow: m.contextWindow,\n }))\n const availableModelIds = new Set<string>([...cachedAvailable, ...app.getSessionAvailableModels(session.id)])\n const persistAvailable = () => app.setSessionAvailableModels(session.id, [...availableModelIds])\n persistAvailable()\n\n // UX(就绪信号):本进程内曾处于「未配置」态——之后一旦翻转为可用,toast 告知用户\n // \"配置完成,可以开始了\"。初始就绪的老用户永不触发(启动时该标志为 false 则保持 false)。\n let wasUnconfigured = availableModelIds.size === 0\n\n const refreshModelsNow = async (announce?: (msg: string) => void): Promise<void> => {\n const rs = await app.refreshModels().catch(() => [])\n models = app.modelRegistry.getAll().filter(m => m.id !== SETUP_PLACEHOLDER_ID).map(m => ({ id: m.id, name: m.name, provider: m.provider, api: m.api, strengths: m.strengths ?? [], contextWindow: m.contextWindow }))\n for (const r of rs) if (r.status === 'ok') for (const id of r.availableIds ?? []) availableModelIds.add(id)\n // 占位模型清理:首次发现真实模型后移除桥接占位,避免永久驻留 registry。\n if (models.length > 0 && app.modelRegistry.getAll().some(m => m.id === SETUP_PLACEHOLDER_ID)) {\n app.modelRegistry.unregister(SETUP_PLACEHOLDER_ID)\n }\n persistAvailable()\n const stillEmpty = availableModelIds.size === 0\n tuiRef?.stores.tasks.setNeedsSetup(stillEmpty)\n if (wasUnconfigured && !stillEmpty) {\n wasUnconfigured = false\n tuiRef?.info.showToast(i18next.t('cmd.titleSetupReady'), i18next.t('cmd.setupReadyMessage'))\n }\n if (announce) {\n const oks = rs.filter(r => r.status === 'ok')\n announce(oks.length > 0\n ? i18next.t('cmd.modelsRefreshed', { summary: oks.map(r => `${r.provider} ${r.total}`).join(' · ') })\n : i18next.t('cmd.modelsRefreshedEmpty'))\n }\n }\n void refreshModelsNow()\n\n const currentModel = modelReady ? session.model?.id : undefined\n // 占用条分母——随 /model 切换更新(不同模型窗口不同),故用 let。\n let contextWindow = app.modelRegistry.getAll().filter(m => m.id !== SETUP_PLACEHOLDER_ID).find(m => m.id === currentModel)?.contextWindow ?? 0\n const contextWindowRef = { get value() { return contextWindow }, set value(v) { contextWindow = v } }\n let currentModelId = currentModel\n const currentModelIdRef = { get value() { return currentModelId }, set value(v) { currentModelId = v } }\n const fmtDisplayModel = (id: string | undefined): string => formatDisplayModel(app, id, session.thinkingLevel)\n // applyModel 闭包需要 toast,但 tui 在之后才创建——用 ref 桥接。\n let tuiRef: Awaited<ReturnType<typeof import('@x-otto/tui').startTui>> | undefined\n const tuiRefBox = { get value() { return tuiRef }, set value(v) { tuiRef = v } }\n // 模型切换后立即重算占用条(窗口=分母变了,cost 速率也变)——同样 tui 后才赋值,ref 桥接。\n let pushBudgetNow: (() => void) | undefined\n const pushBudgetNowRef = { get value() { return pushBudgetNow }, set value(v) { pushBudgetNow = v } }\n // 退出会话回调——finishInteractive 在 startTui 内定义,通过 ref 桥接。\n const exitSessionRef: { value: (() => void) | undefined } = { value: undefined }\n const optionsRef = { value: options }\n\n // RFC-090 M1:EngineCommand dispatch——同进程直调(协议层收口,RFC-091 app-server 模式\n // 复用同一 EngineCommand 类型走 stdio;TUI 保持直调,见 RFC-091 结案)\n const dispatchToEngine = (cmd: EngineCommand) => {\n switch (cmd.kind) {\n case 'steer': session.steer(cmd.input); break\n case 'abort': interactive.abort(); break\n // RFC-180 D3:turn 边界软停止/解除暂停——直调 AgentSession 方法(同 steer/abort\n // 走同进程直调,无需返回值时用 void 丢弃;onExtra 回调需要返回值时改为直接调用\n // session.pauseRun(),不经本 dispatch 通道,见 Esc 弹窗 onExtra 实现)。\n case 'pauseAfterTurn': session.pauseRun(); break\n case 'resumeRun': session.resumeRun(); break\n }\n }\n\n const applyModel = createApplyModel({\n modelReady: modelReadyRef,\n currentModelId: currentModelIdRef,\n contextWindow: contextWindowRef,\n tuiRef: tuiRefBox,\n pushBudgetNow: pushBudgetNowRef,\n app,\n getSession: () => session,\n options: optionsRef,\n fmtDisplayModel,\n onEngineModelAction: dispatchToEngine,\n })\n\n const ensureModelUsableForPrompt = createEnsureModelUsableForPrompt({\n getSession: () => session,\n app,\n tuiRef: tuiRefBox,\n options: optionsRef,\n fmtDisplayModel,\n applyModel,\n onExitSession: () => exitSessionRef.value?.(),\n })\n\n let interactive = new InteractiveMode(session, {\n models,\n currentModel,\n onModelChange: applyModel,\n })\n\n const { startTui } = await import('@x-otto/tui')\n\n let reg = interactive.getRegistry()\n registerBuiltinTuiCommands(reg)\n\n // 文件式命令注册(built-in wins,见 interactive-utils.ts);返回值供 onSubmit 回退分支消费。\n const fileCommands = registerDiscoveredCommands(reg, app)\n\n // 回合/提交状态收敛为单一对象 `turn`(终局架构 review 2026-07-21:11 个散落变量\n // 收拢为一个命名结构体——ARCH-077-13 已把提交/中断路径的 5 个字段合并进 submitState,\n // 本次把顶层 agentProcessing 与 executor 内层的 retryMode/errorRetryAttempt/\n // lastRateLimitInfo 一并纳入,`submitState` 更名为 `turn` 后原 5 字段作为其成员。\n // 纯 rename + 结构合并,零行为变化——不改任何控制流、判忙语义、恒复位时机。\n // 注:`handleSubmit`(onSubmit 自引用闭包)与 `staleBuildDetector`/`finished`\n // (进程收尾生命周期,非回合/提交状态)不纳入——\n // 语义域不同,见 RFC-148 M6 摸底报告对 onSubmit 自引用闭包的风险标注。\n //\n // agent 运行中只有安全命令(读/UI)才能立即执行;\n // 不安全命令(修改会话/上下文/模型/凭证)-> isImmediateCommand 返回 false -> routeSubmit 返回 'queue' 入队。\n const turn = {\n /** 本 onSubmit 处理周期内是否忙(含续轮循环),finally 恒复位。 */\n agentProcessing: false,\n ctrlCCount: 0,\n lastCtrlCTime: 0,\n userAborted: false,\n shellChild: null as import('node:child_process').ChildProcess | null,\n streamedText: false,\n /** ErrorRetryCard/RateLimitCountdown 驱动的重投标记——onSubmit 内跳过重复 echo。 */\n retryMode: false,\n /** per-prompt 错误重试计数(ErrorRetryCard 5 次配额),仅通用错误分支自增。 */\n errorRetryAttempt: 0,\n /** 硬额度耗尽事件(同步先于 catch 抛出)暂存的 resetsAt/limitType,catch 块消费后置 undefined。 */\n lastRateLimitInfo: undefined as { limitType?: RateLimitType; resetsAt?: number } | undefined,\n /** 最近一次用户实际追加/提交的 prompt-bound 文本;错误重试应优先重投最新意图而非最早失败原文。 */\n latestSubmittedPrompt: undefined as { text: string; mode: SubmitMode } | undefined,\n }\n\n // 提交分流/出队/命令立即性的统一判忙:引擎 session.isBusy(真·prompt in-flight) OR cli turn.agentProcessing\n // (本 onSubmit 处理周期内,含续轮循环——其间 session.isBusy 会在两轮 handleInput 之间瞬时落 false,\n // 单看引擎会漏判,让续轮间隙的提交起 reentrant 新回合)。两者取或 → 整个处理周期都判忙,最保守。\n // turn.agentProcessing 在 finally 恒复位(含异常路径),不会 stuck-true;故取或不会卡死提交。\n // 用 @x-otto/tui effectiveBusy(纯函数,有 (false,true)→true 单测)集中语义,别在此 inline 简化成只看引擎。\n const isBusyNow = (): boolean => effectiveBusy(session.isBusy, turn.agentProcessing)\n\n let slashCommands = buildSlashCommands(reg, turn.agentProcessing, SAFE_COMMAND_NAMES, (i18next.t('cmdDesc', { returnObjects: true }) as Record<string, string>), session.pinned, session.isPaused)\n\n const refreshSlashCommands = (): typeof slashCommands => {\n slashCommands = buildSlashCommands(reg, turn.agentProcessing, SAFE_COMMAND_NAMES, (i18next.t('cmdDesc', { returnObjects: true }) as Record<string, string>), session.pinned, session.isPaused)\n return slashCommands\n }\n\n // @ sigil 扩展 + @ 动态上下文 chip 注册(见 interactive-input-setup.ts)\n registerSigilEntries(app)\n registerModelSigil(app, availableModelIds)\n refreshShortcutSigilEntries(app)\n\n const projState = createProjectorState()\n\n const cfgLang = (app.settings.get('language') ?? 'auto') as LangMode\n let currentLang: Lang = cfgLang === 'auto' ? detectLanguage() : (cfgLang as Lang)\n // RFC-283:主题深浅始终自动跟随终端背景色,不再读 settings.theme。\n // RFC-277 M6:启动时装配一次主题预设 registry(内置 + 全部活跃插件贡献,见\n // theme-preset-wiring.ts buildThemeRegistry)+ 读取按 appearance 分槽的 preset 选择。\n const themeRegistry = buildThemeRegistry(app)\n const presetSelection = app.settings.get('theme_preset')\n // 用户快捷键覆盖表 → TUI 运行时按键分发(RFC-174 运行时消费缺口修复)。此前只有\n // /keybindings 面板读它做展示与冲突检测,真正的分发从不读 → 改绑了也不生效。\n // 启动期快照即可:面板保存后提示重启生效(与 language 同款启动期配置语义)。\n const keybindingOverrides = app.settings.get('keybinding_overrides') as\n | Record<string, import('@x-otto/tui').KeyBindingPredicate>\n | undefined\n // RFC-161 M3a:cli i18n 层与 TUI(startTui 的 language 选项)在启动时对齐同一语言;\n // 运行期切换点(/lang、Ctrl 快捷键)由 handlers/misc.ts 与 interactive-tui-callbacks.ts\n // 同步调用两层 setLanguage。\n setCliLanguage(currentLang)\n\n // 版本检查(启动时异步,不阻塞 TUI 启动)\n const currentVersion = getVersion()\n const updateCheckPromise = checkForUpdate({\n currentVersion,\n lastCheck: app.settings.get('last_update_check'),\n autoCheck: app.settings.get('auto_update_check') !== false,\n persistLastCheck: () => app.settings.persist({ last_update_check: Date.now() }),\n })\n\n // 此 async executor 的\"throw 被吞\"是已知模式,但已被陪跑窗口的进程级守卫兜住\n // ——scoped uncaughtException/unhandledRejection 守卫会打印 + kill child + exit(1),\n // 不静默挂死。body 内 setup 后即由 startTui 接管,运行期错误归 handleSubmit 自身处理;setup 唯一\n // 异步点 startTui 抛出会被该守卫接住。把 finished/finishInteractive 外提以让\n // catch 路由到 finishInteractive 需重构 shutdown 核心(修过多轮、只真机暴露),且 PTY 无法\n // 触发 setup-crash 路径来验证修复目标——风险/收益倒挂,故保留此模式。\n await new Promise<void>(async (onDone) => {\n // 重试桥:onSubmit 内将自身赋值给 handleSubmit,turn.retryMode 标记跳过重复 echo。\n // turn.retryMode/errorRetryAttempt/lastRateLimitInfo 已收拢进顶层 `turn` 对象\n // (见其声明处的完整语义注释:RFC-147 M3 D-C1 重试计数规则、硬额度耗尽事件竞态\n // 处理——本次只做变量收拢,不改动任何行为)。\n let handleSubmit: ((text: string, mode: SubmitMode, intent?: 'send' | 'steer') => void) | undefined\n // RFC-230:max 配额从统一重试配置读取(默认 5,可经 .otto/config.json 的\n // resilience.errorRetry.maxAttempts / OTTO_RESILIENCE_CONFIG 覆盖)。\n // 倒计时间隔(默认 30 秒)经 intervalSeconds prop 传给 ErrorRetryCard,见下方 handleSubmitCatchError 调用点。\n const ERROR_RETRY_MAX = app.getResilienceConfig().errorRetry.maxAttempts\n // RFC-133 M3:过期构建检测器(晚绑定,repoRoot 解析为 truthy 时构造/启动)。收尾时 dispose。\n let staleBuildDetector: StaleBuildDetector | undefined\n\n // 单一收尾入口(幂等)。onStop(正常)、catch(tui.lifecycle.stop 抛异常)、\n // teardown 兜底(tui.lifecycle.stop/unmount 挂起)三路汇此 → resolve Promise 让 runInteractiveCommand 返回。\n // 进程级 process.exit 兜底已归位 cli.ts app.stop 看门狗,此处永不 process.exit。\n let finished = false\n const finishInteractive = (): void => {\n if (finished) return\n finished = true\n try { bgController?.clearBgSubs() } catch { /* best-effort */ }\n try { bgController?.stopBgOutputPoll() } catch { /* best-effort */ }\n try { bgController?.stopBgSubagentPoll() } catch { /* best-effort */ }\n try { clearInterval(schedulePollInterval) } catch { /* best-effort */ }\n try { void scheduleService.stop() } catch { /* best-effort */ }\n try { staleBuildDetector?.dispose() } catch { /* best-effort */ }\n onDone()\n }\n exitSessionRef.value = finishInteractive\n // TUI 拆除兜底——tui.lifecycle.stop()/unmount 若挂起致 onStop 永不触发,~3s 后强制\n // finishInteractive() 解阻塞,让 cli.ts 接管 app.stop()(其看门狗再守进程级卡死)。unref 不拖事件循环。\n const stopTuiGuarded = (): void => {\n const guard = setTimeout(finishInteractive, 3000)\n guard.unref()\n try { tuiRef?.lifecycle.stop() } catch { finishInteractive() }\n }\n\n // RFC-289 D0/D1:停止意图统一入口——Ctrl+C / `/abort` / `/exit` 三个入口全部经此,\n // 保证「一个停止意图 = 一个确认框实例」。禁止任何调用方直接 openModal 表达停止语义\n // (由 stop-dialog-single-entry-gate 静态门禁强制)。\n const stopController = createStopController({\n getTui: () => tuiRef ?? null,\n getSession: () => session,\n dispatchAbort: () => dispatchToEngine({ kind: 'abort' }),\n clearTodoLayers: () => {\n if (projState.todoList.length === 0) return\n projState.todoList = []\n tuiRef?.stores.tasks.setTasks([])\n // L1 同步——三层必须齐清(L1 会话 metadata / L2 projState / L3 TUI),否则残留\n // in_progress 项会让下一轮 buildContext 的 todo-reminder 钩子(数据源=L1)\n // 催模型\"继续完成\"用户刚明确中止的任务。\n session.session.setTodoList([])\n },\n markUserAborted: () => { turn.userAborted = true },\n resetCtrlCCount: () => { turn.ctrlCCount = 0 },\n markRestartRequested: () => { restartRequested = true },\n stopTui: stopTuiGuarded,\n isBusy: () => isBusyNow(),\n })\n // RFC-074 D2:可立即执行集 = 目录中具 CLI handler 的命令名+aliases(engine/TUI 核内置\n // exit/abort/copy 由 TUI 解析器处理,不入此集——与重构前行为一致)。\n const allCommandNames = DISPATCHABLE_COMMAND_NAMES\n\n // 持久化输入历史 + 外部 shell 历史——实现在 interactive-history-setup.ts(RFC-088 M4b/RFC-129)。\n const { historyStore, inputHistory, externalHistoryCommands } = await setupInputHistory(\n session,\n app.settings.get('history')?.external,\n )\n\n // 草稿持久化(/draft 命令 + Ctrl+S/O):随会话快照走本地/远端存储(@x-otto/session\n // PersistedSessionConfig.drafts),不再是独立的全局 ~/.otto/drafts.json 单文件方案——\n // 草稿是\"这次对话里暂存的半成品输入\",天然是 per-session 概念,换机器/远端协作时应\n // 跟着会话走。initialDrafts 从当前(可能已 restore 的)会话读取初始态。\n\n // 后台任务面板控制器:startTui 之前创建(handlers 是其入参),getTui 惰性取 tui。\n const bg = createBackgroundController({ app, session, getTui: () => tui })\n bgController = bg\n\n // helpers defined in interactive-submit-helpers.ts; assigned after tui is available\n let runAsyncBash!: (rawCmd: string, echoPrefix?: '!' | '!!') => void\n\n let handleAbortIntercept!: (text: string, mode: SubmitMode) => boolean\n\n let runMemoryCapture!: (text: string) => Promise<void>\n\n // late-bound: assigned after startTui + events are created\n let cliCtx!: CliContext\n const eventsRef: { value: { attachSessionHandler: () => void; getSessionUnsub: () => (() => void) | undefined } | undefined } = { value: undefined }\n\n // RFC-150 D7/M0:TUI 首帧打点——startTui() 内部含 Ink render() 首次 reconciliation,\n // 是\"可交互\"这一里程碑指标(§2 <500ms 目标)的终点标记。\n markStartupPhase('startTui() begin')\n const tui = await startTui({\n stdin: process.stdin,\n stdout: process.stdout,\n modelName: fmtDisplayModel(currentModel),\n needsSetup: availableModelIds.size === 0,\n commands: slashCommands,\n pluginInput: app.pluginInput,\n isImmediateCommand: (name) => {\n // agent 不忙 → 全部注册命令可立即执行\n if (!isBusyNow()) return allCommandNames.has(name)\n // agent 忙 → 仅安全命令可立即执行(断点暂停期 session.isBusy 仍 true 也按忙处理,\n // 不让不安全命令在 in-flight/暂停回合上立即跑)。\n return SAFE_COMMAND_NAMES.has(name)\n },\n isInterruptCommand: (name) => name === 'abort',\n // 提交分流/出队判忙以引擎真源为准(见 startTui-types isAgentBusy)——堵住镜像态去同步导致的\n // \"引擎仍忙却起新回合\" leak,从源头免掉撞裸 prompt() 的 \"Agent is processing\"。\n isAgentBusy: isBusyNow,\n // RFC-180 D6:接线暂停态探针——agentBusy() 组合链据此挡住 drainQueue 自动出队。\n isPaused: () => session.isPaused,\n // RFC-253 M-T5:初始路由——resume 到有历史会话时直接进对话('/',消除首帧闪 Home);\n // 全新会话(或未找到的 --continue)落 Home('/home')。与 restoreResumedSession 内的\n // setTopView 幂等对称(后者 navigate('/',{reset}) 在已 '/' 时无副作用)。\n initialPath: continuedExisting && convoCount > 0 ? '/' : '/home',\n // RFC-345 D5/D6:影子态双通道可见性(状态栏徽标 + 输入区 chip + Home 启动横幅)。\n // 读 app._shadow(与 OTTO_SHADOW 同源,cli.ts flag 翻译层已置),非 isShadowModeEnabled\n // 判断——TUI 侧是纯渲染感知点,不违反 §D1 白名单。\n shadow: app._shadow,\n welcome: {\n cwd: process.cwd(),\n version: SDK_VERSION,\n buildId: BUILD_ID,\n // 具名问候(Welcome banner):settings.nickname 存在时织入问候语并高亮。\n nickname: app.settings.get('nickname') || undefined,\n updateAvailable: undefined, // async fill after promise resolves\n },\n // RFC-234 P2-b:Home 首页选择会话回调——分派 /resume <id>。\n onSessionSelect: (id: string) => {\n void dispatchSlashCommand('resume', [id], cliCtx)\n },\n // RFC-300 M2:更早历史补页数据通道——有状态 pager(锚/去重集合由 CLI 闭包\n // 维护:resume 尾窗口 / 实时启动时 entryCount)。无 repo(非关系化后端)→\n // 不注入(TUI 优雅降级:折叠占位不触发加载)。\n loadHistory: app.sessionRepoRef?.current\n ? await (async () => {\n const repo = app.sessionRepoRef!.current\n if (!repo) return undefined\n const pager = await createHistoryPager(repo, session.id, !!options.continueSession)\n return (req: { limit: number }) => pager.loadEarlier(req.limit)\n })()\n : undefined,\n language: cfgLang,\n themeRegistry,\n presetSelection,\n keybindingOverrides,\n permissionMode: app.getPermissionMode(),\n // DEC1004 终端焦点(opt-in OTTO_TUI_FOCUS=1)→ 喂在场门控(失焦才提醒)。\n onFocusChange: (focused: boolean) => app.setTerminalFocus(focused),\n // 后台任务面板回调(kill/apply/delegate-supply/view-output/switch/subagent-exit)\n ...bg.handlers,\n\n // 输入历史持久化(RFC-088 M4b):独立落盘,不再走 session 快照。\n initialHistory: inputHistory,\n externalHistoryCommands,\n\n // RFC-198 §4.4:PasteStore 内存压力分级来源——只读透传 App 侧 ResidencyGovernor,\n // tui 包零 @x-otto/runtime 编译期依赖(结构化类型兼容,无需显式转换)。\n residencyGovernor: app._residencyGovernor,\n\n // RFC-217 D1:EditedFileFullPane「整个文件 diff」(vs git HEAD)异步取数\n // provider——绑定 projectRoot(app.workspaceDir,同 expandCommandBody 调用点\n // 的既有取值方式)调用 gitFallbackEditedFileContentAsync;catch 到的异常包装为\n // `{error}` 字段供组件展示不可用提示,不冒泡(渲染层旁路,fail-soft)。\n resolveWholeFileDiff: async (path, operation) => {\n try {\n return await gitFallbackEditedFileContentAsync(resolve(app.workspaceDir, path), operation)\n } catch (e) {\n return { error: e instanceof Error ? e.message : String(e) }\n }\n },\n\n // 草稿持久化:随会话快照走本地/远端存储(@x-otto/session PersistedSessionConfig.drafts)。\n initialDrafts: sortDrafts(session.session.getDrafts() ?? []),\n\n // @ 面板「最近使用」(MRU,项目级持久化,见 recent-mentions.ts + @x-otto/setting\n // schema.recent_mentions 注释):启动期从 settings 恢复初始列表,选中后持久化。\n // App.tsx 内部只在 sigilState.sigil==='@' 时才触发此回调(# 面板不计入)。\n initialRecentMentions: toSigilEntries(app.settings.get('recent_mentions') ?? []),\n onSigilSelect: (entry) => {\n const recent = app.settings.get('recent_mentions') ?? []\n const next = pushRecentMention(recent, toRecentMentionEntry(entry))\n void app.settings.persist({ recent_mentions: next }).catch(() => {})\n tuiRef?.stores.tasks.setRecentMentions(toSigilEntries(next))\n },\n\n // RFC-148 M6:onCyclePermissionMode/onHistoryPush/onDraftSave/\n // onDraftDelete/onDraftDeleteMany/onA2uiAction/onPerfRefresh 七个低风险回调\n // 纯搬迁至 interactive-tui-callbacks.ts(createTuiCallbacks)。\n ...createTuiCallbacks({\n app,\n getTui: () => tui,\n getTuiRef: () => tuiRef,\n getSession: () => session,\n getCliCtx: () => cliCtx,\n historyStore,\n // RFC-338:binder 在 startTui() 之后才装配(桥需首帧挂载),\n // 故用 getter 惰性取——本工厂调用时它还是 undefined。\n getPasteBinder: () => cliCtx.callbacks.pasteBinder,\n }),\n\n // QueueEditDialog「直接发送」:TUI 侧已把该项出队,此处按其原始 mode 分流。\n // 'send' 复用 onSubmit/handleSubmit 本体——等价于用户此刻在主输入框直接打字按\n // Enter 提交同一段文字,intent 按当前忙闲态传 'steer'/'send'(与普通 Enter 提交\n // 走的是同一 routeSubmit 语义分支,见 handleSubmit 内 `intent === 'steer'` 分支)。\n // 'bash'/'bash_async'/'memory' 各走原始执行通道(不套 steer 语义——steer 是纯\n // 文本对话补充概念,与 shell/memory 独立执行通道无关,见 QueueEditDialog 头注释)。\n onQueueSendNow: (text, mode) => {\n if (mode === 'memory') {\n void runMemoryCapture(text)\n return\n }\n if (mode === 'bash' || mode === 'bash_async') {\n runAsyncBash(text, mode === 'bash' ? '!' : '!!')\n return\n }\n void handleSubmit?.(text, 'send', isBusyNow() ? 'steer' : 'send')\n },\n onSubmit: handleSubmit = async (text, mode, intent) => {\n turn.ctrlCCount = 0\n // /abort 拦截:routeSubmit 已让它运行中也立即放行到此,命中即走中断入口(处理器抽出)。\n if (handleAbortIntercept(text, mode)) return\n // RFC-180 D6:暂停态下用户提交新消息 = 隐式继续(G4)——先解除暂停,让 isBusy\n // 判定回到正常口径(会话已空闲,非 in-flight),下方逻辑照常走 send 分流,\n // 新消息正常起回合。不 return——继续走后续正常提交路径。\n if (session.isPaused) {\n session.resumeRun()\n }\n // Enter(steer 意图)= 回合之中队列:注入引擎 steeringQueue,当前回合中途消费。\n if (intent === 'steer' && session.isBusy) {\n turn.latestSubmittedPrompt = { text, mode }\n // RFC-337 D3:steer 走延迟-commit——拿 steer id,setPendingSteer 负责回显\n // (scrollback 模式记 pendingSteers 在 live 区渲染;windowMode append streaming\n // 消息进 store)。被模型 drain 消费时经 steer.consumed 事件 commitPendingSteer 转正;\n // ESC 撤回未消费的 steer 时经 onWithdrawSteer→removeSteer 命中撤回(无框)/未命中退化中止。\n const { id } = session.steer(text)\n tui.pending.setPendingSteer(text, id)\n return\n }\n // `#` 手动记忆捕获(处理器抽出为 runMemoryCapture);`!`/`!!` 异步 shell(runAsyncBash,\n // RFC-189 D3:单感叹号此前落入 run-modes.ts 的 execSync 同步阻塞分支,冻结整个\n // TUI 进程最长 30 秒——现与 `!!` 合并为同一异步路径,按实际输入前缀区分回显)。\n if (mode === 'memory') {\n await runMemoryCapture(text)\n return\n }\n if (mode === 'bash' || mode === 'bash_async') {\n runAsyncBash(text, mode === 'bash' ? '!' : '!!')\n return\n }\n const parsed = parseCommand(text)\n let effectiveText = text\n let isFileCmd = false\n if (parsed.isCommand) {\n const handled = await dispatchSlashCommand(parsed.command, parsed.args, cliCtx)\n if (handled) return\n // RFC-105 D7/M105-6:斜杠命令触发激活事件——若有插件声明\n // activationEvents:[\"onCommand:<name>\"],此 fire 将使其代码模块被加载\n // (deferred→active),下一次 reload 时生效。fire-and-forget+best-effort。\n void app.firePluginActivationEvent?.(`onCommand:${parsed.command}`)\n // 内置未命中 → 回退到文件式命令(built-in wins)。展开正文作为本回合 prompt。\n const fc = fileCommands.find(c => c.name === parsed.command)\n if (fc) {\n isFileCmd = true\n effectiveText = await expandCommandBody(fc.body, { args: parsed.args, workspaceDir: app.workspaceDir })\n }\n }\n // Engine builtin commands (exit, help, clear, copy, status) are handled\n // locally by the engine — skip model validation and agent-processing toggle.\n const isEngineBuiltin = parsed.isCommand && !isFileCmd &&\n ENGINE_BUILTIN_COMMANDS.has(parsed.command.toLowerCase())\n if (!isEngineBuiltin) {\n // RFC-189 D3:`mode === 'bash'` 已在 :503 short-circuit return,执行到此处\n // mode 恒不为 'bash'——此前 `mode !== 'bash'` 判断已是死代码残留,一并清理。\n if (!modelReady && (!parsed.isCommand || isFileCmd)) {\n // UX:系统级引导用 toast,不注入对话流(对话消息承载系统错误是语义错位,\n // 且会永久污染会话历史)。输入不回显、不丢失——用户修完配置可直接重发。\n tui.info.showToast(i18next.t('cmd.titleSetupRequired'), i18next.t('cmd.noModelConfigured'))\n return\n }\n }\n if (projState.todoList.length > 0 && projState.todoList.every(t => t.status === 'done' || t.status === 'skipped' || t.status === 'failed')) {\n projState.todoList = []\n tui.stores.tasks.setTasks([])\n // L1 会话持久化同步清空——此前只清 L2(projState)/L3(TUI),L1(session metadata)\n // 残留全终态 todoList;resume 时 restoreSessionPanels 会把它读回来复活成\"过期\n // 全 done 列表\"(真实用户反馈:resume 后看到已完成的旧任务清单)。三层必须同步。\n session.session.setTodoList([])\n }\n // 兜底(defense-in-depth):引擎仍忙,且这是会真正起 agent 回合的提交(普通文本 / 文件式命令展开)\n // 却漏到此处(镜像态去同步残留窗口)→ 绝不让它走到 session.prompt() 撞 isBusy 抛 \"Agent is processing\"。\n // 回落到「回合之后队列」(state.queued,与 Shift+Enter 同一队列),引擎空闲时自动出队起新回合。\n // **必须放在命令派发之后、起回合之前,且只作用于 prompt-bound 提交**——否则会吞掉本应运行中立即\n // 执行的命令(engine builtin /exit·/help 与 dispatchSlashCommand 的 SAFE_COMMAND_NAMES,已在上文 return)。\n // 不在此 echo:与 Shift+Enter 入队路一致(入队只进面板,出队 onSubmit 才 echo),避免与出队 echo 叠成双回显。\n // 判忙用 isBusyNow(含续轮间隙的 agentProcessing),堵住 session.isBusy 单看会漏的 reentrant 窗口。\n if (isBusyNow() && (!parsed.isCommand || isFileCmd)) {\n tui.engine.enqueueFollowUp(text, mode)\n tui.info.showToast(i18next.t('cmd.titleInfo'), i18next.t('queue.dFollowUpToast'))\n return\n }\n if (!isEngineBuiltin) {\n // 首个真实 prompt 前一次性校验模型可用性(乐观启动 → 用时校验)。\n await ensureModelUsableForPrompt()\n tui.engine.pushEngineEvent({ kind: 'agentProcessing', value: true })\n turn.agentProcessing = true\n tui.plugins.setCommands(refreshSlashCommands())\n }\n turn.userAborted = false\n // 剥离 [@model:xxx] chip 并**真实切换当前会话模型**(终局修复)。\n // 旧行为是注入提示词前缀\"[Use model ... for task_delegate/agent_call]\"——只影响\n // 子代理派发、对当前回合的执行模型零作用力(LLM 无法切换自己跑在哪个模型上),\n // 用户感知即\"@model 无效\"。正解:@model 与 /model 同路径走 applyModel(含持久化/\n // 页脚刷新/工具集同步/上下文窗口适配/引擎进程通知全套语义),本回合即用新模型。\n {\n const extracted = extractModelChips(effectiveText)\n effectiveText = extracted.text\n // 多个 chip 取最后一个(与\"后写的指令覆盖先写的\"直觉一致)。\n // 去重 guard 对比 session.model?.id(**真实执行模型**)而非 currentModelId——\n // 后者是 applyModel 的乐观更新值(registry 查找前就写入、未命中不回滚),\n // 用它作 guard 会让\"重复提交同一无效 id\"的第二次被静默跳过、零反馈。\n const targetId = extracted.modelIds.at(-1)\n if (targetId && targetId !== session.model?.id) {\n const msg = await applyModel(targetId)\n // applyModel 未命中注册表时返回 \"not in registry\" 说明文案且不切换——\n // 此时把结果直接亮给用户(toast),不静默吞掉切换失败。\n if (msg.includes('not in registry')) {\n tui.info.showToast(i18next.t('cmd.titleModel'), msg)\n }\n }\n }\n\n // 空内容 gate(真实用户反馈):trim 后为空则不进对话流、不起回合。\n // 判定与两条空来源的说明见 `isEmptySubmit` 的 doc comment。位置约束:必须在\n // 上方 `@model` chip 处理**之后**——否则切模型副作用会被一并跳过。\n if (isEmptySubmit(effectiveText)) return\n turn.latestSubmittedPrompt = { text, mode }\n // 指令(非文件式展开)不回显到对话流——只有普通对话、!shell、文件式命令展开才 echo\n // RFC-147 M3 D-C1:retryMode=false 时是\"新的用户输入\"(首次提交或 follow-up 出队),\n // 清零 per-prompt 错误重试计数;retryMode=true 是 ErrorRetryCard/RateLimitCountdown\n // 驱动的重投,沿用累计计数(不清零,否则 per-prompt 5 次配额形同虚设)。\n if (!turn.retryMode) turn.errorRetryAttempt = 0\n turn.retryMode = false\n // reduce 的 lastTasks 只随 tasks.update 事件更新,但 todo 面板有 4 处\n // 被 out-of-band 清空/恢复(绕过 reducer:本行上方的全完成清理、/abort、Ctrl+C、resume)。\n // 回合开始时以 projState.todoList(面板真源)重新对齐 lastTasks,避免陈旧快照污染本回合\n // 的 done/incomplete 摘要判定(续轮回合靠 tasks.update 事件保持同步,无需重复对齐)。\n redState.lastTasks = projState.todoList.slice() as typeof redState.lastTasks\n try {\n // 展开 @ 资源 chip([@changes]/[@git]/[@clipboard]/插件自定义资源/文件行范围/@def)\n // → RFC-210 D2 统一展开管线(app.expandSigilChips,resolve 已从 cli 下沉 coding,\n // service 场景走同一管线的 REST 端点)。skipPrefixes=['model']:@model 是 action 型\n // chip(切模型副作用,非注入内容),已在上方 extractModelChips 剥离处理(评审 B9\n // 声明式跳过替代隐式顺序耦合)。\n // RFC-189 D3:`mode === 'bash'` 已在 :503 被 short-circuit 到 runAsyncBash,\n // 不会到达此处——input 恒为 effectiveText(此前 `mode === 'bash' ? '!'+text : ...`\n // 的三元分支已成死代码,一并清理)。\n effectiveText = await app.expandSigilChips(effectiveText, { skipPrefixes: new Set(['model']) })\n // 回显展开后的文本(而非原始 [@shortcut:xxx])——expandSigilChips 在提交时替换 chip\n // 为解析内容,TUI 回显应与引擎收到的内容一致,避免用户看到 chip 原文但 LLM 收到展开文本。\n if ((!parsed.isCommand || isFileCmd) && !turn.retryMode) {\n tui.messages.echoUserInput(effectiveText, { pending: true })\n // 标记 pending——模型返回首字节前 ESC 可撤回(不弹 abort 确认框)\n tui.pending.setPendingEcho(effectiveText, mode)\n }\n const input = effectiveText\n turn.streamedText = false\n const result = await interactive.handleInput(input)\n if (result.type === 'exit') { handleExit(result); return }\n if (result.type === 'help') {\n tui.modal.openModal({ kind: 'helpViewer', title: 'Help', lines: buildHelpLines(reg, session.pinned, (i18next.t('cmdDesc', { returnObjects: true }) as Record<string, string>)), onDismiss: () => {} })\n return\n }\n if (result.type !== 'noop' && result.text && !turn.streamedText) {\n // 短系统通知走 toast(/abort、/copy 等),长文本走 pushMessage\n if (result.type === 'system' && result.text.length <= 80) {\n tui.info.showToast(i18next.t('cmd.titleInfo'), result.text)\n } else {\n tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text', text: result.text }] })\n }\n }\n // RFC-283 D4:吞错路径清理——MaxToolTurnsError/PromptBudgetExceededError 被 runPrintMode\n // catch 后正常 return(不抛错),catch 块不执行。此处检查 endReason 补清 pendingEcho\n // + toast 反馈。streaming 已开始(MaxToolTurnsError 必有 text_delta),clearPendingEcho\n // 大概率 no-op(pendingEcho 已被 streaming.start 清空),核心价值在 toast notice。\n if (result.type === 'response' && result.endReason) {\n tui.pending.clearPendingEcho()\n if (result.notice) tui.info.showToast(i18next.t('cmd.titleInfo'), result.notice)\n }\n // 多轮续跑编排(RFC-077 cli-2 §5)——budget-continue only。\n // 撞 max-tool-turns 弹对话框让用户当场决定续跑/停下。每次 prompt 重置工具预算,\n // \"继续\"即获完整新预算。todo auto-continue 已由引擎闸(RFC-094 AgentSession 闸门循环)接管。\n // 宿主只提供渲染/弹框回调。\n await driveContinuation(result, {\n runTurn: (prompt) => interactive.handleInput(prompt),\n onTurnResult: (cont) => {\n if (cont.type === 'exit') { handleExit(cont); return 'exit' }\n if (cont.type !== 'noop' && cont.type !== 'help' && cont.text && !turn.streamedText) {\n tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text', text: cont.text }] })\n }\n return 'continue'\n },\n askBudgetContinue: () =>\n new Promise<boolean>((resolve) => {\n tui.modal.openModal({\n kind: 'select',\n title: i18next.t('budget.exhaustedTitle'),\n layout: 'buttons',\n options: [\n { label: i18next.t('budget.continue'), value: 'go', color: 'accent' },\n { label: i18next.t('budget.stop'), value: 'stop', color: 'amber' },\n ],\n onSelect: (v: string) => resolve(v === 'go'),\n onCancel: () => resolve(false),\n })\n }),\n onBudgetStopped: () => {\n tui.messages.pushMessage({ role: 'assistant', content: [{ type: 'text', text: i18next.t('budget.stoppedNote') }] })\n },\n beforeContinuationTurn: () => { turn.streamedText = false },\n })\n } catch (err) {\n // pending echo 未转正 = LLM 从未响应(handleInput 异常/网络失败)。\n // 撤回 user 消息——对话流不应留下\"发了但没收到\"的灰色残留。\n // 重试时 handleSubmit 会重新 echo + setPendingEcho(retryMode 在 :633 已重置为 false)。\n // withdrawPendingEcho 内部判空:若 pendingEcho 已被 clearPendingEcho 转正则为 no-op。\n tui.pending.withdrawPendingEcho()\n handleSubmitCatchError({\n err,\n submitState: turn,\n provider: session.model?.provider,\n rateLimitInfoRef: {\n get current() { return turn.lastRateLimitInfo },\n set current(v) { turn.lastRateLimitInfo = v },\n },\n retryModeRef: {\n get current() { return turn.retryMode },\n set current(v) { turn.retryMode = v },\n },\n errorRetryAttemptRef: {\n get current() { return turn.errorRetryAttempt },\n set current(v) { turn.errorRetryAttempt = v },\n },\n ERROR_RETRY_MAX,\n ERROR_RETRY_INTERVAL_SECONDS: app.getResilienceConfig().errorRetry.intervalMs / 1000,\n handleSubmit: handleSubmit as ((text: string, mode: string, intent: string) => void) | undefined,\n latestSubmittedPromptRef: {\n get current() { return turn.latestSubmittedPrompt },\n set current(v) { turn.latestSubmittedPrompt = v as { text: string; mode: SubmitMode } | undefined },\n },\n text,\n mode,\n handleModel,\n handleProviderAuth,\n cliCtx,\n tui,\n })\n } finally {\n turn.agentProcessing = false\n turn.userAborted = false\n tui.engine.pushEngineEvent({ kind: 'agentProcessing', value: false })\n tui.plugins.setCommands(refreshSlashCommands())\n }\n },\n onInterrupt: () => {\n // RFC-289:顺序敏感的守卫链整体下沉为纯函数 `resolveCtrlCAction`(可单测)。\n // 曾因内嵌在本闭包中无法测试而漏掉两个缺陷:去重路径误发空闲态 toast、\n // 判忙用裸 agentProcessing 漏判回合起跑窗口——两者叠加造成用户实测的\n // \"先出现 ctrl+c 终止提示,才出现「中止 Agent?」\"。详见该函数 doc。\n const now = Date.now()\n const action = resolveCtrlCAction(now, turn.lastCtrlCTime, {\n shellRunning: !!turn.shellChild,\n // 判忙必须用 isBusyNow()(引擎真源 OR cli 处理周期),不可用裸\n // turn.agentProcessing——后者在回合刚起跑的窗口内仍为 false。\n busy: isBusyNow(),\n stopDialogOpen: stopController.isStopDialogOpen(),\n idleCtrlCCount: turn.ctrlCCount,\n })\n\n // 去重(同一物理按键的第二路信号)不推进时间基准、不产生任何反馈。\n if (action === 'dedup') return\n turn.lastCtrlCTime = now\n\n switch (action) {\n case 'kill-shell':\n turn.shellChild!.kill('SIGTERM')\n setTimeout(() => {\n if (turn.shellChild && !turn.shellChild.killed) turn.shellChild.kill('SIGKILL')\n }, 3000)\n return\n case 'open-stop-dialog':\n // 阶梯一级:弹停止确认框(中止 / 暂停 / 取消,焦点默认在安全的「取消」)。\n // TUI 未就绪(raw stdin/SIGINT 早于首帧)时 requestStop 内部降级为直接中止。\n stopController.requestStop('abort')\n return\n case 'escalate-exit':\n // 阶梯二级:就地升级为「终止并退出」。不能再走 requestStop('abort')——\n // 那只会 replace 成内容相同的框,用户看不到任何反馈。\n stopController.escalateToExit()\n return\n case 'exit-process':\n // 经 stopTuiGuarded 退出——TUI 拆除挂起时 3s 兜底解阻塞,\n // 进程级卡死由 cli.ts app.stop 看门狗守,不在此竞速 process.exit。\n stopTuiGuarded()\n return\n case 'hint-exit':\n turn.ctrlCCount++\n setTimeout(() => { turn.ctrlCCount = 0 }, 800).unref()\n tui.info.showToast(i18next.t('cmd.titleExit'), i18next.t('cmd.pressCtrlCAgain'))\n return\n }\n },\n // RFC-225 M2:问 AI——读取 shell 执行快照,组装 D5 格式消息,经既有 onSubmit\n // (handleSubmit 本体,已含忙闲判断/入队路由)提交到当前会话。不调用 TUI 内部\n // 私有的 submitOrQueue 闭包(cli 层拿不到),面板关闭由 TUI 侧统一处理(见\n // App-keyboard.tsx `shell` 分支与 App.tsx 的 ShellFullOutputDialog onAskAi 包装)。\n onShellAskAi: () => {\n const snapshot = tuiRef?.shell.getShellExecutionSnapshot()\n if (!snapshot) return\n const text = buildShellAskAiPrompt(snapshot)\n void handleSubmit?.(text, 'send', isBusyNow() ? 'steer' : 'send')\n },\n onWithdrawPending: (text: string, _mode: string) => {\n // RFC-283 D3:ESC 撤回——先清理 UI(withdrawPendingEcho 清 pendingEcho + live 区渲染消失),\n // 再回填输入框 + abort 引擎。\n tui.pending.withdrawPendingEcho()\n tui.pending.restoreInputText(text)\n if (turn.agentProcessing) {\n turn.userAborted = true\n dispatchToEngine({ kind: 'abort' })\n tui.info.showToast(i18next.t('cmd.titleAborted'), i18next.t('cmd.runAborted'))\n }\n },\n onWithdrawSteer: (id: string) => {\n // RFC-337 D3:ESC 撤回一条运行中追加(steer)。removeSteer 命中(未被模型消费)→\n // 清 live 回显,无框无提示;未命中(已 drain 进 LLM 上下文)→ 退化为中止整回合\n // (此时该追加已成为回合的一部分,撤回追加 == 中止被追加改变方向的回合)。\n if (session.removeSteer(id)) {\n tui.pending.withdrawPendingSteer(id)\n } else {\n stopController.requestStop('abort')\n }\n },\n onStop: () => {\n finishInteractive()\n },\n })\n tuiRef = tui\n // RFC-164 M164-2 D6:将 TUI 接线到 memory 冲突回调——HttpMemoryStore 的 onConflict\n // 在冲突发生时通过 cli.ts 传入的 ref 对象触发 TUI toast,让用户即时感知并发编辑冲突。\n if (memoryConflictRef) {\n memoryConflictRef.current = (path: string, conflictPath: string) => {\n tui.info.showToast('Memory Conflict', `Concurrent edit detected for ${path} — saved to ${conflictPath}`)\n }\n }\n // RFC-150 D7/M0:startTui() resolve 即首帧已挂载完成(Ink render() 是同步 commit,\n // 见 startTui.tsx 头注释)——这是\"用户可输入\"这一时刻的度量点,对应 §2 目标指标终点。\n markStartupPhase('startTui() ready (first frame mounted)')\n\n // RFC-344 M3:插件 user.ask 的 TUI 渲染出口。只在 TUI 首帧挂载后设置;\n // headless/print/json 模式不设置,App.askPluginUser 会按 M1 D3 返回 auto_recommendation。\n app.setUserAskRenderer((question) => renderGrillQuestion(tui, question))\n\n // RFC-338 D7:粘贴缓存的会话归属绑定。必须在首帧挂载之后——桥(pasteBridgeRef)\n // 由 PromptInput 渲染时赋值,此前 exportPaste()/hydratePaste() 都是 no-op。\n // fail-soft:缓存故障只让 chip 失效(UI 已有 toast + 提交阻断),不阻断会话。\n const pasteBinder = createPasteSessionBinder({\n gateway: () => app.sessionManager.panelState,\n bridge: () => tui.pending,\n onError: (stage, error) => {\n app.logger?.debug?.({ stage, error }, 'paste cache binding failed (non-fatal)')\n },\n })\n // ⚠️ 这里**不能**写 `cliCtx.callbacks.pasteBinder`——`cliCtx` 是 late-bound\n // (声明于本函数上方 `let cliCtx!: CliContext`,真正赋值在下方 buildCliContext),\n // 此刻仍是 `undefined`,写它会抛\n // `TypeError: Cannot read properties of undefined (reading 'callbacks')`\n // 并使整个启动流程 reject。非空断言 `!` 只骗过编译器,不改变运行时行为——\n // 这正是 buildCliContext 上方那条「安全闸」注释所描述的不变式。\n // 赋值已下移到 buildCliContext 之后(搜索 `callbacks.pasteBinder = pasteBinder`)。\n //\n // `load()` 同样下移:它内部经 gateway 读 sessionManager,与 binder 注册同批更安全,\n // 且启动路径上早几行晚几行无差别(都在首帧之后、用户可交互之前)。\n\n // MCP 连接完成通知 + 代理环境变量检测——实现在 interactive-setup.ts(RFC-150 D1 语义原样)。\n wireStartupNotices({ app, tui })\n\n // RFC-287 M4:接入生态检索,使 capability_gap 工具可用(模型发现能力缺口 → 查生态\n // → 命中引导安装 / 未命中给 scaffold 蓝图)。只注入读能力,安装恒经用户信任门(R1)。\n wireEcosystemSearch(app)\n\n // RFC-234 P2-b:异步加载 Home 多会话列表(不阻塞首帧)。\n app.listAllSessions().then((allSessions) => {\n const current = session.id\n const list = allSessions\n .filter((s) => s.status !== 'archived' && (s.entryCount ?? s.messageCount) > 0 && s.id !== current)\n .slice(0, 10)\n .map((s) => ({\n id: s.id,\n title: s.title ?? s.id.slice(0, 8),\n status: (s.id === current ? 'busy' : 'idle-cold') as 'busy' | 'idle' | 'needs-input' | 'idle-cold',\n lastActivityAt: s.updatedAt ? new Date(s.updatedAt).toLocaleString() : '',\n isCurrent: s.id === current,\n isPinned: s.pinned ?? false,\n isArchived: s.status === 'archived',\n isReadOnly: s.writeLocked ?? false,\n }))\n tui.stores.tasks.setSessionList(list)\n }).catch(() => { /* non-critical */ })\n\n // RFC-134 D7:禁用跨进程凭据变更通知。AuthStore 的 fs.watch 仍会更新内存缓存\n // (auth.json 变化会被感知),但本会话不做 toast 通知也不静默刷新可用模型列表——\n // 模型/凭据变更对其他运行中会话的影响被严格限制:各自在下一轮 prompt 时自然读到最新状态。\n\n // RFC-169 D2:传活引用而非值快照——setupScheduleService 内部 getSessionId() 每次调用都读\n // 最新的 session(`/resume` 重新赋值本变量后,闭包立刻感知,不留陈旧 sessionId)。\n // RFC-169 D3/重要事项规则 6:sessionUrl/sessionToken 均已配置时才传 remoteConfig——\n // 与 App 构造期装配 storage.session={url} 的同一判定条件对齐(cli.ts:323),\n // 二者任一缺失时 setupScheduleService 内部据 app.storage.remoteSessionConfigured=false\n // 自动回退本地实现,不会出现\"配了一半\"的中间态。\n const { pollInterval: schedulePollInterval, scheduler: scheduleService } =\n setupScheduleService(\n app,\n { get value() { return session } },\n tui,\n options.sessionUrl && options.sessionToken\n ? { sessionUrl: options.sessionUrl, sessionToken: options.sessionToken }\n : undefined,\n )\n\n // Late-bound helper assignments (factories in interactive-submit-helpers.ts; need tui)\n runAsyncBash = createAsyncBashRunner({ tui, app, submitState: turn })\n handleAbortIntercept = createAbortInterceptor({\n agentProcessingRef: { get value() { return turn.agentProcessing }, set value(v) { turn.agentProcessing = v } },\n tui,\n requestStop: (intent) => stopController.requestStop(intent),\n })\n runMemoryCapture = createMemoryCapture({ app, tui })\n\n // 模型回退提示已移至**首个真实 prompt** 时裁决(ensureModelUsableForPrompt):启动期 catalog\n // 可能未拉完,此处对比 configured vs active 会过早误报(authed-slow 误判为不可用)。改由 prompt\n // 入口在 catalog/认证就绪后统一判断 recover/降级,避免启动期假阳性与双重提示。\n\n // /model 参数补全注册 + 终端标题(RFC-078 M133-03 单一真源回落链)——见 interactive-setup.ts。\n wireModelSuggesterAndTitle({ app, tui, session })\n // 补齐 /schedule /replay /registry /trust plugin /session unlock 的参数补全\n // (复刻 /model 的 RFC-125 D3 模式,见 interactive-setup.ts 头注)。\n wireExtraArgumentSuggesters({ app, tui, getScheduler: () => scheduleService })\n\n // /exit --restart:记 restart 标记,由 cli.ts 在 app.stop() 完成后 spawn 新进程,\n // 不再在 handleExit 内 spawn——避免新进程在旧进程 saveAll() 之前启动而读到旧快照。\n const handleExit = (result: { type: 'exit'; restart?: boolean }): void => {\n // RFC-289 D0/D1:`/exit` 经统一入口,与 Ctrl+C / `/abort` 共用**同一个**确认框实例。\n //\n // 忙时先确认(此前是运行中静默硬中断 + 立即拆 TUI,零挽回机会——破坏性最大的入口\n // 反而保护最弱);空闲时零摩擦直接退出(脚本/自动化场景占绝大多数,无 in-flight\n // 工作可丢)。忙闲判定与 abort/exit 执行细节都在 requestStop 内部,见该模块头注释。\n //\n // 关键:**不在此另开一个退出确认框**——那会与 Ctrl+C 的框并存,把本要修的堆叠问题\n // 在新入口上原样复刻(RFC-289 D0 反面教材)。\n stopController.requestStop(result.restart ? 'exit-restart' : 'exit')\n }\n\n // RFC-148 M6:memoryGovernor 三级降级回调(onWarning/onCritical/onCriticalDuringGrace/\n // onLevelChange)纯搬迁至 interactive-memory-governor.ts(registerMemoryGovernorCallbacks)。\n // RFC-115 D5(P1-1 修正):宽限期只应在\"内存治理触发的自动重启\"后起算,不能用\n // options.continueSession 真值判断——那会连用户手动 otto --continue 恢复历史会话\n // 也一并抑制,错过真正的内存危机信号。改用专用内部标志 memoryRestartGrace\n // (仅 cli.ts 的 chaperone spawn 在检测到 restart.memoryTriggered 时附加)。\n if (options.memoryRestartGrace) {\n app.memoryGovernor.markRestarted()\n }\n // RFC-196 M3 T12a:重启链代际 ≥3 告警——每代重启在链上留一个 chaperone 进程\n // (D1a 压平后每代净增 1,仍是 O(N)),链过深提示用户完整退出重开终端一次性\n // 释放整条链。通知中心(非弹窗)——与 fleet 预算同级的趋势告警。\n const restartGeneration = options.restartGeneration ?? 0\n if (restartGeneration >= 3) {\n tui.info.pushNotification(\n i18next.t('cmd.titleFleetBudget'),\n i18next.t('cmd.restartChainDeep', { generation: restartGeneration }),\n 'system',\n 'warning',\n )\n }\n // 经独立评审 P2-4 补充:setCallbacks 晚于 memoryGovernor.start()(App 构造时即启动),\n // 若采样间隔被配置得极小(如测试/极端场景),首次 tick 可能在 setCallbacks 之前触发、\n // 导致 onLevelChange 丢失首轮同步。此处手动同步一次当前分级,消除\"启动窗口内\n // indicatorLevel 与真实状态不一致\"的隐患(默认 30s 采样间隔下本就不会发生,此为\n // 防御性兜底,零额外开销——currentLevel 是纯字段读取)。registerMemoryGovernorCallbacks\n // 本身挪到 persistence 创建之后(见下方),因需要其 onContextCompacted/recomputeBudget\n // 注入 onWarning 压缩成功路径的 UI 反馈;本段与该顺序无耦合,保留原位置。\n tui.engine.pushEngineEvent({\n kind: 'perf',\n indicatorLevel: app.memoryGovernor.currentLevel,\n memory: app.memoryGovernor.sample(),\n })\n\n // stream.retry 倒计时 / rate.limit 捕获 / crossBoundary toast——实现在\n // interactive-agent-toasts.ts(H1/H7/RFC-144 M6 语义原样)。\n wireAgentToasts({\n session,\n tui,\n retryAccelerate,\n setRateLimitInfo: (info) => { turn.lastRateLimitInfo = info },\n // RFC-303:RetryDialog 的「停止」按钮/Esc——不经确认框直接中止当前回合。\n abortNow: () => { stopController.abortNow() },\n })\n\n // 占用条计算的单一真源——读当前会话消息 usage + 当前窗口/模型,算三段预算并推 TUI。\n // 三处复用:① 启动初始注入 ② 每轮 prompt.end ③ /model 切换后(窗口/速率变了须立即刷新)。\n // 恢复已有会话时从最后一条带 usage 的消息提取,确保 footer 占比条不显示 0%;新会话无消息故为 0。\n // 占用条重算 + 回合面板持久化(见 interactive-persistence.ts)\n const persistence = createInteractivePersistence({\n getSession: () => session,\n app,\n tui,\n projState,\n contextWindow: contextWindowRef,\n currentModelId: currentModelIdRef,\n })\n const { recomputeBudget, persistEditedFiles, persistSubagents, persistTurnCount, onContextCompacted } = persistence\n pushBudgetNow = recomputeBudget\n recomputeBudget()\n app.editFileCallback = persistence.editFileCallback\n\n // registerMemoryGovernorCallbacks 挪到此处(2026-07-19,onWarning 自动压缩成功路径\n // UI 反馈修复):需要 onContextCompacted/recomputeBudget 已就绪才能注入——两者由上方\n // persistence(createInteractivePersistence)产出,先前调用点(app.memoryGovernor.\n // markRestarted() 附近)早于此处,故整体挪动而非仅新增参数。MemoryGovernor 默认\n // 30s 采样间隔,中间跨越的 stream.retry/rate.limit.exhausted/lifecycleAsync 监听器\n // 注册不依赖本回调已就绪,挪动零副作用。\n registerMemoryGovernorCallbacks({\n app,\n tui,\n getSession: () => session,\n setRestartResumePrompt: (text) => { restartResumePrompt = text },\n setRestartMemoryTriggered: (value) => { restartMemoryTriggered = value },\n handleExit,\n onContextCompacted,\n recomputeBudget,\n })\n\n // 通知中心应用内通道——把通知镜像进 Ctrl+N NotificationCenter(门控通过才记)。\n app.registerInAppNotificationChannel((title, message, kind) =>\n tui.info.pushNotification(title, message, 'system', notificationLevel(kind)),\n )\n // 喂 follow-up 队列探针——有待跑 follow-up 时抑制完成通知(否则该抑制形同虚设)。\n app.setFollowUpPendingProbe(() => tui.engine.hasQueuedFollowUp())\n\n // ── 统一指令分发上下文 + 启动后设置(见 interactive-context.ts / interactive-setup.ts)──\n const redState = createReducerState()\n const events = setupInteractiveEvents({\n getSession: () => session,\n app,\n tui,\n projState,\n redState,\n recomputeBudget,\n persistEditedFiles,\n persistSubagents,\n persistTurnCount,\n onContextCompacted,\n markStreamed: () => { turn.streamedText = true },\n })\n eventsRef.value = events\n // 安全闸(经独立评审 P2-1 记录):本行之前的 startTui() 回调选项里,onA2uiAction/\n // onPerfRefresh 等闭包捕获的是 cliCtx 变量引用(非快照值)——赋值前调用会读到\n // `undefined`(非空断言 `!` 只骗过编译器,不改变运行时行为)。此不变式依赖\n // \"await startTui() 到本行之间全程零 await\"(同步代码块,用户键击等异步事件\n // 无法插入其间)。未来在此区间新增任何 await 语句前,必须确认不会造成这些\n // 回调在 cliCtx 赋值前被调用。\n cliCtx = buildCliContext({\n tui, app, themeRegistry,\n sessionRef: { get value() { return session }, set value(v) { session = v } },\n interactiveRef: { get value() { return interactive }, set value(v) { interactive = v } },\n regRef: { get value() { return reg }, set value(v) { reg = v } },\n slashCommandsRef: { get value() { return slashCommands }, set value(v) { slashCommands = v } },\n modelReadyRef: { get value() { return modelReady }, set value(v) { modelReady = v } },\n currentModelIdRef: { get value() { return currentModelId }, set value(v) { currentModelId = v } },\n currentLangRef: { get value() { return currentLang }, set value(v) { currentLang = v } },\n projState,\n agentProcessingRef: { get value() { return turn.agentProcessing }, set value(v) { turn.agentProcessing = v } },\n modelsRef: { get value() { return models }, set value(v) { models = v } },\n availableModelIds,\n restartRequestedRef: { get value() { return restartRequested }, set value(v) { restartRequested = Boolean(v) } },\n applyModel,\n persistAvailable,\n refreshModelsNow,\n refreshSlashCommands,\n events,\n redState,\n agentMessagesToChatMessages,\n PROVIDER_DISPLAY_NAMES,\n apiCredentialId,\n onContextCompacted,\n })\n // RFC-338 D7:粘贴缓存绑定器登记进 ctx(binder 本身在 startTui 之后创建——桥需首帧)。\n // **必须在 buildCliContext 之后**:`cliCtx` 是 late-bound,在它赋值前写\n // `cliCtx.callbacks.*` 会抛 TypeError 并 reject 整个启动流程(真实事故,见上方创建处注释)。\n cliCtx.callbacks.pasteBinder = pasteBinder\n // 启动即恢复当前会话的粘贴缓存(--continue/resume 场景下历史里的 chip 才可用)。\n void pasteBinder.load(session.id)\n // RFC-131 T8:启动时同步插件贡献(状态栏/渲染器/面板,统一入口)——同 M082-04-06\n // 教训(只在事件驱动 reload 跑会导致开机 dormant,要等 /reload 才出现)。\n tui.plugins.setPluginRegistrations(app.getPluginRegistrations())\n\n // ── RFC-326 M1-03b:FTUE 启动编排(单一 controller,取代散落探测 + 固定延迟)──\n //\n // 此前这里内联重算 hasProvider/hasAuth/hasModel,与 setup-wizard.ts 的同名\n // 探测构成两处独立实现(口径可漂移),且用 setTimeout(500) 猜测 TUI 就绪。\n //\n // 两处改正:\n // 1. 事实判定统一走 FtueController → deriveFtueSnapshot。`hasModel` 不再用\n // 乐观的 `modelReady` 布尔,而是经 createModelUsabilityGate 逐个模型判定\n // 真实可用性(凭据无效 / OAuth 订阅 429 风险的模型不算数)——这正是\n // \"向导宣布 All set、首条 prompt 却被回退弹窗拦住\"的根因。\n // 2. 删除 setTimeout(500)。执行到此处时 startTui() 早已 resolve(见上方\n // `markStartupPhase('startTui() ready (first frame mounted)')`,Ink render()\n // 是同步 commit),首帧已挂载,就绪信号是**确定的**而非靠延迟猜测。\n // 慢机上 500ms 也未必够,快机上纯属白等——固定 sleep 两头不讨好。\n // RFC-326 M1-05:依赖装配统一走 buildFtueDeps——与 setup-wizard.ts 共享同一\n // 工厂,杜绝两处各自内联探测导致的口径漂移。\n const ftueController = new FtueController(\n buildFtueDeps({\n modelRegistry: app.modelRegistry,\n authStore: app.authStore,\n settings: app.settings,\n selectedModelId: () => session.model?.id,\n isModelUsable: createModelUsabilityGate(app.authStore),\n }),\n )\n const ftueSnapshot = await ftueController.refresh()\n\n // ── 个性化引导(昵称 + 语言)——RFC-326 §4 原则 1:任务优先于个性化 ──\n //\n // 此前 onboarding 无条件排在 FTUE 之前:新用户刚装完 otto、还不知道它能不能\n // 工作,就先被要求输入昵称、选语言。这把首次体验的焦点从\"让产品可用\"挪到了\n // \"回答可选问题\",也是 RFC-326 现状表第一行记录的问题。\n //\n // 改为:仅当 FTUE 判定**已可用**(provider/凭据/可用模型俱全)时才在启动阶段\n // 询问个性化;否则跳过本轮,把首屏让给 setup 引导。语言默认仍自动检测,\n // 用户随时可用 /preference 修改,故\"跳过\"不造成能力损失。\n //\n // 注意不写 has_completed_onboarding——未就绪只是\"这次不问\",等用户配好后的\n // 下一次启动仍应有机会设置昵称;写标记会让它永久消失。\n if (isFtueReady(ftueSnapshot)) {\n // 触发条件:language=='auto' 且从未完成过 onboarding(用户从未显式设置过语言)。\n // Esc 可跳过任一步骤。完成后写入 has_completed_onboarding=true 防止重复弹出。\n //\n // 注意:openModal(kind:'input') 返回 Promise 自然阻塞;openModal(kind:'select')\n // 是同步 void(fire callback 即返回),但 modalStore.selectRequest 是单槽——\n // 若后继代码再开一个 kind:'select'(如 provider 选择)会无声覆写本 dialog。\n // 因此语言选择步骤必须用 Promise 包装并 await,保证两步串行 + 不被覆写。\n if (cfgLang === 'auto' && !app.settings.get('has_completed_onboarding')) {\n // 三步(昵称→语言→主题)收进**单个** modal 槽位:不再有两段串行 openModal 被\n // 后继 dialog 覆写 selectRequest 的风险(旧实现需 Promise 包装规避,见 git 历史)。\n const onboarding = await tui.modal.openModal({\n kind: 'onboarding',\n // RFC-283:onboarding 不再有主题步——深浅自动跟随终端背景色。\n // 语言选中即切界面语言,让后续步骤文案立刻是目标语言。\n onLanguagePreview: (lang) => {\n const resolved = lang === 'auto' ? detectLanguage() : (lang as Lang)\n setCliLanguage(resolved)\n tui.lifecycle.setLanguage(resolved)\n currentLang = resolved\n },\n })\n if (onboarding.nickname) {\n app.settings.persist({ nickname: onboarding.nickname }).catch(() => {})\n }\n if (onboarding.language) {\n const resolved = onboarding.language === 'auto' ? detectLanguage() : (onboarding.language as Lang)\n setCliLanguage(resolved)\n tui.lifecycle.setLanguage(resolved)\n currentLang = resolved\n app.settings.persist({ language: onboarding.language }).catch(() => {})\n }\n // RFC-283:onboarding 不再有主题选择步——深浅自动跟随终端背景色。\n // 保留 nickname/language 步骤。\n // 标记完成——走完(或逐步 Esc 跳过)后才写,防止进程中途崩溃导致下次永久跳过。\n app.settings.persist({ has_completed_onboarding: true }).catch(() => {})\n }\n }\n\n // FTUE 未就绪 → 自动拉起设置向导(个性化已在上方被跳过,首屏专注于\"让它能用\")。\n if (ftueSnapshot.recommendation.autoOpen) {\n void dispatchSlashCommand('setup', [], cliCtx)\n }\n\n // ── RFC-133 M3:StaleBuildDetector 过期构建检测 ──\n // 从 import.meta.url 反推 otto 自身源码仓库根目录(非用户工作区)。\n // npm 全局安装场景下返回 undefined → detector 不启动,零误报。\n const staleBuildRepoRoot = resolveOttoRepoRoot()\n if (staleBuildRepoRoot) {\n staleBuildDetector = new StaleBuildDetector({\n repoRoot: staleBuildRepoRoot,\n currentBuildId: BUILD_ID,\n logger: app.logger,\n onStale: (newBuildId) => {\n tui.info.pushNotification(\n i18next.t('cmd.titleStaleBuild'),\n i18next.t('cmd.staleBuildMessage', { current: BUILD_ID, latest: newBuildId }),\n 'system',\n 'info',\n )\n },\n })\n staleBuildDetector.start()\n }\n\n await runPostTuiSetup({\n tui, options, app, session, projState,\n resumedNote, resumed,\n updateCheckPromise,\n bg, events,\n })\n\n // RFC-305 D3:resume 检出的崩溃缺口 toast(restore 早于订阅建立,事件通道不可达,\n // 走 runPostTuiSetup 之后的值通道;lostTurns=0 或非 resume 场景为 undefined 静默)。\n if (startupCrashGapRef.value) {\n tui.info.showToast(\n i18next.t('crashGap.toastTitle'),\n i18next.t('crashGap.toastBody', { lostTurns: startupCrashGapRef.value.lostTurns }),\n )\n }\n })\n\n // 退出后提示如何续上本会话\n if (app.codingSessionPool.autoPersist && session.session.messages().length > 0) {\n const networkArgs = options.sessionUrl ? ` --session-url ${options.sessionUrl}` : ''\n const tokenArg = options.sessionToken ? ` --session-token ${options.sessionToken}` : ''\n process.stdout.write(`\\n\\x1b[2m${i18next.t('cmd.resumeContinueLabel')}\\x1b[0motto --continue ${session.id}${networkArgs}${tokenArg}\\n`)\n }\n\n return {\n restart: restartRequested\n ? {\n sessionId: session.id,\n memoryTriggered: restartMemoryTriggered,\n resumePrompt: restartResumePrompt,\n }\n : undefined,\n }\n } finally {\n bgController?.clearBgSubs()\n bgController?.stopBgOutputPoll()\n bgController?.stopBgSubagentPoll()\n }\n}\n","/**\n * /workspace-trust 命令:项目信任审批交互向导。\n */\nimport { createApp, evaluateWorkspaceTrustPrompt, trustProject } from '@x-otto/coding'\nimport { promptWorkspaceTrust, promptParentWorkspaceChoice } from '@x-otto/tui'\n\ntype App = ReturnType<typeof createApp>\n\nexport async function maybePromptWorkspaceTrust(app: App): Promise<void> {\n // 父级 workspace 检测:向上递归找到父级 marker 时先让用户选择\n if (app.hasParentWorkspace()) {\n const info = app.parentWorkspaceExisted!\n const choice = await promptParentWorkspaceChoice({\n parentRoot: info.parentRoot,\n currentRoot: info.currentRoot,\n language: app.settings.get('language'),\n })\n\n if (choice === 'exit') {\n process.stdout.write('\\n')\n process.exit(0)\n }\n if (choice === 'use_parent') {\n await app.adoptParentWorkspace()\n } else {\n // create_new:在当前目录创建新 workspace\n await app.createLocalWorkspace()\n }\n // workspace 身份已建立,继续信任检查\n }\n\n const decision = evaluateWorkspaceTrustPrompt(app.workspaceDir, { interactive: true })\n if (!decision.shouldPrompt) {\n // 不弹框(已信任/关门控/非真项目):若 workspace 身份仍待确认(marker 缺失),\n // 静默建立——不重复打扰用户,行为与\"信任已建立、只是本地 marker 文件丢失\"一致。\n if (app.needsWorkspaceInit()) await app.confirmWorkspaceInit()\n return\n }\n\n const choice = await promptWorkspaceTrust({\n root: decision.root,\n language: app.settings.get('language'),\n })\n\n if (choice === 'exit') {\n process.stdout.write('\\n')\n process.exit(0)\n }\n if (choice === 'trust') {\n trustProject(decision.root)\n await app.settings.reload() // thunk 重新求值 → 已信任 → 高危键/可执行面生效\n if (app.needsWorkspaceInit()) await app.confirmWorkspaceInit()\n return\n }\n // readonly:保持未信任 + 强制只读(会话级 override,最高优先级)——workspace 身份仍需建立\n // (readonly 只收紧权限,不阻止使用/持久化;不建立身份会导致本次会话无法正常落盘/resume)。\n await app.settings.update({ permission_mode: 'readonly' })\n if (app.needsWorkspaceInit()) await app.confirmWorkspaceInit()\n}\n","import { runJsonMode, runPrintMode } from '../modes/run-modes'\nimport type { App } from '@x-otto/coding'\n\nimport type { CLIOptions } from '../types'\n\n/**\n * UX(headless 引导):零配置跑 `otto \"prompt\"` 时 createSession 抛的\n * 'No model configured…createApp()' 是面向嵌入方开发者的 API 错误——对 CLI 用户\n * 完全错位(createApp 是 JS API,用户根本不写代码)。翻译成 CLI 用户能执行的指引。\n */\nconst NO_MODEL_API_ERROR = 'No model configured'\n\nfunction rethrowWithCliGuidance(err: unknown): never {\n if (err instanceof Error && err.message.startsWith(NO_MODEL_API_ERROR)) {\n throw new Error(\n 'No model configured. Run `otto` (interactive) to complete setup — it walks you through: ' +\n 'install a provider plugin → /auth → /model. Or authenticate directly with `otto auth login`.',\n )\n }\n throw err\n}\n\n/**\n * `otto --print \"prompt\"` / `otto run \"prompt\"` — non-interactive single-turn.\n */\nexport async function runPrintCommand(options: CLIOptions, app: App): Promise<void> {\n if (options.prompt) {\n const session = await app.createSession().catch(rethrowWithCliGuidance)\n await runPrintMode(session, options.prompt, { tui: true })\n }\n}\n\n/**\n * `otto --json \"prompt\"` — non-interactive, machine-readable output.\n */\nexport async function runJsonCommand(options: CLIOptions, app: App): Promise<void> {\n if (options.prompt) {\n const session = await app.createSession().catch(rethrowWithCliGuidance)\n const result = await runJsonMode(session, options.prompt)\n process.stdout.write(JSON.stringify(result, null, 2) + '\\n')\n }\n}\n","/**\n * theme-markdown-wiring.ts — RFC-277 M4:把 settings 里已校验的 `theme_overrides.markdown`\n * 接入 `@x-otto/tui` 的 Markdown ANSI 渲染 seam。\n *\n * 为什么单独一个文件:`setMarkdownThemeUserOverrides` 是模块级引用桥接(对标 `theme-ref.ts`\n * 的 `/theme` 命令桥接),调用点分散在启动路径(cli.ts)与运行期切换点(未来 M6 的\n * `/preference theme` 命令)。抽成独立小函数便于两处复用 + 单元测试,不用在 cli.ts 里\n * 内联一次性逻辑。\n */\nimport { setMarkdownThemeUserOverrides } from '@x-otto/tui'\nimport type { App } from '@x-otto/coding'\n\n/**\n * 从 `app.settings`(已经过 `SettingSchema` zod 校验)读取 `theme_overrides.markdown`\n * 并接入渲染 seam。**不做二次校验**——契约是调用方传入的必须是已过 settings 层 schema\n * 校验的合法值(`theme_overrides` 字段是 `.strict()` schema,非法字段/角色/marker 已在\n * 加载期被拒绝并静默丢弃,见 `packages/setting/src/schema.ts`)。\n *\n * 幂等:可在启动时调一次,也可在设置变更后(未来 `/preference theme` 或 `settings.persist`\n * 完成后)重复调用以刷新。\n */\nexport function wireMarkdownThemeOverrides(app: Pick<App, 'settings'>): void {\n const overrides = app.settings.get('theme_overrides')\n setMarkdownThemeUserOverrides(overrides?.markdown)\n}\n","import { homedir } from 'node:os'\nimport { spawn } from 'node:child_process'\n\nimport {\n createApp,\n evaluatePluginTrust,\n resolvePluginCliBin,\n buildPluginCliRegistry,\n findPluginCliNameConflict,\n spawnPluginCli,\n} from '@x-otto/coding'\nimport { globalProcessRuntime, installExitReaper } from '@x-otto/runtime'\nimport { setLogLevel, markStartupPhase, traceStartupPhase, traceStartupPhaseSync, type LogLevel } from '@x-otto/shared'\nimport { loadUserIdentity, OTTO_SESSIONS_DIR, flushEnvWarnings } from '@x-otto/env'\n\n// RFC-150 D7/M0:模块顶层 import 全部完成后打点——这是 cli.ts 自身及其静态依赖\n// (@x-otto/coding/@x-otto/runtime/@x-otto/env 等)的 import 图谱耗时,相对进程启动时刻。\nmarkStartupPhase('cli.ts imports resolved')\n\nimport { parseCLI, SUB_COMMAND_NAMES } from './parse-cli'\nimport type { SubCommandContext, SubCommandHandler } from './types'\nimport { clearTerminalOnExit } from './terminal-clear-on-exit'\n\n// RFC-150 D3:`runInteractiveCommand`/`maybePromptWorkspaceTrust`/`runPrintCommand`/\n// `runJsonCommand` 是主 prompt 路径(interactive/print/json 模式)必经之路,本就要拉全量\n// @x-otto/coding+@x-otto/tui——保留静态 import。轻子命令(doctor/auth/serve/… 见下方\n// SUB_COMMANDS)全部改为按需动态 import,顶层不再静态引入它们的模块(及其传递依赖),\n// 使 `otto doctor`/`otto extension` 等轻命令不再承担这些依赖的 import 开销(实测\n// `otto --help` 曾达 ~700ms,主因是无论跑什么命令都静态 import 了全部子命令处理器)。\nimport { discoverPluginsWithBundled } from './discover-with-bundled'\nimport { runInteractiveCommand } from './commands/interactive'\nimport { maybePromptWorkspaceTrust } from './commands/workspace-trust'\nimport { runPrintCommand, runJsonCommand } from './commands/run-modes'\nimport { wireMarkdownThemeOverrides } from './theme-markdown-wiring'\n\n/**\n * 子命令注册表:name → handler(替代原来 runCli 中的 if-chain)。\n * RFC-150 D3:每个 handler 内部动态 `import()` 对应模块——只有真正命中该子命令时才加载\n * 其代码与传递依赖,顶层 cli.ts 不再静态 import 全部十余个子命令处理器。\n */\nconst SUB_COMMANDS: Map<string, SubCommandHandler> = new Map([\n ['doctor', async (ctx) => {\n const { runDoctorCommand } = await import('./commands/doctor')\n return runDoctorCommand(ctx.args, { stdout: ctx.stdout, stderr: ctx.stderr })\n }],\n ['auth', async (ctx) => {\n const { runAuthCommand } = await import('./auth-command')\n return runAuthCommand(ctx.args, {\n bootstrap: {\n workspaceDir: ctx.options.projectDir,\n projectConfigPath: ctx.options.configPath,\n verbose: ctx.options.verbose,\n },\n stdout: ctx.stdout,\n stderr: ctx.stderr,\n })\n }],\n ['serve', async (ctx) => {\n const { runServeCommand } = await import('./commands/serve')\n return runServeCommand(ctx.args, {\n projectDir: ctx.options.projectDir,\n configPath: ctx.options.configPath,\n modelId: ctx.options.model,\n verbose: ctx.options.verbose,\n })\n }],\n ['daemon', async (ctx) => {\n const { runDaemonCommand } = await import('./commands/daemon')\n return runDaemonCommand(ctx.args, {\n projectDir: ctx.options.projectDir,\n stdout: ctx.stdout,\n stderr: ctx.stderr,\n })\n }],\n ['persistenced', async (ctx) => {\n const { runPersistencedCommand } = await import('./commands/persistenced')\n return runPersistencedCommand(ctx.args)\n }],\n ['remote-persistence-server', async (ctx) => {\n const { runRemotePersistenceServerCommand } = await import('./commands/remote-persistence-server')\n return runRemotePersistenceServerCommand(ctx.args)\n }],\n ['observe', async (ctx) => {\n const { runObserveCommand } = await import('./commands/observe')\n return runObserveCommand(ctx.args)\n }],\n ['debug', async (ctx) => {\n const { runDebugCommand } = await import('./commands/debug')\n return runDebugCommand(ctx.args)\n }],\n ['time-travel', async (ctx) => {\n const { runTimeTravelCommand } = await import('./commands/time-travel')\n return runTimeTravelCommand(ctx.args)\n }],\n ['mcp', async (ctx) => {\n const { runMcpCommand } = await import('./commands/mcp')\n return runMcpCommand(ctx.args)\n }],\n ['config', async (ctx) => {\n const { runConfigMigrationCommand } = await import('./commands/config-migration')\n return runConfigMigrationCommand(ctx.args, { stdout: ctx.stdout, stderr: ctx.stderr })\n }],\n ['migrate', async (ctx) => {\n const { runMigrateCommand } = await import('./commands/migrate')\n return runMigrateCommand(ctx.args, { stdout: ctx.stdout, stderr: ctx.stderr })\n }],\n ['extension', async (ctx) => {\n const { runExtensionCommand } = await import('./commands/extension')\n return runExtensionCommand(ctx.args, { stdout: ctx.stdout, stderr: ctx.stderr })\n }],\n ['ps', async (ctx) => {\n const { runPsCommand } = await import('./commands/ps')\n return runPsCommand(ctx.args, { stdout: ctx.stdout, stderr: ctx.stderr })\n }],\n ['feedback', async (ctx) => {\n const { runFeedbackCommand } = await import('./commands/feedback')\n return runFeedbackCommand(ctx.args, {\n stdout: ctx.stdout,\n stderr: ctx.stderr,\n projectDir: ctx.options.projectDir,\n configPath: ctx.options.configPath,\n })\n }],\n])\n\n/**\n * RFC-112 D2/D3/D4:尝试把 `commandName [args...]` 路由到已安装插件声明的 CLI 可执行文件。\n * 返回 `undefined` 表示\"没有插件声明这个命令名\"——调用方应回退到既有的 prompt 处理路径;\n * 返回数字表示\"确实路由到了某个插件\",无论成功执行(子进程退出码)还是被拒绝(信任门/\n * 解析失败,返回非 0),都不应该再把该参数当 prompt 发给 AI。\n */\n/** 导出供单测直接调用(避免测试必须走完整 `runCli()`/`process.argv` 真实子进程)。 */\nexport async function tryDispatchPluginCli(\n commandName: string,\n args: string[],\n options: { projectDir: string },\n): Promise<number | undefined> {\n const plugins = discoverPluginsWithBundled({ cwd: options.projectDir, homedir: homedir() })\n const reservedNames: Set<string> = new Set(SUB_COMMAND_NAMES)\n const registry = buildPluginCliRegistry(plugins, reservedNames)\n const plugin = registry.get(commandName)\n if (!plugin) {\n // RFC-124 §10.2.2:区分\"压根没人声明这个命令名\"(静默回退到 prompt 处理,用户可能\n // 只是打了个普通问题)与\"曾有 ≥2 个插件声明但被 fail-closed 剔除\"(应给出明确诊断,\n // 而非把用户的命令行参数静默当 prompt 发给 AI——那会让用户误以为打错字而非命令名冲突)。\n const conflictingPluginIds = findPluginCliNameConflict(plugins, reservedNames, commandName)\n if (conflictingPluginIds) {\n process.stderr.write(\n `otto: 命令 \"${commandName}\" 被多个插件声明(${conflictingPluginIds.join(', ')}),已禁用以避免歧义。\\n` +\n `请重命名其中一个插件的 \\`contributes.cli.name\\` 后重试(编辑对应插件目录的 otto-plugin.json)。\\n`,\n )\n return 1\n }\n return undefined\n }\n\n const verdict = evaluatePluginTrust(plugin)\n if (verdict.gated) {\n process.stderr.write(\n `otto: 插件 \"${plugin.id}\" 声明了 \\`otto ${commandName}\\` 命令但尚未被信任。\\n` +\n `请先在 TUI 内运行 \\`/trust plugin ${plugin.id}\\`(或 \\`/extension\\` 信任该插件)后重试。\\n`,\n )\n return 1\n }\n\n const binPath = resolvePluginCliBin(plugin)\n if (!binPath) {\n process.stderr.write(\n `otto: 插件 \"${plugin.id}\" 声明了 \\`contributes.cli\\` 但找不到可执行入口,` +\n `请检查其 manifest 的 bin 字段或重新安装该插件。\\n`,\n )\n return 1\n }\n\n try {\n return await spawnPluginCli(binPath, args, plugin.dir)\n } catch (error) {\n process.stderr.write(`otto: 插件 \"${plugin.id}\" 的 CLI 命令执行失败:${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n\n/**\n * TUI 残影根因修复(2026-07-13,见 docs/review/findings-backlog.md「TUI 残影回归」):\n * `setLogLevel` 只够得着宿主进程内 `@x-otto/shared` 这一份 logger 单例。插件(如\n * `extensions/plugin-otto-wire-protocols`)经 esbuild 打包(`module-loader.ts`),`@x-otto/shared`\n * 的 logger.ts 被整体复制进每个插件 bundle,形成独立的第二份 pino 单例——宿主\n * `setLogLevel` 的调用够不着它。bundle 内 `LOG_LEVEL` 常量读的是同一个 `process.env`\n * (`packages/env/src/index.ts:120`,未设时非 production 默认 trace),是唯一能跨 bundle\n * 生效的静默通道。interactive 模式下必须在插件加载之前同步写 env,插件 bundle 之后\n * require() 时读到的已是静默级别——已用真实 PTY 复现验证有效(`OTTO_LOG_LEVEL=fatal`\n * 后终端残影从 5 份降为 0,与无泄漏基线逐字节一致)。\n * `--verbose` 时用户显式要 debug 级日志(含插件),不静默——与 `cliLogLevel` 判断保持一致。\n * 已设置 `OTTO_LOG_LEVEL` 时尊重用户/环境显式配置,不覆盖。\n * 抽成纯函数(不直接读写 `process.env`)便于单测钉住行为。\n */\nexport function resolvePluginBundleLogLevelSilencing(\n options: Pick<import('./types').CLIOptions, 'mode' | 'verbose'>,\n currentEnv: string | undefined,\n): string | undefined {\n if (options.mode === 'interactive' && !options.verbose && !currentEnv) return 'fatal'\n return currentEnv\n}\n\n/**\n * RFC-185 D1:stderr 通道关闭决策(TUI 残影架构级根治,级别压制 → 通道关闭)。\n * interactive 模式下设 `OTTO_LOG_STDERR='0'`——`createTransportTargets`(宿主单例与\n * 每个插件 bundle 内的副本,调用时直读 process.env)据此不再把 stderr 加入 targets,\n * 日志改落文件(D2 兜底 `~/.otto/logs/otto.log`)。\n *\n * 与 `resolvePluginBundleLogLevelSilencing`(P0 级别压制)的两点不同:\n * - **`--verbose` 不豁免**:verbose 的语义是\"要更多日志\",不是\"要日志打进 TUI 画面\"\n * ——通道关闭后 debug 级日志完整进文件,无信息损失;而 P0 的级别压制会吞日志所以\n * 必须豁免 verbose。\n * - 同款约定:用户显式设置 `OTTO_LOG_STDERR`(如 =1 诊断逃生门)时尊重不覆盖。\n * 必须在首次 `createLogger`(root logger 创建 → pino.transport 建 worker)之前调用——\n * 顺序不变量由 cli-logger-order.test.ts 钉住。\n */\nexport function resolveStderrSilencing(\n options: Pick<import('./types').CLIOptions, 'mode'>,\n currentEnv: string | undefined,\n): string | undefined {\n if (options.mode === 'interactive' && !currentEnv) return '0'\n return currentEnv\n}\n\n/**\n * 重启陪跑 spawn 参数构建(2026-07-13,见 `otto --continue 64e33033...` 卡住排查):\n * `baseArgs`(= 当前进程的 `process.argv.slice(1)`)在本进程本身就是通过\n * `otto --continue <old>` 或更早一轮重启链启动时,已经带着 `--continue <old>`——\n * 若直接无条件 `[...baseArgs, '--continue', restartSessionId]` 追加,子进程 argv 会\n * 出现重复的 `--continue` flag(值不同或相同均会累积)。虽然 `parse-cli.ts` 的\n * `OPTION_LOOKUP` 按后出现者覆盖前者、行为上无害,但会随重启链深度线性累积一串\n * 废弃 flag,污染 argv、拖慢排查、掩盖真实参数。此处先剥离已存在的 `--continue`\n * 及其紧跟的值,再追加本次的新值,保证子进程 argv 中 `--continue` 恒只出现一次。\n * 纯函数(不读写 `process.argv`),便于单测钉住去重行为。\n */\nexport function buildRestartChildArgs(\n baseArgs: readonly string[],\n restartSessionId: string,\n extra: {\n sessionUrl?: string\n sessionToken?: string\n memoryRestartGrace?: boolean\n resumePrompt?: string\n /**\n * RFC-196 M3(链条压平):本进程的 `process.execArgv`(含 wrapper 注入的\n * `--max-old-space-size`)与代际计数。execArgv 前置到 args 头部(V8 flag 必须\n * 位于脚本路径之前)——子进程 `bin/otto.js` 的 `alreadySet` 检测命中,跳过\n * 二次 respawn,消掉每代的 wrapper 进程(净增 2→1)。注意 execArgv 继承的是\n * 代际 0 wrapper 解析的值:重启链中途改 OTTO_MAX_OLD_SPACE_MB 不生效(可接受,\n * 文档化——改堆上限需要完整退出重开)。\n */\n execArgv?: readonly string[]\n /** 子进程的重启代际(本进程代际+1)。展示/告警用,不驱动行为分支。 */\n generation?: number\n } = {},\n): string[] {\n const stripped: string[] = []\n for (let i = 0; i < baseArgs.length; i++) {\n // 剥离会随重启链累积的 flag:--continue(本函数每次重新追加)与\n // --restart-generation(每次以新代际重新追加,防重复累积——同 --continue 模式)。\n if (baseArgs[i] === '--continue') {\n i++ // 跳过紧跟的值\n continue\n }\n if (baseArgs[i]!.startsWith('--restart-generation=')) continue\n stripped.push(baseArgs[i]!)\n }\n return [\n ...(extra.execArgv ?? []),\n ...stripped,\n '--continue', restartSessionId,\n ...(extra.generation !== undefined ? [`--restart-generation=${extra.generation}`] : []),\n ...(extra.sessionUrl ? ['--session-url', extra.sessionUrl] : []),\n ...(extra.sessionToken ? ['--session-token', extra.sessionToken] : []),\n ...(extra.memoryRestartGrace ? ['--memory-restart-grace'] : []),\n ...(extra.resumePrompt ? ['--resume-prompt', extra.resumePrompt] : []),\n ]\n}\n\nexport async function runCli(): Promise<number> {\n // restart handoff guard — when parent spawns a child with stdio:'inherit'\n // and immediately process.exit(0), the child's Ink hooks may try setRawMode on\n // a briefly unstable fd (macOS). Catch EIO to prevent the child crashing.\n process.stdin.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EIO') return\n })\n\n const { options, subCommand, subCommandArgs, unresolvedCommandName, unresolvedCommandArgs } =\n parseCLI(process.argv)\n\n // RFC-345 M1:把 --shadow flag 翻译成 OTTO_SHADOW env,必须在任何 handler 分派与\n // createApp 之前——这样①子命令 handler 也进入影子态;②env 经 buildAllowedEnv 的\n // OTTO_ 前缀白名单自动传染全部子进程/worker(零额外接线)。这是\"flag→env 翻译层\",\n // 不是\"业务判断 shadow\",不违反 §D1 白名单(白名单约束的是消费该开关的位置)。\n if (options.shadow) process.env['OTTO_SHADOW'] = '1'\n\n const cliLogLevel: LogLevel = options.verbose\n ? 'debug'\n : options.mode === 'interactive'\n ? 'fatal'\n : options.quiet\n ? 'error'\n : 'info'\n setLogLevel(cliLogLevel)\n\n const resolvedPluginLogLevel = resolvePluginBundleLogLevelSilencing(options, process.env['OTTO_LOG_LEVEL'])\n if (resolvedPluginLogLevel) process.env['OTTO_LOG_LEVEL'] = resolvedPluginLogLevel\n\n const resolvedStderrGate = resolveStderrSilencing(options, process.env['OTTO_LOG_STDERR'])\n if (resolvedStderrGate) process.env['OTTO_LOG_STDERR'] = resolvedStderrGate\n\n // RFC-324:首条可能被用户看到的输出前,flush env 非法值 warning 到受控 sink。\n // interactive → stderr 已关(OTTO_LOG_STDERR=0),warning 落 LOG_FILE 或丢弃;\n // 非 interactive → stderr 安全。用 arrow 包一层绑定 this(R6)。\n if (process.env['OTTO_LOG_STDERR'] !== '0') {\n flushEnvWarnings((msg) => process.stderr.write(`${msg}\\n`))\n } else {\n flushEnvWarnings(() => {}) // 丢弃——interactive 模式 stderr 关闭\n }\n // 兜底:handler 动态 import 可能引入新的顶层 normalizeEnv 调用,产生新 warning。\n // beforeExit 在事件循环空闲、进程退出前触发,覆盖此场景。\n process.on('beforeExit', () => {\n if (process.env['OTTO_LOG_STDERR'] !== '0') {\n flushEnvWarnings((msg) => process.stderr.write(`${msg}\\n`))\n }\n })\n\n const subArgs = subCommandArgs.split(' ').filter(Boolean)\n const handler = SUB_COMMANDS.get(subCommand ?? '')\n if (handler) {\n const ctx: SubCommandContext = {\n args: subArgs,\n options,\n stdout: (s) => process.stdout.write(s),\n stderr: (s) => process.stderr.write(s),\n }\n return handler(ctx)\n }\n\n // RFC-112 D2/D3/D4:插件顶层 CLI 命令透传——内建命令未命中时,查已安装插件是否\n // 声明了同名 `contributes.cli`。命中则走信任门检查 → 透传执行;未命中任何插件命令名\n // 则什么都不做,让下方既有逻辑把该参数当作 prompt 处理(parseCLI 已经算好了\n // options.prompt,本分支不改变那条回退路径)。\n if (unresolvedCommandName) {\n const exitCode = await tryDispatchPluginCli(unresolvedCommandName, unresolvedCommandArgs ?? [], options)\n if (exitCode !== undefined) {\n return exitCode\n }\n }\n\n // `otto -h`/`--help` 打印纯文本帮助并直接退出——不进入 interactive 模式。\n // otto 命令本身与 TUI(@x-otto/tui,RFC-128 builtin scope 的渲染层)是独立关系:\n // TUI 是随 otto 分发的默认渲染前端,但 `--help` 属于命令行本身的契约(POSIX 惯例:\n // -h/--help 应立即打印帮助并退出,不应有任何其他副作用),不应绑死在 TUI 启动路径\n // 上——否则 TUI 渲染栈的任何问题(未来若可选安装/懒加载)都会连带影响这个最基础的\n // 命令行契约。`getHelpLines()` 已是 plain-text/TUI InfoDialog 共用的同一份文案。\n if (options.showHelp) {\n const { getHelpLines } = await import('./parse-cli')\n process.stdout.write(getHelpLines().join('\\n') + '\\n')\n return 0\n }\n\n if (!options.prompt && options.mode !== 'interactive') {\n process.stderr.write('otto: no prompt provided. Use --help for usage.\\n')\n return 1\n }\n\n // RFC-196 review P0 修复(bc37146b)→ RFC-199 D5 收口:会话存储目录经 @x-otto/env 的\n // OTTO_SESSIONS_DIR 单源派生(见下方 storage.session 装配),锁目录分裂事故的教训\n // 固化在 env 常量注释(OTTO_SESSION_LOCKS_DIR)。\n\n const userIdentity = await loadUserIdentity()\n const sessionUrl = options.sessionUrl ?? userIdentity?.serviceUrl\n const sessionToken = options.sessionToken ?? userIdentity?.token\n\n // RFC-150 D7/M0:App 构造是同步耗时段(实测 ~59ms),单独打点区分于 import/start()。\n // RFC-164 M164-2 D6:memory 远端冲突回调 ref——createApp 将 onMemoryConflict 闭包捕获\n // 此 ref,runInteractiveCommand 在 startTui 返回后设置 ref.current→tui.info.showToast。\n const memoryConflictHandler: { current?: (path: string, conflictPath: string) => void } = {}\n const app = traceStartupPhaseSync('new App() ctor', () =>\n createApp({\n inspect: options.inspect !== undefined || options.mode === 'interactive',\n logger: {\n name: 'otto-cli',\n level: cliLogLevel,\n destination: 'stderr',\n },\n workspaceDir: options.projectDir,\n workspaceId: options.workspaceId,\n projectConfigPath: options.configPath,\n interactive: options.mode === 'interactive',\n shadow: options.shadow ?? false, // RFC-345 M1:与 OTTO_SHADOW 同源(cli.ts 上方已据 flag 写 env)\n modelId: options.model,\n userIdentity: userIdentity ?? undefined,\n ...(options.maxToolTurns !== undefined ? { maxToolTurns: options.maxToolTurns } : {}),\n ...(options.maxToolTurnExtensions !== undefined\n ? { maxToolTurnExtensions: options.maxToolTurnExtensions }\n : {}),\n ...(options.promptTokenBudget !== undefined\n ? { promptOutputTokenBudget: options.promptTokenBudget }\n : {}),\n ...(options.promptTimeBudgetMin !== undefined\n ? { promptWallClockBudgetMs: options.promptTimeBudgetMin * 60_000 }\n : {}),\n ...(options.sessionCostBudget !== undefined\n ? { sessionCostBudgetUSD: options.sessionCostBudget }\n : {}),\n // RFC-204 D4:--no-stall-detection 独立生效(false,优先级最高);否则仅当\n // window/threshold 两者都提供时才组装自定义对象(缺一则维持 undefined,走\n // ENGINE_DEFAULTS 缺省——不允许\"只给窗口不给阈值\"这种半配置的静默行为)。\n ...(options.noStallDetection\n ? { stallDetection: false as const }\n : options.stallWindowTurns !== undefined && options.stallRepeatThreshold !== undefined\n ? {\n stallDetection: {\n windowTurns: options.stallWindowTurns,\n repeatThreshold: options.stallRepeatThreshold,\n },\n }\n : {}),\n storage: sessionUrl && sessionToken\n ? {\n session: { url: sessionUrl },\n remote: {\n getAuth: async () => ({ token: sessionToken }),\n },\n }\n : {\n session: { dir: OTTO_SESSIONS_DIR }, // RFC-199 D5:经 @x-otto/env 单源(= OTTO_HOME/sessions)\n },\n onMemoryConflict: (path, conflictPath) => memoryConflictHandler.current?.(path, conflictPath),\n }),\n )\n\n await traceStartupPhase('app.start()', () => app.start())\n\n // RFC-277 M4:settings 已加载完成(app.start() 内含 settings.load()),此刻\n // `theme_overrides.markdown` 已过 schema 校验——接入 Markdown ANSI 渲染 seam。\n // 覆盖交互式(interactive)与非交互式(run-modes/printMessages)两条路径,因为两者\n // 都消费同一个 renderMarkdownToLines 单一真源。\n wireMarkdownThemeOverrides(app)\n\n // RFC-085 M4 Layer 3:process.on('exit') 硬兜底 → killAllSync(进程组 SIGKILL)。\n // 幂等;不覆盖信号退出(C2),靠下方 Layer 2 把信号转 process.exit 间接触发。\n installExitReaper(globalProcessRuntime)\n\n // RFC-085 M3 Layer 2:全局 SIGTERM/SIGHUP handler\n // 不装 SIGINT——TUI raw 模式 Ctrl+C 是字节 0x03 走两段式 onInterrupt,不产生 SIGINT\n // 进 chaperone 前必须 removeListener(H1 回归护栏,见下方 restartSessionId 块)\n let shuttingDown = false\n const onShutdownSignal = () => {\n if (shuttingDown) {\n process.exit(1) // 第二次信号:立即退出\n }\n shuttingDown = true\n void Promise.race([\n app.stop(),\n new Promise<void>((r) => setTimeout(r, 10_000)),\n ]).finally(() => {\n // RFC-291 legacy fallback:默认 scoped viewport 已由 TUI 自行释放;仅旧 scrollback\n // 路径在 app.stop 结算后清屏,避免与仍在渲染的 Ink 竞态。\n if (options.mode === 'interactive') clearTerminalOnExit()\n process.exit(1)\n })\n }\n process.on('SIGTERM', onShutdownSignal)\n process.on('SIGHUP', onShutdownSignal)\n\n let restartSessionId: string | undefined\n // RFC-115 M3:本次重启是否由内存治理触发——决定 spawn 子进程时是否附加\n // --memory-restart-grace(供子进程起算 D5 宽限期,区别于用户手动 --continue)。\n let restartMemoryTriggered = false\n // RFC-115 M4:重启前会话正忙时的续跑原文——决定 spawn 子进程时是否附加 --resume-prompt。\n let restartResumePrompt: string | undefined\n\n // RFC-335 T335-07 S4-b:--attach <sessionId>——不 createApp、不进本地 session/store,\n // 直接连 `otto serve` 并起 attach TUI(只读预览 + Chat.query;WS/写租约归二期)。放\n // default mode 走之前,不进 workspace trust / memory 等本地生命周期。\n if (options.attachSession) {\n const baseUrl = process.env['OTTO_SERVICE_URL'] ?? 'http://127.0.0.1:8417'\n const { runAttach } = await import('./commands/run-attach')\n await runAttach({\n sessionId: options.attachSession,\n baseUrl,\n token: process.env['OTTO_SERVICE_TOKEN'],\n forceWriteLease: options.forceWriteLease,\n })\n return 0\n }\n\n try {\n switch (options.mode) {\n case 'print':\n await runPrintCommand(options, app)\n break\n case 'json':\n await runJsonCommand(options, app)\n break\n case 'interactive': {\n // 工作区信任门(doc §8):未信任的真项目先弹\"信任此文件夹吗\",再进 TUI。\n await maybePromptWorkspaceTrust(app)\n const result = await runInteractiveCommand(options, app, memoryConflictHandler)\n restartSessionId = result.restart?.sessionId\n restartMemoryTriggered = Boolean(result.restart?.memoryTriggered)\n restartResumePrompt = result.restart?.resumePrompt\n break\n }\n }\n } finally {\n // P1 fix: tui.lifecycle.stop() → lifecycle.cleanup() has removed the old SIGINT handler.\n // During app.stop() (saveAll/killAll/dispose) the default SIGINT would kill the\n // process, truncating persistence. Re-register a no-op handler so Ctrl+C is harmless.\n const sigintGuard = (): void => {}\n process.on('SIGINT', sigintGuard)\n // 进程级兜底归位:看门狗与它守护的 app.stop() 同处一个 try/finally,\n // app.stop 成功即 clearTimeout——「忘记清除」在结构上不可能。\n // app.stop() 含进程组 SIGKILL grace(~2s)+ saveAll + MCP 收尾,可合法耗时数秒,故给足 10s 余量。\n // 这是交互路径唯一会 process.exit 的兜底点(另一处仅正常退出收口 ↓)。ref(不 unref)以保证\n // app.stop 真卡死时必触发;正常路径 clearTimeout 先到,看门狗不会拖住事件循环。\n const watchdog = setTimeout(() => {\n try { process.stdout.write('\\x1b[?25h') } catch { /* best-effort cursor restore */ }\n // RFC-291 legacy fallback:默认 scoped viewport 已由 TUI 自行释放;旧 scrollback 路径兜底清屏。\n if (options.mode === 'interactive') clearTerminalOnExit()\n process.exit(0)\n }, 10_000)\n try {\n await app.stop()\n } finally {\n clearTimeout(watchdog)\n process.removeListener('SIGINT', sigintGuard)\n }\n }\n\n if (restartSessionId) {\n // RFC-085 H1 护栏:进 chaperone 前拆除全局 SIGTERM/SIGHUP handler\n // chaperone 已安装独立 signal guard(吞信号保护子进程),\n // 全局 handler 若留驻会双触发 → SIGTERM 杀父 → 子 orphaned →「重启打不了字」\n process.removeListener('SIGTERM', onShutdownSignal)\n process.removeListener('SIGHUP', onShutdownSignal)\n\n const sessionUrl = options.sessionUrl\n const sessionToken = options.sessionToken\n // parent 退出关闭 fd 0 → 子进程 stdin 断链只是表象。真因是 parent 退出后子进程的\n // 进程组变为 orphaned+background → Ink setRawMode(true) 触发内核 SIGTTOU(默认 STOP 信号),\n // 子进程被挂起(SIGSTOP),表现为「重进后无法输入」。\n //\n // 修复:parent 不退出,保持存活(不 unref child)。parent+child 同属一个进程组且 parent 在\n // shell 前台组内 → 进程组非 orphaned → child 的 tcsetattr 正常执行。child 退出时 parent\n // 传播其退出码后自然排干事件循环退出。\n // 陪跑不变式·向量②:父进程在陪跑期间必须活过子进程,否则子进程\n // orphaned+background → SIGTTOU/SIGSTOP →「无法输入」(同 Bug A 的孪生触发器)。父子 stdio:inherit\n // 同进程组:子会话在**非 raw 窗口**(! shell-out / pager / 启动·关停瞬间)按 Ctrl+C 会向整个前台组\n // 发 SIGINT,无守卫的父进程默认动作即被终止。故陪跑期吞掉 SIGINT/SIGTERM——子进程自有信号语义\n // (raw 模式读字节 / 自身 handler),父只负责存活并传播退出码。child 退出后摘除守卫,自然排干退出。\n const chaperoneSignalGuard = (): void => {}\n // 陪跑不变式·向量③:陪跑期父进程未捕获异常/拒绝若使父静默退出,子进程同样\n // orphaned+background → SIGTTOU/SIGSTOP →「无法输入」。仅在陪跑窗口内(spawn 后、child.exit 前)\n // 装 handler 并在 child.exit 摘除;**不静默吞**——打印错误 + 显式终止子进程(避免孤儿)+ 非零退出。\n // uncaughtException 后进程状态未定义,按 Node 约定只做同步清理即退出。失败模式从「静默挂死、终端\n // 打不了字」降级为「清晰报错 + 子进程干净终止」;窗口极窄(父 spawn 后几乎只等 child.exit)。\n let child: ReturnType<typeof spawn> | undefined\n const chaperoneCrashGuard = (err: unknown): void => {\n try {\n console.error(`\\n[otto] 重启陪跑期父进程异常,终止子会话以避免孤儿挂起:${err instanceof Error ? (err.stack ?? err.message) : String(err)}`)\n } catch { /* 日志尽力而为 */ }\n try { child?.kill('SIGTERM') } catch { /* child 可能已退出 */ }\n process.exit(1)\n }\n process.on('SIGINT', chaperoneSignalGuard)\n process.on('SIGTERM', chaperoneSignalGuard)\n process.on('uncaughtException', chaperoneCrashGuard)\n process.on('unhandledRejection', chaperoneCrashGuard)\n child = spawn(process.execPath, buildRestartChildArgs(\n process.argv.slice(1),\n restartSessionId,\n {\n sessionUrl,\n sessionToken,\n // RFC-115 M3(P1-1 修正):仅内存治理触发的重启附加此标志,供子进程起算 D5 宽限期——\n // 用户手动 /exit --restart 或 /upgrade --force 触发的重启不附加,不应被宽限期抑制。\n memoryRestartGrace: restartMemoryTriggered,\n // RFC-115 M4:重启前会话正忙时的续跑原文——spawn 用数组形式传参(非 shell 字符串),\n // 换行/特殊字符不经 shell 解析,无注入风险;子进程消费后随进程退出自然消失,不持久化。\n resumePrompt: restartResumePrompt,\n // RFC-196 M3(链条压平):本进程 execArgv(含 wrapper 注入的 --max-old-space-size)\n // 前置到子进程 args——子进程 bin/otto.js 的 alreadySet 检测命中,跳过二次 respawn,\n // 每次重启净增进程从 2(wrapper+actual)降为 1(actual only)。代际计数 +1 供\n // ≥3 代告警与 otto ps GEN 列(见 interactive.ts 启动告警)。\n execArgv: process.execArgv,\n generation: (options.restartGeneration ?? 0) + 1,\n },\n // 自重新生成(--continue)——必须继承完整 process.env\n ), {\n stdio: 'inherit',\n env: { ...process.env },\n })\n child.on('exit', (code, signal) => {\n process.removeListener('SIGINT', chaperoneSignalGuard)\n process.removeListener('SIGTERM', chaperoneSignalGuard)\n process.removeListener('uncaughtException', chaperoneCrashGuard)\n process.removeListener('unhandledRejection', chaperoneCrashGuard)\n // 陪跑终点硬退出(2026-07-14,生产 5 个 ppid=1 僵尸陪跑进程 inspector 实锤):此前依赖\n // 「自然排干事件循环退出」,但父进程若残留任何 ref 住事件循环的孤儿句柄(实锤案例:\n // plugin worker 加载失败路径泄漏的 worker_threads kPublicPort),child 退出后父进程\n // 永不排干——挂 launchd 下常驻、占内存。child 已退出 → 父进程再无任何职责;自身\n // app.stop() 早在 spawn 前完成,stdio:'inherit' 下无待 flush 的管道缓冲,TTY 写为同步\n // ——process.exit 无截断风险。与 interactive 正常路径末尾的硬退出(下方)语义对齐。\n process.exit(code ?? (signal ? 1 : 0))\n })\n // 不 unref、不提前退出——父进程陪跑直到子进程退出(child.exit 里硬退出收口)\n return 0\n }\n\n // fix: /exit·Ctrl+C 退不掉——交互模式(TUI)退出后显式硬退出,不再依赖事件循环自然排干。inspector 的常驻 trace tail\n // 等孤儿句柄会吊住事件循环,导致 /exit、Ctrl+C 后进程不退。此处 app.stop() 已 await 完成全部关键收尾\n // (saveAll/MCP/store close),TTY 输出为同步写、process.exit 不截断。app.stop() 自身卡死的场景另有\n // 上方 cli.ts app.stop 看门狗(~10s,> 进程组 SIGKILL grace 2s + saveAll)兜底。\n // print/json 模式刻意不在此强退——避免管道下 stdout 缓冲被 process.exit 截断。\n if (options.mode === 'interactive') {\n // RFC-291 legacy fallback:默认 scoped viewport 已由 TUI 自行释放,终端回到启动 otto 前;\n // 仅 OTTO_TUI_SCOPED_VIEWPORT=0 的旧 scrollback 路径需要最后一刻全局清屏。\n clearTerminalOnExit()\n process.exit(process.exitCode ?? 0)\n }\n\n return 0\n}\n","import { runCli } from './cli'\n\nrunCli().then((code) => {\n if (code !== 0) {\n process.exitCode = code\n }\n}).catch((err) => {\n process.stderr.write(`otto: ${err instanceof Error ? err.message : String(err)}\\n`)\n process.exitCode = 1\n})\n\nexport { runCli }\n"],"mappings":"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"}
|