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.
- 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 +176 -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 +316 -2
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/scripts/tui-transcript-perf-test.mjs +279 -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 +44 -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/compaction-runner.mjs +15 -0
- 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 +67 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
- 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/buffered-appender.mjs +13 -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/lifecycle-api.mjs +4 -0
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +36 -2
- package/src/session-runtime/session-turn-api.mjs +12 -0
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +53 -70
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +33 -1
- 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 +10 -10
- package/src/tui/app/use-transcript-window.mjs +38 -56
- 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 +866 -300
- package/src/tui/engine/agent-job-feed.mjs +216 -19
- 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 +14 -2
- package/src/tui/engine/session-flow.mjs +22 -2
- package/src/tui/engine/tool-card-results.mjs +64 -37
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +172 -77
- package/src/tui/engine.mjs +199 -39
- package/src/tui/index.jsx +2 -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
package/src/tui/engine/turn.mjs
CHANGED
|
@@ -6,16 +6,31 @@ import { applyUsageDelta } from './session-stats.mjs';
|
|
|
6
6
|
import { pickVerb, pickDoneVerb, compactEventLabel, compactEventDetail } from './labels.mjs';
|
|
7
7
|
import { toolResultText, toolErrorDisplay } from './tool-result-text.mjs';
|
|
8
8
|
import { toolCallId, toolResultCallId, toolCallName, toolCallArgs } from './tool-call-fields.mjs';
|
|
9
|
-
import { toolResultStatus, isErrorToolStatus } from './agent-envelope.mjs';
|
|
10
9
|
import { promptDisplayText, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
11
|
-
import { memoryCoreResultErrorText } from '../app/input-parsers.mjs';
|
|
12
10
|
import { yieldToRenderer } from './render-timing.mjs';
|
|
13
|
-
import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, assignAggregateSummaryOrder } from './tool-result-status.mjs';
|
|
11
|
+
import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, assignAggregateSummaryOrder, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
|
|
14
12
|
|
|
15
13
|
export function createRunTurn(bag) {
|
|
16
14
|
const {
|
|
17
|
-
runtime, nextId, tuiDebug, LEAD_TURN_TIMEOUT_MS, flags, pending, itemIndexById, getState, set, pushItem, patchItem, pushNotice, pushUserOrSyntheticItem, markToolCallActive, markToolCallDone, clearActiveToolSummary, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, requestToolApproval, patchToolCardResult, flushToolResults, flushDeferredExecutionPendingResumeKick, drain, drainPendingSteering,
|
|
15
|
+
runtime, nextId, tuiDebug, LEAD_TURN_TIMEOUT_MS, flags, pending, itemIndexById, getState, set, pushItem, patchItem, updateStreamingTail: updateStreamingTailFromStore, settleStreamingTail: settleStreamingTailFromStore, clearStreamingTail: clearStreamingTailFromStore, pushNotice, pushUserOrSyntheticItem, markToolCallActive, markToolCallDone, clearActiveToolSummary, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, requestToolApproval, patchToolCardResult, flushToolResults, flushDeferredExecutionPendingResumeKick, drain, drainPendingSteering,
|
|
18
16
|
} = bag;
|
|
17
|
+
// Small fallbacks keep isolated createRunTurn harnesses source-compatible;
|
|
18
|
+
// the real engine supplies atomic implementations that also maintain revision.
|
|
19
|
+
const updateStreamingTail = updateStreamingTailFromStore || ((id, patch = {}) => {
|
|
20
|
+
set({ streamingTail: { ...(getState().streamingTail || {}), ...patch, kind: 'assistant', id, streaming: true } });
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
const settleStreamingTail = settleStreamingTailFromStore || ((id, patch = {}) => {
|
|
24
|
+
const tail = getState().streamingTail;
|
|
25
|
+
if (!tail || tail.id !== id) return false;
|
|
26
|
+
pushItem({ ...tail, ...patch, kind: 'assistant', id, streaming: false });
|
|
27
|
+
set({ streamingTail: null });
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
const clearStreamingTail = clearStreamingTailFromStore || ((id = null) => {
|
|
31
|
+
if (id == null || getState().streamingTail?.id === id) set({ streamingTail: null });
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
19
34
|
|
|
20
35
|
async function runTurn(userText, options = {}) {
|
|
21
36
|
const turnIndex = getState().stats.turns || 0;
|
|
@@ -42,6 +57,9 @@ export function createRunTurn(bag) {
|
|
|
42
57
|
reclaimed: false,
|
|
43
58
|
committed: false,
|
|
44
59
|
requeueEntries: Array.isArray(options.requeueOnAbort) ? options.requeueOnAbort.slice() : [],
|
|
60
|
+
discardExecutionPendingResumeKeys: Array.isArray(options.discardExecutionPendingResumeKeys)
|
|
61
|
+
? options.discardExecutionPendingResumeKeys.slice()
|
|
62
|
+
: [],
|
|
45
63
|
};
|
|
46
64
|
set({ busy: true, lastTurn: null, spinner: { active: true, verb: pickVerb(turnIndex), startedAt, responseLength: 0, inputTokens: 0, outputTokens: 0, mode: 'requesting' } });
|
|
47
65
|
|
|
@@ -62,22 +80,63 @@ export function createRunTurn(bag) {
|
|
|
62
80
|
let watchdogGraceTimer = null;
|
|
63
81
|
let lastProgressAt = startedAt;
|
|
64
82
|
let lastProgressLabel = 'start';
|
|
83
|
+
let watchdogDeferralCeilingAt = 0;
|
|
84
|
+
const configuredLeadToolMaxMs = Number(process.env.MIXDOG_LEAD_TOOL_MAX_MS);
|
|
85
|
+
const leadToolMaxMs = Number.isFinite(configuredLeadToolMaxMs) && configuredLeadToolMaxMs > 0
|
|
86
|
+
? configuredLeadToolMaxMs
|
|
87
|
+
: 30 * 60 * 1000;
|
|
65
88
|
const clearWatchdog = () => {
|
|
66
89
|
if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
|
|
67
90
|
if (watchdogGraceTimer) { clearTimeout(watchdogGraceTimer); watchdogGraceTimer = null; }
|
|
68
91
|
};
|
|
92
|
+
const refreshWatchdogFromRuntimeLiveness = () => {
|
|
93
|
+
let liveness;
|
|
94
|
+
try { liveness = runtime.getTurnLiveness?.(); } catch { return false; }
|
|
95
|
+
if (liveness?.stage !== 'tool_running') watchdogDeferralCeilingAt = 0;
|
|
96
|
+
const progressAt = Number(liveness?.lastProgressAt);
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - LEAD_TURN_TIMEOUT_MS) return false;
|
|
99
|
+
|
|
100
|
+
if (liveness.stage === 'tool_running') {
|
|
101
|
+
watchdogDeferralCeilingAt = 0;
|
|
102
|
+
const toolStartedAt = Number(liveness.toolStartedAt);
|
|
103
|
+
if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
|
|
104
|
+
const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
|
|
105
|
+
const toolCeilingMs = Math.max(
|
|
106
|
+
Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 60_000 : 0,
|
|
107
|
+
leadToolMaxMs,
|
|
108
|
+
);
|
|
109
|
+
if (now - toolStartedAt >= toolCeilingMs) return false;
|
|
110
|
+
watchdogDeferralCeilingAt = toolStartedAt + toolCeilingMs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lastProgressAt = progressAt;
|
|
114
|
+
lastProgressLabel = `orchestrator:${String(liveness.stage || 'unknown')}`;
|
|
115
|
+
armWatchdog();
|
|
116
|
+
return true;
|
|
117
|
+
};
|
|
69
118
|
const armWatchdog = () => {
|
|
70
119
|
if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
|
|
71
120
|
if (watchdogTripped) return;
|
|
72
|
-
const
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS - Math.max(0, now - lastProgressAt));
|
|
123
|
+
const ceilingRemaining = watchdogDeferralCeilingAt > 0
|
|
124
|
+
? Math.max(1, watchdogDeferralCeilingAt - now)
|
|
125
|
+
: Infinity;
|
|
126
|
+
const delay = Math.min(remaining, ceilingRemaining);
|
|
73
127
|
watchdogTimer = setTimeout(() => {
|
|
74
128
|
if (!isCurrentTurn()) return;
|
|
75
129
|
if (watchdogTripped) return;
|
|
76
|
-
const
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const idleMs = now - lastProgressAt;
|
|
77
132
|
if (idleMs < LEAD_TURN_TIMEOUT_MS) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
if (watchdogDeferralCeilingAt > 0 && now >= watchdogDeferralCeilingAt) {
|
|
134
|
+
if (refreshWatchdogFromRuntimeLiveness()) return;
|
|
135
|
+
} else {
|
|
136
|
+
armWatchdog();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
} else if (refreshWatchdogFromRuntimeLiveness()) return;
|
|
81
140
|
watchdogTripped = true;
|
|
82
141
|
if (_batchTimer !== null) {
|
|
83
142
|
clearTimeout(_batchTimer);
|
|
@@ -88,7 +147,7 @@ export function createRunTurn(bag) {
|
|
|
88
147
|
_pendingThinkingLastEndedAt = 0;
|
|
89
148
|
const elapsed = Date.now() - startedAt;
|
|
90
149
|
tuiDebug(`runTurn WATCHDOG TRIP turn=${turnIndex} elapsedMs=${elapsed} idleMs=${idleMs} lastProgress=${lastProgressLabel} — aborting stuck turn`);
|
|
91
|
-
pushNotice(`Turn timed out after ${Math.round(idleMs / 1000)}s idle — aborting stuck request. Input will be released shortly if abort does not unwind.`, 'warn', { transcript: true });
|
|
150
|
+
pushNotice(`Turn timed out after ${Math.round(idleMs / 1000)}s idle (last progress: ${lastProgressLabel}) — aborting stuck request. Input will be released shortly if abort does not unwind.`, 'warn', { transcript: true });
|
|
92
151
|
try { runtime.abort('cli-react-abort-watchdog'); } catch {}
|
|
93
152
|
// Belt-and-suspenders: if runtime.abort() did not reject runtime.ask()
|
|
94
153
|
// (unwind starved), hard-release the turn after a short grace so the
|
|
@@ -104,6 +163,11 @@ export function createRunTurn(bag) {
|
|
|
104
163
|
finalizeToolHeaders();
|
|
105
164
|
clearDeferredTimers();
|
|
106
165
|
flags.flushDeferredBeforeImmediatePush = null;
|
|
166
|
+
if (currentAssistantId && currentAssistantText.trim()) {
|
|
167
|
+
settleStreamingTail(currentAssistantId, { text: currentAssistantText });
|
|
168
|
+
} else {
|
|
169
|
+
clearStreamingTail(currentAssistantId);
|
|
170
|
+
}
|
|
107
171
|
// Bump the epoch FIRST so this (still-stuck) turn's later finally becomes
|
|
108
172
|
// a no-op for shared getState() and cannot corrupt the turn we hand off to.
|
|
109
173
|
flags.leadTurnEpoch++;
|
|
@@ -117,7 +181,7 @@ export function createRunTurn(bag) {
|
|
|
117
181
|
flushDeferredExecutionPendingResumeKick();
|
|
118
182
|
}, 5_000);
|
|
119
183
|
watchdogGraceTimer.unref?.();
|
|
120
|
-
},
|
|
184
|
+
}, delay);
|
|
121
185
|
watchdogTimer.unref?.();
|
|
122
186
|
};
|
|
123
187
|
const markTurnProgress = (label) => {
|
|
@@ -125,11 +189,19 @@ export function createRunTurn(bag) {
|
|
|
125
189
|
if (watchdogTripped) return;
|
|
126
190
|
lastProgressAt = Date.now();
|
|
127
191
|
lastProgressLabel = String(label || 'progress');
|
|
128
|
-
armWatchdog();
|
|
129
192
|
return true;
|
|
130
193
|
};
|
|
131
194
|
armWatchdog();
|
|
132
195
|
let currentAssistantText = '';
|
|
196
|
+
// Segments sealed as their own assistant item(s) this turn via
|
|
197
|
+
// commitAssistantSegment (e.g. a tool preamble, then a no-newline tail
|
|
198
|
+
// committed by onSteerMessage before an injected steering row). Kept as an
|
|
199
|
+
// ordered list — NOT one concatenated string — so finalization can strip
|
|
200
|
+
// each committed segment out of the provider's final content individually.
|
|
201
|
+
// A single concatenation breaks when the provider omits an earlier segment
|
|
202
|
+
// (e.g. a tool preamble) from result.content: the combined prefix no longer
|
|
203
|
+
// matches and the tail would duplicate after the steering row.
|
|
204
|
+
const committedSegments = [];
|
|
133
205
|
let thinkingText = '';
|
|
134
206
|
let thinkingStartedAt = 0;
|
|
135
207
|
let thinkingSegmentStartedAt = 0;
|
|
@@ -147,6 +219,7 @@ export function createRunTurn(bag) {
|
|
|
147
219
|
const earlyResultBuffer = new Map();
|
|
148
220
|
const aggregateCards = []; // active aggregate cards in the current consecutive tool block
|
|
149
221
|
let tailAggregate = null; // most recently touched aggregate card; only the tail may absorb the next same-bucket call
|
|
222
|
+
let providerToolBatch = 0;
|
|
150
223
|
|
|
151
224
|
// ── Deferred tool-card push (scroll/text sync) ────────────────────────────
|
|
152
225
|
// A tool card used to enter the transcript the instant onToolCall fired,
|
|
@@ -163,13 +236,11 @@ export function createRunTurn(bag) {
|
|
|
163
236
|
// cards are pushed alongside the result-bearing one before their own delay
|
|
164
237
|
// elapses and would otherwise paint an empty reserved band.
|
|
165
238
|
// Mirrors components/ToolExecution.jsx TOOL_PENDING_SHOW_DELAY_MS.
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
// surfaced, preserving call-order flush semantics.
|
|
172
|
-
const TOOL_CARD_PUSH_DELAY_MS = 0;
|
|
239
|
+
// Keep a fast call off-screen until it either resolves or has genuinely
|
|
240
|
+
// been pending long enough to communicate work. This avoids a one-frame
|
|
241
|
+
// Running→Finished flash while preserving the deferred entry's ordered,
|
|
242
|
+
// result-forced materialization path.
|
|
243
|
+
const TOOL_CARD_PUSH_DELAY_MS = 1000;
|
|
173
244
|
let deferredSeqCounter = 0;
|
|
174
245
|
const deferredEntries = []; // creation-order list; each is pushed at most once
|
|
175
246
|
// Push this entry AND every earlier-created still-deferred entry, in order,
|
|
@@ -281,7 +352,7 @@ export function createRunTurn(bag) {
|
|
|
281
352
|
const it = newItems[i];
|
|
282
353
|
if (it?.id != null) itemIndexById.set(it.id, base + i);
|
|
283
354
|
}
|
|
284
|
-
set({ items, ...extra });
|
|
355
|
+
set({ items, structureRevision: (Number(getState().structureRevision) || 0) + 1, ...extra });
|
|
285
356
|
};
|
|
286
357
|
|
|
287
358
|
const markPromptCommitted = () => {
|
|
@@ -317,7 +388,7 @@ export function createRunTurn(bag) {
|
|
|
317
388
|
changed = true;
|
|
318
389
|
return { ...item, headerFinalized: true };
|
|
319
390
|
});
|
|
320
|
-
if (changed) set({ items });
|
|
391
|
+
if (changed) set({ items, structureRevision: (Number(getState().structureRevision) || 0) + 1 });
|
|
321
392
|
return changed;
|
|
322
393
|
};
|
|
323
394
|
|
|
@@ -328,13 +399,15 @@ export function createRunTurn(bag) {
|
|
|
328
399
|
aggregate.ensureVisible?.();
|
|
329
400
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
330
401
|
const callErrors = allCalls.filter((r) => r.isCallError).length;
|
|
402
|
+
const exitErrors = allCalls.filter((r) => r.isExitError).length;
|
|
331
403
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
332
|
-
const succeeded = completed - errors;
|
|
404
|
+
const succeeded = Math.max(0, completed - errors - exitErrors);
|
|
333
405
|
const rawResult = aggregateRawResult(allCalls);
|
|
334
|
-
// Merged count summary (see patchToolCardResult); failures keep
|
|
335
|
-
// 'N
|
|
336
|
-
|
|
337
|
-
|
|
406
|
+
// Merged count summary (see patchToolCardResult); real failures keep
|
|
407
|
+
// 'N Failed', shell command-exits render 'Exit N'/'Y Exit'. Raw
|
|
408
|
+
// preserved for ctrl+o expansion.
|
|
409
|
+
const displayDetail = errors > 0 || exitErrors > 0
|
|
410
|
+
? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode })
|
|
338
411
|
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
339
412
|
patchItem(aggregate.itemId, {
|
|
340
413
|
result: displayDetail,
|
|
@@ -343,6 +416,7 @@ export function createRunTurn(bag) {
|
|
|
343
416
|
isError: errors > 0,
|
|
344
417
|
errorCount: errors,
|
|
345
418
|
callErrorCount: callErrors,
|
|
419
|
+
exitErrorCount: exitErrors,
|
|
346
420
|
count: allCalls.length,
|
|
347
421
|
completedCount: allCalls.length,
|
|
348
422
|
doneCategories: aggregateDoneCategories(allCalls),
|
|
@@ -438,7 +512,7 @@ export function createRunTurn(bag) {
|
|
|
438
512
|
// Seed the new row with the already-visible text so the ● gutter and the
|
|
439
513
|
// first body line appear in the SAME set()/emit() — no empty "●-only"
|
|
440
514
|
// row that scrolls once on its own and again when the body lands.
|
|
441
|
-
|
|
515
|
+
updateStreamingTail(currentAssistantId, { text: String(initialText || '') });
|
|
442
516
|
}
|
|
443
517
|
return currentAssistantId;
|
|
444
518
|
};
|
|
@@ -451,7 +525,6 @@ export function createRunTurn(bag) {
|
|
|
451
525
|
_lastNewlineIdx = -1;
|
|
452
526
|
_emittedNewlineIdx = -2;
|
|
453
527
|
_emittedVisibleText = '';
|
|
454
|
-
_cachedAssistantIndex = -1;
|
|
455
528
|
};
|
|
456
529
|
|
|
457
530
|
const commitAssistantSegment = ({ sealToolBlock = false } = {}) => {
|
|
@@ -462,7 +535,8 @@ export function createRunTurn(bag) {
|
|
|
462
535
|
}
|
|
463
536
|
if (sealToolBlock) clearAggregateContinuation();
|
|
464
537
|
const id = currentAssistantId || ensureAssistant(text);
|
|
465
|
-
|
|
538
|
+
settleStreamingTail(id, { text });
|
|
539
|
+
committedSegments.push(text);
|
|
466
540
|
closeAssistantSegment();
|
|
467
541
|
return true;
|
|
468
542
|
};
|
|
@@ -505,7 +579,6 @@ export function createRunTurn(bag) {
|
|
|
505
579
|
let _lastNewlineIdx = -1; // offset of the last completed-line '\n' found so far
|
|
506
580
|
let _emittedNewlineIdx = -2; // newline offset backing _emittedVisibleText (-2 forces first compute)
|
|
507
581
|
let _emittedVisibleText = ''; // cached visible slice for the current newline offset
|
|
508
|
-
let _cachedAssistantIndex = -1;// cached getState().items index of the streaming assistant row
|
|
509
582
|
// Engine-local streaming scalars. Neither responseLength nor thinkingText is
|
|
510
583
|
// rendered per-token by any consumer: App reads getState().thinking only as a
|
|
511
584
|
// boolean (App.jsx `!!(getState().thinking || liveSpinner?.thinking)`) and the
|
|
@@ -573,22 +646,9 @@ export function createRunTurn(bag) {
|
|
|
573
646
|
// until there is real content to paint.
|
|
574
647
|
if (currentAssistantId || streamingVisibleText.trim()) {
|
|
575
648
|
const id = ensureAssistant(streamingVisibleText);
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
// added/removed) instead of a findIndex scan on every flush.
|
|
580
|
-
let index = _cachedAssistantIndex;
|
|
581
|
-
if (index < 0 || getState().items[index]?.id !== id) {
|
|
582
|
-
index = getState().items.findIndex((it) => it.id === id);
|
|
583
|
-
_cachedAssistantIndex = index;
|
|
584
|
-
}
|
|
585
|
-
if (index >= 0) {
|
|
586
|
-
const current = getState().items[index];
|
|
587
|
-
if (!Object.is(current.text, streamingVisibleText) || current.streaming !== true) {
|
|
588
|
-
const items = getState().items.slice();
|
|
589
|
-
items[index] = { ...current, text: streamingVisibleText, streaming: true };
|
|
590
|
-
patch.items = items;
|
|
591
|
-
}
|
|
649
|
+
const current = getState().streamingTail;
|
|
650
|
+
if (!current || current.id !== id || !Object.is(current.text, streamingVisibleText)) {
|
|
651
|
+
patch.streamingTail = { kind: 'assistant', id, text: streamingVisibleText, streaming: true };
|
|
592
652
|
}
|
|
593
653
|
}
|
|
594
654
|
// Only touch the spinner when there is a real reason: a visible-line
|
|
@@ -596,7 +656,7 @@ export function createRunTurn(bag) {
|
|
|
596
656
|
// pending thinking end timestamp. Refresh responseLength here so the
|
|
597
657
|
// finalize outputTokens fallback stays valid without a per-token push.
|
|
598
658
|
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
599
|
-
const visibleLineChanged = patch.
|
|
659
|
+
const visibleLineChanged = patch.streamingTail !== undefined;
|
|
600
660
|
const thinkingTransition = _publishedThinkingActive === true; // was thinking, now responding
|
|
601
661
|
if (getState().spinner && (visibleLineChanged || thinkingTransition || _pendingThinkingLastEndedAt)) {
|
|
602
662
|
patch.spinner = { ...getState().spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || getState().spinner.thinkingLastEndedAt, mode: compactingActive ? 'compacting' : 'responding' };
|
|
@@ -658,37 +718,38 @@ export function createRunTurn(bag) {
|
|
|
658
718
|
if (!callRec || callRec.resolved || callRec.completedEarly) return;
|
|
659
719
|
aggregate.ensureVisible?.();
|
|
660
720
|
const rawText = toolResultText(message?.content);
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
// Same split as tool-card-results.mjs: a REAL tool-call error drives the
|
|
667
|
-
// red ● dot; command/result failures only mark the card Failed in L2.
|
|
668
|
-
const isCallError = message?.isError === true || message?.toolKind === 'error';
|
|
669
|
-
const isResultError = /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText)) || (isMemoryCall && memoryCoreResultErrorText(rawText) != null);
|
|
670
|
-
const isError = isCallError || isResultError;
|
|
721
|
+
// Tool result text (including HTTP/domain failures, zero matches, task
|
|
722
|
+
// statuses, and shell output) is detail, not a failed invocation. Only
|
|
723
|
+
// the provider's isError/error-tool envelope drives failure counts/red.
|
|
724
|
+
const { exitCode, isExitError, isCallError } = toolCallOutcome(message, rawText);
|
|
725
|
+
const isError = isCallError;
|
|
671
726
|
const text = isError ? toolErrorDisplay(rawText, callRec.name || 'tool') : rawText;
|
|
672
727
|
callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
|
|
673
728
|
assignAggregateSummaryOrder(aggregate, callRec);
|
|
674
729
|
callRec.isError = isError;
|
|
675
730
|
callRec.isCallError = isCallError;
|
|
731
|
+
callRec.isExitError = isExitError;
|
|
732
|
+
callRec.exitCode = exitCode;
|
|
676
733
|
callRec.resultText = text;
|
|
677
734
|
callRec.completedEarly = true;
|
|
678
735
|
const allCalls = [...aggregate.calls.values()];
|
|
679
736
|
const completedCount = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
680
737
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
681
738
|
const callErrors = allCalls.filter((r) => r.isCallError).length;
|
|
682
|
-
const
|
|
739
|
+
const exitErrors = allCalls.filter((r) => r.isExitError).length;
|
|
740
|
+
const succeeded = Math.max(0, completedCount - errors - exitErrors);
|
|
683
741
|
const rawResult = aggregateRawResult(allCalls);
|
|
684
742
|
// Collapsed detail carries the merged per-call count summary even on
|
|
685
743
|
// the early-notify path; patching '' here flipped the detail row back
|
|
686
744
|
// to the 'Running' placeholder between count updates (the visible
|
|
687
745
|
// jitter). Failures keep 'N Failed'. Raw preserved for ctrl+o expansion.
|
|
688
|
-
const displayDetail = errors > 0
|
|
689
|
-
? (succeeded
|
|
746
|
+
const displayDetail = errors > 0 || exitErrors > 0
|
|
747
|
+
? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode })
|
|
690
748
|
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
691
|
-
const
|
|
749
|
+
const currentIndex = itemIndexById.get(card.itemId);
|
|
750
|
+
const currentItem = Number.isInteger(currentIndex) && getState().items[currentIndex]?.id === card.itemId
|
|
751
|
+
? getState().items[currentIndex]
|
|
752
|
+
: null;
|
|
692
753
|
const visualCompleted = Math.max(
|
|
693
754
|
completedCount,
|
|
694
755
|
Math.min(allCalls.length, Number(currentItem?.completedCount || 0)),
|
|
@@ -699,6 +760,7 @@ export function createRunTurn(bag) {
|
|
|
699
760
|
isError: errors > 0,
|
|
700
761
|
errorCount: errors,
|
|
701
762
|
callErrorCount: callErrors,
|
|
763
|
+
exitErrorCount: exitErrors,
|
|
702
764
|
count: allCalls.length,
|
|
703
765
|
completedCount: visualCompleted,
|
|
704
766
|
};
|
|
@@ -725,6 +787,9 @@ export function createRunTurn(bag) {
|
|
|
725
787
|
try {
|
|
726
788
|
const { result, session } = await runtime.ask(userText, {
|
|
727
789
|
drainSteering: (_sessionId, drainOptions) => (isCurrentTurn() ? drainPendingSteering(drainOptions) : []),
|
|
790
|
+
onStreamDelta: () => {
|
|
791
|
+
markTurnProgress('stream-delta');
|
|
792
|
+
},
|
|
728
793
|
onSteerMessage: (text) => {
|
|
729
794
|
if (!markTurnProgress('steer-message')) return;
|
|
730
795
|
// A suppressed live-completion twin is model-visible only; its
|
|
@@ -737,10 +802,12 @@ export function createRunTurn(bag) {
|
|
|
737
802
|
// assistant segment first so the steered user turn and the next
|
|
738
803
|
// assistant response do not get visually merged into one bubble.
|
|
739
804
|
flushStreamBatch();
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
805
|
+
// Commit any pending assistant segment — including a streamed tail
|
|
806
|
+
// that never got a trailing '\n' (no row/currentAssistantId created
|
|
807
|
+
// yet). Using the shared segment-commit helper ensures that tail is
|
|
808
|
+
// materialized as an assistant item instead of being dropped when a
|
|
809
|
+
// steering/agent-completion injection races turn finalization.
|
|
810
|
+
commitAssistantSegment({ sealToolBlock: true });
|
|
744
811
|
assistantText = '';
|
|
745
812
|
const value = String(text || '').trim();
|
|
746
813
|
if (value) {
|
|
@@ -769,6 +836,7 @@ export function createRunTurn(bag) {
|
|
|
769
836
|
}
|
|
770
837
|
const batchCalls = (calls || []).filter(Boolean);
|
|
771
838
|
if (batchCalls.length === 0) return;
|
|
839
|
+
const agentBatch = ++providerToolBatch;
|
|
772
840
|
const committedAssistantSegment = commitAssistantSegment({ sealToolBlock: true });
|
|
773
841
|
if (committedAssistantSegment) {
|
|
774
842
|
// Let the pre-tool assistant preamble paint and settle before the
|
|
@@ -792,11 +860,10 @@ export function createRunTurn(bag) {
|
|
|
792
860
|
// Category drives the aggregate bucket so only same-category calls
|
|
793
861
|
// merge into one card; classify first, then bucket by it.
|
|
794
862
|
const category = classifyToolCategory(name, args);
|
|
795
|
-
// Agent
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
|
|
799
|
-
const bucket = category === 'Agent' ? null : aggregateBucketForCategory(category);
|
|
863
|
+
// Agent actions aggregate only within this provider-emitted batch.
|
|
864
|
+
// They stay outbound category cards; asynchronous inbound Responses
|
|
865
|
+
// are separately tailed by the notification feed and never mix here.
|
|
866
|
+
const bucket = aggregateBucketForCategory(category, { agentBatch });
|
|
800
867
|
const callId = toolCallId(c);
|
|
801
868
|
const callKey = callId || `__tool_${toolCards.length}_${i}`;
|
|
802
869
|
// The old App scan counted multi-pattern calls via
|
|
@@ -859,7 +926,7 @@ export function createRunTurn(bag) {
|
|
|
859
926
|
...categoryEntry,
|
|
860
927
|
count: Number(prevCategory?.count || 0) + Number(categoryEntry.count || 1),
|
|
861
928
|
});
|
|
862
|
-
aggregateCard.calls.set(callKey, { name, args, category, summary: null, summarySeq: null, isError: false, isCallError: false, resultText: null, resolved: false, completedEarly: false });
|
|
929
|
+
aggregateCard.calls.set(callKey, { name, args, category, summary: null, summarySeq: null, isError: false, isCallError: false, isExitError: false, exitCode: null, resultText: null, resolved: false, completedEarly: false });
|
|
863
930
|
touchedAggregates.add(aggregateCard);
|
|
864
931
|
const card = { itemId: aggregateCard.itemId, callId: callKey, done: false, aggregate: aggregateCard };
|
|
865
932
|
if (callId) {
|
|
@@ -1043,19 +1110,40 @@ export function createRunTurn(bag) {
|
|
|
1043
1110
|
syncContextStats({ allowEstimated: true });
|
|
1044
1111
|
|
|
1045
1112
|
const finalText = result?.content != null ? String(result.content) : '';
|
|
1046
|
-
|
|
1113
|
+
// Strip text already sealed as its own item(s) this turn (a tool
|
|
1114
|
+
// preamble, then a no-newline tail committed by onSteerMessage before an
|
|
1115
|
+
// injected steering row) so finalization reconciles only the uncommitted
|
|
1116
|
+
// remainder — never re-creating a committed segment as a duplicate item
|
|
1117
|
+
// that also reorders after the steering row. Walk the segments IN ORDER,
|
|
1118
|
+
// peeling each off the front of the remaining content; skip leading
|
|
1119
|
+
// whitespace/newlines between segments. A segment that does not match at
|
|
1120
|
+
// the current position (provider omitted it from result.content, e.g. a
|
|
1121
|
+
// tool preamble) is left in place and the walk moves on.
|
|
1122
|
+
let finalRemainder = finalText;
|
|
1123
|
+
for (const seg of committedSegments) {
|
|
1124
|
+
// Compare against the whitespace-skipped remainder AND a
|
|
1125
|
+
// whitespace-trimmed segment: a segment sealed with its own leading
|
|
1126
|
+
// newline ('\nTAIL') would otherwise never match the skipped remainder
|
|
1127
|
+
// ('TAIL') and duplicate after the steering row.
|
|
1128
|
+
const skipped = finalRemainder.replace(/^\s+/, '');
|
|
1129
|
+
const trimmedSeg = seg ? seg.replace(/^\s+/, '') : '';
|
|
1130
|
+
if (trimmedSeg && skipped.startsWith(trimmedSeg)) {
|
|
1131
|
+
finalRemainder = skipped.slice(trimmedSeg.length);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (finalRemainder.trim()) {
|
|
1047
1135
|
// The persisted transcript is written from the provider's final content,
|
|
1048
1136
|
// while the live TUI row is fed by streaming deltas. If a provider/parser
|
|
1049
1137
|
// misses or suppresses an early delta, keeping the streamed buffer here
|
|
1050
1138
|
// leaves the final on-screen assistant row missing leading characters even
|
|
1051
1139
|
// though the transcript is correct. Always reconcile the active segment to
|
|
1052
1140
|
// the final provider text when it is available.
|
|
1053
|
-
const id = currentAssistantId || ensureAssistant(
|
|
1054
|
-
currentAssistantText =
|
|
1055
|
-
|
|
1141
|
+
const id = currentAssistantId || ensureAssistant(finalRemainder);
|
|
1142
|
+
currentAssistantText = finalRemainder;
|
|
1143
|
+
settleStreamingTail(id, { text: finalRemainder });
|
|
1056
1144
|
} else if (currentAssistantId && (currentAssistantText.trim() || assistantText.trim())) {
|
|
1057
1145
|
const streamedText = currentAssistantText || assistantText;
|
|
1058
|
-
|
|
1146
|
+
settleStreamingTail(currentAssistantId, { text: streamedText });
|
|
1059
1147
|
}
|
|
1060
1148
|
turnFinishedNormally = true;
|
|
1061
1149
|
}
|
|
@@ -1068,7 +1156,7 @@ export function createRunTurn(bag) {
|
|
|
1068
1156
|
if (error?.name === 'SessionClosedError') {
|
|
1069
1157
|
cancelled = true;
|
|
1070
1158
|
if (assistantText.trim() && currentAssistantId) {
|
|
1071
|
-
|
|
1159
|
+
settleStreamingTail(currentAssistantId, { text: currentAssistantText || assistantText });
|
|
1072
1160
|
}
|
|
1073
1161
|
// Finalize pending tool cards so they don't stay "Running..." forever
|
|
1074
1162
|
// after cancellation. Without this, the spinner vanishes and TurnDone
|
|
@@ -1112,6 +1200,13 @@ export function createRunTurn(bag) {
|
|
|
1112
1200
|
if (isStaleUnwind) {
|
|
1113
1201
|
tuiDebug(`runTurn STALE UNWIND turn=${turnIndex} — force-released; skipping shared UI/state writes`);
|
|
1114
1202
|
} else {
|
|
1203
|
+
if (currentAssistantId && getState().streamingTail?.id === currentAssistantId) {
|
|
1204
|
+
if (currentAssistantText.trim()) {
|
|
1205
|
+
settleStreamingTail(currentAssistantId, { text: currentAssistantText });
|
|
1206
|
+
} else {
|
|
1207
|
+
clearStreamingTail(currentAssistantId);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1115
1210
|
const producedTranscriptItem =
|
|
1116
1211
|
getState().items.length + closingItems.length > itemsAtTurnStart;
|
|
1117
1212
|
const reclaimed = cancelled && flags.activePromptRestore?.reclaimed === true;
|