mocode-ai 1.4.2 → 1.4.3

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.
Files changed (47) hide show
  1. package/README.md +13 -1
  2. package/dist/agent/core.js +14 -936
  3. package/dist/agent/index.js +37 -13
  4. package/dist/agent/model-turn.js +218 -0
  5. package/dist/agent/pipeline.js +18 -0
  6. package/dist/agent/run-contracts.js +1 -0
  7. package/dist/agent/run-coordinator.js +758 -0
  8. package/dist/agent/runtime-context.js +118 -24
  9. package/dist/agent/spawn.js +11 -7
  10. package/dist/agent/stages/context-trimmer.js +63 -0
  11. package/dist/agent/stages/contracts.js +12 -0
  12. package/dist/agent/stages/history-manager.js +178 -0
  13. package/dist/agent/stages/legacy-adapters.js +19 -0
  14. package/dist/agent/stages/model-runner.js +29 -0
  15. package/dist/agent/stages/run-policy.js +73 -0
  16. package/dist/agent/stages/tool-dispatcher.js +341 -0
  17. package/dist/agent/tool-helpers.js +12 -12
  18. package/dist/agent/tool-turn.js +87 -0
  19. package/dist/agent/trace-state.js +97 -101
  20. package/dist/agent/turn-lifecycle.js +110 -0
  21. package/dist/config/index.js +14 -0
  22. package/dist/host/stdio.js +101 -40
  23. package/dist/llm/index.js +51 -35
  24. package/dist/llm/providers/anthropic.js +16 -10
  25. package/dist/llm/runtime.js +1 -0
  26. package/dist/permissions/index.js +21 -5
  27. package/dist/repl/commands/compact.js +2 -2
  28. package/dist/repl/commands/session.js +3 -12
  29. package/dist/repl/message-format.js +5 -0
  30. package/dist/repl/runtime.js +95 -55
  31. package/dist/rollback/index.js +29 -624
  32. package/dist/rollback/store.js +593 -0
  33. package/dist/runtime/index.js +1 -0
  34. package/dist/runtime/runtime.js +307 -0
  35. package/dist/session/compact.js +22 -14
  36. package/dist/session/index.js +1 -0
  37. package/dist/session/persist.js +10 -146
  38. package/dist/session/scheduler.js +28 -16
  39. package/dist/session/state.js +16 -12
  40. package/dist/session/store.js +218 -0
  41. package/dist/session/trace.js +5 -15
  42. package/dist/tools/policy.js +19 -15
  43. package/dist/tools/registry.js +21 -229
  44. package/dist/tools/router.js +5 -3
  45. package/dist/tools/tool-runtime.js +267 -0
  46. package/dist/ui/layout-internal/content-write.js +4 -0
  47. package/package.json +7 -3
@@ -4,6 +4,7 @@ import { config, isModelConfigured, isMemoryEnabled, buildBasePrompt, getPlanMod
4
4
  import { t } from '../i18n/index.js';
5
5
  import { listPresets, migrateCurrentToPreset } from '../config/presets.js';
6
6
  import { runAgent } from '../agent/index.js';
7
+ import { defaultRuntime } from '../runtime/index.js';
7
8
  import { getAgentMode, setAgentMode, onModeChange } from '../agent/mode.js';
8
9
  import { sendState } from '../pet/bridge.js';
9
10
  import { setSandboxRoot } from '../sandbox/root.js';
@@ -19,12 +20,10 @@ import '../tools/builtins/index.js';
19
20
  import { initializeAllMcp, getMcpTools, closeAllMcp } from '../mcp/index.js';
20
21
  import { refreshChatTools, classifyChatError } from '../llm/index.js';
21
22
  import { renderChip } from '../attachments/image.js';
22
- import { contextState, newSessionId, saveSession, loadSession, appendCurrentSessionRuntimeEvent, } from '../session/index.js';
23
- import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, getCurrentTurnId, } from '../rollback/index.js';
23
+ import { contextState, appendCurrentSessionRuntimeEvent } from '../session/index.js';
24
24
  import { effectiveSystemPrompt } from '../skills/index.js';
25
25
  import { clearSkillActivation } from '../skills/activation.js';
26
26
  import { buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, } from '../memory/index.js';
27
- import { setCurrentSessionId } from '../session/state.js';
28
27
  import { collectQueryHistory } from '../session/query-history.js';
29
28
  import { ToolPolicyController } from '../tools/policy.js';
30
29
  import { routeToolGroups } from '../tools/router.js';
@@ -165,6 +164,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
165
164
  // 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
166
165
  // 纯边界记录(不 chdir),jail.ts 内部 resolve。子 agent 同进程继承全局 root。
167
166
  setSandboxRoot(sandboxRootOverride ?? config.sandboxRoot ?? process.cwd());
167
+ const runtime = defaultRuntime;
168
+ await runtime.start();
169
+ const runtimeContext = runtime.context;
168
170
  // MCP 在工具表和 LLM schema 创建前连接;失败的单个 server 只给提示,不阻断 REPL。
169
171
  const mcpReport = await initializeAllMcp();
170
172
  registerToolsExtension('mcp', getMcpTools());
@@ -172,8 +174,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
172
174
  // --resume:读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
173
175
  let currentSessionId = sessionId;
174
176
  if (!currentSessionId)
175
- currentSessionId = newSessionId(); // 新会话立即分配 ID,确保 prompt 里有正确路径
176
- setCurrentSessionId(currentSessionId, process.cwd()); // 必须在 buildBasePrompt() 之前,让 prompt 能读到 sessionId
177
+ currentSessionId = runtime.session.create();
178
+ else
179
+ runtime.session.resume(currentSessionId);
177
180
  // 构造系统提示:auto 用 base;plan 在 base 后追加按当前开关现拼的 plan suffix。
178
181
  // 切模式时 applyMode 重算 history[0](history[0] 恒 system,compaction 保它,不破坏)。
179
182
  // 活跃 plan 摘要拼在 memory 段后(systemPrompt 的尾段),todo 工具变更后 listener 重写 history[0]。
@@ -194,10 +197,6 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
194
197
  if (initialHistory && initialHistory.length && history[0]?.role === 'system') {
195
198
  history[0] = { role: 'system', content: buildSystemMessage(false) };
196
199
  }
197
- if (sessionId && initialHistory && initialHistory.length) {
198
- if (!loadSnapshots(sessionId))
199
- rebuildFromHistory(history);
200
- }
201
200
  // 反思 cadence 计数:每 reflectEveryN 轮 fire-and-forget 一次后台反思 pass。
202
201
  let turnCount = 0;
203
202
  // 本轮 token 累计:runAgent 返回后写入,供底栏模式 chip 右边显示。undefined=无实测
@@ -207,7 +206,11 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
207
206
  model: config.model,
208
207
  baseURL: config.baseURL,
209
208
  cwd: process.cwd(),
210
- tools: new ToolPolicyController({ groups: lastToolGroups, maxExpansions: 0 })
209
+ tools: new ToolPolicyController({
210
+ groups: lastToolGroups,
211
+ maxExpansions: 0,
212
+ tools: runtimeContext.toolRuntime.tools,
213
+ })
211
214
  .snapshot(getAgentMode() === 'plan')
212
215
  .tools.map((tool) => tool.function.name)
213
216
  .join(' · '),
@@ -325,7 +328,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
325
328
  * 撤销文件 / 只撤销消息)。选轮 + 方式菜单均走 raw mode。预填经 pendingPrefill 注入下轮 INPUT。
326
329
  */
327
330
  const rollbackFlow = async () => {
328
- const turnList = listTurns();
331
+ const turnList = runtime.rollback.list();
329
332
  if (turnList.length < 1) {
330
333
  layout.contentWrite(`${ui.dim}(没有可回滚的轮次)${ui.reset}\n`);
331
334
  return;
@@ -348,7 +351,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
348
351
  return; // Esc 取消
349
352
  // picked(0-based)= 第 (picked+1) 轮:删该轮及之后(planRollback(picked) 保 1..picked),预填该轮 user 输入
350
353
  const prefillText = userTexts[picked] ?? '';
351
- const plan = planRollback(picked, history);
354
+ const plan = runtime.rollback.plan(picked, history);
352
355
  // 清屏(擦选轮菜单 + /rollback 回显)+ 复位 lastView(dim 空),给文件询问一个干净、resize 安全的画面
353
356
  layout.clearContent();
354
357
  layout.paintInput({
@@ -379,14 +382,13 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
379
382
  for (const c of revertable)
380
383
  revertPaths.add(c.path);
381
384
  }
382
- const rolledBackFromTurnId = getCurrentTurnId();
383
- const turnCountBeforeRollback = listTurns().length;
384
- const rollbackResult = applyRollback(plan, history, revertPaths);
385
+ const rolledBackFromTurnId = runtimeContext.getCurrentTurnId();
386
+ const turnCountBeforeRollback = runtime.rollback.list().length;
387
+ const rollbackResult = runtime.rollback.apply(plan, history, revertPaths);
385
388
  // 被撤销轮次的最终 route 无法再作为可靠 continuation 基线;下一真实 turn 重新由 LLM 选择。
386
389
  lastToolGroups = [];
387
390
  if (!currentSessionId)
388
- currentSessionId = newSessionId();
389
- setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
391
+ currentSessionId = runtime.session.create();
390
392
  appendCurrentSessionRuntimeEvent('rollback', {
391
393
  status: 'applied',
392
394
  rolledBackFromTurnId,
@@ -399,12 +401,11 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
399
401
  requestedFileCount: revertPaths.size,
400
402
  }, plan.cutoffTurnId);
401
403
  try {
402
- saveSession(history, currentSessionId, queryHistory);
404
+ runtime.session.save(history, currentSessionId, queryHistory);
403
405
  }
404
406
  catch {
405
407
  // 落盘失败不阻断
406
408
  }
407
- persistSnapshots(currentSessionId);
408
409
  // 复显剩余对话(无提示行),输入框预填该轮 user 输入 → 下轮 Enter 重新跑
409
410
  layout.clearContent();
410
411
  renderHistory(history);
@@ -428,33 +429,23 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
428
429
  * plan 模式传 planMode=true(runAgent 用 planChatTools 只读子集)。返 ok=正常结束(未中断 / 未抛错),
429
430
  * 供调用方决定是否弹审批面板。execute 轮的合成输入也走这里。
430
431
  *
431
- * 多模态:把 pendingAttachments flush userInput:有图时构造 ContentPart[] 数组;
432
- * 无图时保持 string(向后兼容,且 messageTokens estimateTokens 不走 IMAGE_TOKEN_COST)。
432
+ * routedCancel=true 表示 LLM 预路由阶段被 Ctrl+C 撤回(路由还没跑完、runAgent 未启动):
433
+ * 不报错不落盘,主循环擦气泡 + pendingPrefill 回填输入框(与 500ms 撤回窗口同语义)。
434
+ *
435
+ * 多模态:路由完成后才把 pendingAttachments flush 进 userInput(延迟到路由之后——路由阶段
436
+ * 撤回保留附件且 side channel 不错位);有图时构造 ContentPart[] 数组,无图时保持 string
437
+ * (向后兼容,且 messageTokens 走 estimateTokens 不走 IMAGE_TOKEN_COST)。
433
438
  * side channel 记录本轮 msg 在 history 的 index → attachments,供 renderHistory 复显文件名。
434
439
  */
435
440
  const runTurn = async (input, planMode, placeholder, inheritedToolPolicy) => {
436
- const imgs = pendingAttachments;
437
- pendingAttachments = []; // 入口即清,即使后续抛错也不留陈旧附件
438
- const userInput = imgs.length === 0
439
- ? input
440
- : [
441
- { type: 'text', text: input },
442
- // detail 故意不设(留 undefined,JSON.stringify 时被丢弃):OpenAI 认 'auto'/'low'/'high',
443
- // 但 MiniMax 只认 'low'/'default'/'high'——'auto' 不是合法枚举值,某些后端会 400。
444
- // 不传 detail 让各 provider 用自己的默认值(OpenAI 默认视为 auto,MiniMax 默认 default),
445
- // 是唯一在两边都不出错的写法。
446
- ...imgs.map((a) => ({
447
- type: 'image_url',
448
- image_url: { url: a.dataUrl },
449
- })),
450
- ];
451
- const msgIndex = history.length; // runAgent push 后 = 这个 index
452
- if (imgs.length)
453
- messageAttachments.set(msgIndex, imgs);
454
441
  let ok = false;
442
+ let routedCancel = false;
455
443
  let signal;
456
444
  let toolPolicy = inheritedToolPolicy;
457
445
  let initialToolRoute;
446
+ // 继承 toolPolicy 的合成执行轮没有独立路由;真实用户轮在 await routeToolGroups 完成后置 true。
447
+ // 路由完成前 Ctrl+C = 撤回本轮消息(routedCancel),之后才是"中断 agent 执行"(原 abort 语义)。
448
+ let routingDone = toolPolicy != null;
458
449
  try {
459
450
  signal = startRunningListener(placeholder);
460
451
  // 入口设定本轮初始模式(合成执行轮传 false→auto;用户轮传当前 mode)。
@@ -466,19 +457,34 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
466
457
  clearSkillActivation();
467
458
  // 每个真实用户 turn 强制由轻量 LLM 选择最小工具簇。plan 审批后的合成执行轮
468
459
  // 传 inheritedToolPolicy,复用同一个 controller/version,不做第二次路由。
460
+ // 路由 await 期间 Ctrl+C(signal.aborted=true)→ 撤回本轮。两条路径都覆盖:
461
+ // (a) routeToolGroups 抛 abort(流读取中途 abort)→ 下方 catch 分支;
462
+ // (b) OpenAI 流式在响应接近读完时 abort 会正常 resolve 而非 reject
463
+ // (trace 实测:Ctrl+C 后 8ms 路由仍返回)→ 返回后必须显式查 signal.aborted。
469
464
  if (!toolPolicy) {
470
465
  const previousGroups = [...lastToolGroups];
471
466
  const decision = await routeToolGroups({
472
467
  input,
473
468
  previousGroups,
474
469
  planMode,
475
- attachmentNames: imgs.map((image) => image.name),
470
+ attachmentNames: pendingAttachments.map((image) => image.name),
476
471
  signal,
472
+ transport: runtimeContext.modelTransport,
473
+ tools: runtimeContext.toolRuntime.tools,
477
474
  });
475
+ // 路由期间用户按了 Ctrl+C(signal 已 abort)→ 撤回,丢弃路由结果。
476
+ // 关键:即使 routeToolGroups 因流已读完而"成功"返回,也必须据此撤回,
477
+ // 否则 routingDone 被误置 true → 进 runAgent → 普通 abort(不回填)。
478
+ if (signal?.aborted) {
479
+ routedCancel = true;
480
+ return { ok: false, toolPolicy: undefined, routedCancel: true };
481
+ }
482
+ routingDone = true; // 路由完成且未被中断 → Ctrl+C 回归"中断执行"语义
478
483
  toolPolicy = new ToolPolicyController({
479
484
  groups: decision.groups,
480
485
  reason: decision.reason,
481
486
  confidence: decision.confidence,
487
+ tools: runtimeContext.toolRuntime.tools,
482
488
  });
483
489
  lastToolGroups = toolPolicy.groupNames;
484
490
  initialToolRoute = {
@@ -493,12 +499,32 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
493
499
  planMode,
494
500
  };
495
501
  }
502
+ // 路由完成后才 flush 附件:路由阶段撤回(routedCancel)保留 pendingAttachments,
503
+ // 回填后用户再发随消息一起带走;side channel 也不会在未 push user msg 时错写 index。
504
+ const imgs = pendingAttachments;
505
+ pendingAttachments = [];
506
+ const userInput = imgs.length === 0
507
+ ? input
508
+ : [
509
+ { type: 'text', text: input },
510
+ // detail 故意不设(留 undefined,JSON.stringify 时被丢弃):OpenAI 认 'auto'/'low'/'high',
511
+ // 但 MiniMax 只认 'low'/'default'/'high'——'auto' 不是合法枚举值,某些后端会 400。
512
+ // 不传 detail 让各 provider 用自己的默认值(OpenAI 默认视为 auto,MiniMax 默认 default),
513
+ // 是唯一在两边都不出错的写法。
514
+ ...imgs.map((a) => ({
515
+ type: 'image_url',
516
+ image_url: { url: a.dataUrl },
517
+ })),
518
+ ];
519
+ const msgIndex = history.length; // runAgent push 后 = 这个 index
520
+ if (imgs.length)
521
+ messageAttachments.set(msgIndex, imgs);
496
522
  // 运行中每步 chat() 返回后刷新状态行 context 用量条(用 fresh lastUsage / 估算),
497
523
  // 否则整轮冻结在轮首 refreshStatusBase 的值,「执行 grep」时 2k/1000k 不动。
498
524
  const result = await runAgent(history, userInput, signal, () => {
499
525
  refreshStatusBase(history);
500
526
  layout.drawStatusBar();
501
- }, toolPolicy, initialToolRoute);
527
+ }, toolPolicy, initialToolRoute, runtime);
502
528
  if (toolPolicy)
503
529
  lastToolGroups = toolPolicy.groupNames;
504
530
  // 本轮 token 累计(底栏模式 chip 右边显示)。undefined = 后端不开 include_usage。
@@ -507,15 +533,13 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
507
533
  ok = !signal.aborted; // 中断(Ctrl+C)→ runAgent 已还原 history,ok=false 不弹审批
508
534
  // 成功轮次自动落盘(崩溃也保住上一轮);新会话首轮分配 id
509
535
  if (!currentSessionId)
510
- currentSessionId = newSessionId();
511
- setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
536
+ currentSessionId = runtime.session.create();
512
537
  try {
513
- saveSession(history, currentSessionId, queryHistory, lastToolGroups);
538
+ runtime.session.save(history, currentSessionId, queryHistory, lastToolGroups);
514
539
  }
515
540
  catch {
516
541
  // 落盘失败不阻断 REPL
517
542
  }
518
- persistSnapshots(currentSessionId); // 随会话落盘回滚快照(/resume 后仍可撤销)
519
543
  // 后台反思:每 reflectEveryN 轮 fire-and-forget 一次(与下一轮 agent 并发,不阻塞)。
520
544
  // 已有在飞任务 / autoReflect 关 → kickoff 内部自守卫。快照同步取(避免下一轮 mutate history 竞态)。
521
545
  turnCount++;
@@ -527,12 +551,18 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
527
551
  ok = false;
528
552
  if (toolPolicy)
529
553
  lastToolGroups = toolPolicy.groupNames;
554
+ // 路由阶段 Ctrl+C = 撤回本轮消息(非"中断执行"):query 未进 queryHistory、无 user msg
555
+ // 进 history,故不报错不落盘;finally 结算状态栏后早退,主循环擦气泡 + pendingPrefill
556
+ // 回填输入框(与 500ms 撤回窗口同语义)。e 为 routeToolGroups rethrow 的 abort,不消费、不打印。
557
+ if (!routingDone && signal?.aborted) {
558
+ routedCancel = true;
559
+ return { ok: false, toolPolicy: undefined, routedCancel: true };
560
+ }
530
561
  // 请求失败也保存已确认提交的 query,确保立即退出后仍可通过 ↑ 或 resume 找回。
531
562
  if (!currentSessionId)
532
- currentSessionId = newSessionId();
533
- setCurrentSessionId(currentSessionId, process.cwd());
563
+ currentSessionId = runtime.session.create();
534
564
  try {
535
- saveSession(history, currentSessionId, queryHistory, lastToolGroups);
565
+ runtime.session.save(history, currentSessionId, queryHistory, lastToolGroups);
536
566
  }
537
567
  catch {
538
568
  // 落盘失败不覆盖原始请求错误
@@ -578,21 +608,21 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
578
608
  refreshStatusBase(history, lastTurnUsage);
579
609
  layout.drawStatusBar();
580
610
  }
581
- layout.contentWrite('\n'); // 轮次之间空行
582
- return { ok, toolPolicy };
611
+ if (!routedCancel)
612
+ layout.contentWrite('\n'); // 轮次之间空行(路由撤回由主循环 rewindContent 擦除,不补空行)
613
+ return { ok, toolPolicy, routedCancel };
583
614
  };
584
615
  /** 把 picker 选中的会话加载进 REPL(刷 history + 重建 snapshots + 重画)。/resume / /sessions 共用。 */
585
616
  async function resumeFromPick(pick) {
586
617
  if (!pick)
587
618
  return; // Esc / Ctrl+D 取消
588
- const loaded = loadSession(pick.id);
619
+ const loaded = runtime.session.resume(pick.id);
589
620
  if (!loaded || !loaded.history.length) {
590
621
  layout.contentWrite(`${ui.yellow}${t('repl.loadFailed')}${ui.reset}\n`);
591
622
  return;
592
623
  }
593
624
  // Bind before rebuilding the prompt, or it can retain the previous session's notes path.
594
625
  currentSessionId = loaded.id;
595
- setCurrentSessionId(loaded.id, process.cwd());
596
626
  if (loaded.history[0]?.role === 'system') {
597
627
  loaded.history[0] = { role: 'system', content: buildSystemMessage(false) };
598
628
  }
@@ -601,9 +631,6 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
601
631
  queryHistory = loaded.queryHistory ? [...loaded.queryHistory] : queryHistoryFromMessages(loaded.history);
602
632
  lastToolGroups = [...(loaded.lastToolGroups ?? [])];
603
633
  setAgentMode('auto'); // 续接重置为 auto(mode 不落盘;listener 重写 history[0] 回 auto,与 loaded 幂等)
604
- // 读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
605
- if (!loadSnapshots(loaded.id))
606
- rebuildFromHistory(history);
607
634
  contextState.lastUsage = undefined;
608
635
  contextState.lifecycleStats = undefined;
609
636
  contextState.ephemeralText = undefined;
@@ -688,6 +715,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
688
715
  inputLines: input,
689
716
  history,
690
717
  contextState,
718
+ runtime,
691
719
  // 访问器暴露闭包 `let`:命令会写它们(/clear 换 session id、/resume 换整个会话),传值会丢写回。
692
720
  state: {
693
721
  get currentSessionId() {
@@ -787,6 +815,17 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
787
815
  queryHistory.push(joined);
788
816
  const initialPlan = getAgentMode() === 'plan'; // 轮首模式(在 runTurn 之前读)
789
817
  const turn = await runTurn(joined, initialPlan, placeholder);
818
+ if (turn.routedCancel) {
819
+ // 路由阶段 Ctrl+C 撤回:与 500ms 撤回窗口同语义——气泡擦掉 + 行回填输入框
820
+ // (下轮 promptWithSlashMenu 经 initialLines 消费)+ 切回 INPUT 视觉。
821
+ // queryHistory 在 runTurn 前已 push,撤回的 query 未真正执行,撤销;
822
+ // pendingAttachments 保留(路由完成前未 flush),再发时随消息一起带走。
823
+ queryHistory.pop();
824
+ layout.rewindContent(bubbleRows);
825
+ pendingPrefill = input;
826
+ layout.enterInputMode(t('repl.idle'));
827
+ continue;
828
+ }
790
829
  // plan 轮正常结束(未中断 / 未抛错)→ 看轮末模式决定:
791
830
  // - 仍 plan:模型只产计划就 STOP(模型已无 switch_mode 工具)→ 弹审批面板(原行为)。
792
831
  // - 已 auto:本轮被切到 auto 模式(用户用 /auto 触发的合成执行轮)→ 跳过审批,不重复打扰。
@@ -811,6 +850,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
811
850
  }
812
851
  // 退出前等在飞反思收尾(Ctrl+C 走 SIGINT 直退不等;fire-and-forget 不承诺中断时完成)。
813
852
  await drainMemoryBackground();
853
+ await runtime.close();
814
854
  await closeAllMcp();
815
855
  layout.exitAltScreen();
816
856
  }