mixdog 0.9.35 → 0.9.37

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 (105) hide show
  1. package/package.json +3 -2
  2. package/scripts/abort-recovery-test.mjs +118 -0
  3. package/scripts/compact-smoke.mjs +7 -7
  4. package/scripts/explore-bench-tmp.mjs +19 -0
  5. package/scripts/explore-bench.mjs +36 -6
  6. package/scripts/explore-prompt-policy-test.mjs +27 -0
  7. package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
  8. package/scripts/openai-ws-early-settle-test.mjs +176 -0
  9. package/scripts/output-style-smoke.mjs +17 -17
  10. package/scripts/provider-toolcall-test.mjs +333 -14
  11. package/scripts/recall-bench-cases.json +3 -3
  12. package/scripts/recall-bench.mjs +1 -1
  13. package/scripts/recall-quality-cases.json +4 -4
  14. package/scripts/recall-usecase-cases.json +2 -2
  15. package/scripts/session-bench.mjs +13 -13
  16. package/scripts/steering-drain-buckets-test.mjs +72 -11
  17. package/scripts/submit-commandbusy-race-test.mjs +114 -0
  18. package/scripts/tool-smoke.mjs +83 -10
  19. package/src/app.mjs +2 -1
  20. package/src/defaults/cycle3-review-prompt.md +9 -0
  21. package/src/defaults/skills/setup/SKILL.md +93 -293
  22. package/src/lib/rules-builder.cjs +3 -2
  23. package/src/output-styles/default.md +2 -2
  24. package/src/output-styles/extreme-minimal.md +1 -1
  25. package/src/output-styles/minimal.md +1 -1
  26. package/src/output-styles/simple.md +2 -3
  27. package/src/rules/agent/30-explorer.md +15 -7
  28. package/src/rules/lead/01-general.md +4 -0
  29. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
  30. package/src/runtime/agent/orchestrator/config.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -7
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
  33. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  34. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
  35. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
  36. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
  37. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
  40. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
  41. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
  42. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
  43. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
  44. package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
  45. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
  47. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
  48. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
  49. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
  50. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
  51. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
  52. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
  54. package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
  56. package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
  58. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  59. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
  62. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
  63. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
  64. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
  65. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
  66. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
  67. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
  68. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
  69. package/src/runtime/channels/lib/worker-main.mjs +5 -0
  70. package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
  71. package/src/runtime/memory/lib/ko-morph.mjs +1 -1
  72. package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
  73. package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
  74. package/src/runtime/memory/lib/recall-format.mjs +3 -3
  75. package/src/runtime/shared/child-spawn-gate.mjs +15 -0
  76. package/src/runtime/shared/config.mjs +98 -12
  77. package/src/runtime/shared/process-listener-headroom.mjs +12 -0
  78. package/src/session-runtime/cwd-plugins.mjs +6 -3
  79. package/src/session-runtime/model-route-api.mjs +50 -2
  80. package/src/session-runtime/provider-auth-api.mjs +8 -1
  81. package/src/session-runtime/resource-api.mjs +22 -0
  82. package/src/session-runtime/runtime-core.mjs +13 -2
  83. package/src/session-runtime/settings-api.mjs +11 -2
  84. package/src/standalone/agent-tool.mjs +15 -9
  85. package/src/standalone/explore-tool.mjs +4 -1
  86. package/src/tui/App.jsx +6 -5
  87. package/src/tui/app/transcript-window.mjs +60 -10
  88. package/src/tui/app/use-transcript-window.mjs +2 -1
  89. package/src/tui/components/ContextPanel.jsx +4 -1
  90. package/src/tui/components/ToolExecution.jsx +15 -12
  91. package/src/tui/components/TranscriptItem.jsx +1 -1
  92. package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
  93. package/src/tui/dist/index.mjs +473 -155
  94. package/src/tui/engine/agent-job-feed.mjs +26 -6
  95. package/src/tui/engine/context-state.mjs +13 -4
  96. package/src/tui/engine/notification-plan.mjs +3 -3
  97. package/src/tui/engine/render-timing.mjs +104 -8
  98. package/src/tui/engine/session-api.mjs +58 -3
  99. package/src/tui/engine/session-flow.mjs +78 -28
  100. package/src/tui/engine/tool-card-results.mjs +28 -13
  101. package/src/tui/engine/turn.mjs +232 -156
  102. package/src/tui/engine.mjs +17 -8
  103. package/src/tui/index.jsx +10 -1
  104. package/src/workflows/default/WORKFLOW.md +8 -4
  105. package/src/workflows/solo/WORKFLOW.md +8 -4
@@ -6020,7 +6020,7 @@ function percent(value, total) {
6020
6020
  function percentLabel(value, total) {
6021
6021
  const pct = percent(value, total);
6022
6022
  if (pct === null) return "N/A";
6023
- return `${pct > 0 && pct < 1 ? pct.toFixed(1) : Math.round(pct)}%`;
6023
+ return `${pct > 0 && pct < 1 ? pct.toFixed(1) : Math.floor(pct)}%`;
6024
6024
  }
6025
6025
  function usageColor(pct) {
6026
6026
  if (!Number.isFinite(Number(pct))) return theme.text;
@@ -10479,6 +10479,7 @@ function computeTranscriptItemVariantKey(item) {
10479
10479
  const count = Number(item.count ?? 0);
10480
10480
  const completed = item.completedCount === void 0 ? "u" : Number(item.completedCount);
10481
10481
  const errors = item.errorCount === void 0 ? "u" : Number(item.errorCount);
10482
+ const callErrors = item.callErrorCount === void 0 ? "u" : Number(item.callErrorCount);
10482
10483
  const isError = item.isError ? 1 : 0;
10483
10484
  const normalizedName = String(normalizeToolName(item.name) || "").toLowerCase();
10484
10485
  const aggregate = item.aggregate ? 1 : 0;
@@ -10489,17 +10490,28 @@ function computeTranscriptItemVariantKey(item) {
10489
10490
  const bgPrompt = textShapeFingerprint(bgArgs.prompt);
10490
10491
  const bgMessage = textShapeFingerprint(bgArgs.message);
10491
10492
  const bgError = textShapeFingerprint(bgArgs.error);
10492
- return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
10493
+ return `x${expanded}:n${normalizedName}:g${aggregate}:r${resultShape}:R${rawShape}:c${count}:d${completed}:e${errors}:ce${callErrors}:E${isError}:bt${bgType}:bs${bgStatus}:bk${bgTaskId}:bp${bgPrompt}:bm${bgMessage}:be${bgError}`;
10493
10494
  }
10494
10495
  return `x${expanded}:s${textShapeFingerprint(item.text ?? item.result ?? "")}`;
10495
10496
  }
10496
10497
  var transcriptRowsCache = /* @__PURE__ */ new WeakMap();
10497
10498
  var transcriptMeasuredRowsCache = /* @__PURE__ */ new WeakMap();
10498
10499
  var streamingMeasuredRowsById = /* @__PURE__ */ new Map();
10500
+ var streamingEstimateHighWaterById = /* @__PURE__ */ new Map();
10501
+ function hasStreamingRowStateToPrune() {
10502
+ return streamingMeasuredRowsById.size > 0 || streamingEstimateHighWaterById.size > 0;
10503
+ }
10499
10504
  function pruneStreamingMeasuredRowsById(liveIds) {
10500
- if (!liveIds || streamingMeasuredRowsById.size === 0) return;
10501
- for (const id of streamingMeasuredRowsById.keys()) {
10502
- if (!liveIds.has(id)) streamingMeasuredRowsById.delete(id);
10505
+ if (!liveIds) return;
10506
+ if (streamingMeasuredRowsById.size > 0) {
10507
+ for (const id of streamingMeasuredRowsById.keys()) {
10508
+ if (!liveIds.has(id)) streamingMeasuredRowsById.delete(id);
10509
+ }
10510
+ }
10511
+ if (streamingEstimateHighWaterById.size > 0) {
10512
+ for (const id of streamingEstimateHighWaterById.keys()) {
10513
+ if (!liveIds.has(id)) streamingEstimateHighWaterById.delete(id);
10514
+ }
10503
10515
  }
10504
10516
  }
10505
10517
  function carryTranscriptMeasuredRowsCache(prevItem, nextItem) {
@@ -10534,7 +10546,17 @@ function streamingEstimateRows(item, columns, toolOutputExpanded) {
10534
10546
  const trimmedText = assistantTextForStreamingRowEstimate(item.text);
10535
10547
  const estimateItem = trimmedText === item.text ? item : { ...item, text: trimmedText };
10536
10548
  const raw = Math.max(1, Math.ceil(estimateTranscriptItemRows(estimateItem, columns, toolOutputExpanded)));
10537
- return Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
10549
+ const quantized = Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
10550
+ const id = item?.id;
10551
+ if (id == null) return quantized;
10552
+ const toolExpanded = toolOutputExpanded ? 1 : 0;
10553
+ const prev = streamingEstimateHighWaterById.get(id);
10554
+ if (!prev || prev.columns !== columns || prev.toolExpanded !== toolExpanded) {
10555
+ streamingEstimateHighWaterById.set(id, { rows: quantized, columns, toolExpanded });
10556
+ return quantized;
10557
+ }
10558
+ if (quantized > prev.rows) prev.rows = quantized;
10559
+ return prev.rows;
10538
10560
  }
10539
10561
  function streamingTailMountedGrowth(items, columns, toolOutputExpanded) {
10540
10562
  const list = Array.isArray(items) ? items : [];
@@ -10563,11 +10585,15 @@ function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
10563
10585
  }
10564
10586
  return idEntry.rows;
10565
10587
  }
10566
- if (idEntry) streamingMeasuredRowsById.delete(item.id);
10588
+ if (idEntry) {
10589
+ streamingMeasuredRowsById.delete(item.id);
10590
+ streamingEstimateHighWaterById.delete(item.id);
10591
+ }
10567
10592
  return streamingEstimateRows(item, columns, toolOutputExpanded);
10568
10593
  }
10569
- if (item.kind === "assistant" && streamingMeasuredRowsById.has(item.id)) {
10570
- streamingMeasuredRowsById.delete(item.id);
10594
+ if (item.kind === "assistant") {
10595
+ if (streamingMeasuredRowsById.has(item.id)) streamingMeasuredRowsById.delete(item.id);
10596
+ if (streamingEstimateHighWaterById.has(item.id)) streamingEstimateHighWaterById.delete(item.id);
10571
10597
  }
10572
10598
  const variantKey = transcriptItemVariantKey(item);
10573
10599
  const toolExpanded = toolOutputExpanded ? 1 : 0;
@@ -11999,7 +12025,7 @@ function useTranscriptWindow({
11999
12025
  if (!els.has(key)) refCache.delete(key);
12000
12026
  }
12001
12027
  }
12002
- if (streamingMeasuredRowsById.size > 0) {
12028
+ if (hasStreamingRowStateToPrune()) {
12003
12029
  pruneStreamingMeasuredRowsById(new Set(liveItems.keys()));
12004
12030
  }
12005
12031
  });
@@ -14269,7 +14295,7 @@ function createOnboardingSteps({
14269
14295
  }
14270
14296
 
14271
14297
  // src/runtime/shared/config.mjs
14272
- import { readFileSync as readFileSync5, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
14298
+ import { readFileSync as readFileSync5, statSync as statSync4, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
14273
14299
  import { join as join6, dirname as dirname6 } from "path";
14274
14300
  import { createRequire as createRequire2 } from "module";
14275
14301
 
@@ -14986,6 +15012,56 @@ var { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret,
14986
15012
  var DATA_DIR = resolvePluginData();
14987
15013
  var CONFIG_PATH = join6(DATA_DIR, "mixdog-config.json");
14988
15014
  var GENERATED_KEY = "_generated";
15015
+ var _configReadCache = null;
15016
+ var CONFIG_READ_TTL_MS = (() => {
15017
+ const n = Number(process.env.MIXDOG_CONFIG_READ_TTL_MS);
15018
+ return Number.isFinite(n) && n >= 0 ? n : 0;
15019
+ })();
15020
+ function invalidateConfigReadCache() {
15021
+ _configReadCache = null;
15022
+ }
15023
+ function readConfigRawCached() {
15024
+ const now = Date.now();
15025
+ if (_configReadCache) {
15026
+ if (CONFIG_READ_TTL_MS > 0 && now - _configReadCache.atMs < CONFIG_READ_TTL_MS) {
15027
+ return _configReadCache.raw;
15028
+ }
15029
+ try {
15030
+ const st2 = statSync4(CONFIG_PATH);
15031
+ if (st2.mtimeMs === _configReadCache.mtimeMs && st2.size === _configReadCache.size) {
15032
+ _configReadCache.atMs = now;
15033
+ return _configReadCache.raw;
15034
+ }
15035
+ } catch {
15036
+ }
15037
+ }
15038
+ let st = null;
15039
+ try {
15040
+ st = statSync4(CONFIG_PATH);
15041
+ } catch (err) {
15042
+ if (err.code === "ENOENT") {
15043
+ _configReadCache = null;
15044
+ return null;
15045
+ }
15046
+ process.stderr.write(`[config] readConfigRawCached: unexpected stat error for ${CONFIG_PATH}: ${err.message}
15047
+ `);
15048
+ throw err;
15049
+ }
15050
+ let raw;
15051
+ try {
15052
+ raw = readFileSync5(CONFIG_PATH, "utf8");
15053
+ } catch (err) {
15054
+ if (err.code === "ENOENT") {
15055
+ _configReadCache = null;
15056
+ return null;
15057
+ }
15058
+ process.stderr.write(`[config] readJsonFile: unexpected read error for ${CONFIG_PATH}: ${err.message}
15059
+ `);
15060
+ throw err;
15061
+ }
15062
+ _configReadCache = { raw, mtimeMs: st.mtimeMs, size: st.size, atMs: now };
15063
+ return raw;
15064
+ }
14989
15065
  function isPlainObject2(value) {
14990
15066
  return !!value && typeof value === "object" && !Array.isArray(value);
14991
15067
  }
@@ -14996,18 +15072,24 @@ function stripGeneratedMarker(data) {
14996
15072
  }
14997
15073
  function readJsonFile(path2) {
14998
15074
  let raw;
14999
- try {
15000
- raw = readFileSync5(path2, "utf8");
15001
- } catch (err) {
15002
- if (err.code === "ENOENT") return null;
15003
- process.stderr.write(`[config] readJsonFile: unexpected read error for ${path2}: ${err.message}
15075
+ if (path2 === CONFIG_PATH) {
15076
+ raw = readConfigRawCached();
15077
+ } else {
15078
+ try {
15079
+ raw = readFileSync5(path2, "utf8");
15080
+ } catch (err) {
15081
+ if (err.code === "ENOENT") return null;
15082
+ process.stderr.write(`[config] readJsonFile: unexpected read error for ${path2}: ${err.message}
15004
15083
  `);
15005
- throw err;
15084
+ throw err;
15085
+ }
15006
15086
  }
15087
+ if (raw == null) return null;
15007
15088
  try {
15008
15089
  return JSON.parse(raw);
15009
15090
  } catch (err) {
15010
15091
  if (path2 === CONFIG_PATH) {
15092
+ invalidateConfigReadCache();
15011
15093
  const corrupt = `${path2}.corrupt-${Date.now()}`;
15012
15094
  try {
15013
15095
  renameWithRetrySync(path2, corrupt);
@@ -15029,6 +15111,7 @@ function writeJsonFile(path2, data) {
15029
15111
  }
15030
15112
  writeJsonAtomicSync(path2, data, { lock: false, fsyncDir: true, mode: 384, secret: true });
15031
15113
  if (path2 === CONFIG_PATH) {
15114
+ invalidateConfigReadCache();
15032
15115
  try {
15033
15116
  markUserDataInitialized(DATA_DIR);
15034
15117
  } catch {
@@ -18879,12 +18962,12 @@ function clampFailureCount(errorCount, groupCount, isError) {
18879
18962
  if (Number.isFinite(explicit)) return Math.max(0, Math.min(groupCount, Math.floor(explicit)));
18880
18963
  return isError ? groupCount : 0;
18881
18964
  }
18882
- function toolStatusColor({ pending, groupCount, failedCount, terminalStatus = "" }) {
18965
+ function toolStatusColor({ pending, groupCount, callFailedCount = 0, terminalStatus = "" }) {
18883
18966
  if (pending) return theme.text;
18884
18967
  const status = normalizeTerminalStatus(terminalStatus);
18885
18968
  if (status === "cancelled") return theme.warning;
18886
- if (failedCount <= 0) return status === "failed" ? theme.error : theme.success;
18887
- if (groupCount > 1 && failedCount < groupCount) return theme.mixdogOrange || theme.warning;
18969
+ if (callFailedCount <= 0) return theme.success;
18970
+ if (groupCount > 1 && callFailedCount < groupCount) return theme.mixdogOrange || theme.warning;
18888
18971
  return theme.error;
18889
18972
  }
18890
18973
 
@@ -18911,13 +18994,12 @@ function ResultBody({ lines, rawText, pathArg = "", isShell = false, columns, co
18911
18994
  // src/tui/components/ToolExecution.jsx
18912
18995
  import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
18913
18996
  var TOOL_BLINK_MS = 500;
18914
- var TOOL_BLINK_LIMIT_MS = 3e3;
18915
18997
  var TOOL_PENDING_SHOW_DELAY_MS = 1e3;
18916
18998
  var TOOL_ANIM_TICK_MS = TOOL_BLINK_MS;
18917
18999
  function statusCopy(name, label, count, doneCount, pending, isError, args = {}) {
18918
19000
  return formatToolActionHeader(name, args, { pending, count });
18919
19001
  }
18920
- function ToolExecution({ name, args, result, rawResult, isError, errorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
19002
+ function ToolExecution({ name, args, result, rawResult, isError, errorCount, callErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false }) {
18921
19003
  const rowWidth = Math.max(1, Number(columns || 80));
18922
19004
  const groupCount = Math.max(1, Number(count || 1));
18923
19005
  const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount || (result == null ? 0 : groupCount))));
@@ -18933,14 +19015,14 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
18933
19015
  const hasVisibleProgress = doneCount > 0 || Boolean(String(rt || "").trim());
18934
19016
  const pendingDisplayReady = !pending || !startedAtMs || pendingDelayElapsed || pendingAgeMs >= TOOL_PENDING_SHOW_DELAY_MS || hasVisibleProgress || deferredDisplayReady;
18935
19017
  const blinkActive = pending && pendingDisplayReady;
18936
- const blinkExpired = blinkActive && startedAtMs > 0 && nowMs - startedAtMs >= TOOL_BLINK_LIMIT_MS;
18937
- const blinkOn = !blinkActive || blinkExpired ? true : Math.floor(nowMs / TOOL_BLINK_MS) % 2 === 0;
19018
+ const blinkOn = !blinkActive ? true : Math.floor(nowMs / TOOL_BLINK_MS) % 2 === 0;
18938
19019
  const headerPending = pending || headerFinalized === false;
18939
19020
  const hasResult = result != null && Boolean(String(rt || "").trim());
18940
19021
  const hasRawResult = rawResult != null && Boolean(String(rawRt || "").trim());
18941
19022
  const elapsedMs = startedAtMs ? Math.max(0, (pending ? nowMs : completedAtMs || nowMs) - startedAtMs) : 0;
18942
19023
  const elapsed = elapsedMs >= 1e3 ? formatElapsed(elapsedMs) : "";
18943
19024
  const failedCount = clampFailureCount(errorCount, groupCount, isError);
19025
+ const callFailedCount = clampFailureCount(callErrorCount, groupCount, false);
18944
19026
  const displayGroupCount = groupCount;
18945
19027
  const displayCategories = normalizeCountMap(categories || {});
18946
19028
  const normalizedDoneCategories = doneCategories ? normalizeCountMap(doneCategories) : displayCategories;
@@ -18966,8 +19048,8 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
18966
19048
  detailText = "";
18967
19049
  }
18968
19050
  const aggregateTerminalStatus = pending ? "running" : resultTerminalStatus(rt) || (isError || failedCount > 0 ? "failed" : "completed");
18969
- const dotColor2 = toolStatusColor({ pending, groupCount, failedCount, terminalStatus: aggregateTerminalStatus });
18970
- const dotText2 = pending && !blinkExpired && !blinkOn ? " " : TURN_MARKER;
19051
+ const dotColor2 = toolStatusColor({ pending, groupCount, callFailedCount, terminalStatus: aggregateTerminalStatus });
19052
+ const dotText2 = pending && !blinkOn ? " " : TURN_MARKER;
18971
19053
  const gutter2 = 2;
18972
19054
  const showHeaderExpandHint2 = hasRawResult;
18973
19055
  const hintLabel2 = `ctrl+o ${expanded ? "collapse" : "expand"}`;
@@ -19066,12 +19148,12 @@ function ToolExecution({ name, args, result, rawResult, isError, errorCount, exp
19066
19148
  const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
19067
19149
  visibleDetailLines = keepAgentDetail ? [agentDetailLine] : [];
19068
19150
  }
19069
- const finalStatusColor = toolStatusColor({ pending, groupCount, failedCount, terminalStatus });
19151
+ const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, terminalStatus });
19070
19152
  const dotColor = finalStatusColor;
19071
19153
  const markerGlyph = isAgentResponse ? AGENT_RESPONSE_MARKER : isAgentSurfaceCard ? AGENT_CALL_MARKER : TURN_MARKER;
19072
19154
  const isDirectionalMarker = isAgentResponse || isAgentSurfaceCard;
19073
19155
  const markerText = isDirectionalMarker ? `${markerGlyph} ` : markerGlyph;
19074
- const dotText = pending && !blinkExpired && !blinkOn ? " " : markerText;
19156
+ const dotText = pending && !blinkOn ? " " : markerText;
19075
19157
  let labelText;
19076
19158
  if (isAgentResponse) labelText = agentResponseTitle(parsedArgs);
19077
19159
  else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
@@ -19282,7 +19364,7 @@ var Item = React19.memo(function Item2({ item, prevKind, columns, toolOutputExpa
19282
19364
  node = /* @__PURE__ */ jsx19(ToolHookDenialCard, { item, columns });
19283
19365
  break;
19284
19366
  }
19285
- node = /* @__PURE__ */ jsx19(ToolExecution, { name: item.name, args: item.args, result: item.result, rawResult: item.rawResult, isError: item.isError, errorCount: item.errorCount, expanded: toolOutputExpanded || item.expanded, columns, attached: false, count: item.count, completedCount: item.completedCount, startedAt: item.startedAt, completedAt: item.completedAt, aggregate: item.aggregate, categories: item.categories, doneCategories: item.doneCategories, headerFinalized: item.headerFinalized, deferredDisplayReady: item.deferredDisplayReady, themeEpoch });
19367
+ node = /* @__PURE__ */ jsx19(ToolExecution, { name: item.name, args: item.args, result: item.result, rawResult: item.rawResult, isError: item.isError, errorCount: item.errorCount, callErrorCount: item.callErrorCount, expanded: toolOutputExpanded || item.expanded, columns, attached: false, count: item.count, completedCount: item.completedCount, startedAt: item.startedAt, completedAt: item.completedAt, aggregate: item.aggregate, categories: item.categories, doneCategories: item.doneCategories, headerFinalized: item.headerFinalized, deferredDisplayReady: item.deferredDisplayReady });
19286
19368
  break;
19287
19369
  }
19288
19370
  case "notice":
@@ -20339,7 +20421,8 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20339
20421
  const n = Number(value || 0);
20340
20422
  const d = Number(total || 0);
20341
20423
  if (!d) return "N/A";
20342
- return `${(n / d * 100).toFixed(n > 0 && n < d / 100 ? 1 : 0)}%`;
20424
+ const p = Math.max(0, Math.min(100, n / d * 100));
20425
+ return `${p > 0 && p < 1 ? p.toFixed(1) : Math.floor(p)}%`;
20343
20426
  };
20344
20427
  const fmt = (value) => {
20345
20428
  const n = Number(value || 0);
@@ -20941,11 +21024,11 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
20941
21024
  }
20942
21025
  }
20943
21026
  if (!commandText) return false;
20944
- if (state.commandBusy) {
20945
- store.pushNotice("wait for the current command to finish", "warn");
20946
- return false;
20947
- }
20948
21027
  if (commandText.startsWith("/")) {
21028
+ if (state.commandBusy) {
21029
+ store.pushNotice("wait for the current command to finish", "warn");
21030
+ return false;
21031
+ }
20949
21032
  const [cmd, ...rest] = commandText.slice(1).split(/\s+/);
20950
21033
  const accepted2 = runSlashCommand(cmd, rest.join(" ").trim());
20951
21034
  if (accepted2 !== false) clearPastedImagesSnapshot();
@@ -22702,7 +22785,7 @@ function resolveTuiRuntimeNotificationDelivery(event, text) {
22702
22785
  if (meta.kind === "update-notice") {
22703
22786
  const ver = String(meta.version || "").trim();
22704
22787
  const displayText = ver ? `mixdog v${ver} ready \u2014 restart to apply.` : trimmed;
22705
- return { action: "notice", displayText, tone: meta.tone === "warn" ? "warn" : "info" };
22788
+ return { action: "notice", displayText, tone: meta.tone === "warn" ? "warn" : "info", transcript: false };
22706
22789
  }
22707
22790
  if (!isExecutionNotification(event, trimmed, parsed)) {
22708
22791
  return { action: "enqueue", displayText: trimmed, modelContent: trimmed };
@@ -22719,9 +22802,68 @@ function resolveTuiRuntimeNotificationDelivery(event, text) {
22719
22802
  }
22720
22803
 
22721
22804
  // src/tui/engine/render-timing.mjs
22722
- var RENDER_THROTTLE_FLUSH_MS = 12;
22723
- var yieldToRenderer = () => new Promise((resolve5) => {
22724
- setTimeout(resolve5, RENDER_THROTTLE_FLUSH_MS);
22805
+ var RENDER_ACK_HANG_GUARD_MS = 250;
22806
+ var RENDER_SETTLE_IDLE_MS = 64;
22807
+ var pendingRenderAcks = [];
22808
+ var renderAckSeq = 0;
22809
+ var scheduleRenderFrameAck = () => {
22810
+ const seq = ++renderAckSeq;
22811
+ setImmediate(() => notifyRenderFrame(seq));
22812
+ };
22813
+ var notifyRenderFrame = (seq = ++renderAckSeq) => {
22814
+ if (pendingRenderAcks.length === 0) return;
22815
+ const acks = pendingRenderAcks;
22816
+ pendingRenderAcks = [];
22817
+ for (const ack of acks) ack(seq);
22818
+ };
22819
+ var yieldToRenderer = ({ frames = 1 } = {}) => new Promise((resolve5) => {
22820
+ const minSeq = renderAckSeq;
22821
+ let remainingFrames = Math.max(1, Math.floor(Number(frames) || 1));
22822
+ let settled = false;
22823
+ let sawRealFrame = false;
22824
+ let timer2 = null;
22825
+ const finish = () => {
22826
+ if (settled) return;
22827
+ settled = true;
22828
+ if (timer2) clearTimeout(timer2);
22829
+ const idx = pendingRenderAcks.indexOf(onFrame);
22830
+ if (idx !== -1) pendingRenderAcks.splice(idx, 1);
22831
+ resolve5();
22832
+ };
22833
+ const onTimeout = () => {
22834
+ if (settled) return;
22835
+ if (!sawRealFrame && renderAckSeq > minSeq) {
22836
+ setImmediate(() => {
22837
+ if (!settled && !sawRealFrame) finish();
22838
+ });
22839
+ return;
22840
+ }
22841
+ finish();
22842
+ };
22843
+ const armWait = () => {
22844
+ if (settled) return;
22845
+ if (!pendingRenderAcks.includes(onFrame)) pendingRenderAcks.push(onFrame);
22846
+ if (timer2) clearTimeout(timer2);
22847
+ timer2 = setTimeout(onTimeout, sawRealFrame ? RENDER_SETTLE_IDLE_MS : RENDER_ACK_HANG_GUARD_MS);
22848
+ };
22849
+ const onFrame = (seq = 0) => {
22850
+ if (settled) return;
22851
+ if (seq <= minSeq) {
22852
+ if (!pendingRenderAcks.includes(onFrame)) pendingRenderAcks.push(onFrame);
22853
+ return;
22854
+ }
22855
+ sawRealFrame = true;
22856
+ if (timer2) {
22857
+ clearTimeout(timer2);
22858
+ timer2 = null;
22859
+ }
22860
+ const idx = pendingRenderAcks.indexOf(onFrame);
22861
+ if (idx !== -1) pendingRenderAcks.splice(idx, 1);
22862
+ remainingFrames -= 1;
22863
+ if (remainingFrames <= 0) finish();
22864
+ else armWait();
22865
+ };
22866
+ armWait();
22725
22867
  });
22726
22868
 
22727
22869
  // src/tui/engine/tool-result-status.mjs
@@ -22920,6 +23062,7 @@ function createToolCardResults({
22920
23062
  patchItem,
22921
23063
  markToolCallDone,
22922
23064
  updateAgentJobCard,
23065
+ buildAgentJobCardPatch,
22923
23066
  agentStatusState
22924
23067
  }) {
22925
23068
  function patchToolItem(id, patch) {
@@ -22940,7 +23083,9 @@ function createToolCardResults({
22940
23083
  const aggregate = card.aggregate;
22941
23084
  const callRec = aggregate && callId ? aggregate.calls.get(callId) : null;
22942
23085
  const isMemoryCall = classifyToolCategory(callRec?.name || card?.name || "", callRec?.args || {}) === "Memory";
22943
- const isError = message?.isError === true || message?.toolKind === "error" || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || isMemoryCall && memoryCoreResultErrorText(rawText) != null;
23086
+ const isCallError = message?.isError === true || message?.toolKind === "error";
23087
+ const isResultError = /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || isMemoryCall && memoryCoreResultErrorText(rawText) != null;
23088
+ const isError = isCallError || isResultError;
22944
23089
  const text = isError ? toolErrorDisplay(rawText, card?.name || "tool") : rawText;
22945
23090
  if (aggregate && card.itemId === aggregate.itemId) {
22946
23091
  if (!callRec) return false;
@@ -22952,11 +23097,13 @@ function createToolCardResults({
22952
23097
  callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
22953
23098
  assignAggregateSummaryOrder(aggregate, callRec);
22954
23099
  callRec.isError = isError;
23100
+ callRec.isCallError = isCallError;
22955
23101
  callRec.resultText = text;
22956
23102
  callRec.resolved = true;
22957
23103
  const allCalls = [...aggregate.calls.values()];
22958
23104
  const completed = allCalls.filter((r) => r.resolved).length;
22959
23105
  const errors = allCalls.filter((r) => r.isError).length;
23106
+ const callErrors = allCalls.filter((r) => r.isCallError).length;
22960
23107
  const succeeded = completed - errors;
22961
23108
  const detailText = errors > 0 ? succeeded > 0 ? `${succeeded} Ok \xB7 ${errors} Failed` : `${errors} Failed` : formatAggregateDetail(aggregateSummaries(aggregate));
22962
23109
  const currentItem = getState().items.find((it) => it.id === card.itemId);
@@ -22970,6 +23117,7 @@ function createToolCardResults({
22970
23117
  rawResult: rawResult || null,
22971
23118
  isError: errors > 0,
22972
23119
  errorCount: errors,
23120
+ callErrorCount: callErrors,
22973
23121
  count: allCalls.length,
22974
23122
  completedCount: visualCompleted,
22975
23123
  doneCategories: aggregateDoneCategories(allCalls),
@@ -22979,9 +23127,10 @@ function createToolCardResults({
22979
23127
  if (callId) done.add(callId);
22980
23128
  return true;
22981
23129
  }
22982
- const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
23130
+ const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, callErrors: 0, results: [] };
22983
23131
  group.completed = Math.min(group.count, group.completed + 1);
22984
23132
  group.errors += isError ? 1 : 0;
23133
+ group.callErrors = (group.callErrors || 0) + (isCallError ? 1 : 0);
22985
23134
  group.results.push({ text, isError });
22986
23135
  toolGroups.set(card.itemId, group);
22987
23136
  const resultText = groupedToolResultText(group);
@@ -22991,6 +23140,7 @@ function createToolCardResults({
22991
23140
  text: displayResult,
22992
23141
  isError: group.errors > 0,
22993
23142
  errorCount: group.errors,
23143
+ callErrorCount: group.callErrors || 0,
22994
23144
  count: group.count,
22995
23145
  completedCount: group.completed,
22996
23146
  completedAt: Date.now()
@@ -23000,19 +23150,11 @@ function createToolCardResults({
23000
23150
  if (body) patch.rawResult = text || rawText;
23001
23151
  const parsedAgent = parseAgentJob(rawText);
23002
23152
  if (parsedAgent) {
23003
- patch.args = agentArgsWithResultMetadata(getState().items.find((it) => it.id === card.itemId)?.args, parsedAgent);
23004
23153
  set(agentStatusState({ force: true }));
23005
23154
  }
23155
+ Object.assign(patch, buildAgentJobCardPatch(card.itemId, rawText, isError));
23006
23156
  }
23007
23157
  patchToolItem(card.itemId, patch);
23008
- if (group.count <= 1) {
23009
- const beforeAgent = getState().items.find((it) => it.id === card.itemId);
23010
- updateAgentJobCard(card.itemId, rawText, isError);
23011
- const afterAgent = getState().items.find((it) => it.id === card.itemId);
23012
- if (afterAgent && beforeAgent && afterAgent !== beforeAgent) {
23013
- carryTranscriptMeasuredRowsCache(beforeAgent, afterAgent);
23014
- }
23015
- }
23016
23158
  card.done = true;
23017
23159
  if (callId) done.add(callId);
23018
23160
  return true;
@@ -23060,6 +23202,7 @@ function createToolCardResults({
23060
23202
  const completed = allCalls.filter((r) => r.resolved).length;
23061
23203
  const totalCompleted = completed;
23062
23204
  const errors = allCalls.filter((r) => r.isError).length;
23205
+ const callErrors = allCalls.filter((r) => r.isCallError).length;
23063
23206
  const succeeded = completed - errors;
23064
23207
  const rawResult = aggregateRawResult(allCalls);
23065
23208
  let displayDetail = errors > 0 ? succeeded > 0 ? `${succeeded} Ok \xB7 ${errors} Failed` : `${errors} Failed` : formatAggregateDetail(aggregateSummaries(aggregate));
@@ -23073,6 +23216,7 @@ function createToolCardResults({
23073
23216
  rawResult: rawResult || null,
23074
23217
  isError: errors > 0,
23075
23218
  errorCount: errors,
23219
+ callErrorCount: callErrors,
23076
23220
  count: allCalls.length,
23077
23221
  completedCount: totalCompleted,
23078
23222
  doneCategories: aggregateDoneCategories(allCalls),
@@ -23093,7 +23237,7 @@ function createToolCardResults({
23093
23237
  const currentItem = getState().items.find((it) => it.id === card.itemId);
23094
23238
  resultText = withCancelledResultMarker(resultText, currentItem);
23095
23239
  }
23096
- patchToolItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, count: group.count, completedCount: group.completed, completedAt: Date.now() });
23240
+ patchToolItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, callErrorCount: group.callErrors || 0, count: group.count, completedCount: group.completed, completedAt: Date.now() });
23097
23241
  card.done = true;
23098
23242
  if (card.callId) done.add(card.callId);
23099
23243
  }
@@ -23153,18 +23297,21 @@ function createAgentJobFeed({
23153
23297
  function scheduleExecutionPendingResumeKick(body = "") {
23154
23298
  queueMicrotask(() => kickExecutionPendingResume(body));
23155
23299
  }
23156
- function updateAgentJobCard(itemId, text, isError = false) {
23300
+ function buildAgentJobCardPatch(itemId, text, isError = false) {
23157
23301
  const parsed = parseAgentJob(text);
23158
23302
  const current = getState().items.find((it) => it.id === itemId);
23159
23303
  const rawDisplayText = agentJobResultText(text, parsed) || String(text ?? "").trim();
23160
23304
  const displayText = isError ? toolErrorDisplay(rawDisplayText, "agent") : rawDisplayText;
23161
- patchItem(itemId, {
23305
+ return {
23162
23306
  result: displayText,
23163
23307
  text: displayText,
23164
23308
  isError,
23165
23309
  errorCount: isError ? 1 : 0,
23166
23310
  ...parsed ? { args: agentArgsWithResultMetadata(current?.args, parsed) } : {}
23167
- });
23311
+ };
23312
+ }
23313
+ function updateAgentJobCard(itemId, text, isError = false) {
23314
+ patchItem(itemId, buildAgentJobCardPatch(itemId, text, isError));
23168
23315
  }
23169
23316
  function subscribeRuntimeNotifications() {
23170
23317
  if (typeof runtime.onNotification !== "function") return null;
@@ -23177,7 +23324,7 @@ function createAgentJobFeed({
23177
23324
  const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
23178
23325
  if (delivery.action === "ignore") return;
23179
23326
  if (delivery.action === "notice") {
23180
- pushNotice?.(delivery.displayText, delivery.tone || "info", { transcript: true });
23327
+ pushNotice?.(delivery.displayText, delivery.tone || "info", { transcript: delivery.transcript === true });
23181
23328
  return true;
23182
23329
  }
23183
23330
  if (delivery.action === "status-only") {
@@ -23193,7 +23340,15 @@ function createAgentJobFeed({
23193
23340
  if (parsed?.taskId) set(agentStatusState({ force: true }));
23194
23341
  const resumeBody = String(delivery.modelContent || "").trim();
23195
23342
  if (resumeBody) {
23196
- scheduleExecutionPendingResumeKick(resumeBody);
23343
+ enqueue(resumeBody, {
23344
+ mode: "task-notification",
23345
+ // Claude Code parity: live execution completions are queued as
23346
+ // task notifications so the active loop can attach them after the
23347
+ // next tool batch; no special pending-resume bypass.
23348
+ priority: "next",
23349
+ key: notificationKey || void 0,
23350
+ displayText: delivery.displayText || text
23351
+ });
23197
23352
  }
23198
23353
  return true;
23199
23354
  }
@@ -23205,7 +23360,9 @@ function createAgentJobFeed({
23205
23360
  if (!modelContent && imagePaths.length === 0) return true;
23206
23361
  const enqueueOpts = {
23207
23362
  mode: "task-notification",
23208
- priority: "next",
23363
+ // Claude Code parity: task/schedule notifications are lower-priority
23364
+ // queue items and drain between turns, behind direct user input.
23365
+ priority: "later",
23209
23366
  key: notificationKey || void 0,
23210
23367
  displayText: delivery.displayText || text
23211
23368
  };
@@ -23241,6 +23398,7 @@ function createAgentJobFeed({
23241
23398
  flushDeferredExecutionPendingResumeKick,
23242
23399
  scheduleExecutionPendingResumeKick,
23243
23400
  updateAgentJobCard,
23401
+ buildAgentJobCardPatch,
23244
23402
  subscribeRuntimeNotifications
23245
23403
  };
23246
23404
  }
@@ -23461,11 +23619,13 @@ function createContextState({ runtime, getState, getPendingSessionReset }) {
23461
23619
  });
23462
23620
  const routeState = () => {
23463
23621
  const state = getState();
23622
+ const base = baseRouteState();
23623
+ const sameContextRoute = state.sessionId === base.sessionId && state.clientHostPid === base.clientHostPid && state.provider === base.provider && state.model === base.model && state.effort === base.effort && state.fast === base.fast && state.contextWindow === base.contextWindow && state.rawContextWindow === base.rawContextWindow;
23464
23624
  return {
23465
- ...baseRouteState(),
23466
- displayContextWindow: state.displayContextWindow || 0,
23467
- compactBoundaryTokens: state.compactBoundaryTokens || 0,
23468
- autoCompactTokenLimit: state.autoCompactTokenLimit || 0
23625
+ ...base,
23626
+ displayContextWindow: sameContextRoute ? state.displayContextWindow || 0 : 0,
23627
+ compactBoundaryTokens: sameContextRoute ? state.compactBoundaryTokens || 0 : 0,
23628
+ autoCompactTokenLimit: sameContextRoute ? state.autoCompactTokenLimit || 0 : 0
23469
23629
  };
23470
23630
  };
23471
23631
  function syncContextDisplayFields(ctx = null) {
@@ -23617,9 +23777,6 @@ function createSessionFlow(bag) {
23617
23777
  }
23618
23778
  function removeQueuedEntries(entries) {
23619
23779
  const ids = new Set(entries.map((entry) => entry.id));
23620
- const keys = entries.map((entry) => entry.key).filter(Boolean);
23621
- for (const key of keys) pendingNotificationKeys.delete(key);
23622
- for (const key of keys) displayedExecutionNotificationKeys.delete(key);
23623
23780
  const queued = getState().queued.filter((q) => !ids.has(q.id));
23624
23781
  if (queued.length !== getState().queued.length) set({ queued });
23625
23782
  }
@@ -23632,8 +23789,9 @@ function createSessionFlow(bag) {
23632
23789
  displayText: entry.displayText || (entry.mode === "task-notification" ? notificationDisplayText(entry.text) : String(entry.text || ""))
23633
23790
  };
23634
23791
  if (next.mode === "task-notification" && next.key) {
23635
- if (pendingNotificationKeys.has(next.key)) continue;
23636
- pendingNotificationKeys.add(next.key);
23792
+ const duplicateQueued = pending.some((entry2) => entry2?.mode === "task-notification" && entry2?.key === next.key);
23793
+ if (duplicateQueued) continue;
23794
+ if (!pendingNotificationKeys.has(next.key)) pendingNotificationKeys.add(next.key);
23637
23795
  }
23638
23796
  restored.push(next);
23639
23797
  }
@@ -23674,14 +23832,49 @@ function createSessionFlow(bag) {
23674
23832
  removeQueuedEntries(batch);
23675
23833
  return batch;
23676
23834
  }
23835
+ function scheduleBlockedDrainRetry() {
23836
+ if (pending.length === 0) return;
23837
+ if (flags.blockedDrainRetryTimer) return;
23838
+ const timer2 = setTimeout(() => {
23839
+ flags.blockedDrainRetryTimer = null;
23840
+ if (pending.length > 0) void drain();
23841
+ }, 50);
23842
+ if (typeof timer2.unref === "function") timer2.unref();
23843
+ flags.blockedDrainRetryTimer = timer2;
23844
+ }
23845
+ function clearBlockedDrainRetry() {
23846
+ if (!flags.blockedDrainRetryTimer) return;
23847
+ clearTimeout(flags.blockedDrainRetryTimer);
23848
+ flags.blockedDrainRetryTimer = null;
23849
+ }
23850
+ function hasModelDrainablePending() {
23851
+ return pending.some((entry) => !isSlashQueuedEntry(entry));
23852
+ }
23677
23853
  async function drain() {
23678
23854
  if (flags.draining) return;
23679
- if (flags.autoClearRunning) return;
23855
+ if (flags.autoClearRunning || getState().commandBusy) {
23856
+ scheduleBlockedDrainRetry();
23857
+ return;
23858
+ }
23859
+ if (getState().busy) {
23860
+ tuiDebug2(`busy-queue drain deferred while active pending=${pending.length}`);
23861
+ return;
23862
+ }
23863
+ clearBlockedDrainRetry();
23864
+ const drainEpoch = (Number(flags.drainEpoch) || 0) + 1;
23865
+ flags.drainEpoch = drainEpoch;
23680
23866
  flags.draining = true;
23681
23867
  let firstBatch = true;
23682
23868
  try {
23683
23869
  while (pending.length > 0) {
23684
- const batch = dequeueQueueBatch("later", { limit: firstBatch ? 1 : Infinity });
23870
+ if (flags.drainEpoch !== drainEpoch) return;
23871
+ const batch = dequeueQueueBatch("later", {
23872
+ limit: firstBatch ? 1 : Infinity,
23873
+ // Slash commands must run through the TUI command dispatcher, not be
23874
+ // delivered to the model as plain text. Claude Code's queueProcessor
23875
+ // similarly handles slash entries outside the queued_command drain.
23876
+ predicate: (entry) => !isSlashQueuedEntry(entry)
23877
+ });
23685
23878
  firstBatch = false;
23686
23879
  if (batch.length === 0) break;
23687
23880
  tuiDebug2(`busy-queue drain batch=${batch.length} remaining=${pending.length}`);
@@ -23703,6 +23896,7 @@ function createSessionFlow(bag) {
23703
23896
  restorable: nonEditable.length === 0,
23704
23897
  requeueOnAbort: nonEditable
23705
23898
  });
23899
+ if (flags.drainEpoch !== drainEpoch) return;
23706
23900
  flushDeferredClearedSessionUi();
23707
23901
  const scheduledReset = runtime.consumePendingSessionReset?.();
23708
23902
  if ((scheduledReset === "clear" || scheduledReset === "compact_clear") && turnStatus !== "cancelled" && !getState().commandBusy) {
@@ -23717,10 +23911,12 @@ function createSessionFlow(bag) {
23717
23911
  if (turnStatus === "cancelled" && pending.length === 0) break;
23718
23912
  }
23719
23913
  } finally {
23720
- flags.draining = false;
23721
- flushDeferredClearedSessionUi();
23722
- if (pending.length > 0) void drain();
23723
- else flushDeferredExecutionPendingResumeKick();
23914
+ if (flags.drainEpoch === drainEpoch) {
23915
+ flags.draining = false;
23916
+ flushDeferredClearedSessionUi();
23917
+ if (hasModelDrainablePending()) void drain();
23918
+ else flushDeferredExecutionPendingResumeKick();
23919
+ }
23724
23920
  }
23725
23921
  }
23726
23922
  function enqueue(text, options = {}) {
@@ -23738,11 +23934,17 @@ function createSessionFlow(bag) {
23738
23934
  void drain();
23739
23935
  return true;
23740
23936
  }
23741
- function drainPendingSteering() {
23742
- const predicate = (entry) => !isSlashQueuedEntry(entry);
23937
+ function drainPendingSteering(_sessionIdOrOptions = null, maybeOptions = null) {
23938
+ const options = maybeOptions && typeof maybeOptions === "object" ? maybeOptions : _sessionIdOrOptions && typeof _sessionIdOrOptions === "object" ? _sessionIdOrOptions : {};
23939
+ const maxPriority = options.maxPriority || "next";
23940
+ const predicate = (entry) => {
23941
+ if (isSlashQueuedEntry(entry)) return false;
23942
+ const mode = entry?.mode || "prompt";
23943
+ return mode === "prompt" || mode === "task-notification";
23944
+ };
23743
23945
  const out = [];
23744
23946
  for (; ; ) {
23745
- const batch = dequeueQueueBatch("later", { predicate });
23947
+ const batch = dequeueQueueBatch(maxPriority, { predicate });
23746
23948
  if (batch.length === 0) break;
23747
23949
  for (const entry of batch) {
23748
23950
  const content = entry.content;
@@ -24019,6 +24221,7 @@ function createRunTurn(bag) {
24019
24221
  const turnIndex = getState().stats.turns || 0;
24020
24222
  const startedAt = Date.now();
24021
24223
  const turnEpoch = ++flags.leadTurnEpoch;
24224
+ const isCurrentTurn = () => !flags.disposed && flags.leadTurnEpoch === turnEpoch;
24022
24225
  const inputBaseline = getState().stats.inputTokens;
24023
24226
  const outputBaseline = getState().stats.outputTokens;
24024
24227
  const submittedIds = Array.isArray(options.submittedIds) ? options.submittedIds : [];
@@ -24042,6 +24245,8 @@ function createRunTurn(bag) {
24042
24245
  let watchdogTripped = false;
24043
24246
  let watchdogTimer = null;
24044
24247
  let watchdogGraceTimer = null;
24248
+ let lastProgressAt = startedAt;
24249
+ let lastProgressLabel = "start";
24045
24250
  const clearWatchdog = () => {
24046
24251
  if (watchdogTimer) {
24047
24252
  clearTimeout(watchdogTimer);
@@ -24052,31 +24257,67 @@ function createRunTurn(bag) {
24052
24257
  watchdogGraceTimer = null;
24053
24258
  }
24054
24259
  };
24055
- watchdogTimer = setTimeout(() => {
24056
- if (flags.disposed) return;
24057
- watchdogTripped = true;
24058
- const elapsed = Date.now() - startedAt;
24059
- tuiDebug2(`runTurn WATCHDOG TRIP turn=${turnIndex} elapsedMs=${elapsed} \u2014 aborting stuck turn`);
24060
- pushNotice(`Turn timed out after ${Math.round(elapsed / 1e3)}s \u2014 aborting stuck request. Input is available again.`, "warn", { transcript: true });
24061
- try {
24062
- runtime.abort("cli-react-abort-watchdog");
24063
- } catch {
24260
+ const armWatchdog = () => {
24261
+ if (watchdogTimer) {
24262
+ clearTimeout(watchdogTimer);
24263
+ watchdogTimer = null;
24064
24264
  }
24065
- watchdogGraceTimer = setTimeout(() => {
24066
- if (flags.disposed) return;
24067
- if (!getState().busy) return;
24068
- if (flags.leadTurnEpoch !== turnEpoch) return;
24069
- tuiDebug2(`runTurn WATCHDOG FORCE-RELEASE turn=${turnIndex} \u2014 abort unwind starved`);
24070
- flags.leadTurnEpoch++;
24071
- set({ busy: false, spinner: null, thinking: null, lastTurn: null });
24072
- flags.activePromptRestore = null;
24073
- if (flags.draining) flags.draining = false;
24074
- if (pending.length > 0) void drain();
24075
- flushDeferredExecutionPendingResumeKick();
24076
- }, 5e3);
24077
- watchdogGraceTimer.unref?.();
24078
- }, LEAD_TURN_TIMEOUT_MS2);
24079
- watchdogTimer.unref?.();
24265
+ if (watchdogTripped) return;
24266
+ const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS2 - Math.max(0, Date.now() - lastProgressAt));
24267
+ watchdogTimer = setTimeout(() => {
24268
+ if (!isCurrentTurn()) return;
24269
+ if (watchdogTripped) return;
24270
+ const idleMs = Date.now() - lastProgressAt;
24271
+ if (idleMs < LEAD_TURN_TIMEOUT_MS2) {
24272
+ armWatchdog();
24273
+ return;
24274
+ }
24275
+ watchdogTripped = true;
24276
+ if (_batchTimer !== null) {
24277
+ clearTimeout(_batchTimer);
24278
+ _batchTimer = null;
24279
+ }
24280
+ _pendingTextFlush = false;
24281
+ _pendingThinkFlush = false;
24282
+ _pendingThinkingLastEndedAt = 0;
24283
+ const elapsed = Date.now() - startedAt;
24284
+ tuiDebug2(`runTurn WATCHDOG TRIP turn=${turnIndex} elapsedMs=${elapsed} idleMs=${idleMs} lastProgress=${lastProgressLabel} \u2014 aborting stuck turn`);
24285
+ pushNotice(`Turn timed out after ${Math.round(idleMs / 1e3)}s idle \u2014 aborting stuck request. Input will be released shortly if abort does not unwind.`, "warn", { transcript: true });
24286
+ try {
24287
+ runtime.abort("cli-react-abort-watchdog");
24288
+ } catch {
24289
+ }
24290
+ watchdogGraceTimer = setTimeout(() => {
24291
+ if (flags.disposed) return;
24292
+ if (!getState().busy) return;
24293
+ if (flags.leadTurnEpoch !== turnEpoch) return;
24294
+ tuiDebug2(`runTurn WATCHDOG FORCE-RELEASE turn=${turnIndex} \u2014 abort unwind starved`);
24295
+ denyAllToolApprovals("turn timed out");
24296
+ clearActiveToolSummary();
24297
+ flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
24298
+ finalizeToolHeaders();
24299
+ clearDeferredTimers();
24300
+ flags.flushDeferredBeforeImmediatePush = null;
24301
+ flags.leadTurnEpoch++;
24302
+ set({ busy: false, spinner: null, thinking: null, lastTurn: null });
24303
+ flags.activePromptRestore = null;
24304
+ if (flags.draining) flags.draining = false;
24305
+ if (pending.length > 0) void drain();
24306
+ flushDeferredExecutionPendingResumeKick();
24307
+ }, 5e3);
24308
+ watchdogGraceTimer.unref?.();
24309
+ }, remaining);
24310
+ watchdogTimer.unref?.();
24311
+ };
24312
+ const markTurnProgress = (label) => {
24313
+ if (!isCurrentTurn()) return false;
24314
+ if (watchdogTripped) return;
24315
+ lastProgressAt = Date.now();
24316
+ lastProgressLabel = String(label || "progress");
24317
+ armWatchdog();
24318
+ return true;
24319
+ };
24320
+ armWatchdog();
24080
24321
  let currentAssistantText = "";
24081
24322
  let thinkingText = "";
24082
24323
  let thinkingStartedAt = 0;
@@ -24097,6 +24338,7 @@ function createRunTurn(bag) {
24097
24338
  let deferredSeqCounter = 0;
24098
24339
  const deferredEntries = [];
24099
24340
  const flushDeferredUpTo = (entry) => {
24341
+ if (!isCurrentTurn()) return;
24100
24342
  if (!entry) return;
24101
24343
  const specs = collectDeferredUpTo(entry);
24102
24344
  if (!specs.length) return;
@@ -24141,7 +24383,7 @@ function createRunTurn(bag) {
24141
24383
  deferredEntries.push(entry);
24142
24384
  entry.timer = setTimeout(() => {
24143
24385
  entry.timer = null;
24144
- if (flags.disposed) return;
24386
+ if (!isCurrentTurn()) return;
24145
24387
  flushDeferredUpTo(entry);
24146
24388
  }, TOOL_CARD_PUSH_DELAY_MS);
24147
24389
  entry.timer.unref?.();
@@ -24173,7 +24415,7 @@ function createRunTurn(bag) {
24173
24415
  deferredEntries.push(entry);
24174
24416
  entry.timer = setTimeout(() => {
24175
24417
  entry.timer = null;
24176
- if (flags.disposed) return;
24418
+ if (!isCurrentTurn()) return;
24177
24419
  flushDeferredUpTo(entry);
24178
24420
  }, TOOL_CARD_PUSH_DELAY_MS);
24179
24421
  entry.timer.unref?.();
@@ -24203,6 +24445,7 @@ function createRunTurn(bag) {
24203
24445
  return specs;
24204
24446
  };
24205
24447
  const appendItemsBatch = (newItems, extra = {}) => {
24448
+ if (!isCurrentTurn()) return;
24206
24449
  if (!newItems || !newItems.length) {
24207
24450
  set(extra);
24208
24451
  return;
@@ -24257,6 +24500,7 @@ function createRunTurn(bag) {
24257
24500
  if (allCalls.length === 0) continue;
24258
24501
  aggregate.ensureVisible?.();
24259
24502
  const errors = allCalls.filter((r) => r.isError).length;
24503
+ const callErrors = allCalls.filter((r) => r.isCallError).length;
24260
24504
  const completed = allCalls.filter((r) => r.resolved).length;
24261
24505
  const succeeded = completed - errors;
24262
24506
  const rawResult = aggregateRawResult(allCalls);
@@ -24266,6 +24510,8 @@ function createRunTurn(bag) {
24266
24510
  text: displayDetail,
24267
24511
  rawResult: rawResult || null,
24268
24512
  isError: errors > 0,
24513
+ errorCount: errors,
24514
+ callErrorCount: callErrors,
24269
24515
  count: allCalls.length,
24270
24516
  completedCount: allCalls.length,
24271
24517
  doneCategories: aggregateDoneCategories(allCalls),
@@ -24389,6 +24635,12 @@ function createRunTurn(bag) {
24389
24635
  clearTimeout(_batchTimer);
24390
24636
  _batchTimer = null;
24391
24637
  }
24638
+ if (!isCurrentTurn()) {
24639
+ _pendingTextFlush = false;
24640
+ _pendingThinkFlush = false;
24641
+ _pendingThinkingLastEndedAt = 0;
24642
+ return;
24643
+ }
24392
24644
  if (_pendingTextFlush) {
24393
24645
  _pendingTextFlush = false;
24394
24646
  const textLen = currentAssistantText.length;
@@ -24467,16 +24719,20 @@ function createRunTurn(bag) {
24467
24719
  aggregate.ensureVisible?.();
24468
24720
  const rawText = toolResultText(message?.content);
24469
24721
  const isMemoryCall = classifyToolCategory(callRec.name, callRec.args) === "Memory";
24470
- const isError = message?.isError === true || message?.toolKind === "error" || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || isMemoryCall && memoryCoreResultErrorText(rawText) != null;
24722
+ const isCallError = message?.isError === true || message?.toolKind === "error";
24723
+ const isResultError = /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || isMemoryCall && memoryCoreResultErrorText(rawText) != null;
24724
+ const isError = isCallError || isResultError;
24471
24725
  const text = isError ? toolErrorDisplay(rawText, callRec.name || "tool") : rawText;
24472
24726
  callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
24473
24727
  assignAggregateSummaryOrder(aggregate, callRec);
24474
24728
  callRec.isError = isError;
24729
+ callRec.isCallError = isCallError;
24475
24730
  callRec.resultText = text;
24476
24731
  callRec.completedEarly = true;
24477
24732
  const allCalls = [...aggregate.calls.values()];
24478
24733
  const completedCount = allCalls.filter((r) => r.resolved || r.completedEarly).length;
24479
24734
  const errors = allCalls.filter((r) => r.isError).length;
24735
+ const callErrors = allCalls.filter((r) => r.isCallError).length;
24480
24736
  const succeeded = completedCount - errors;
24481
24737
  const rawResult = aggregateRawResult(allCalls);
24482
24738
  const displayDetail = errors > 0 ? succeeded > 0 ? `${succeeded} Ok \xB7 ${errors} Failed` : `${errors} Failed` : formatAggregateDetail(aggregateSummaries(aggregate));
@@ -24490,6 +24746,7 @@ function createRunTurn(bag) {
24490
24746
  text: displayDetail,
24491
24747
  isError: errors > 0,
24492
24748
  errorCount: errors,
24749
+ callErrorCount: callErrors,
24493
24750
  count: allCalls.length,
24494
24751
  completedCount: visualCompleted
24495
24752
  };
@@ -24510,8 +24767,9 @@ function createRunTurn(bag) {
24510
24767
  };
24511
24768
  try {
24512
24769
  const { result, session } = await runtime.ask(userText, {
24513
- drainSteering: () => drainPendingSteering(),
24770
+ drainSteering: (_sessionId, drainOptions) => isCurrentTurn() ? drainPendingSteering(drainOptions) : [],
24514
24771
  onSteerMessage: (text) => {
24772
+ if (!markTurnProgress("steer-message")) return;
24515
24773
  flushStreamBatch();
24516
24774
  if (currentAssistantId) {
24517
24775
  patchItem(currentAssistantId, { text: currentAssistantText || assistantText, streaming: false });
@@ -24525,6 +24783,7 @@ function createRunTurn(bag) {
24525
24783
  }
24526
24784
  },
24527
24785
  onToolCall: async (_iter, calls) => {
24786
+ if (!markTurnProgress("tool-call")) return;
24528
24787
  markPromptCommitted();
24529
24788
  flushStreamBatch();
24530
24789
  if (thinkingText && getState().thinking) {
@@ -24538,7 +24797,8 @@ function createRunTurn(bag) {
24538
24797
  if (batchCalls.length === 0) return;
24539
24798
  const committedAssistantSegment = commitAssistantSegment({ sealToolBlock: true });
24540
24799
  if (committedAssistantSegment) {
24541
- await yieldToRenderer();
24800
+ await yieldToRenderer({ frames: 2 });
24801
+ if (!isCurrentTurn()) return;
24542
24802
  }
24543
24803
  const touchedAggregates = /* @__PURE__ */ new Set();
24544
24804
  let standaloneReserve = null;
@@ -24590,7 +24850,7 @@ function createRunTurn(bag) {
24590
24850
  ...categoryEntry,
24591
24851
  count: Number(prevCategory?.count || 0) + Number(categoryEntry.count || 1)
24592
24852
  });
24593
- aggregateCard.calls.set(callKey, { name, args, category, summary: null, summarySeq: null, isError: false, resultText: null, resolved: false, completedEarly: false });
24853
+ aggregateCard.calls.set(callKey, { name, args, category, summary: null, summarySeq: null, isError: false, isCallError: false, resultText: null, resolved: false, completedEarly: false });
24594
24854
  touchedAggregates.add(aggregateCard);
24595
24855
  const card = { itemId: aggregateCard.itemId, callId: callKey, done: false, aggregate: aggregateCard };
24596
24856
  if (callId) {
@@ -24614,6 +24874,7 @@ function createRunTurn(bag) {
24614
24874
  await yieldToRenderer();
24615
24875
  },
24616
24876
  onToolResult: (message) => {
24877
+ if (!markTurnProgress("tool-result")) return;
24617
24878
  const callId = toolResultCallId(message);
24618
24879
  if (callId && !cardByCallId.has(callId) && !resultsDone.has(callId)) {
24619
24880
  earlyResultBuffer.set(callId, message);
@@ -24622,12 +24883,16 @@ function createRunTurn(bag) {
24622
24883
  deliverToolResultMessage(message);
24623
24884
  },
24624
24885
  onToolApproval: async (request) => {
24886
+ if (!markTurnProgress("tool-approval")) return { approved: false, reason: "turn no longer active" };
24625
24887
  markPromptCommitted();
24626
24888
  flushStreamBatch();
24627
24889
  if (getState().spinner) set({ spinner: { ...getState().spinner, mode: "tool-approval" } });
24628
- return await requestToolApproval(request);
24890
+ const approval = await requestToolApproval(request);
24891
+ if (!isCurrentTurn()) return { approved: false, reason: "turn no longer active" };
24892
+ return approval;
24629
24893
  },
24630
24894
  onCompactEvent: (event) => {
24895
+ if (!markTurnProgress("compact-event")) return;
24631
24896
  flushStreamBatch();
24632
24897
  clearAggregateContinuation();
24633
24898
  pushItem({
@@ -24638,6 +24903,7 @@ function createRunTurn(bag) {
24638
24903
  });
24639
24904
  },
24640
24905
  onStageChange: async (stage) => {
24906
+ if (!markTurnProgress(`stage:${String(stage || "")}`)) return;
24641
24907
  if (!getState().spinner) return;
24642
24908
  const value = String(stage || "");
24643
24909
  if (value === "compacting") {
@@ -24667,6 +24933,7 @@ function createRunTurn(bag) {
24667
24933
  onTextDelta: (chunk) => {
24668
24934
  const textChunk = String(chunk ?? "");
24669
24935
  if (!textChunk) return;
24936
+ if (!markTurnProgress("text-delta")) return;
24670
24937
  markPromptCommitted();
24671
24938
  const thinkingLastEndedAt = closeThinkingSegment();
24672
24939
  _pendingThinkFlush = false;
@@ -24683,6 +24950,7 @@ function createRunTurn(bag) {
24683
24950
  onAssistantText: (text) => {
24684
24951
  const full = String(text ?? "");
24685
24952
  if (!full.trim()) return;
24953
+ if (!markTurnProgress("assistant-text")) return;
24686
24954
  if (currentAssistantText.trim()) return;
24687
24955
  markPromptCommitted();
24688
24956
  closeThinkingSegment();
@@ -24697,13 +24965,18 @@ function createRunTurn(bag) {
24697
24965
  flushStreamBatch();
24698
24966
  },
24699
24967
  onReasoningDelta: (chunk) => {
24700
- if (String(chunk ?? "")) markPromptCommitted();
24968
+ if (!isCurrentTurn() || watchdogTripped) return;
24969
+ if (String(chunk ?? "")) {
24970
+ if (!markTurnProgress("reasoning-delta")) return;
24971
+ markPromptCommitted();
24972
+ }
24701
24973
  startThinkingSegment();
24702
24974
  thinkingText += String(chunk ?? "");
24703
24975
  _pendingThinkFlush = true;
24704
24976
  scheduleStreamFlush();
24705
24977
  },
24706
24978
  onUsageDelta: (delta) => {
24979
+ if (!markTurnProgress("usage-delta")) return;
24707
24980
  applyUsageDelta(getState().stats, delta);
24708
24981
  syncContextStats({ allowEstimated: true });
24709
24982
  const currentTurnInput = Math.max(0, getState().stats.inputTokens - inputBaseline);
@@ -24715,64 +24988,73 @@ function createRunTurn(bag) {
24715
24988
  }
24716
24989
  }
24717
24990
  });
24718
- askResult = result;
24719
- markPromptCommitted();
24720
- flushToolResults(session?.messages || [], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
24721
- finalizeToolHeaders();
24722
- flushStreamBatch();
24723
- syncContextStats({ allowEstimated: true });
24724
- const finalText = result?.content != null ? String(result.content) : "";
24725
- if (finalText.trim()) {
24726
- const id = currentAssistantId || ensureAssistant(finalText);
24727
- currentAssistantText = finalText;
24728
- patchItem(id, { text: finalText, streaming: false });
24729
- } else if (currentAssistantId && (currentAssistantText.trim() || assistantText.trim())) {
24730
- const streamedText = currentAssistantText || assistantText;
24731
- patchItem(currentAssistantId, { text: streamedText, streaming: false });
24732
- }
24733
- turnFinishedNormally = true;
24734
- } catch (error) {
24735
- flushStreamBatch();
24736
- if (error?.name === "SessionClosedError") {
24991
+ if (!isCurrentTurn()) {
24737
24992
  cancelled = true;
24738
- if (assistantText.trim() && currentAssistantId) {
24739
- patchItem(currentAssistantId, { text: currentAssistantText || assistantText, streaming: false });
24740
- }
24741
- flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
24742
- finalizeToolHeaders();
24743
24993
  } else {
24994
+ askResult = result;
24995
+ markPromptCommitted();
24996
+ flushToolResults(session?.messages || [], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
24744
24997
  finalizeToolHeaders();
24745
- pushNotice(toolErrorDisplay(error, "turn"), "error");
24998
+ flushStreamBatch();
24999
+ syncContextStats({ allowEstimated: true });
25000
+ const finalText = result?.content != null ? String(result.content) : "";
25001
+ if (finalText.trim()) {
25002
+ const id = currentAssistantId || ensureAssistant(finalText);
25003
+ currentAssistantText = finalText;
25004
+ patchItem(id, { text: finalText, streaming: false });
25005
+ } else if (currentAssistantId && (currentAssistantText.trim() || assistantText.trim())) {
25006
+ const streamedText = currentAssistantText || assistantText;
25007
+ patchItem(currentAssistantId, { text: streamedText, streaming: false });
25008
+ }
25009
+ turnFinishedNormally = true;
25010
+ }
25011
+ } catch (error) {
25012
+ const staleCatch = !isCurrentTurn();
25013
+ if (staleCatch) {
25014
+ cancelled = true;
25015
+ } else {
25016
+ flushStreamBatch();
25017
+ if (error?.name === "SessionClosedError") {
25018
+ cancelled = true;
25019
+ if (assistantText.trim() && currentAssistantId) {
25020
+ patchItem(currentAssistantId, { text: currentAssistantText || assistantText, streaming: false });
25021
+ }
25022
+ flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
25023
+ finalizeToolHeaders();
25024
+ } else {
25025
+ finalizeToolHeaders();
25026
+ pushNotice(toolErrorDisplay(error, "turn"), "error");
25027
+ }
24746
25028
  }
24747
25029
  } finally {
24748
- denyAllToolApprovals(cancelled ? "turn cancelled" : "turn finished");
25030
+ const isStaleUnwind = !isCurrentTurn();
25031
+ if (!isStaleUnwind) denyAllToolApprovals(cancelled ? "turn cancelled" : "turn finished");
24749
25032
  clearWatchdog();
24750
- const isStaleUnwind = flags.leadTurnEpoch !== turnEpoch;
24751
25033
  let closingItems = [];
24752
25034
  if (deferredEntries.length) {
24753
- const last = deferredEntries[deferredEntries.length - 1];
24754
- closingItems = collectDeferredUpTo(last);
25035
+ if (!isStaleUnwind) {
25036
+ const last = deferredEntries[deferredEntries.length - 1];
25037
+ closingItems = collectDeferredUpTo(last);
25038
+ }
24755
25039
  clearDeferredTimers();
24756
25040
  }
24757
- flags.flushDeferredBeforeImmediatePush = null;
24758
- const producedTranscriptItem = getState().items.length + closingItems.length > itemsAtTurnStart;
24759
- const reclaimed = cancelled && flags.activePromptRestore?.reclaimed === true;
24760
- if (!isStaleUnwind) flags.activePromptRestore = null;
25041
+ if (!isStaleUnwind) flags.flushDeferredBeforeImmediatePush = null;
24761
25042
  closeThinkingSegment();
24762
- const elapsedMs = Date.now() - startedAt;
24763
- const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
24764
- const finalAssistantLen = Math.max(assistantText.length, currentAssistantText.length);
24765
- const finalResponseLength = finalAssistantLen + thinkingText.length;
24766
- const finalOutputTokens = Math.max(0, Number(getState().spinner?.outputTokens || 0), Math.round(finalResponseLength / 4));
24767
- const turnStatus = cancelled ? "cancelled" : "done";
24768
- const resultContent = askResult?.content != null ? String(askResult.content).trim() : "";
24769
- const assistantOutput = (currentAssistantText || assistantText || "").trim();
24770
- const isNoOpTurn = turnFinishedNormally && !cancelled && toolCards.length === 0 && !resultContent && !assistantOutput && !producedTranscriptItem;
24771
25043
  if (isStaleUnwind) {
24772
- if (closingItems.length) appendItemsBatch(closingItems);
24773
- tuiDebug2(`runTurn STALE UNWIND turn=${turnIndex} \u2014 force-released; skipping shared-getState() writes`);
24774
- flushDeferredExecutionPendingResumeKick();
25044
+ tuiDebug2(`runTurn STALE UNWIND turn=${turnIndex} \u2014 force-released; skipping shared UI/state writes`);
24775
25045
  } else {
25046
+ const producedTranscriptItem = getState().items.length + closingItems.length > itemsAtTurnStart;
25047
+ const reclaimed = cancelled && flags.activePromptRestore?.reclaimed === true;
25048
+ flags.activePromptRestore = null;
25049
+ const elapsedMs = Date.now() - startedAt;
25050
+ const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
25051
+ const finalAssistantLen = Math.max(assistantText.length, currentAssistantText.length);
25052
+ const finalResponseLength = finalAssistantLen + thinkingText.length;
25053
+ const finalOutputTokens = Math.max(0, Number(getState().spinner?.outputTokens || 0), Math.round(finalResponseLength / 4));
25054
+ const turnStatus = cancelled ? "cancelled" : "done";
25055
+ const resultContent = askResult?.content != null ? String(askResult.content).trim() : "";
25056
+ const assistantOutput = (currentAssistantText || assistantText || "").trim();
25057
+ const isNoOpTurn = turnFinishedNormally && !cancelled && toolCards.length === 0 && !resultContent && !assistantOutput && !producedTranscriptItem;
24776
25058
  if (!isNoOpTurn) {
24777
25059
  getState().stats.turns = (getState().stats.turns || 0) + 1;
24778
25060
  }
@@ -25478,6 +25760,10 @@ async function buildDoctorReport(runtime = {}, getState = () => ({})) {
25478
25760
  }
25479
25761
 
25480
25762
  // src/tui/engine/session-api.mjs
25763
+ var MANUAL_ABORT_RECOVERY_MS = (() => {
25764
+ const v = Number(process.env.MIXDOG_MANUAL_ABORT_RECOVERY_MS);
25765
+ return Number.isFinite(v) && v > 0 ? v : 4e3;
25766
+ })();
25481
25767
  function createEngineApi(bag) {
25482
25768
  return { ...createEngineApiA(bag), ...createEngineApiB(bag) };
25483
25769
  }
@@ -25504,7 +25790,9 @@ function createEngineApiA(bag) {
25504
25790
  enqueue,
25505
25791
  autoClearBeforeSubmit,
25506
25792
  restoreQueued,
25507
- resetStatsAndSyncContext
25793
+ resetStatsAndSyncContext,
25794
+ drain,
25795
+ flushDeferredExecutionPendingResumeKick
25508
25796
  } = bag;
25509
25797
  return {
25510
25798
  getState: () => getState(),
@@ -25528,12 +25816,16 @@ function createEngineApiA(bag) {
25528
25816
  enqueue(text, queueOptions);
25529
25817
  return true;
25530
25818
  }
25531
- if (getState().commandBusy) return false;
25819
+ if (getState().commandBusy) {
25820
+ enqueue(text, queueOptions);
25821
+ return true;
25822
+ }
25532
25823
  if (getState().busy) {
25533
25824
  enqueue(text, queueOptions);
25534
25825
  return true;
25535
25826
  }
25536
- void autoClearBeforeSubmit().then(() => enqueue(text, queueOptions));
25827
+ void autoClearBeforeSubmit().catch(() => {
25828
+ }).then(() => enqueue(text, queueOptions));
25537
25829
  return true;
25538
25830
  },
25539
25831
  restoreQueued,
@@ -26013,6 +26305,22 @@ function createEngineApiA(bag) {
26013
26305
  restoreState.restorable = false;
26014
26306
  restoreState.requeueEntries = [];
26015
26307
  }
26308
+ const abortEpoch = flags.leadTurnEpoch;
26309
+ const recoveryMs = Number(flags.manualAbortRecoveryMs) > 0 ? Number(flags.manualAbortRecoveryMs) : MANUAL_ABORT_RECOVERY_MS;
26310
+ const recoveryTimer = setTimeout(() => {
26311
+ if (flags.disposed) return;
26312
+ if (!getState().busy) return;
26313
+ if (flags.leadTurnEpoch !== abortEpoch) return;
26314
+ flags.leadTurnEpoch = (Number(flags.leadTurnEpoch) || 0) + 1;
26315
+ set({ busy: false, spinner: null, thinking: null, lastTurn: null });
26316
+ flags.activePromptRestore = null;
26317
+ flags.drainEpoch = (Number(flags.drainEpoch) || 0) + 1;
26318
+ if (flags.draining) flags.draining = false;
26319
+ pushNotice("Interrupt did not settle \u2014 input restored.", "warn", { transcript: true });
26320
+ if (pending.length > 0 && typeof drain === "function") void drain();
26321
+ if (typeof flushDeferredExecutionPendingResumeKick === "function") flushDeferredExecutionPendingResumeKick();
26322
+ }, recoveryMs);
26323
+ recoveryTimer.unref?.();
26016
26324
  return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages, pastedTexts: restorePastedTexts, discardPastedTexts };
26017
26325
  }
26018
26326
  };
@@ -26149,8 +26457,12 @@ async function createEngineSession({
26149
26457
  }
26150
26458
  }
26151
26459
  if (!changed) return false;
26460
+ const commandBusyReleased = state.commandBusy === true && Object.prototype.hasOwnProperty.call(patch, "commandBusy") && patch.commandBusy === false;
26152
26461
  state = { ...state, ...patch };
26153
26462
  emit();
26463
+ if (commandBusyReleased) queueMicrotask(() => {
26464
+ void bag.drain?.();
26465
+ });
26154
26466
  return true;
26155
26467
  };
26156
26468
  const itemIndexById = /* @__PURE__ */ new Map();
@@ -26342,6 +26654,7 @@ async function createEngineSession({
26342
26654
  flushDeferredExecutionPendingResumeKick,
26343
26655
  scheduleExecutionPendingResumeKick,
26344
26656
  updateAgentJobCard,
26657
+ buildAgentJobCardPatch,
26345
26658
  subscribeRuntimeNotifications
26346
26659
  } = createAgentJobFeed({
26347
26660
  runtime,
@@ -26375,6 +26688,7 @@ async function createEngineSession({
26375
26688
  patchItem,
26376
26689
  markToolCallDone,
26377
26690
  updateAgentJobCard,
26691
+ buildAgentJobCardPatch,
26378
26692
  agentStatusState
26379
26693
  });
26380
26694
  Object.assign(bag, {
@@ -26676,7 +26990,10 @@ function installTuiPerfProbe() {
26676
26990
  };
26677
26991
  }
26678
26992
  function makeRenderProfiler() {
26679
- if (!PERF_ENABLED) return void 0;
26993
+ const ackRenderedFrame = () => {
26994
+ scheduleRenderFrameAck();
26995
+ };
26996
+ if (!PERF_ENABLED) return ackRenderedFrame;
26680
26997
  let count = 0;
26681
26998
  let sum = 0;
26682
26999
  let max = 0;
@@ -26684,6 +27001,7 @@ function makeRenderProfiler() {
26684
27001
  let maxGap = 0;
26685
27002
  let lastFrameAt = 0;
26686
27003
  return ({ renderTime } = {}) => {
27004
+ ackRenderedFrame();
26687
27005
  const now = performance3.now();
26688
27006
  const ms = Number(renderTime) || 0;
26689
27007
  const gap = lastFrameAt ? now - lastFrameAt : 0;