mixdog 0.9.38 → 0.9.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +174 -0
- package/scripts/explore-bench.mjs +38 -2
- package/scripts/explore-prompt-policy-test.mjs +152 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/steering-drain-buckets-test.mjs +179 -0
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +50 -60
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -45
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/store.mjs +223 -42
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +35 -2
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +45 -69
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +31 -0
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +1 -0
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +517 -142
- package/src/tui/engine/agent-job-feed.mjs +144 -17
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +19 -1
- package/src/tui/engine/tool-card-results.mjs +54 -32
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +77 -42
- package/src/tui/engine.mjs +63 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -21,9 +21,15 @@ import {
|
|
|
21
21
|
import { toolErrorDisplay } from './tool-result-text.mjs';
|
|
22
22
|
import {
|
|
23
23
|
notificationQueueKey,
|
|
24
|
+
executionCardKey,
|
|
24
25
|
resolveTuiRuntimeNotificationDelivery,
|
|
25
26
|
} from './notification-plan.mjs';
|
|
27
|
+
import { shortTextFingerprint } from './queue-helpers.mjs';
|
|
26
28
|
import { readImageAttachmentFromPath } from '../paste-attachments.mjs';
|
|
29
|
+
import {
|
|
30
|
+
isDeliveredCompletion,
|
|
31
|
+
recordDeliveredCompletion,
|
|
32
|
+
} from '../../runtime/agent/orchestrator/session/manager/delivered-completions.mjs';
|
|
27
33
|
|
|
28
34
|
// Channel inbound images arrive as a JSON-array-of-paths meta value (stringified
|
|
29
35
|
// across the notify IPC boundary). Parse defensively; a malformed value simply
|
|
@@ -48,13 +54,26 @@ export function createAgentJobFeed({
|
|
|
48
54
|
enqueue,
|
|
49
55
|
drain,
|
|
50
56
|
pushUserOrSyntheticItem,
|
|
57
|
+
pushAsyncAgentResponse,
|
|
51
58
|
makeQueueEntry,
|
|
52
59
|
getPending,
|
|
53
60
|
agentStatusState,
|
|
54
61
|
displayedExecutionNotificationKeys,
|
|
55
62
|
pushNotice,
|
|
63
|
+
now = () => Date.now(),
|
|
64
|
+
executionResumeTombstoneTtlMs = 30_000,
|
|
65
|
+
executionResumeTombstoneLimit = 128,
|
|
56
66
|
}) {
|
|
57
67
|
let executionResumeKickDeferred = false;
|
|
68
|
+
// Completion keys explicitly abandoned by Esc. Tombstones are per-feed
|
|
69
|
+
// (therefore per TUI session), short-lived, and bounded: they catch a late
|
|
70
|
+
// duplicate racing the abort without permanently reserving execution IDs or
|
|
71
|
+
// retaining completion bodies.
|
|
72
|
+
const discardedExecutionResumeKeys = new Map();
|
|
73
|
+
// Tracks whether an execution's visible response is only a bodyless preview
|
|
74
|
+
// or has reached its final body. A preview must not permanently suppress the
|
|
75
|
+
// later body, while repeated body retries remain idempotent.
|
|
76
|
+
const displayedExecutionResponseStates = new Map();
|
|
58
77
|
|
|
59
78
|
// FIFO accumulation of model-visible bodies from completions that arrived
|
|
60
79
|
// while busy (or while a pending-resume entry was already queued). A single
|
|
@@ -62,8 +81,49 @@ export function createAgentJobFeed({
|
|
|
62
81
|
// the queue preserves every body and merges them into the resume turn.
|
|
63
82
|
const executionResumeKickBodies = [];
|
|
64
83
|
|
|
65
|
-
function
|
|
66
|
-
if (
|
|
84
|
+
function executionResumeKey(body, completionKey = '') {
|
|
85
|
+
if (completionKey && typeof completionKey === 'object') {
|
|
86
|
+
completionKey = completionKey.executionId || completionKey.key || '';
|
|
87
|
+
}
|
|
88
|
+
const explicitKey = String(completionKey || '').trim();
|
|
89
|
+
if (explicitKey.startsWith('execution:') || explicitKey.startsWith('body:')) return explicitKey;
|
|
90
|
+
if (explicitKey) return `execution:${explicitKey}`;
|
|
91
|
+
const value = String(body || '').trim();
|
|
92
|
+
return value ? `body:${shortTextFingerprint(value)}` : '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function pruneDiscardedExecutionResumeKeys() {
|
|
96
|
+
const nowMs = Number(now()) || Date.now();
|
|
97
|
+
for (const [key, expiresAt] of discardedExecutionResumeKeys) {
|
|
98
|
+
if (expiresAt <= nowMs) discardedExecutionResumeKeys.delete(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isDiscardedExecutionResumeKey(key) {
|
|
103
|
+
if (!key) return false;
|
|
104
|
+
pruneDiscardedExecutionResumeKeys();
|
|
105
|
+
return discardedExecutionResumeKeys.has(key);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function rememberDiscardedExecutionResumeKey(key) {
|
|
109
|
+
if (!key) return;
|
|
110
|
+
pruneDiscardedExecutionResumeKeys();
|
|
111
|
+
const limit = Math.max(1, Number(executionResumeTombstoneLimit) || 128);
|
|
112
|
+
while (!discardedExecutionResumeKeys.has(key) && discardedExecutionResumeKeys.size >= limit) {
|
|
113
|
+
const oldest = discardedExecutionResumeKeys.keys().next().value;
|
|
114
|
+
if (oldest == null) break;
|
|
115
|
+
discardedExecutionResumeKeys.delete(oldest);
|
|
116
|
+
}
|
|
117
|
+
const ttlMs = Math.max(1, Number(executionResumeTombstoneTtlMs) || 30_000);
|
|
118
|
+
// Refresh insertion order so the bounded map evicts the oldest tombstone.
|
|
119
|
+
discardedExecutionResumeKeys.delete(key);
|
|
120
|
+
discardedExecutionResumeKeys.set(key, (Number(now()) || Date.now()) + ttlMs);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function kickExecutionPendingResume(body = '', completionKey = '') {
|
|
124
|
+
const key = executionResumeKey(body, completionKey);
|
|
125
|
+
if (body && isDiscardedExecutionResumeKey(key)) return;
|
|
126
|
+
if (body) executionResumeKickBodies.push({ body, key });
|
|
67
127
|
if (getDisposed()) return;
|
|
68
128
|
if (getState().busy) {
|
|
69
129
|
executionResumeKickDeferred = true;
|
|
@@ -77,8 +137,15 @@ export function createAgentJobFeed({
|
|
|
77
137
|
executionResumeKickDeferred = false;
|
|
78
138
|
// Drain every accumulated body into ONE resume turn so no completion body
|
|
79
139
|
// is lost when several deferred while busy.
|
|
80
|
-
const
|
|
81
|
-
|
|
140
|
+
const resumeBodies = executionResumeKickBodies.splice(0);
|
|
141
|
+
const resumeBody = resumeBodies.map(({ body: value }) => value).filter(Boolean).join('\n\n');
|
|
142
|
+
const resumeCompletionKeys = resumeBodies.map(({ key }) => key).filter(Boolean);
|
|
143
|
+
pending.push(makeQueueEntry(resumeBody, {
|
|
144
|
+
mode: 'pending-resume',
|
|
145
|
+
priority: 'next',
|
|
146
|
+
abortDiscardOnAbort: true,
|
|
147
|
+
resumeCompletionKeys,
|
|
148
|
+
}));
|
|
82
149
|
void drain();
|
|
83
150
|
}
|
|
84
151
|
|
|
@@ -87,11 +154,25 @@ export function createAgentJobFeed({
|
|
|
87
154
|
kickExecutionPendingResume();
|
|
88
155
|
}
|
|
89
156
|
|
|
90
|
-
function scheduleExecutionPendingResumeKick(body = '') {
|
|
157
|
+
function scheduleExecutionPendingResumeKick(body = '', completionKey = '') {
|
|
91
158
|
// Carry the model-visible body directly into the pending-resume entry so
|
|
92
159
|
// the resumed turn sends it, instead of relying on the session-pending
|
|
93
160
|
// completion marker (dropped by askSession pre-drain).
|
|
94
|
-
queueMicrotask(() => kickExecutionPendingResume(body));
|
|
161
|
+
queueMicrotask(() => kickExecutionPendingResume(body, completionKey));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function discardExecutionPendingResume(completionKeys = []) {
|
|
165
|
+
const keys = (Array.isArray(completionKeys) ? completionKeys : [completionKeys])
|
|
166
|
+
.map((key) => executionResumeKey('', key))
|
|
167
|
+
.filter(Boolean);
|
|
168
|
+
if (keys.length === 0) return;
|
|
169
|
+
for (const key of keys) rememberDiscardedExecutionResumeKey(key);
|
|
170
|
+
for (let i = executionResumeKickBodies.length - 1; i >= 0; i -= 1) {
|
|
171
|
+
if (isDiscardedExecutionResumeKey(executionResumeKickBodies[i].key)) {
|
|
172
|
+
executionResumeKickBodies.splice(i, 1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
executionResumeKickDeferred = executionResumeKickBodies.length > 0;
|
|
95
176
|
}
|
|
96
177
|
|
|
97
178
|
// Pure builder for the agent-job card patch. Split out so callers that are
|
|
@@ -117,6 +198,13 @@ export function createAgentJobFeed({
|
|
|
117
198
|
patchItem(itemId, buildAgentJobCardPatch(itemId, text, isError));
|
|
118
199
|
}
|
|
119
200
|
|
|
201
|
+
function refreshAgentStatus(parsed) {
|
|
202
|
+
if (!parsed?.taskId) return;
|
|
203
|
+
const status = String(parsed.status || '').toLowerCase();
|
|
204
|
+
const terminal = /^(completed|complete|done|success|succeeded|ok|failed|error|timeout|killed|cancelled|canceled|denied)$/.test(status);
|
|
205
|
+
set(agentStatusState(terminal ? { force: true } : undefined));
|
|
206
|
+
}
|
|
207
|
+
|
|
120
208
|
function subscribeRuntimeNotifications() {
|
|
121
209
|
if (typeof runtime.onNotification !== 'function') return null;
|
|
122
210
|
return runtime.onNotification((event) => {
|
|
@@ -132,43 +220,81 @@ export function createAgentJobFeed({
|
|
|
132
220
|
return true;
|
|
133
221
|
}
|
|
134
222
|
if (delivery.action === 'status-only') {
|
|
135
|
-
|
|
223
|
+
refreshAgentStatus(parsed);
|
|
136
224
|
return true;
|
|
137
225
|
}
|
|
138
226
|
if (delivery.action === 'execution-ui') {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
227
|
+
const cardKey = executionCardKey(event, text, parsed);
|
|
228
|
+
const firstDelivery = !cardKey || !displayedExecutionNotificationKeys.has(cardKey);
|
|
229
|
+
const executionId = String(event?.meta?.execution_id || parsed?.taskId || '').trim();
|
|
230
|
+
const status = String(event?.meta?.status || parsed?.status || '').toLowerCase();
|
|
231
|
+
const hasBody = /\n\s*\n[\s\S]*\S/.test(text);
|
|
232
|
+
const isFailure = /^(failed|error|timeout|killed|cancelled|canceled|denied)$/.test(status);
|
|
233
|
+
const successfulPreview = !hasBody && !isFailure && /^(completed|complete|done|success|succeeded|ok)$/.test(status);
|
|
234
|
+
const responseState = executionId ? displayedExecutionResponseStates.get(executionId) : '';
|
|
235
|
+
const bodyAlreadyDisplayed = responseState === 'body';
|
|
236
|
+
if (firstDelivery && !successfulPreview && !bodyAlreadyDisplayed) {
|
|
237
|
+
if (cardKey) displayedExecutionNotificationKeys.add(cardKey);
|
|
238
|
+
if (executionId) displayedExecutionResponseStates.set(executionId, hasBody ? 'body' : 'preview');
|
|
239
|
+
// Execution completions are inbound agent responses. The engine keeps
|
|
240
|
+
// their aggregation tail-safe (only an immediately-adjacent inbound
|
|
241
|
+
// response of the same preview/body phase can merge); the fallback
|
|
242
|
+
// preserves the standalone path for minimal/test harnesses.
|
|
243
|
+
(pushAsyncAgentResponse || pushUserOrSyntheticItem)(delivery.displayText, nextId(), 'injected', { responseKey: executionId });
|
|
143
244
|
}
|
|
144
|
-
|
|
245
|
+
refreshAgentStatus(parsed);
|
|
145
246
|
const resumeBody = String(delivery.modelContent || '').trim();
|
|
146
247
|
if (resumeBody) {
|
|
147
|
-
|
|
248
|
+
const completionKey = executionResumeKey(resumeBody, executionId);
|
|
249
|
+
// Consolidated completion dedup keyed off execution_id (+ text hash):
|
|
250
|
+
// if this exact execution completion was already delivered — either by
|
|
251
|
+
// an earlier TUI enqueue here or by runtime-core's ack — do NOT enqueue
|
|
252
|
+
// a duplicate model-visible twin. A re-arriving completion while IDLE
|
|
253
|
+
// would otherwise re-enqueue and let post-turn drain() spawn a fresh
|
|
254
|
+
// turn. Still ack (modelVisibleDelivered) so runtime-core's
|
|
255
|
+
// mirror/fallback stays suppressed; the card first-delivery push and
|
|
256
|
+
// status refresh above already ran.
|
|
257
|
+
if (isDiscardedExecutionResumeKey(completionKey) || isDeliveredCompletion({ executionId, text: resumeBody })) {
|
|
258
|
+
if (event && typeof event === 'object') event.modelVisibleDelivered = true;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
const enqueued = enqueue(resumeBody, {
|
|
148
262
|
mode: 'task-notification',
|
|
149
263
|
// Claude Code parity: live execution completions are queued as
|
|
150
264
|
// task notifications so the active loop can attach them after the
|
|
151
265
|
// next tool batch; no special pending-resume bypass.
|
|
152
266
|
priority: 'next',
|
|
153
267
|
key: notificationKey || undefined,
|
|
268
|
+
abortDiscardOnAbort: true,
|
|
269
|
+
resumeCompletionKeys: completionKey ? [completionKey] : [],
|
|
154
270
|
displayText: delivery.displayText || text,
|
|
155
271
|
// The immediate Response card was already pushed above
|
|
156
272
|
// (pushUserOrSyntheticItem). Keep this queued twin model-visible
|
|
157
273
|
// but suppress its drain-time transcript card to avoid a duplicate.
|
|
158
274
|
suppressDisplay: true,
|
|
159
275
|
});
|
|
276
|
+
// Self-sufficient TUI dedup: mark delivered right here (mark-once at
|
|
277
|
+
// delivery, keyed by execution_id) so a re-arriving completion is
|
|
278
|
+
// skipped above without depending solely on the runtime-core ack
|
|
279
|
+
// roundtrip — removes the split-brain asymmetry. Gated on a CONFIRMED
|
|
280
|
+
// enqueue: enqueue() returns false only when an identical-key
|
|
281
|
+
// task-notification twin is already queued (session-flow.mjs
|
|
282
|
+
// pendingNotificationKeys.has(key)), in which case the completion is
|
|
283
|
+
// already pending delivery and must not be double-recorded.
|
|
284
|
+
if (enqueued) recordDeliveredCompletion({ executionId, text: resumeBody });
|
|
160
285
|
// EXPLICIT ack to the emitting runtime: the model-visible completion
|
|
161
286
|
// body was injected into the active loop here, so notifyFnForSession
|
|
162
287
|
// must NOT also mirror it into the pending queue (double injection).
|
|
163
288
|
// A bare truthy return below is display/status handling only — this
|
|
164
|
-
// flag is the sole model-visible-delivery signal.
|
|
289
|
+
// flag is the sole model-visible-delivery signal. Set for BOTH the
|
|
290
|
+
// real-enqueue and the enqueue===false (already-queued twin) cases:
|
|
291
|
+
// either way the completion is pending delivery on the TUI path, so
|
|
292
|
+
// runtime-core must still not mirror it.
|
|
165
293
|
if (event && typeof event === 'object') event.modelVisibleDelivered = true;
|
|
166
294
|
}
|
|
167
295
|
return true;
|
|
168
296
|
}
|
|
169
|
-
|
|
170
|
-
set(agentStatusState({ force: true }));
|
|
171
|
-
}
|
|
297
|
+
refreshAgentStatus(parsed);
|
|
172
298
|
const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
|
|
173
299
|
const imagePaths = parseInboundImagePaths(event?.meta?.image_paths);
|
|
174
300
|
if (!modelContent && imagePaths.length === 0) return true;
|
|
@@ -212,6 +338,7 @@ export function createAgentJobFeed({
|
|
|
212
338
|
kickExecutionPendingResume,
|
|
213
339
|
flushDeferredExecutionPendingResumeKick,
|
|
214
340
|
scheduleExecutionPendingResumeKick,
|
|
341
|
+
discardExecutionPendingResume,
|
|
215
342
|
updateAgentJobCard,
|
|
216
343
|
buildAgentJobCardPatch,
|
|
217
344
|
subscribeRuntimeNotifications,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Pure, tail-only inbound-agent response aggregation. The caller decides
|
|
2
|
+
// whether the transcript tail is eligible; this helper never searches older
|
|
3
|
+
// rows, so a retry cannot mutate across a real transcript boundary.
|
|
4
|
+
function responseEntry(response = {}) {
|
|
5
|
+
return {
|
|
6
|
+
key: String(response.key || ''),
|
|
7
|
+
raw: String(response.rawResult ?? response.raw ?? '').trim(),
|
|
8
|
+
result: response.result,
|
|
9
|
+
hasBody: response.hasBody === true,
|
|
10
|
+
isError: response.isError === true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatAgentResponseRaw(entries = []) {
|
|
15
|
+
return entries
|
|
16
|
+
.map((entry, index) => {
|
|
17
|
+
const raw = String(entry?.raw || '').trim();
|
|
18
|
+
return raw ? `${index + 1}. agent\n${raw}` : '';
|
|
19
|
+
})
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.join('\n\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function priorEntries(previous) {
|
|
25
|
+
if (Array.isArray(previous?.agentResponseEntries) && previous.agentResponseEntries.length) {
|
|
26
|
+
return previous.agentResponseEntries.map(responseEntry);
|
|
27
|
+
}
|
|
28
|
+
return [responseEntry({
|
|
29
|
+
key: previous?.agentResponseKey,
|
|
30
|
+
rawResult: previous?.rawResult ?? previous?.result,
|
|
31
|
+
result: previous?.result,
|
|
32
|
+
hasBody: previous?.agentResponseHasBody,
|
|
33
|
+
isError: previous?.isError,
|
|
34
|
+
})];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function appendAgentResponseTail(previous, response, now = Date.now()) {
|
|
38
|
+
if (previous?.kind !== 'tool' || previous.agentDirection !== 'inbound') return null;
|
|
39
|
+
const next = responseEntry(response);
|
|
40
|
+
const entries = priorEntries(previous);
|
|
41
|
+
const existingIndex = next.key ? entries.findIndex((entry) => entry.key === next.key) : -1;
|
|
42
|
+
|
|
43
|
+
if (existingIndex >= 0) {
|
|
44
|
+
// A preview/final/retry for the same execution is one response. Replace
|
|
45
|
+
// that entry in-place only because this aggregate is still the transcript
|
|
46
|
+
// tail; never turn it into another count or a nested raw block.
|
|
47
|
+
entries[existingIndex] = next;
|
|
48
|
+
} else {
|
|
49
|
+
// Distinct responses can merge only when their presentation phase matches.
|
|
50
|
+
// A visible bodyless failure stays a separate row from a later full body.
|
|
51
|
+
if (previous.agentResponseHasBody !== next.hasBody) return null;
|
|
52
|
+
entries.push(next);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
args: response.args,
|
|
57
|
+
result: response.result,
|
|
58
|
+
rawResult: formatAgentResponseRaw(entries) || null,
|
|
59
|
+
isError: entries.some((entry) => entry.isError),
|
|
60
|
+
count: entries.length,
|
|
61
|
+
completedCount: entries.length,
|
|
62
|
+
completedAt: now,
|
|
63
|
+
agentResponseEntries: entries,
|
|
64
|
+
agentResponseKeys: entries.map((entry) => entry.key).filter(Boolean),
|
|
65
|
+
agentResponseHasBody: entries.every((entry) => entry.hasBody),
|
|
66
|
+
agentResponseAggregate: entries.length > 1,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -47,6 +47,22 @@ export function notificationQueueKey(event, text, parsed) {
|
|
|
47
47
|
return [id, type || fallbackKind, status, hasBody].filter(Boolean).join(':');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Stable dedup key for the execution transcript CARD. Unlike notificationQueueKey
|
|
52
|
+
* it drops the volatile type/status dimensions so a duplicate completion re-arriving
|
|
53
|
+
* with a different type/status (same execution_id) cannot push a second card. The
|
|
54
|
+
* hasBody dimension is preserved so a bodyless preview (b0) and the real result (b1)
|
|
55
|
+
* remain distinct cards (intentional preview→result upgrade). Falls back to
|
|
56
|
+
* notificationQueueKey when no execution_id is present.
|
|
57
|
+
*/
|
|
58
|
+
export function executionCardKey(event, text, parsed) {
|
|
59
|
+
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
60
|
+
const executionId = String(meta.execution_id || '').trim();
|
|
61
|
+
if (!executionId) return notificationQueueKey(event, text, parsed);
|
|
62
|
+
const hasBody = /\n\s*\n[\s\S]*\S/.test(String(text || '')) ? 'b1' : 'b0';
|
|
63
|
+
return `card:${executionId}:${hasBody}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
export function isExecutionNotification(event, text, parsed) {
|
|
51
67
|
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
52
68
|
if (meta.execution_id || meta.execution_surface) return true;
|
|
@@ -24,7 +24,7 @@ export function createEngineApi(bag) {
|
|
|
24
24
|
|
|
25
25
|
export function createEngineApiA(bag) {
|
|
26
26
|
const {
|
|
27
|
-
runtime, nextId, flags, pending, listeners, getState, set, pushItem, patchItem, replaceItems, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, resetStatsAndSyncContext, drain, flushDeferredExecutionPendingResumeKick,
|
|
27
|
+
runtime, nextId, flags, pending, listeners, getState, set, pushItem, patchItem, replaceItems, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, resetStatsAndSyncContext, drain, flushDeferredExecutionPendingResumeKick, discardExecutionPendingResume,
|
|
28
28
|
} = bag;
|
|
29
29
|
return {
|
|
30
30
|
getState: () => getState(),
|
|
@@ -573,10 +573,15 @@ export function createEngineApiA(bag) {
|
|
|
573
573
|
? restoreState.pastedTexts
|
|
574
574
|
: null;
|
|
575
575
|
const requeueEntries = restoreState && !restoreState.committed && Array.isArray(restoreState.requeueEntries)
|
|
576
|
-
? restoreState.requeueEntries.
|
|
576
|
+
? restoreState.requeueEntries.filter(
|
|
577
|
+
(entry) => entry?.abortDiscardOnAbort !== true && entry?.mode !== 'pending-resume',
|
|
578
|
+
)
|
|
577
579
|
: [];
|
|
578
580
|
const aborted = runtime.abort('cli-react-abort');
|
|
579
581
|
if (restoreState) {
|
|
582
|
+
if (aborted !== false && Array.isArray(restoreState.discardExecutionPendingResumeKeys)) {
|
|
583
|
+
discardExecutionPendingResume?.(restoreState.discardExecutionPendingResumeKeys);
|
|
584
|
+
}
|
|
580
585
|
if ((restoreText || requeueEntries.length > 0) && aborted !== false) {
|
|
581
586
|
restoreState.reclaimed = true;
|
|
582
587
|
const idSet = new Set((restoreState.submittedIds || []).filter((id) => id != null));
|
|
@@ -592,6 +597,7 @@ export function createEngineApiA(bag) {
|
|
|
592
597
|
}
|
|
593
598
|
restoreState.restorable = false;
|
|
594
599
|
restoreState.requeueEntries = [];
|
|
600
|
+
restoreState.discardExecutionPendingResumeKeys = [];
|
|
595
601
|
}
|
|
596
602
|
// ── Bounded manual-abort recovery ───────────────────────────────────
|
|
597
603
|
// runtime.abort() above normally rejects the in-flight runtime.ask() so
|
|
@@ -54,6 +54,12 @@ export function createSessionFlow(bag) {
|
|
|
54
54
|
skipSlashCommands: options.skipSlashCommands === true,
|
|
55
55
|
displayText: mode === 'task-notification' ? notificationDisplayText(displayText) : String(displayText || ''),
|
|
56
56
|
suppressDisplay: options.suppressDisplay === true,
|
|
57
|
+
// Completion resumes are consumed exactly once: Esc abandons their
|
|
58
|
+
// uncommitted body instead of putting it back at the queue front.
|
|
59
|
+
abortDiscardOnAbort: options.abortDiscardOnAbort === true,
|
|
60
|
+
resumeCompletionKeys: Array.isArray(options.resumeCompletionKeys)
|
|
61
|
+
? options.resumeCompletionKeys.filter((key) => key != null && String(key).trim())
|
|
62
|
+
: [],
|
|
57
63
|
steeringPersistId: options.steeringPersistId || null,
|
|
58
64
|
steeringPersistRestored: options.steeringPersistRestored === true,
|
|
59
65
|
};
|
|
@@ -194,6 +200,17 @@ export function createSessionFlow(bag) {
|
|
|
194
200
|
pushUserOrSyntheticItem(entry.text, entry.id, isQueuedEntryEditable(entry) ? 'user' : 'injected');
|
|
195
201
|
}
|
|
196
202
|
const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
|
|
203
|
+
// A completion resume is owned by the completion that woke it. Esc
|
|
204
|
+
// consumes that ownership; unlike ordinary notifications it must never
|
|
205
|
+
// be requeued from an uncommitted turn. Keep normal task notifications
|
|
206
|
+
// recoverable exactly as before.
|
|
207
|
+
const discardOnAbort = nonEditable.filter(
|
|
208
|
+
(entry) => entry?.abortDiscardOnAbort === true || entry?.mode === 'pending-resume',
|
|
209
|
+
);
|
|
210
|
+
const requeueOnAbort = nonEditable.filter((entry) => !discardOnAbort.includes(entry));
|
|
211
|
+
const discardExecutionPendingResumeKeys = discardOnAbort.flatMap(
|
|
212
|
+
(entry) => Array.isArray(entry?.resumeCompletionKeys) ? entry.resumeCompletionKeys : [],
|
|
213
|
+
);
|
|
197
214
|
const batchPastedImages = mergePastedImages(batch);
|
|
198
215
|
const batchPastedTexts = mergePastedTexts(batch);
|
|
199
216
|
const turnStatus = await bag.runTurn(merged, {
|
|
@@ -203,7 +220,8 @@ export function createSessionFlow(bag) {
|
|
|
203
220
|
onCommitted: () => commitSteeringQueueEntries(batch),
|
|
204
221
|
submittedIds: [...ids],
|
|
205
222
|
restorable: nonEditable.length === 0,
|
|
206
|
-
requeueOnAbort
|
|
223
|
+
requeueOnAbort,
|
|
224
|
+
discardExecutionPendingResumeKeys,
|
|
207
225
|
});
|
|
208
226
|
if (flags.drainEpoch !== drainEpoch) return;
|
|
209
227
|
// A deferred cleared-session UI sync (from a late-settling abandoned
|
|
@@ -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
|
|
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 {
|
|
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';
|
|
@@ -34,6 +35,17 @@ export function createToolCardResults({
|
|
|
34
35
|
buildAgentJobCardPatch,
|
|
35
36
|
agentStatusState,
|
|
36
37
|
}) {
|
|
38
|
+
// A finalized/failed non-aggregate card must never carry an empty body:
|
|
39
|
+
// an empty-body error card is classified fully-failed-with-no-body upstream
|
|
40
|
+
// (transcript-tool-failures) and null-renders (card disappears). Stamp a
|
|
41
|
+
// minimal non-empty fallback, preferring meaningful text, then Exit N, then
|
|
42
|
+
// a bare Failed status.
|
|
43
|
+
function finalizedErrorFallbackBody(body, text, exitCode) {
|
|
44
|
+
if (String(body || '').trim()) return body;
|
|
45
|
+
if (String(text || '').trim()) return text;
|
|
46
|
+
if (exitCode != null) return `Exit ${exitCode}`;
|
|
47
|
+
return 'Failed';
|
|
48
|
+
}
|
|
37
49
|
function patchToolItem(id, patch) {
|
|
38
50
|
const prev = getState().items.find((it) => it.id === id);
|
|
39
51
|
const ok = patchItem(id, patch);
|
|
@@ -60,22 +72,11 @@ export function createToolCardResults({
|
|
|
60
72
|
// Aggregate card handling — collect semantic summaries per call
|
|
61
73
|
const aggregate = card.aggregate;
|
|
62
74
|
const callRec = aggregate && callId ? aggregate.calls.get(callId) : null;
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
75
|
+
// Only a provider-marked invocation failure contributes to failure count,
|
|
76
|
+
// red state, or Failed aggregate copy. Tool-reported HTTP/domain/status
|
|
77
|
+
// outcomes remain successful calls with their raw/semantic result detail.
|
|
78
|
+
const { exitCode, isExitError, isCallError } = toolCallOutcome(message, rawText);
|
|
79
|
+
const isError = isCallError;
|
|
79
80
|
const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
|
|
80
81
|
|
|
81
82
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
@@ -89,19 +90,22 @@ export function createToolCardResults({
|
|
|
89
90
|
assignAggregateSummaryOrder(aggregate, callRec);
|
|
90
91
|
callRec.isError = isError;
|
|
91
92
|
callRec.isCallError = isCallError;
|
|
93
|
+
callRec.isExitError = isExitError;
|
|
94
|
+
callRec.exitCode = exitCode;
|
|
92
95
|
callRec.resultText = text;
|
|
93
96
|
callRec.resolved = true;
|
|
94
97
|
const allCalls = [...aggregate.calls.values()];
|
|
95
98
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
96
99
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
97
100
|
const callErrors = allCalls.filter((r) => r.isCallError).length;
|
|
101
|
+
const exitErrors = allCalls.filter((r) => r.isExitError).length;
|
|
98
102
|
// Collapsed detail carries the merged per-call count summary
|
|
99
103
|
// ("512 lines, 6 matches, 3 files") so the finished card answers "how
|
|
100
104
|
// much" without ctrl+o. Failures keep a bare 'N Ok · N Failed' status so
|
|
101
105
|
// an error stays visible while collapsed.
|
|
102
|
-
const succeeded = completed - errors;
|
|
103
|
-
const detailText = errors > 0
|
|
104
|
-
? (succeeded
|
|
106
|
+
const succeeded = Math.max(0, completed - errors - exitErrors);
|
|
107
|
+
const detailText = errors > 0 || exitErrors > 0
|
|
108
|
+
? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode })
|
|
105
109
|
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
106
110
|
const currentItem = getState().items.find((it) => it.id === card.itemId);
|
|
107
111
|
const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
@@ -116,6 +120,7 @@ export function createToolCardResults({
|
|
|
116
120
|
isError: errors > 0,
|
|
117
121
|
errorCount: errors,
|
|
118
122
|
callErrorCount: callErrors,
|
|
123
|
+
exitErrorCount: exitErrors,
|
|
119
124
|
count: allCalls.length,
|
|
120
125
|
completedCount: visualCompleted,
|
|
121
126
|
doneCategories: aggregateDoneCategories(allCalls),
|
|
@@ -127,20 +132,25 @@ export function createToolCardResults({
|
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
// Non-aggregate (legacy agent-job cards, etc.)
|
|
130
|
-
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, callErrors: 0, results: [] };
|
|
135
|
+
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, callErrors: 0, exitErrors: 0, results: [] };
|
|
131
136
|
group.completed = Math.min(group.count, group.completed + 1);
|
|
132
137
|
group.errors += isError ? 1 : 0;
|
|
133
138
|
group.callErrors = (group.callErrors || 0) + (isCallError ? 1 : 0);
|
|
134
|
-
group.
|
|
139
|
+
group.exitErrors = (group.exitErrors || 0) + (isExitError ? 1 : 0);
|
|
140
|
+
group.results.push({ text, isError, isExitError, exitCode });
|
|
135
141
|
toolGroups.set(card.itemId, group);
|
|
136
142
|
const resultText = groupedToolResultText(group);
|
|
137
|
-
|
|
143
|
+
let displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
|
|
144
|
+
if (group.errors > 0 && !String(displayResult || '').trim()) {
|
|
145
|
+
displayResult = finalizedErrorFallbackBody(displayResult, text, exitCode);
|
|
146
|
+
}
|
|
138
147
|
const patch = {
|
|
139
148
|
result: displayResult,
|
|
140
149
|
text: displayResult,
|
|
141
150
|
isError: group.errors > 0,
|
|
142
151
|
errorCount: group.errors,
|
|
143
152
|
callErrorCount: group.callErrors || 0,
|
|
153
|
+
exitErrorCount: group.exitErrors || 0,
|
|
144
154
|
count: group.count,
|
|
145
155
|
completedCount: group.completed,
|
|
146
156
|
completedAt: Date.now(),
|
|
@@ -159,6 +169,11 @@ export function createToolCardResults({
|
|
|
159
169
|
// The agent fields win (final display) while patch keeps the completion
|
|
160
170
|
// metadata (count/completedCount/completedAt/rawResult) for expand.
|
|
161
171
|
Object.assign(patch, buildAgentJobCardPatch(card.itemId, rawText, isError));
|
|
172
|
+
// Re-apply the empty-body guard: buildAgentJobCardPatch may overwrite the
|
|
173
|
+
// stamped fallback with an empty agent-job body, letting the card vanish.
|
|
174
|
+
if (group.errors > 0 && !String(patch.result || '').trim()) {
|
|
175
|
+
patch.result = patch.text = finalizedErrorFallbackBody(patch.result, text, exitCode);
|
|
176
|
+
}
|
|
162
177
|
}
|
|
163
178
|
patchToolItem(card.itemId, patch);
|
|
164
179
|
card.done = true;
|
|
@@ -223,12 +238,14 @@ export function createToolCardResults({
|
|
|
223
238
|
const totalCompleted = completed;
|
|
224
239
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
225
240
|
const callErrors = allCalls.filter((r) => r.isCallError).length;
|
|
226
|
-
const
|
|
241
|
+
const exitErrors = allCalls.filter((r) => r.isExitError).length;
|
|
242
|
+
const succeeded = Math.max(0, completed - errors - exitErrors);
|
|
227
243
|
const rawResult = aggregateRawResult(allCalls);
|
|
228
|
-
// Collapsed detail carries the merged per-call count summary;
|
|
229
|
-
// keep
|
|
230
|
-
|
|
231
|
-
|
|
244
|
+
// Collapsed detail carries the merged per-call count summary; real
|
|
245
|
+
// failures keep 'N Failed', shell command-exits show 'Exit N'/'Y Exit'.
|
|
246
|
+
// Raw is kept for ctrl+o.
|
|
247
|
+
let displayDetail = errors > 0 || exitErrors > 0
|
|
248
|
+
? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode })
|
|
232
249
|
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
233
250
|
if (cancelled) {
|
|
234
251
|
// Cancelled aggregates MUST keep the [status: cancelled] marker on the
|
|
@@ -244,6 +261,7 @@ export function createToolCardResults({
|
|
|
244
261
|
isError: errors > 0,
|
|
245
262
|
errorCount: errors,
|
|
246
263
|
callErrorCount: callErrors,
|
|
264
|
+
exitErrorCount: exitErrors,
|
|
247
265
|
count: allCalls.length,
|
|
248
266
|
completedCount: totalCompleted,
|
|
249
267
|
doneCategories: aggregateDoneCategories(allCalls),
|
|
@@ -257,15 +275,19 @@ export function createToolCardResults({
|
|
|
257
275
|
continue;
|
|
258
276
|
}
|
|
259
277
|
// Non-aggregate finalize
|
|
260
|
-
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
|
|
278
|
+
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, exitErrors: 0, results: [] };
|
|
261
279
|
group.completed = Math.min(group.count, group.completed + 1);
|
|
262
280
|
toolGroups.set(card.itemId, group);
|
|
263
281
|
let resultText = groupedToolResultText(group);
|
|
282
|
+
if (group.errors > 0 && !String(resultText || '').trim()) {
|
|
283
|
+
const exitRec = (group.results || []).find((r) => r && r.isExitError);
|
|
284
|
+
resultText = finalizedErrorFallbackBody(resultText, exitRec?.text, exitRec?.exitCode);
|
|
285
|
+
}
|
|
264
286
|
if (cancelled) {
|
|
265
287
|
const currentItem = getState().items.find((it) => it.id === card.itemId);
|
|
266
288
|
resultText = withCancelledResultMarker(resultText, currentItem);
|
|
267
289
|
}
|
|
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() });
|
|
290
|
+
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
291
|
card.done = true;
|
|
270
292
|
if (card.callId) done.add(card.callId);
|
|
271
293
|
}
|