mixdog 0.9.38 → 0.9.40

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 (124) hide show
  1. package/package.json +9 -4
  2. package/scripts/abort-recovery-test.mjs +43 -2
  3. package/scripts/agent-tag-reuse-smoke.mjs +146 -6
  4. package/scripts/agent-terminal-reap-test.mjs +127 -0
  5. package/scripts/agent-trace-io-test.mjs +69 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +224 -0
  7. package/scripts/execution-completion-dedup-test.mjs +157 -0
  8. package/scripts/execution-pending-resume-kick-test.mjs +57 -2
  9. package/scripts/execution-resume-esc-integration-test.mjs +176 -0
  10. package/scripts/explore-bench.mjs +38 -2
  11. package/scripts/explore-prompt-policy-test.mjs +152 -11
  12. package/scripts/find-fuzzy-hidden-test.mjs +122 -0
  13. package/scripts/internal-comms-bench-test.mjs +226 -0
  14. package/scripts/internal-comms-bench.mjs +185 -58
  15. package/scripts/internal-comms-smoke.mjs +171 -23
  16. package/scripts/live-worker-smoke.mjs +38 -2
  17. package/scripts/memory-cycle-routing-test.mjs +111 -0
  18. package/scripts/memory-rule-contract-test.mjs +93 -0
  19. package/scripts/output-style-smoke.mjs +2 -2
  20. package/scripts/rg-runner-test.mjs +240 -0
  21. package/scripts/routing-corpus-test.mjs +349 -0
  22. package/scripts/routing-corpus.mjs +211 -32
  23. package/scripts/session-orphan-sweep-test.mjs +83 -0
  24. package/scripts/steering-drain-buckets-test.mjs +316 -2
  25. package/scripts/tool-smoke.mjs +21 -13
  26. package/scripts/tool-tui-presentation-test.mjs +202 -0
  27. package/scripts/tui-transcript-perf-test.mjs +279 -0
  28. package/src/agents/heavy-worker/AGENT.md +10 -7
  29. package/src/agents/reviewer/AGENT.md +6 -4
  30. package/src/agents/worker/AGENT.md +7 -5
  31. package/src/rules/agent/00-common.md +4 -4
  32. package/src/rules/agent/00-core.md +11 -14
  33. package/src/rules/agent/20-skip-protocol.md +3 -3
  34. package/src/rules/agent/30-explorer.md +50 -60
  35. package/src/rules/agent/40-cycle1-agent.md +15 -24
  36. package/src/rules/agent/41-cycle2-agent.md +33 -57
  37. package/src/rules/agent/42-cycle3-agent.md +28 -42
  38. package/src/rules/lead/01-general.md +7 -10
  39. package/src/rules/lead/lead-brief.md +11 -14
  40. package/src/rules/lead/lead-tool.md +6 -5
  41. package/src/rules/shared/01-tool.md +44 -45
  42. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
  43. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
  44. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
  47. package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
  48. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
  50. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
  51. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
  52. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
  53. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
  56. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
  59. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
  60. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
  62. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
  68. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
  70. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
  72. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
  74. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
  75. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
  76. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
  77. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
  78. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
  81. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
  82. package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
  83. package/src/runtime/shared/buffered-appender.mjs +13 -2
  84. package/src/runtime/shared/tool-primitives.mjs +4 -1
  85. package/src/runtime/shared/tool-status.mjs +27 -0
  86. package/src/runtime/shared/tool-surface.mjs +6 -3
  87. package/src/session-runtime/config-helpers.mjs +14 -0
  88. package/src/session-runtime/context-status.mjs +1 -0
  89. package/src/session-runtime/effort.mjs +6 -2
  90. package/src/session-runtime/lifecycle-api.mjs +4 -0
  91. package/src/session-runtime/model-recency.mjs +5 -2
  92. package/src/session-runtime/runtime-core.mjs +36 -2
  93. package/src/session-runtime/session-turn-api.mjs +12 -0
  94. package/src/session-runtime/tool-catalog.mjs +34 -0
  95. package/src/standalone/agent-tool/notify.mjs +13 -0
  96. package/src/standalone/agent-tool.mjs +53 -70
  97. package/src/standalone/explore-tool.mjs +6 -7
  98. package/src/tui/App.jsx +33 -1
  99. package/src/tui/app/model-options.mjs +5 -3
  100. package/src/tui/app/model-picker.mjs +12 -24
  101. package/src/tui/app/transcript-window.mjs +10 -10
  102. package/src/tui/app/use-transcript-window.mjs +38 -56
  103. package/src/tui/components/ToolExecution.jsx +11 -6
  104. package/src/tui/components/TranscriptItem.jsx +1 -1
  105. package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
  106. package/src/tui/components/tool-execution/text-format.mjs +10 -19
  107. package/src/tui/dist/index.mjs +866 -300
  108. package/src/tui/engine/agent-job-feed.mjs +216 -19
  109. package/src/tui/engine/agent-response-tail.mjs +68 -0
  110. package/src/tui/engine/notification-plan.mjs +16 -0
  111. package/src/tui/engine/session-api.mjs +14 -2
  112. package/src/tui/engine/session-flow.mjs +22 -2
  113. package/src/tui/engine/tool-card-results.mjs +64 -37
  114. package/src/tui/engine/tool-result-status.mjs +75 -21
  115. package/src/tui/engine/turn.mjs +172 -77
  116. package/src/tui/engine.mjs +199 -39
  117. package/src/tui/index.jsx +2 -2
  118. package/src/workflows/bench/WORKFLOW.md +25 -35
  119. package/src/workflows/default/WORKFLOW.md +38 -32
  120. package/src/workflows/solo/WORKFLOW.md +19 -22
  121. package/scripts/_jitter-fuzz.mjs +0 -44410
  122. package/scripts/_jitter-fuzz2.mjs +0 -44400
  123. package/scripts/_jitter-probe.mjs +0 -44397
  124. package/scripts/_jp2.mjs +0 -45614
@@ -10,17 +10,18 @@
10
10
  * the factory argument (getters/callbacks) — never stale snapshots. Every body
11
11
  * is the original engine.mjs logic verbatim.
12
12
  */
13
- import { summarizeToolResult, aggregateDoneCategories, classifyToolCategory } from '../../runtime/shared/tool-surface.mjs';
13
+ import { summarizeToolResult, aggregateDoneCategories } from '../../runtime/shared/tool-surface.mjs';
14
14
  import { toolResultText, toolErrorDisplay, toolGroupedDisplayFallback } from './tool-result-text.mjs';
15
15
  import { toolResultCallId } from './tool-call-fields.mjs';
16
- import { memoryCoreResultErrorText } from '../app/input-parsers.mjs';
17
- import { parseAgentJob, toolResultStatus, isErrorToolStatus } from './agent-envelope.mjs';
16
+ import { parseAgentJob } from './agent-envelope.mjs';
18
17
  import {
19
18
  withCancelledResultMarker,
20
19
  groupedToolResultText,
21
20
  aggregateRawResult,
22
21
  aggregateSummaries,
23
22
  assignAggregateSummaryOrder,
23
+ failureDetailText,
24
+ toolCallOutcome,
24
25
  } from './tool-result-status.mjs';
25
26
  import { formatAggregateDetail } from '../../runtime/shared/tool-surface.mjs';
26
27
  import { carryTranscriptMeasuredRowsCache } from '../app/transcript-window.mjs';
@@ -33,12 +34,28 @@ export function createToolCardResults({
33
34
  updateAgentJobCard,
34
35
  buildAgentJobCardPatch,
35
36
  agentStatusState,
37
+ itemIndexById,
36
38
  }) {
39
+ const itemById = (id) => {
40
+ const index = itemIndexById?.get(id);
41
+ return Number.isInteger(index) ? getState().items[index]?.id === id ? getState().items[index] : null : null;
42
+ };
43
+ // A finalized/failed non-aggregate card must never carry an empty body:
44
+ // an empty-body error card is classified fully-failed-with-no-body upstream
45
+ // (transcript-tool-failures) and null-renders (card disappears). Stamp a
46
+ // minimal non-empty fallback, preferring meaningful text, then Exit N, then
47
+ // a bare Failed status.
48
+ function finalizedErrorFallbackBody(body, text, exitCode) {
49
+ if (String(body || '').trim()) return body;
50
+ if (String(text || '').trim()) return text;
51
+ if (exitCode != null) return `Exit ${exitCode}`;
52
+ return 'Failed';
53
+ }
37
54
  function patchToolItem(id, patch) {
38
- const prev = getState().items.find((it) => it.id === id);
55
+ const prev = itemById(id);
39
56
  const ok = patchItem(id, patch);
40
57
  if (!ok || !prev) return ok;
41
- const next = getState().items.find((it) => it.id === id);
58
+ const next = itemById(id);
42
59
  if (next && next !== prev) carryTranscriptMeasuredRowsCache(prev, next);
43
60
  return ok;
44
61
  }
@@ -60,22 +77,11 @@ export function createToolCardResults({
60
77
  // Aggregate card handling — collect semantic summaries per call
61
78
  const aggregate = card.aggregate;
62
79
  const callRec = aggregate && callId ? aggregate.calls.get(callId) : null;
63
- // Backend "core" memory-op failures are flattened to plain text (isError
64
- // dropped upstream see memoryCoreResultErrorText); recover that signal so
65
- // failed memory writes are excluded from the done count. Gated to Memory
66
- // calls: the text-matcher's ^(error|failed) catch-all would otherwise
67
- // misflag legitimate non-memory success output.
68
- const isMemoryCall = classifyToolCategory(callRec?.name || card?.name || '', callRec?.args || {}) === 'Memory';
69
- // Split the failure signal into two:
70
- // isCallError — a REAL tool-call failure (backend isError / error
71
- // toolKind). ONLY this paints the ● dot red.
72
- // isResultError — a command/result failure (shell exit code, [error…]
73
- // text, failed status text, flattened core memory-op
74
- // failure). These still mark the card Failed in the L2
75
- // detail but must NOT turn the dot red.
76
- const isCallError = message?.isError === true || message?.toolKind === 'error';
77
- const isResultError = /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || (isMemoryCall && memoryCoreResultErrorText(rawText) != null);
78
- const isError = isCallError || isResultError;
80
+ // Only a provider-marked invocation failure contributes to failure count,
81
+ // red state, or Failed aggregate copy. Tool-reported HTTP/domain/status
82
+ // outcomes remain successful calls with their raw/semantic result detail.
83
+ const { exitCode, isExitError, isCallError } = toolCallOutcome(message, rawText);
84
+ const isError = isCallError;
79
85
  const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
80
86
 
81
87
  if (aggregate && card.itemId === aggregate.itemId) {
@@ -89,21 +95,24 @@ export function createToolCardResults({
89
95
  assignAggregateSummaryOrder(aggregate, callRec);
90
96
  callRec.isError = isError;
91
97
  callRec.isCallError = isCallError;
98
+ callRec.isExitError = isExitError;
99
+ callRec.exitCode = exitCode;
92
100
  callRec.resultText = text;
93
101
  callRec.resolved = true;
94
102
  const allCalls = [...aggregate.calls.values()];
95
103
  const completed = allCalls.filter((r) => r.resolved).length;
96
104
  const errors = allCalls.filter((r) => r.isError).length;
97
105
  const callErrors = allCalls.filter((r) => r.isCallError).length;
106
+ const exitErrors = allCalls.filter((r) => r.isExitError).length;
98
107
  // Collapsed detail carries the merged per-call count summary
99
108
  // ("512 lines, 6 matches, 3 files") so the finished card answers "how
100
109
  // much" without ctrl+o. Failures keep a bare 'N Ok · N Failed' status so
101
110
  // an error stays visible while collapsed.
102
- const succeeded = completed - errors;
103
- const detailText = errors > 0
104
- ? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
111
+ const succeeded = Math.max(0, completed - errors - exitErrors);
112
+ const detailText = errors > 0 || exitErrors > 0
113
+ ? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode })
105
114
  : formatAggregateDetail(aggregateSummaries(aggregate));
106
- const currentItem = getState().items.find((it) => it.id === card.itemId);
115
+ const currentItem = itemById(card.itemId);
107
116
  const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
108
117
  const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
109
118
  const rawResult = aggregateRawResult(allCalls);
@@ -116,6 +125,7 @@ export function createToolCardResults({
116
125
  isError: errors > 0,
117
126
  errorCount: errors,
118
127
  callErrorCount: callErrors,
128
+ exitErrorCount: exitErrors,
119
129
  count: allCalls.length,
120
130
  completedCount: visualCompleted,
121
131
  doneCategories: aggregateDoneCategories(allCalls),
@@ -127,20 +137,25 @@ export function createToolCardResults({
127
137
  }
128
138
 
129
139
  // Non-aggregate (legacy agent-job cards, etc.)
130
- const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, callErrors: 0, results: [] };
140
+ const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, callErrors: 0, exitErrors: 0, results: [] };
131
141
  group.completed = Math.min(group.count, group.completed + 1);
132
142
  group.errors += isError ? 1 : 0;
133
143
  group.callErrors = (group.callErrors || 0) + (isCallError ? 1 : 0);
134
- group.results.push({ text, isError });
144
+ group.exitErrors = (group.exitErrors || 0) + (isExitError ? 1 : 0);
145
+ group.results.push({ text, isError, isExitError, exitCode });
135
146
  toolGroups.set(card.itemId, group);
136
147
  const resultText = groupedToolResultText(group);
137
- const displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
148
+ let displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
149
+ if (group.errors > 0 && !String(displayResult || '').trim()) {
150
+ displayResult = finalizedErrorFallbackBody(displayResult, text, exitCode);
151
+ }
138
152
  const patch = {
139
153
  result: displayResult,
140
154
  text: displayResult,
141
155
  isError: group.errors > 0,
142
156
  errorCount: group.errors,
143
157
  callErrorCount: group.callErrors || 0,
158
+ exitErrorCount: group.exitErrors || 0,
144
159
  count: group.count,
145
160
  completedCount: group.completed,
146
161
  completedAt: Date.now(),
@@ -159,6 +174,11 @@ export function createToolCardResults({
159
174
  // The agent fields win (final display) while patch keeps the completion
160
175
  // metadata (count/completedCount/completedAt/rawResult) for expand.
161
176
  Object.assign(patch, buildAgentJobCardPatch(card.itemId, rawText, isError));
177
+ // Re-apply the empty-body guard: buildAgentJobCardPatch may overwrite the
178
+ // stamped fallback with an empty agent-job body, letting the card vanish.
179
+ if (group.errors > 0 && !String(patch.result || '').trim()) {
180
+ patch.result = patch.text = finalizedErrorFallbackBody(patch.result, text, exitCode);
181
+ }
162
182
  }
163
183
  patchToolItem(card.itemId, patch);
164
184
  card.done = true;
@@ -223,18 +243,20 @@ export function createToolCardResults({
223
243
  const totalCompleted = completed;
224
244
  const errors = allCalls.filter((r) => r.isError).length;
225
245
  const callErrors = allCalls.filter((r) => r.isCallError).length;
226
- const succeeded = completed - errors;
246
+ const exitErrors = allCalls.filter((r) => r.isExitError).length;
247
+ const succeeded = Math.max(0, completed - errors - exitErrors);
227
248
  const rawResult = aggregateRawResult(allCalls);
228
- // Collapsed detail carries the merged per-call count summary; failures
229
- // keep a bare 'N Ok · N Failed' status. Raw is kept for ctrl+o.
230
- let displayDetail = errors > 0
231
- ? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
249
+ // Collapsed detail carries the merged per-call count summary; real
250
+ // failures keep 'N Failed', shell command-exits show 'Exit N'/'Y Exit'.
251
+ // Raw is kept for ctrl+o.
252
+ let displayDetail = errors > 0 || exitErrors > 0
253
+ ? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode })
232
254
  : formatAggregateDetail(aggregateSummaries(aggregate));
233
255
  if (cancelled) {
234
256
  // Cancelled aggregates MUST keep the [status: cancelled] marker on the
235
257
  // result so terminalStatus parsing resolves to 'cancelled'. Only normal
236
258
  // completions drop the summary; cancelled ones prepend the marker.
237
- const currentItem = getState().items.find((it) => it.id === card.itemId);
259
+ const currentItem = itemById(card.itemId);
238
260
  displayDetail = withCancelledResultMarker(displayDetail, currentItem);
239
261
  }
240
262
  patchToolItem(card.itemId, {
@@ -244,6 +266,7 @@ export function createToolCardResults({
244
266
  isError: errors > 0,
245
267
  errorCount: errors,
246
268
  callErrorCount: callErrors,
269
+ exitErrorCount: exitErrors,
247
270
  count: allCalls.length,
248
271
  completedCount: totalCompleted,
249
272
  doneCategories: aggregateDoneCategories(allCalls),
@@ -257,15 +280,19 @@ export function createToolCardResults({
257
280
  continue;
258
281
  }
259
282
  // Non-aggregate finalize
260
- const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
283
+ const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, exitErrors: 0, results: [] };
261
284
  group.completed = Math.min(group.count, group.completed + 1);
262
285
  toolGroups.set(card.itemId, group);
263
286
  let resultText = groupedToolResultText(group);
287
+ if (group.errors > 0 && !String(resultText || '').trim()) {
288
+ const exitRec = (group.results || []).find((r) => r && r.isExitError);
289
+ resultText = finalizedErrorFallbackBody(resultText, exitRec?.text, exitRec?.exitCode);
290
+ }
264
291
  if (cancelled) {
265
- const currentItem = getState().items.find((it) => it.id === card.itemId);
292
+ const currentItem = itemById(card.itemId);
266
293
  resultText = withCancelledResultMarker(resultText, currentItem);
267
294
  }
268
- 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() });
295
+ patchToolItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, callErrorCount: group.callErrors || 0, exitErrorCount: group.exitErrors || 0, count: group.count, completedCount: group.completed, completedAt: Date.now() });
269
296
  card.done = true;
270
297
  if (card.callId) done.add(card.callId);
271
298
  }
@@ -8,27 +8,75 @@
8
8
  * keeps calling them unchanged.
9
9
  */
10
10
  import { toolErrorDisplay } from './tool-result-text.mjs';
11
+ import {
12
+ normalizeToolTerminalStatus,
13
+ toolResultTerminalStatus,
14
+ } from '../../runtime/shared/tool-status.mjs';
11
15
 
12
16
  export const CANCELLED_RESULT_STATUS_LINE = '[status: cancelled]';
13
17
 
18
+ // Detect a shell command that RAN but exited non-zero (a process exit code)
19
+ // as opposed to a real tool-call failure (`[shell-tool-failed]`) or a
20
+ // timeout/abort. bash-tool.mjs emits `Error: [shell-run-failed] [exit code: N]`
21
+ // for a plain non-zero exit; timeout/signal cases carry `[timeout: …]`/
22
+ // `[signal: …]` instead of an `[exit code: …]` marker. Returns the numeric
23
+ // exit code (>= 0) for a command-exit, or null otherwise.
24
+ export function shellCommandExitCode(text) {
25
+ const body = String(text || '');
26
+ // Anchor to the START of the result so a success/non-shell body that merely
27
+ // QUOTES the marker mid-output is never misclassified. bash-tool emits
28
+ // `Error: [shell-run-failed] [exit code: N]` as the leading marker header.
29
+ if (!/^\s*(?:Error:\s*)?\[shell-run-failed\]/i.test(body)) return null;
30
+ // Restrict marker parsing to the header region (first line) so only the
31
+ // engine-emitted status header — not quoted command output below — counts.
32
+ const header = body.split('\n', 1)[0] || '';
33
+ // Timeout / signal / abort are NOT a plain command exit — keep them "Failed".
34
+ if (/\[timeout:|\[signal:|timed out|aborted|interrupted/i.test(header)) return null;
35
+ const m = header.match(/\[exit code:\s*(\d+)\]/i);
36
+ if (!m) return null;
37
+ const code = Number(m[1]);
38
+ return Number.isFinite(code) ? code : null;
39
+ }
40
+
41
+ // Only provider envelope metadata establishes a failed invocation. Result
42
+ // bodies can contain "Error:"/HTTP/domain/task status text without making the
43
+ // call itself fail. A recognized plain shell exit wins over provider envelope
44
+ // error flags: adapters commonly label non-zero process exits as tool errors
45
+ // even though the shell tool itself ran successfully.
46
+ export function toolCallOutcome(message, rawText) {
47
+ const exitCode = shellCommandExitCode(rawText);
48
+ if (exitCode != null) {
49
+ return { isCallError: false, isExitError: true, exitCode };
50
+ }
51
+ const isCallError = message?.isError === true || message?.toolKind === 'error';
52
+ return {
53
+ isCallError,
54
+ isExitError: false,
55
+ exitCode,
56
+ };
57
+ }
58
+
59
+ // Build the collapsed failure/exit detail string. Real tool-call/result
60
+ // failures keep the red-adjacent "Failed" wording; shell command-exits render
61
+ // as the distinct neutral "Exit" state ("Exit N" for a single exit, "Y Exit"
62
+ // grouped). A mixed group surfaces both ("1 Ok · 1 Failed · 1 Exit").
63
+ export function failureDetailText({ succeeded = 0, realErrors = 0, exitErrors = 0, exitCode } = {}) {
64
+ const parts = [];
65
+ if (succeeded > 0) parts.push(`${succeeded} Ok`);
66
+ if (realErrors > 0) parts.push(`${realErrors} Failed`);
67
+ if (exitErrors > 0) {
68
+ const solo = exitErrors === 1 && realErrors === 0 && succeeded === 0;
69
+ parts.push(solo && Number.isFinite(exitCode) ? `Exit ${exitCode}` : `${exitErrors} Exit`);
70
+ }
71
+ return parts.join(' · ');
72
+ }
73
+
14
74
  export function normalizedResultStatusToken(value) {
15
- const raw = String(value || '').trim().toLowerCase();
16
- if (!raw) return '';
17
- if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
18
- if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
19
- if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
20
- if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
21
- return '';
75
+ return normalizeToolTerminalStatus(value);
22
76
  }
23
77
 
24
78
  export function resultTextTerminalStatus(text) {
25
- const body = String(text || '');
26
- const tagged = body.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
27
- if (tagged) return normalizedResultStatusToken(tagged);
28
- const bracketed = body.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
29
- if (bracketed) return normalizedResultStatusToken(bracketed);
30
- const inline = body.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
31
- return normalizedResultStatusToken(inline);
79
+ return toolResultTerminalStatus(text);
32
80
  }
33
81
 
34
82
  export function itemHasKnownTerminalStatus(item, texts = []) {
@@ -57,16 +105,18 @@ export function withCancelledResultMarker(text, item) {
57
105
  export function groupedToolResultText(group) {
58
106
  const completed = Math.min(group.count, group.completed);
59
107
  if (group.count <= 1) return group.results.at(-1)?.text ?? '';
60
- if (group.errors > 0) {
61
- const succeeded = Math.max(0, completed - group.errors);
108
+ const exitErrors = Number(group.exitErrors || 0);
109
+ if (group.errors > 0 || exitErrors > 0) {
110
+ const realErrors = Math.max(0, Number(group.callErrors || group.errors || 0));
111
+ const succeeded = Math.max(0, completed - group.errors - exitErrors);
112
+ const exitCode = group.results.find((result) => result?.isExitError)?.exitCode;
113
+ // Command-exits carry no failure reason line; only real failures do.
62
114
  const reasons = group.results
63
- .filter((result) => result?.isError)
115
+ .filter((result) => result?.isError && !result?.isExitError)
64
116
  .map((result) => firstErrorLine(result?.text))
65
117
  .filter(Boolean);
66
118
  const uniqueReasons = [...new Set(reasons)].slice(0, 2);
67
- const base = succeeded > 0
68
- ? `${succeeded} Ok · ${group.errors} Failed`
69
- : `${group.errors} Failed`;
119
+ const base = failureDetailText({ succeeded, realErrors, exitErrors, exitCode });
70
120
  return [
71
121
  `${base}${uniqueReasons[0] ? ` · ${uniqueReasons[0]}` : ''}`,
72
122
  ...uniqueReasons.slice(1),
@@ -102,7 +152,7 @@ export function aggregateRawResult(calls) {
102
152
  return chunks.join('\n\n');
103
153
  }
104
154
 
105
- export function aggregateBucketForCategory(category) {
155
+ export function aggregateBucketForCategory(category, { agentBatch = '' } = {}) {
106
156
  // Merge consecutive tool calls of the SAME category into one aggregate card;
107
157
  // a different category opens a fresh card (no cross-category merge). The
108
158
  // bucket key is the category itself, so a run of Search calls collapses into
@@ -117,6 +167,10 @@ export function aggregateBucketForCategory(category) {
117
167
  // header still spells out both. State-changing categories (Patch/Shell/…)
118
168
  // stay separate.
119
169
  if (key === 'Read' || key === 'Search') return 'category:Read+Search';
170
+ // Agent actions are grouped only inside the provider callback that emitted
171
+ // them. This keeps a later provider batch from changing an older outbound
172
+ // card above an intervening result/transcript boundary.
173
+ if (key === 'Agent') return agentBatch ? `category:Agent:${agentBatch}` : 'category:Agent';
120
174
  return key ? `category:${key}` : 'default';
121
175
  }
122
176