mixdog 0.9.37 → 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/dispatch-persist-recovery-test.mjs +141 -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 +101 -8
- package/scripts/explore-prompt-policy-test.mjs +156 -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/notify-completion-mirror-test.mjs +73 -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/session-sweep.mjs +266 -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 +56 -48
- 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 -41
- 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/dispatch-persist.mjs +31 -8
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
- 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/ask-session.mjs +18 -52
- 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 +339 -63
- package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
- 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/provider-models.mjs +3 -3
- package/src/session-runtime/runtime-core.mjs +78 -10
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/session-runtime/warmup-schedulers.mjs +7 -1
- 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 +533 -143
- package/src/tui/engine/agent-job-feed.mjs +153 -16
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/queue-helpers.mjs +8 -0
- package/src/tui/engine/session-api.mjs +8 -2
- package/src/tui/engine/session-flow.mjs +34 -2
- 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 +83 -43
- 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,33 +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,
|
|
271
|
+
// The immediate Response card was already pushed above
|
|
272
|
+
// (pushUserOrSyntheticItem). Keep this queued twin model-visible
|
|
273
|
+
// but suppress its drain-time transcript card to avoid a duplicate.
|
|
274
|
+
suppressDisplay: true,
|
|
155
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 });
|
|
285
|
+
// EXPLICIT ack to the emitting runtime: the model-visible completion
|
|
286
|
+
// body was injected into the active loop here, so notifyFnForSession
|
|
287
|
+
// must NOT also mirror it into the pending queue (double injection).
|
|
288
|
+
// A bare truthy return below is display/status handling only — this
|
|
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.
|
|
293
|
+
if (event && typeof event === 'object') event.modelVisibleDelivered = true;
|
|
156
294
|
}
|
|
157
295
|
return true;
|
|
158
296
|
}
|
|
159
|
-
|
|
160
|
-
set(agentStatusState({ force: true }));
|
|
161
|
-
}
|
|
297
|
+
refreshAgentStatus(parsed);
|
|
162
298
|
const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
|
|
163
299
|
const imagePaths = parseInboundImagePaths(event?.meta?.image_paths);
|
|
164
300
|
if (!modelContent && imagePaths.length === 0) return true;
|
|
@@ -202,6 +338,7 @@ export function createAgentJobFeed({
|
|
|
202
338
|
kickExecutionPendingResume,
|
|
203
339
|
flushDeferredExecutionPendingResumeKick,
|
|
204
340
|
scheduleExecutionPendingResumeKick,
|
|
341
|
+
discardExecutionPendingResume,
|
|
205
342
|
updateAgentJobCard,
|
|
206
343
|
buildAgentJobCardPatch,
|
|
207
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;
|
|
@@ -12,6 +12,14 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const QUEUE_PRIORITY = { now: 0, next: 1, later: 2 };
|
|
14
14
|
|
|
15
|
+
// Sentinel display text for a steering entry that is model-visible only: its
|
|
16
|
+
// immediate transcript ("Response …") card was already pushed at delivery time
|
|
17
|
+
// (live execution completion), so the mid-turn steering drain must NOT render a
|
|
18
|
+
// second card for it. drainPendingSteering emits this as the entry's display
|
|
19
|
+
// text; turn.mjs's onSteerMessage recognizes it and skips the push while the
|
|
20
|
+
// model still receives the entry's content.
|
|
21
|
+
export const STEERING_SUPPRESSED_DISPLAY = '\u0000mixdog:suppress-steer-display\u0000';
|
|
22
|
+
|
|
15
23
|
export function queuePriorityValue(value) {
|
|
16
24
|
return QUEUE_PRIORITY[String(value || 'next')] ?? QUEUE_PRIORITY.next;
|
|
17
25
|
}
|
|
@@ -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
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { presentErrorText } from '../../runtime/shared/err-text.mjs';
|
|
5
5
|
import { createSessionStats } from './session-stats.mjs';
|
|
6
|
-
import { queuePriorityValue, defaultQueuePriority, isQueuedEntryEditable, isQueuedEntryVisible, isSlashQueuedEntry, notificationDisplayText, sessionActivityTimestamp, promptDisplayText, mergePromptContents, mergePastedImages, mergePastedTexts, callCommitCallbacks } from './queue-helpers.mjs';
|
|
6
|
+
import { queuePriorityValue, defaultQueuePriority, isQueuedEntryEditable, isQueuedEntryVisible, isSlashQueuedEntry, notificationDisplayText, sessionActivityTimestamp, promptDisplayText, mergePromptContents, mergePastedImages, mergePastedTexts, callCommitCallbacks, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
7
7
|
import { appendTuiSteeringPersist, dropTuiSteeringPersist, drainTuiSteeringPersist } from './tui-steering-persist.mjs';
|
|
8
8
|
|
|
9
9
|
export function createSessionFlow(bag) {
|
|
@@ -53,6 +53,13 @@ export function createSessionFlow(bag) {
|
|
|
53
53
|
key: options.key || null,
|
|
54
54
|
skipSlashCommands: options.skipSlashCommands === true,
|
|
55
55
|
displayText: mode === 'task-notification' ? notificationDisplayText(displayText) : String(displayText || ''),
|
|
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
|
+
: [],
|
|
56
63
|
steeringPersistId: options.steeringPersistId || null,
|
|
57
64
|
steeringPersistRestored: options.steeringPersistRestored === true,
|
|
58
65
|
};
|
|
@@ -186,9 +193,24 @@ export function createSessionFlow(bag) {
|
|
|
186
193
|
const merged = mergePromptContents(batch);
|
|
187
194
|
for (const entry of batch) {
|
|
188
195
|
if (entry.mode === 'pending-resume') continue;
|
|
196
|
+
// Live execution completions push their own immediate Response card
|
|
197
|
+
// at delivery time; the queued twin is model-visible only and must
|
|
198
|
+
// NOT render a second transcript card here (no fall-back to content).
|
|
199
|
+
if (entry.suppressDisplay) continue;
|
|
189
200
|
pushUserOrSyntheticItem(entry.text, entry.id, isQueuedEntryEditable(entry) ? 'user' : 'injected');
|
|
190
201
|
}
|
|
191
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
|
+
);
|
|
192
214
|
const batchPastedImages = mergePastedImages(batch);
|
|
193
215
|
const batchPastedTexts = mergePastedTexts(batch);
|
|
194
216
|
const turnStatus = await bag.runTurn(merged, {
|
|
@@ -198,7 +220,8 @@ export function createSessionFlow(bag) {
|
|
|
198
220
|
onCommitted: () => commitSteeringQueueEntries(batch),
|
|
199
221
|
submittedIds: [...ids],
|
|
200
222
|
restorable: nonEditable.length === 0,
|
|
201
|
-
requeueOnAbort
|
|
223
|
+
requeueOnAbort,
|
|
224
|
+
discardExecutionPendingResumeKeys,
|
|
202
225
|
});
|
|
203
226
|
if (flags.drainEpoch !== drainEpoch) return;
|
|
204
227
|
// A deferred cleared-session UI sync (from a late-settling abandoned
|
|
@@ -274,6 +297,15 @@ export function createSessionFlow(bag) {
|
|
|
274
297
|
if (batch.length === 0) break;
|
|
275
298
|
for (const entry of batch) {
|
|
276
299
|
const content = entry.content;
|
|
300
|
+
if (entry.suppressDisplay) {
|
|
301
|
+
// Model-visible twin of an already-rendered live completion: deliver
|
|
302
|
+
// content to the model but flag onSteerMessage to skip the duplicate
|
|
303
|
+
// transcript card (no fall-back to content-derived display text).
|
|
304
|
+
if (Array.isArray(content) ? content.length > 0 : String(content ?? '').trim().length > 0) {
|
|
305
|
+
out.push({ text: STEERING_SUPPRESSED_DISPLAY, content });
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
277
309
|
const value = typeof content === 'string'
|
|
278
310
|
? content.trim()
|
|
279
311
|
: { text: String(entry.text || '').trim(), content };
|