mixdog 0.9.136 → 0.9.138
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 +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +0 -1
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -15
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +31 -57
- package/src/runtime/agent/orchestrator/session/evidence-union.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -5
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.test.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +1 -6
- package/src/runtime/memory/lib/memory-embed.mjs +2 -2
- package/src/runtime/memory/lib/memory.mjs +3 -4
- package/src/runtime/memory/lib/session-ingest.mjs +5 -9
- package/src/runtime/shared/background-tasks.mjs +3 -1
- package/src/runtime/shared/task-notification-envelope.mjs +1 -1
- package/src/runtime/shared/tool-card-model.mjs +4 -3
- package/src/runtime/shared/tool-execution-contract.mjs +16 -33
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/session-runtime/notification-bus.test.mjs +1 -1
- package/src/session-runtime/provider-request-snapshot.mjs +10 -17
- package/src/session-runtime/provider-request-snapshot.test.mjs +37 -0
- package/src/standalone/hook-bus/handlers.mjs +0 -2
- package/src/standalone/hook-bus.mjs +0 -3
- package/src/tui/dist/index.mjs +3 -3
- package/src/tui/session/agent-envelope.mjs +24 -19
- package/src/tui/session/agent-job-feed.mjs +6 -5
- package/src/tui/session/completion-card-restore.test.mjs +2 -2
- package/src/tui/session/notification-plan.mjs +4 -0
- package/src/tui/session/session-api-ext.mjs +6 -6
- package/src/tui/session-local.mjs +2 -0
package/package.json
CHANGED
|
@@ -311,7 +311,6 @@ export function buildStableProviderPromptCacheKey(provider, opts, prefix = {}) {
|
|
|
311
311
|
effort: cleanString(prefix.effort ?? opts?.effort),
|
|
312
312
|
fast: prefix.fast === true || opts?.fast === true,
|
|
313
313
|
serviceTier: cleanString(prefix.serviceTier),
|
|
314
|
-
toolChoice: cleanString(prefix.toolChoice),
|
|
315
314
|
parallelToolCalls: prefix.parallelToolCalls === false ? false : true,
|
|
316
315
|
cacheLaneSlot: laneEnabled ? shardSlot : null,
|
|
317
316
|
cacheLaneShards: autoLane ? 'auto' : shardCount > 1 ? shardCount : null,
|
|
@@ -828,7 +828,7 @@ export function loadScopedRoleInstructions(agent, provider = null) {
|
|
|
828
828
|
}
|
|
829
829
|
|
|
830
830
|
// --- Compose system prompt — 4-BP cache layout ---
|
|
831
|
-
// Returns
|
|
831
|
+
// Returns the three stable system blocks and the BP3 core used for refreshes.
|
|
832
832
|
// directly to the breakpoint plan:
|
|
833
833
|
// BP1 (1h, system block #1) = baseRules — shared tool policy
|
|
834
834
|
// BP2 (1h, system block #2) = stableSystemContext — profile, skills, deferred/MCP
|
|
@@ -894,20 +894,7 @@ export function composeSystemPrompt(opts) {
|
|
|
894
894
|
? sessionMarkerParts.join('\n\n---\n\n')
|
|
895
895
|
: '';
|
|
896
896
|
|
|
897
|
-
|
|
898
|
-
// Raw role, permission, and task labels are intentionally omitted: role
|
|
899
|
-
// selection already shapes the session/rules/tools, permissions are
|
|
900
|
-
// enforced structurally, and the task body is sent as the actual user turn
|
|
901
|
-
// by askSession().
|
|
902
|
-
const volatileParts = [];
|
|
903
|
-
// workspaceContext's discovered-project list is intentionally not injected:
|
|
904
|
-
// BP3 already carries the scoped session/project environment, while the
|
|
905
|
-
// full layout would add redundant cache fragmentation.
|
|
906
|
-
const volatileTail = volatileParts.length > 0
|
|
907
|
-
? volatileParts.join('\n\n')
|
|
908
|
-
: '';
|
|
909
|
-
|
|
910
|
-
return { baseRules, stableSystemContext, sessionMarkerCore, sessionMarker, volatileTail };
|
|
897
|
+
return { baseRules, stableSystemContext, sessionMarkerCore, sessionMarker };
|
|
911
898
|
}
|
|
912
899
|
// --- Helpers ---
|
|
913
900
|
function readSafe(path) {
|
|
@@ -325,9 +325,9 @@ export function _toAnthropicMessagesForTest(messages) {
|
|
|
325
325
|
// sees, so the cache breakpoint is stable across turns.
|
|
326
326
|
// message-anchor: prefer a safe tool_result tail, then a previous real user
|
|
327
327
|
// text turn if another slot remains. Synthetic
|
|
328
|
-
//
|
|
329
|
-
// are excluded so per-call
|
|
330
|
-
//
|
|
328
|
+
// synthetic reminder messages and current pure-text prompts
|
|
329
|
+
// are excluded so per-call prompt content never becomes a
|
|
330
|
+
// 1h prefix key.
|
|
331
331
|
// messageTtl === null disables the tail. BP3 (tier3) now rides a system block,
|
|
332
332
|
// so it is no longer marked here.
|
|
333
333
|
// ANTHROPIC_MSG_SLOTS=0 is honoured upstream by passing messageTtl = null.
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
DEFAULT_COMPACT_TYPE,
|
|
16
16
|
} from './compact.mjs';
|
|
17
17
|
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
18
|
-
import { stripSoftWarns } from '../tool-loop-guard.mjs';
|
|
19
18
|
import { tryReadCached, setReadCached, invalidatePathForSession, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
|
|
20
19
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
21
20
|
|
|
@@ -34,6 +33,7 @@ import { executeTool, _scopedCacheOutcomeForCall, resolveLiveToolCwd } from './l
|
|
|
34
33
|
// this file; import it from there directly rather than via this module.
|
|
35
34
|
import { recordToolBatch } from '../tools/tool-batch-trace.mjs';
|
|
36
35
|
import { projectProviderEvidence } from './evidence-union.mjs';
|
|
36
|
+
import { prepareProviderPrefixGuard } from './provider-prefix-guard.mjs';
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
import { resolve as resolvePath, isAbsolute } from 'path';
|
|
@@ -185,9 +185,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
185
185
|
const signal = opts.signal || null;
|
|
186
186
|
const sessionAgent = opts.session?.agent;
|
|
187
187
|
const forcedFirstTool = opts.forcedFirstTool ?? null;
|
|
188
|
-
const forcedFirstToolDef = forcedFirstTool
|
|
189
|
-
? tools.find(tool => tool?.name === forcedFirstTool)
|
|
190
|
-
: null;
|
|
191
188
|
// Opaque providerState passthrough. The loop never inspects provider-native
|
|
192
189
|
// payloads; the originating provider owns them. Stateful Responses
|
|
193
190
|
// providers may use it for continuation anchors.
|
|
@@ -205,6 +202,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
205
202
|
}
|
|
206
203
|
};
|
|
207
204
|
const sessionRef = opts.session || null;
|
|
205
|
+
let _providerPrefixGuardState = sessionRef?._providerPrefixGuardState || null;
|
|
206
|
+
let _fixedProviderToolSurface = sessionRef?._providerToolSurfaceSnapshot || null;
|
|
208
207
|
const loopUsageMetricsEpoch = () => Number(sessionRef?.usageMetricsEpoch) || 0;
|
|
209
208
|
const loopUsageMetricsTurnId = () => Number(sessionRef?.usageMetricsTurnId) || 0;
|
|
210
209
|
// Sub-agent (worker/heavy-worker/reviewer/…) sessions
|
|
@@ -427,25 +426,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
427
426
|
// sessionRef.cwd is the live SSOT. The legacy positional cwd is only the
|
|
428
427
|
// turn-start snapshot and becomes stale after an in-turn cwd tool call.
|
|
429
428
|
cwd = resolveLiveToolCwd(cwd, sessionRef);
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
// a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
|
|
433
|
-
// of the cap steering the model to converge — warnings only, nothing is
|
|
434
|
-
// cut off early. Other runaway protection is behavior-based (steering
|
|
435
|
-
// ladder hints, REPEAT_FAIL_LIMIT), never a lower iteration count.
|
|
436
|
-
let _iterWarnStage = 0;
|
|
437
|
-
// Tiny-cap loops can't afford staged 50/75/90%
|
|
438
|
-
// steers — the 50% stage lands on iteration 1 in every session, spamming
|
|
439
|
-
// the normal batch→answer path. For caps < 10 emit ONE wrap-up warning at
|
|
440
|
-
// the penultimate iteration instead; caps >= 10 keep staged behavior.
|
|
441
|
-
const _singleWarn = maxLoopIterations < 10;
|
|
442
|
-
const _iterWarnAt = _singleWarn
|
|
443
|
-
? [Math.max(1, maxLoopIterations - 1)]
|
|
444
|
-
: [
|
|
445
|
-
Math.floor(maxLoopIterations * 0.5),
|
|
446
|
-
Math.floor(maxLoopIterations * 0.75),
|
|
447
|
-
Math.floor(maxLoopIterations * 0.9),
|
|
448
|
-
];
|
|
429
|
+
// The hard cap is the sole count-based steering injection. Behavioral
|
|
430
|
+
// guards below handle repeated failures/dedup without periodic reminders.
|
|
449
431
|
while (true) {
|
|
450
432
|
// A cwd tool call updates sessionRef in place. Refresh before building
|
|
451
433
|
// this iteration's eager dispatcher and cache keys so every following
|
|
@@ -478,26 +460,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
478
460
|
messages.push({ role: 'user', content: `<system-reminder>\n${finalTurnReminder}\n</system-reminder>`, meta: 'hook' });
|
|
479
461
|
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); forcing final text turn.\n`);
|
|
480
462
|
}
|
|
481
|
-
if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
|
|
482
|
-
_iterWarnStage += 1;
|
|
483
|
-
const warnAt = _iterWarnAt[_iterWarnStage - 1];
|
|
484
|
-
const stageMsg = _singleWarn
|
|
485
|
-
? `Iteration budget nearly spent: ${warnAt} of ${maxLoopIterations} iterations used — answer NOW with the best anchors you already hold.`
|
|
486
|
-
: _iterWarnStage === 1
|
|
487
|
-
? `Iteration budget notice: ${warnAt} of ${maxLoopIterations} iterations used. Converge on a conclusion: prefer finishing the current objective over opening new exploration.`
|
|
488
|
-
: `Iteration budget warning (stage ${_iterWarnStage}): ${warnAt} of ${maxLoopIterations} iterations used — the loop hard-stops at ${maxLoopIterations}. Wrap up now: summarize progress, state what remains, and finish with your best current result.`;
|
|
489
|
-
messages.push({ role: 'user', content: `<system-reminder>\n${stageMsg}\n</system-reminder>`, meta: 'hook' });
|
|
490
|
-
process.stderr.write(`[loop] iteration warning stage ${_iterWarnStage} at ${iterations} (sess=${sessionId || 'unknown'}); continuing with steer.\n`);
|
|
491
|
-
try {
|
|
492
|
-
appendAgentTrace({
|
|
493
|
-
sessionId,
|
|
494
|
-
iteration: iterations,
|
|
495
|
-
kind: 'steer',
|
|
496
|
-
payload: { tag: 'iteration_warning', stage: _iterWarnStage, at: iterations, unit: maxLoopIterations },
|
|
497
|
-
agent: sessionAgent || null,
|
|
498
|
-
});
|
|
499
|
-
} catch { /* best-effort */ }
|
|
500
|
-
}
|
|
501
463
|
// Drain queued steering/prompts BEFORE the pre-send compact check, but
|
|
502
464
|
// only immediately after a tool batch has completed: queued entries
|
|
503
465
|
// are attached after tool results are appended and before the recursive
|
|
@@ -510,9 +472,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
510
472
|
_toolBatchJustCompleted = false;
|
|
511
473
|
_lastToolBatchHadSleep = false;
|
|
512
474
|
}
|
|
513
|
-
const baseSendTools =
|
|
514
|
-
? tools
|
|
515
|
-
: (forcedFirstToolDef && toolCallsTotal === 0 ? [forcedFirstToolDef] : tools);
|
|
475
|
+
const baseSendTools = tools;
|
|
516
476
|
let sendTools;
|
|
517
477
|
let requestToolScope;
|
|
518
478
|
let compactChanged;
|
|
@@ -527,23 +487,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
527
487
|
|| messages.some((message, index) => message !== messagesBeforeTranscriptRepair[index]))) {
|
|
528
488
|
opts.cacheBreakIntent = 'transcript_rebuild';
|
|
529
489
|
}
|
|
530
|
-
|
|
531
|
-
const _m = messages[_i];
|
|
532
|
-
if (_m && _m.role === 'tool' && typeof _m.content === 'string' && _m.content.includes('⚠')) {
|
|
533
|
-
const _stripped = stripSoftWarns(_m.content);
|
|
534
|
-
if (_stripped !== _m.content) {
|
|
535
|
-
_m.content = _stripped;
|
|
536
|
-
if (!opts.cacheBreakIntent) opts.cacheBreakIntent = 'soft_warn_strip';
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
sendTools = snapshotProviderRequestTools({
|
|
490
|
+
const _candidateSendTools = snapshotProviderRequestTools({
|
|
541
491
|
provider: sessionRef?.provider || provider?.name,
|
|
542
492
|
tools: baseSendTools,
|
|
543
493
|
nativeTools: opts.nativeTools,
|
|
544
494
|
messages,
|
|
545
495
|
session: sessionRef,
|
|
546
496
|
});
|
|
497
|
+
if (!_fixedProviderToolSurface) {
|
|
498
|
+
_fixedProviderToolSurface = _candidateSendTools;
|
|
499
|
+
if (sessionRef) sessionRef._providerToolSurfaceSnapshot = _fixedProviderToolSurface;
|
|
500
|
+
}
|
|
501
|
+
sendTools = _fixedProviderToolSurface;
|
|
547
502
|
requestToolScope = {
|
|
548
503
|
session: sessionRef,
|
|
549
504
|
provider: sessionRef?.provider || provider?.name,
|
|
@@ -655,8 +610,25 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
655
610
|
const _evidenceProjection = projectProviderEvidence(_providerMessageSource, {
|
|
656
611
|
enabled: !_evidenceUnionDisabled,
|
|
657
612
|
apply: !_evidenceUnionShadow,
|
|
613
|
+
// Path aliases are a whole-history projection: a later repeated
|
|
614
|
+
// path can rewrite already-sent tool results and invalidate every
|
|
615
|
+
// provider's prefix cache. Row/exact-result references are
|
|
616
|
+
// append-only, so retain those and disable only the unsafe pass.
|
|
617
|
+
pathAliases: false,
|
|
658
618
|
});
|
|
659
619
|
const _providerMessages = _evidenceProjection.messages;
|
|
620
|
+
const _providerPrefixGuardCandidate = prepareProviderPrefixGuard(
|
|
621
|
+
_providerPrefixGuardState,
|
|
622
|
+
_providerMessages,
|
|
623
|
+
{
|
|
624
|
+
tools: sendTools,
|
|
625
|
+
nativeTools: Array.isArray(opts.nativeTools) ? opts.nativeTools : [],
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
provider: sessionRef?.provider || provider?.name || null,
|
|
629
|
+
cacheBreakIntent: opts.cacheBreakIntent,
|
|
630
|
+
},
|
|
631
|
+
);
|
|
660
632
|
if (_evidenceProjection.stats.reusedRows > 0
|
|
661
633
|
|| _evidenceProjection.stats.exactResultRefs > 0
|
|
662
634
|
|| _evidenceProjection.stats.pathAliases > 0) {
|
|
@@ -723,6 +695,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
723
695
|
continue;
|
|
724
696
|
}
|
|
725
697
|
response = _sendResult.response;
|
|
698
|
+
_providerPrefixGuardState = _providerPrefixGuardCandidate;
|
|
699
|
+
if (sessionRef) sessionRef._providerPrefixGuardState = _providerPrefixGuardState;
|
|
726
700
|
if (_imageStripActive && Array.isArray(_pendingImageStripPersistMessages)) {
|
|
727
701
|
messages.splice(0, messages.length, ..._pendingImageStripPersistMessages);
|
|
728
702
|
_imageStripActive = false;
|
|
@@ -526,7 +526,18 @@ export function projectProviderEvidence(messages, options = {}) {
|
|
|
526
526
|
}
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
const pathProjection =
|
|
529
|
+
const pathProjection = options.pathAliases === false
|
|
530
|
+
? {
|
|
531
|
+
messages: projected || messages,
|
|
532
|
+
stats: {
|
|
533
|
+
pathFacts: 0,
|
|
534
|
+
pathAliases: 0,
|
|
535
|
+
reusedPathFacts: 0,
|
|
536
|
+
pathAliasBytesSaved: 0,
|
|
537
|
+
changedIndexes: [],
|
|
538
|
+
},
|
|
539
|
+
}
|
|
540
|
+
: projectProviderPathAliases(projected || messages, { apply });
|
|
530
541
|
stats.pathFacts = pathProjection.stats.pathFacts;
|
|
531
542
|
stats.pathAliases = pathProjection.stats.pathAliases;
|
|
532
543
|
stats.reusedPathFacts = pathProjection.stats.reusedPathFacts;
|
|
@@ -209,3 +209,19 @@ test('shadow mode reports savings without changing provider messages', () => {
|
|
|
209
209
|
assert.equal(projected.stats.reusedRows, 1);
|
|
210
210
|
assert.ok(projected.stats.afterBytes < projected.stats.beforeBytes);
|
|
211
211
|
});
|
|
212
|
+
|
|
213
|
+
test('provider projection stays append-only when path aliases are disabled', () => {
|
|
214
|
+
const repeated = `src/${'nested/'.repeat(10)}feature.mjs`;
|
|
215
|
+
const firstMessages = [
|
|
216
|
+
call('glob_1', 'glob'),
|
|
217
|
+
result('glob_1', `${repeated}\nsrc/unique.mjs`),
|
|
218
|
+
];
|
|
219
|
+
const first = projectProviderEvidence(firstMessages, { pathAliases: false });
|
|
220
|
+
const later = projectProviderEvidence([
|
|
221
|
+
...firstMessages,
|
|
222
|
+
call('read_1', 'read', { file_path: repeated }),
|
|
223
|
+
result('read_1', `${repeated} [ok]\n1→export const value = 1;`),
|
|
224
|
+
], { pathAliases: false });
|
|
225
|
+
assert.deepEqual(later.messages.slice(0, first.messages.length), first.messages);
|
|
226
|
+
assert.equal(later.stats.pathAliases, 0);
|
|
227
|
+
});
|
|
@@ -319,7 +319,7 @@ export function createSession(opts) {
|
|
|
319
319
|
? describeShellStartupPolicy()
|
|
320
320
|
: '',
|
|
321
321
|
].filter(Boolean).join('\n');
|
|
322
|
-
const { baseRules, stableSystemContext, sessionMarkerCore, sessionMarker
|
|
322
|
+
const { baseRules, stableSystemContext, sessionMarkerCore, sessionMarker } = composeSystemPrompt({
|
|
323
323
|
userPrompt: opts.systemPrompt,
|
|
324
324
|
agentRules: injectedRules || undefined,
|
|
325
325
|
roleRules: roleRules || undefined,
|
|
@@ -357,10 +357,6 @@ export function createSession(opts) {
|
|
|
357
357
|
// the field and serialize content as a normal system instruction).
|
|
358
358
|
messages.push({ role: 'system', content: sessionMarker, cacheTier: 'tier3' });
|
|
359
359
|
}
|
|
360
|
-
if (volatileTail) {
|
|
361
|
-
messages.push({ role: 'user', content: `<system-reminder>\n${volatileTail}\n</system-reminder>` });
|
|
362
|
-
messages.push({ role: 'assistant', content: '.' });
|
|
363
|
-
}
|
|
364
360
|
if (opts.files?.length) {
|
|
365
361
|
const fileContext = opts.files
|
|
366
362
|
.map(f => `### ${f.path}\n\`\`\`\n${f.content}\n\`\`\``)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
const COMPACTION_INTENTS = new Set([
|
|
4
|
+
'automatic_compaction',
|
|
5
|
+
'deferred_body_compaction',
|
|
6
|
+
'manual_compaction',
|
|
7
|
+
'post_turn_compaction',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function digest(value) {
|
|
11
|
+
const encoded = JSON.stringify(value);
|
|
12
|
+
if (typeof encoded !== 'string') {
|
|
13
|
+
throw new TypeError('provider prefix value is not serializable');
|
|
14
|
+
}
|
|
15
|
+
return createHash('sha256').update(encoded).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function snapshot(messages, requestPrefix) {
|
|
19
|
+
return {
|
|
20
|
+
messageHashes: messages.map(digest),
|
|
21
|
+
requestPrefixHash: digest(requestPrefix),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ProviderPrefixMutationError extends Error {
|
|
26
|
+
constructor(message, details = {}) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'ProviderPrefixMutationError';
|
|
29
|
+
this.code = 'PROVIDER_PREFIX_MUTATION';
|
|
30
|
+
this.details = details;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isCompactionPrefixReset(intent) {
|
|
35
|
+
return COMPACTION_INTENTS.has(String(intent || ''));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function prepareProviderPrefixGuard(previous, messages, requestPrefix, options = {}) {
|
|
39
|
+
const next = snapshot(Array.isArray(messages) ? messages : [], requestPrefix);
|
|
40
|
+
if (!previous || isCompactionPrefixReset(options.cacheBreakIntent)) return next;
|
|
41
|
+
|
|
42
|
+
if (previous.requestPrefixHash !== next.requestPrefixHash) {
|
|
43
|
+
throw new ProviderPrefixMutationError('provider request prefix changed outside compaction', {
|
|
44
|
+
provider: options.provider || null,
|
|
45
|
+
kind: 'request_prefix',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (next.messageHashes.length < previous.messageHashes.length) {
|
|
49
|
+
throw new ProviderPrefixMutationError('provider message history shrank outside compaction', {
|
|
50
|
+
provider: options.provider || null,
|
|
51
|
+
kind: 'history_shrink',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for (let index = 0; index < previous.messageHashes.length; index += 1) {
|
|
55
|
+
if (previous.messageHashes[index] === next.messageHashes[index]) continue;
|
|
56
|
+
throw new ProviderPrefixMutationError('provider message prefix changed outside compaction', {
|
|
57
|
+
provider: options.provider || null,
|
|
58
|
+
kind: 'message_prefix',
|
|
59
|
+
index,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
prepareProviderPrefixGuard,
|
|
5
|
+
ProviderPrefixMutationError,
|
|
6
|
+
} from './provider-prefix-guard.mjs';
|
|
7
|
+
|
|
8
|
+
const CACHE_PROVIDERS = [
|
|
9
|
+
'anthropic',
|
|
10
|
+
'anthropic-oauth',
|
|
11
|
+
'openai',
|
|
12
|
+
'openai-oauth',
|
|
13
|
+
'xai',
|
|
14
|
+
'grok-oauth',
|
|
15
|
+
'gemini',
|
|
16
|
+
'deepseek',
|
|
17
|
+
'opencode-go',
|
|
18
|
+
'cursor-oauth',
|
|
19
|
+
'ollama',
|
|
20
|
+
'lmstudio',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
test('accepts append-only provider history for every provider surface', () => {
|
|
24
|
+
for (const provider of CACHE_PROVIDERS) {
|
|
25
|
+
const first = prepareProviderPrefixGuard(
|
|
26
|
+
null,
|
|
27
|
+
[{ role: 'user', content: 'one' }],
|
|
28
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
29
|
+
{ provider },
|
|
30
|
+
);
|
|
31
|
+
assert.doesNotThrow(() => prepareProviderPrefixGuard(
|
|
32
|
+
first,
|
|
33
|
+
[
|
|
34
|
+
{ role: 'user', content: 'one' },
|
|
35
|
+
{ role: 'assistant', content: 'two' },
|
|
36
|
+
],
|
|
37
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
38
|
+
{ provider },
|
|
39
|
+
), provider);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('rejects prior-message and tool-prefix rewrites outside compaction', () => {
|
|
44
|
+
const first = prepareProviderPrefixGuard(
|
|
45
|
+
null,
|
|
46
|
+
[{ role: 'user', content: 'one' }],
|
|
47
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
48
|
+
);
|
|
49
|
+
assert.throws(
|
|
50
|
+
() => prepareProviderPrefixGuard(
|
|
51
|
+
first,
|
|
52
|
+
[{ role: 'user', content: 'rewritten' }],
|
|
53
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
54
|
+
),
|
|
55
|
+
ProviderPrefixMutationError,
|
|
56
|
+
);
|
|
57
|
+
assert.throws(
|
|
58
|
+
() => prepareProviderPrefixGuard(
|
|
59
|
+
first,
|
|
60
|
+
[{ role: 'user', content: 'one' }],
|
|
61
|
+
{ tools: [{ name: 'read' }, { name: 'shell' }], nativeTools: [] },
|
|
62
|
+
),
|
|
63
|
+
ProviderPrefixMutationError,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('allows only compaction intents to establish a new prefix', () => {
|
|
68
|
+
const first = prepareProviderPrefixGuard(
|
|
69
|
+
null,
|
|
70
|
+
[{ role: 'user', content: 'one' }],
|
|
71
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
72
|
+
);
|
|
73
|
+
for (const cacheBreakIntent of [
|
|
74
|
+
'automatic_compaction',
|
|
75
|
+
'deferred_body_compaction',
|
|
76
|
+
'manual_compaction',
|
|
77
|
+
'post_turn_compaction',
|
|
78
|
+
]) {
|
|
79
|
+
assert.doesNotThrow(() => prepareProviderPrefixGuard(
|
|
80
|
+
first,
|
|
81
|
+
[{ role: 'user', content: 'compacted' }],
|
|
82
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
83
|
+
{ cacheBreakIntent },
|
|
84
|
+
));
|
|
85
|
+
}
|
|
86
|
+
assert.throws(
|
|
87
|
+
() => prepareProviderPrefixGuard(
|
|
88
|
+
first,
|
|
89
|
+
[{ role: 'user', content: 'repaired' }],
|
|
90
|
+
{ tools: [{ name: 'read' }], nativeTools: [] },
|
|
91
|
+
{ cacheBreakIntent: 'transcript_rebuild' },
|
|
92
|
+
),
|
|
93
|
+
ProviderPrefixMutationError,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
@@ -67,7 +67,7 @@ function archivedAgentNotification(content, sessionId) {
|
|
|
67
67
|
const text = typeof content === 'string' ? content : '';
|
|
68
68
|
const marker = '\n\nResult:\n';
|
|
69
69
|
const markerAt = text.indexOf(marker);
|
|
70
|
-
if (markerAt < 0 || !/
|
|
70
|
+
if (markerAt < 0 || !/Async agent task .* finished\./.test(text.slice(0, markerAt))) return null;
|
|
71
71
|
const lines = text.slice(markerAt + marker.length)
|
|
72
72
|
.split(/\r?\n/)
|
|
73
73
|
.map((line) => line.replace(/^>\s?/, ''));
|
|
@@ -684,7 +684,6 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
684
684
|
task ? renderBackgroundTask(task) : (result.jobId ? `[task_id: ${result.jobId}]` : null),
|
|
685
685
|
'',
|
|
686
686
|
result.backgroundMessage || 'auto-backgrounded; still running — judge from the partial output whether waiting can finish in budget, or diagnose and pursue an alternative.',
|
|
687
|
-
result.jobId ? 'You will be notified when it completes; do not poll.' : null,
|
|
688
687
|
partialOutput ? `\n${partialOutput}` : '',
|
|
689
688
|
].filter((l) => l !== null && l !== '');
|
|
690
689
|
return _prependDestructiveWarning(command, lines.join('\n'));
|
|
@@ -135,12 +135,7 @@ export async function executeTaskTool(args, options = {}) {
|
|
|
135
135
|
if (isShellTask) refreshShellTask(taskId, { includeRunning: true });
|
|
136
136
|
const latest = getBackgroundTask(taskId, { context: options }) || task;
|
|
137
137
|
const rendered = renderBackgroundTask(latest, { includeResult: true });
|
|
138
|
-
|
|
139
|
-
return [
|
|
140
|
-
rendered,
|
|
141
|
-
'',
|
|
142
|
-
'Still running. Completion will be delivered automatically; do not poll or call task again unless the user explicitly asks for another snapshot.',
|
|
143
|
-
].join('\n');
|
|
138
|
+
return rendered;
|
|
144
139
|
}
|
|
145
140
|
|
|
146
141
|
if (action === 'cancel') {
|
|
@@ -104,7 +104,7 @@ const RAW_EMBED_EXCLUDE_CONTENT_RES = [
|
|
|
104
104
|
/^\s*\[tool_call\b/i,
|
|
105
105
|
/^\s*\[tool_result\b/i,
|
|
106
106
|
/^\s*\[mixdog-runtime\]/i,
|
|
107
|
-
/^\s*
|
|
107
|
+
/^\s*Async .+ finished\./i,
|
|
108
108
|
/^\s*background task\b/i,
|
|
109
109
|
]
|
|
110
110
|
const RAW_EMBED_EXCLUDE_NON_CONVERSATION_CONTENT_RES = [
|
|
@@ -126,7 +126,7 @@ function isRawEmbeddable(role, content) {
|
|
|
126
126
|
|
|
127
127
|
const RAW_EMBED_SQL_EXCLUDE_ROLE_VALUES = [...RAW_EMBED_EXCLUDE_ROLES]
|
|
128
128
|
const RAW_EMBED_SQL_ALWAYS_EXCLUDE_CONTENT_RE =
|
|
129
|
-
'^\\s*(\\[tool_call(?:\\s|\\]|$)|\\[tool_result(?:\\s|\\]|$)|\\[mixdog-runtime\\]|
|
|
129
|
+
'^\\s*(\\[tool_call(?:\\s|\\]|$)|\\[tool_result(?:\\s|\\]|$)|\\[mixdog-runtime\\]|Async .+ finished\\.|background task(\\s|$))'
|
|
130
130
|
const RAW_EMBED_SQL_NON_CONVERSATION_EXCLUDE_CONTENT_RE =
|
|
131
131
|
'^\\s*\\[(system|log|offload|debug|trace|info|warn|warning|error|fatal)\\]'
|
|
132
132
|
|
|
@@ -447,9 +447,8 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
|
447
447
|
} catch (err) {
|
|
448
448
|
__mixdogMemoryLog(`[memory] attachment-placeholder cleanup failed: ${err?.message || err}\n`)
|
|
449
449
|
}
|
|
450
|
-
// One-time cleanup: runtime tool-completion notification rows ("
|
|
451
|
-
//
|
|
452
|
-
// step." followed by an unquoted or `> `-quoted Result body, and
|
|
450
|
+
// One-time cleanup: runtime tool-completion notification rows ("Async ...
|
|
451
|
+
// finished." followed by an unquoted or `> `-quoted Result body, and
|
|
453
452
|
// "[mixdog-runtime] ..." nudges) that were persisted by the transcript
|
|
454
453
|
// watcher before it gained the shouldExcludeIngestMessage exclusion
|
|
455
454
|
// (transcript-ingest.mjs). Gated by a meta flag so this DELETE runs at
|
|
@@ -467,7 +466,7 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
|
467
466
|
const candidates = await db.query(
|
|
468
467
|
`SELECT id, content FROM entries
|
|
469
468
|
WHERE role = 'user' AND (
|
|
470
|
-
content LIKE '
|
|
469
|
+
content LIKE 'Async % finished.%'
|
|
471
470
|
OR content LIKE '[mixdog-runtime]%'
|
|
472
471
|
OR content LIKE 'background task%'
|
|
473
472
|
)`,
|
|
@@ -274,16 +274,13 @@ export function sessionMessageContentForIngest(m) {
|
|
|
274
274
|
return base
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
// Head-line shape of toolCompletionInstruction()
|
|
278
|
-
//
|
|
279
|
-
// `> ` prefix on the Result body (real DB rows look like:
|
|
280
|
-
// "The async shell task <id> has finished (completed, exit 0) - review this
|
|
281
|
-
// result in your next step.\nResult:\nbackground task\ntask_id: ...").
|
|
277
|
+
// Head-line shape of toolCompletionInstruction() as persisted by
|
|
278
|
+
// mgr.enqueuePendingMessage — unquoted, with no `> ` prefix on Result.
|
|
282
279
|
// isModelVisibleToolCompletionWrapper only matches the QUOTED (`> `) shape
|
|
283
280
|
// mirrored via the notify-wrapper's own quoting, so it misses this unquoted
|
|
284
281
|
// persisted form. The instruction head alone is a sufficient, unambiguous
|
|
285
282
|
// fingerprint (only the runtime ever emits this exact phrase).
|
|
286
|
-
const UNQUOTED_TOOL_COMPLETION_HEAD_RE = /^
|
|
283
|
+
const UNQUOTED_TOOL_COMPLETION_HEAD_RE = /^Async .+ finished\./i
|
|
287
284
|
|
|
288
285
|
// Exported so memory.mjs's one-time cleanup (ensureCurrentSchemaExtensions)
|
|
289
286
|
// can confirm SQL-prefiltered candidate rows with the SAME predicate the live
|
|
@@ -328,9 +325,8 @@ export function shouldExcludeIngestMessage(m) {
|
|
|
328
325
|
const text = typeof raw === 'string' ? raw : ''
|
|
329
326
|
if (/^\[mixdog-runtime\]/.test(text.trimStart())) return true
|
|
330
327
|
if (isInternalRuntimeNotificationText(text)) return true
|
|
331
|
-
// Model-visible tool-completion mirror rows
|
|
332
|
-
// modelVisibleToolCompletionMessage
|
|
333
|
-
// finished ... - review this result in your next step.\n\nResult:\n> ...").
|
|
328
|
+
// Model-visible tool-completion mirror rows from
|
|
329
|
+
// modelVisibleToolCompletionMessage ("Async ... finished.\n\nResult:\n> ...").
|
|
334
330
|
// These are runtime notifications, not conversation, on both ingest paths.
|
|
335
331
|
if (isModelVisibleToolCompletionWrapper(text)) return true
|
|
336
332
|
// Unquoted persisted form of the same wrapper (see comment above).
|
|
@@ -466,6 +466,8 @@ export function renderBackgroundTask(taskOrId, { includeResult = false } = {}) {
|
|
|
466
466
|
const lines = [
|
|
467
467
|
'background task',
|
|
468
468
|
`task_id: ${task.taskId}`,
|
|
469
|
+
`surface: ${task.surface}`,
|
|
470
|
+
`operation: ${task.operation}`,
|
|
469
471
|
task.label ? `label: ${task.label}` : null,
|
|
470
472
|
`status: ${task.status}`,
|
|
471
473
|
`started: ${task.startedAt}`,
|
|
@@ -479,7 +481,7 @@ export function renderBackgroundTask(taskOrId, { includeResult = false } = {}) {
|
|
|
479
481
|
&& _so.slice(0, -11) === _se.slice(0, -11)
|
|
480
482
|
? _so.slice(0, -11) : null;
|
|
481
483
|
for (const [key, value] of Object.entries(visibleMeta)) {
|
|
482
|
-
if (key === 'task_id'
|
|
484
|
+
if (key === 'task_id' || key === 'surface' || key === 'operation') continue;
|
|
483
485
|
if (logsBase && (key === 'stdout' || key === 'stderr')) continue;
|
|
484
486
|
lines.push(`${key}: ${value}`);
|
|
485
487
|
}
|
|
@@ -70,7 +70,7 @@ export function renderShellCompletionEnvelope({
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// Build the shell completion instruction via the shared wording so all async
|
|
73
|
-
// surfaces read identically ("
|
|
73
|
+
// surfaces read identically ("Async shell task … finished."). The
|
|
74
74
|
// exit detail is folded into the shared detail slot.
|
|
75
75
|
export function shellCompletionInstruction({ jobId, status, exitCode = null } = {}) {
|
|
76
76
|
return toolCompletionInstruction({
|
|
@@ -260,10 +260,11 @@ function joinActionAgent(action, agent) {
|
|
|
260
260
|
export function agentResponseTitle(args, count = 1) {
|
|
261
261
|
const total = Math.max(1, Number(count) || 1);
|
|
262
262
|
if (total > 1) return `Responses ${total} agents`;
|
|
263
|
-
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
263
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '') || 'Agent';
|
|
264
264
|
// The agent + model identify the responder; the response summary itself
|
|
265
265
|
// is hidden in the collapsed card (expanding still shows the full body).
|
|
266
|
-
//
|
|
266
|
+
// Keep the surface identifiable even when a failed/legacy completion has no
|
|
267
|
+
// concrete agent identity.
|
|
267
268
|
return withModelAndTag(joinActionAgent('Response', name), args);
|
|
268
269
|
}
|
|
269
270
|
|
|
@@ -322,7 +323,7 @@ export function hasAgentResponseResult(value) {
|
|
|
322
323
|
}
|
|
323
324
|
if (/^agent result\b/i.test(trimmed)) continue;
|
|
324
325
|
if (/^(?:undefined|null)$/i.test(trimmed)) continue;
|
|
325
|
-
if (/^<\/?(?:final-answer|task-
|
|
326
|
+
if (/^<\/?(?:final-answer|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
|
|
326
327
|
if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|agent|preset|model|effort|fast|limits|started|finished|error|notification|queueDepth):?\s*/i.test(trimmed)) continue;
|
|
327
328
|
if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
|
|
328
329
|
if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
|
|
@@ -38,26 +38,9 @@ export function backgroundTaskHeaderStatus(text) {
|
|
|
38
38
|
return clean(match?.[1]).toLowerCase();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
function notificationHead(text) {
|
|
42
|
-
const value = String(text || '').trim();
|
|
43
|
-
const match = /\n\s*\n/.exec(value);
|
|
44
|
-
if (!match) return value;
|
|
45
|
-
return value.slice(0, match.index).trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isInternalTaskNotificationEnvelope(text) {
|
|
49
|
-
const value = String(text || '').trim();
|
|
50
|
-
if (!value) return false;
|
|
51
|
-
if (/^background task\b/i.test(value)) return false;
|
|
52
|
-
if (/^<task-notification\b/i.test(value)) return true;
|
|
53
|
-
const head = notificationHead(value);
|
|
54
|
-
return /^<task-notification\b/i.test(head);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
export function shouldPersistModelVisibleToolCompletion(text, meta = {}) {
|
|
58
42
|
const message = String(text || '').trim();
|
|
59
43
|
if (!message) return false;
|
|
60
|
-
if (isInternalTaskNotificationEnvelope(message)) return false;
|
|
61
44
|
|
|
62
45
|
const metaStatus = clean(meta?.status).toLowerCase();
|
|
63
46
|
if (NON_PERSISTENT_TOOL_STATUSES.has(metaStatus)) return false;
|
|
@@ -96,7 +79,6 @@ export function isBracketedShellNotificationEnvelope(text) {
|
|
|
96
79
|
export function isInternalRuntimeNotificationText(text) {
|
|
97
80
|
const value = String(text ?? '').trim();
|
|
98
81
|
if (!value) return false;
|
|
99
|
-
if (isInternalTaskNotificationEnvelope(value)) return true;
|
|
100
82
|
if (isBracketedShellNotificationEnvelope(value)) return true;
|
|
101
83
|
if (/^background task\b/i.test(value)
|
|
102
84
|
&& /^task_id:\s*\S+/mi.test(value)
|
|
@@ -132,7 +114,7 @@ export function toolCompletionInstruction({ surface = 'tool', id, status, detail
|
|
|
132
114
|
? 'agent task'
|
|
133
115
|
: `${surface} execution`;
|
|
134
116
|
const statusText = status ? ` (${status}${detail ? `, ${detail}` : ''})` : '';
|
|
135
|
-
return `
|
|
117
|
+
return `Async ${label} ${id || ''}${statusText} finished.`;
|
|
136
118
|
}
|
|
137
119
|
|
|
138
120
|
function toolCompletionMeta({
|
|
@@ -156,8 +138,6 @@ function toolCompletionMeta({
|
|
|
156
138
|
};
|
|
157
139
|
}
|
|
158
140
|
|
|
159
|
-
const MODEL_VISIBLE_COMPLETION_INSTRUCTION_RE = /\b(async (?:agent task|shell task|\w+ execution)|Async \S+)/i;
|
|
160
|
-
const MODEL_VISIBLE_COMPLETION_REVIEW_RE = /has finished\b[\s\S]*review this result in your next step/i;
|
|
161
141
|
const MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE = /^Async .+ finished\./i;
|
|
162
142
|
|
|
163
143
|
export function isModelVisibleToolCompletionWrapper(text) {
|
|
@@ -167,9 +147,7 @@ export function isModelVisibleToolCompletionWrapper(text) {
|
|
|
167
147
|
if (!resultSplit) return false;
|
|
168
148
|
const preamble = value.slice(0, resultSplit.index).trim();
|
|
169
149
|
if (!preamble) return false;
|
|
170
|
-
const instructionLike =
|
|
171
|
-
|| MODEL_VISIBLE_COMPLETION_REVIEW_RE.test(preamble)
|
|
172
|
-
|| MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
150
|
+
const instructionLike = MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
173
151
|
if (!instructionLike) return false;
|
|
174
152
|
const quotedSection = value.slice(resultSplit.index + resultSplit[0].length);
|
|
175
153
|
const quotedLines = quotedSection.split(/\r?\n/).filter((line) => line.length > 0);
|
|
@@ -193,9 +171,7 @@ export function isLikelyToolCompletionWrapper(text) {
|
|
|
193
171
|
if (!resultSplit) return false;
|
|
194
172
|
const preamble = value.slice(0, resultSplit.index).trim();
|
|
195
173
|
if (!preamble) return false;
|
|
196
|
-
const instructionLike =
|
|
197
|
-
|| MODEL_VISIBLE_COMPLETION_REVIEW_RE.test(preamble)
|
|
198
|
-
|| MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
174
|
+
const instructionLike = MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
199
175
|
if (!instructionLike) return false;
|
|
200
176
|
const quotedSection = value.slice(resultSplit.index + resultSplit[0].length);
|
|
201
177
|
const quotedLines = quotedSection.split(/\r?\n/).filter((line) => line.length > 0);
|
|
@@ -205,11 +181,10 @@ export function isLikelyToolCompletionWrapper(text) {
|
|
|
205
181
|
}
|
|
206
182
|
|
|
207
183
|
const INTERNAL_TRANSCRIPT_CONTEXT_RE =
|
|
208
|
-
/^<(?:system-reminder|
|
|
184
|
+
/^<(?:system-reminder|skill|memory-context|mcp-instructions|available-deferred-tools|event)\b/i;
|
|
209
185
|
const INTERNAL_TRANSCRIPT_SYNTHETIC_RE =
|
|
210
186
|
/^(?:\[mixdog-runtime\]|A previous model worked on this task and produced the compacted handoff summary below\b|Re-attached after compaction\b|Reference files:\s)/i;
|
|
211
|
-
const INTERNAL_TRANSCRIPT_ASYNC_HEAD_RE =
|
|
212
|
-
/^The async (?:agent task|shell task|\w+ execution) \S+ has (?:finished|completed)\b/i;
|
|
187
|
+
const INTERNAL_TRANSCRIPT_ASYNC_HEAD_RE = /^Async .+ finished\./i;
|
|
213
188
|
// Persisted USER cancellation control rows ("[Request interrupted by user]"
|
|
214
189
|
// and its tool-use variant) exist for the next model step, not for humans:
|
|
215
190
|
// the human already saw the cancel they typed. The live engine path already
|
|
@@ -222,6 +197,16 @@ const INTERNAL_TRANSCRIPT_ASYNC_HEAD_RE =
|
|
|
222
197
|
// (scripts/turn-checkpoint-crash-test.mjs).
|
|
223
198
|
const INTERNAL_TRANSCRIPT_INTERRUPT_RE =
|
|
224
199
|
/^\[request interrupted by user(?: for tool use)?\]$/i;
|
|
200
|
+
const TRANSCRIPT_CANCELLED_STATUS_RE =
|
|
201
|
+
/^\[request interrupted(?: by process restart)?\]$/i;
|
|
202
|
+
|
|
203
|
+
// Crash/implicit interruption markers remain in model-visible history for
|
|
204
|
+
// recovery, but every human transcript renders them as a Cancelled status row.
|
|
205
|
+
// Explicit user-cancel markers stay on the hidden-control-row path above
|
|
206
|
+
// because the live surface already emitted its cancellation status.
|
|
207
|
+
export function isTranscriptCancelledStatusText(text) {
|
|
208
|
+
return TRANSCRIPT_CANCELLED_STATUS_RE.test(String(text ?? '').trim());
|
|
209
|
+
}
|
|
225
210
|
|
|
226
211
|
// One display-only policy for every transcript surface. Runtime control rows
|
|
227
212
|
// may be persisted as ordinary role:user messages so the next model step can
|
|
@@ -245,9 +230,7 @@ export function isInternalTranscriptDisplayText(text) {
|
|
|
245
230
|
const resultSplit = /\r?\n(?:[ \t]*\r?\n)?Result:[ \t]*\r?\n/i.exec(value);
|
|
246
231
|
if (!resultSplit) return false;
|
|
247
232
|
const preamble = value.slice(0, resultSplit.index).trim();
|
|
248
|
-
const instructionLike =
|
|
249
|
-
|| MODEL_VISIBLE_COMPLETION_REVIEW_RE.test(preamble)
|
|
250
|
-
|| MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
233
|
+
const instructionLike = MODEL_VISIBLE_COMPLETION_ASYNC_HEADER_RE.test(preamble);
|
|
251
234
|
if (!instructionLike) return false;
|
|
252
235
|
const normalizedBody = value.slice(resultSplit.index + resultSplit[0].length)
|
|
253
236
|
.split(/\r?\n/)
|
|
@@ -169,7 +169,7 @@ function firstAgentResultLine(text) {
|
|
|
169
169
|
const trimmed = line.trim();
|
|
170
170
|
if (!trimmed) continue;
|
|
171
171
|
if (/^agent result\b/i.test(trimmed)) continue;
|
|
172
|
-
if (/^<\/?(?:final-answer|task-
|
|
172
|
+
if (/^<\/?(?:final-answer|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
|
|
173
173
|
if (/^(?:agent task|status|type|target|role|agent|preset|model|effort|fast|limits|session|task-id|task_id|notification|queueDepth|worker|worker_stage|last_progress|silent_for|watchdog|queued_followups|diagnostic|started|finished|elapsed|reused):\s*/i.test(trimmed)) continue;
|
|
174
174
|
if (/^\[[a-z-]+:\s*[^\]]*\]$/i.test(trimmed)) continue;
|
|
175
175
|
return truncateSingleLine(trimmed, AGENT_SURFACE_BRIEF_MAX);
|
|
@@ -18,7 +18,7 @@ const completionMeta = {
|
|
|
18
18
|
execution_surface: 'agent',
|
|
19
19
|
execution_id: 'task-agent-1',
|
|
20
20
|
status: 'completed',
|
|
21
|
-
instruction: '
|
|
21
|
+
instruction: 'Async agent task task-agent-1 (completed) finished.',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
test('session-id authority delivers to only the requested Lead among simultaneous listeners', () => {
|
|
@@ -306,30 +306,23 @@ export function snapshotProviderRequestTools(options = {}) {
|
|
|
306
306
|
|| names.size === 0) {
|
|
307
307
|
return finish();
|
|
308
308
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (source && source !== normalizedProvider
|
|
314
|
-
&& !(ANTHROPIC_NATIVE_PROVIDERS.has(source)
|
|
315
|
-
&& ANTHROPIC_NATIVE_PROVIDERS.has(normalizedProvider))) continue;
|
|
316
|
-
for (const name of parseToolSelection(native?.toolReferences)) discovered.add(name);
|
|
317
|
-
}
|
|
318
|
-
if (discovered.size === 0) return finish();
|
|
319
|
-
|
|
320
|
-
// Catalog arrays have no separate key map, so `name` is their explicit
|
|
321
|
-
// selection key contract: capture it once, skip undiscovered/duplicate
|
|
322
|
-
// entries without touching schemas, and seed selected normalization with the
|
|
323
|
-
// captured value to avoid a second getter evaluation.
|
|
309
|
+
// Anthropic tools precede every cache_control breakpoint. Adding a schema
|
|
310
|
+
// after tool_search therefore invalidates the whole provider cache prefix.
|
|
311
|
+
// Send the complete defer_loading catalog from the first request instead;
|
|
312
|
+
// tool_search controls callability, not provider-visible schema membership.
|
|
324
313
|
const seenCatalogRefs = new WeakSet();
|
|
325
|
-
|
|
314
|
+
const catalog = [
|
|
315
|
+
...(Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : []),
|
|
316
|
+
...(Array.isArray(session?.deferredLateToolCatalog) ? session.deferredLateToolCatalog : []),
|
|
317
|
+
];
|
|
318
|
+
for (const tool of catalog) {
|
|
326
319
|
if (tool && typeof tool === 'object') {
|
|
327
320
|
if (activeCandidateRefs.has(tool) || seenCatalogRefs.has(tool)) continue;
|
|
328
321
|
seenCatalogRefs.add(tool);
|
|
329
322
|
}
|
|
330
323
|
const capturedName = tool?.name;
|
|
331
324
|
const selectionName = typeof capturedName === 'string' ? clean(capturedName) : '';
|
|
332
|
-
if (!selectionName ||
|
|
325
|
+
if (!selectionName || names.has(selectionName)) continue;
|
|
333
326
|
appendSnapshot(tool, capturedName, true);
|
|
334
327
|
}
|
|
335
328
|
return finish();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { snapshotProviderRequestTools } from './provider-request-snapshot.mjs';
|
|
4
|
+
|
|
5
|
+
test('Anthropic deferred tool surface is complete and byte-stable from the first send', () => {
|
|
6
|
+
const session = {
|
|
7
|
+
provider: 'anthropic-oauth',
|
|
8
|
+
deferredNativeTools: true,
|
|
9
|
+
deferredToolCatalog: [
|
|
10
|
+
{ name: 'shell', inputSchema: { type: 'object', properties: {} } },
|
|
11
|
+
{ name: 'recall', inputSchema: { type: 'object', properties: {} } },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
const tools = [{ name: 'load_tool', inputSchema: { type: 'object', properties: {} } }];
|
|
15
|
+
const first = snapshotProviderRequestTools({
|
|
16
|
+
provider: session.provider,
|
|
17
|
+
tools,
|
|
18
|
+
messages: [],
|
|
19
|
+
session,
|
|
20
|
+
});
|
|
21
|
+
const later = snapshotProviderRequestTools({
|
|
22
|
+
provider: session.provider,
|
|
23
|
+
tools,
|
|
24
|
+
messages: [{
|
|
25
|
+
role: 'tool',
|
|
26
|
+
nativeToolSearch: {
|
|
27
|
+
provider: 'anthropic-oauth',
|
|
28
|
+
toolReferences: ['shell'],
|
|
29
|
+
},
|
|
30
|
+
}],
|
|
31
|
+
session,
|
|
32
|
+
});
|
|
33
|
+
assert.deepEqual(JSON.parse(JSON.stringify(later)), JSON.parse(JSON.stringify(first)));
|
|
34
|
+
assert.deepEqual(first.map((tool) => tool.name), ['load_tool', 'shell', 'recall']);
|
|
35
|
+
assert.equal(first[1].deferLoading, true);
|
|
36
|
+
assert.equal(first[2].deferLoading, true);
|
|
37
|
+
});
|
|
@@ -519,7 +519,6 @@ export function parseHandlerOutput(run, eventName) {
|
|
|
519
519
|
updatedToolName: null,
|
|
520
520
|
updatedToolOutput: null,
|
|
521
521
|
additionalContext: null,
|
|
522
|
-
systemMessage: null,
|
|
523
522
|
suppressOutput: false,
|
|
524
523
|
continueFlag: undefined,
|
|
525
524
|
askReason: null,
|
|
@@ -552,7 +551,6 @@ export function parseHandlerOutput(run, eventName) {
|
|
|
552
551
|
out.reason = limitText(json.reason || out.reason || `blocked by ${eventName} hook`);
|
|
553
552
|
}
|
|
554
553
|
if (json.suppressOutput) out.suppressOutput = true;
|
|
555
|
-
if (typeof json.systemMessage === 'string') out.systemMessage = limitText(json.systemMessage);
|
|
556
554
|
if (typeof json.additionalContext === 'string') out.additionalContext = limitText(json.additionalContext);
|
|
557
555
|
|
|
558
556
|
const hso = json.hookSpecificOutput;
|
|
@@ -271,7 +271,6 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
|
|
|
271
271
|
updatedToolName: null,
|
|
272
272
|
updatedToolOutput: null,
|
|
273
273
|
additionalContext: [],
|
|
274
|
-
systemMessage: null,
|
|
275
274
|
ask: false,
|
|
276
275
|
askReason: null,
|
|
277
276
|
handlersRun: handlers.length,
|
|
@@ -303,7 +302,6 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
|
|
|
303
302
|
}
|
|
304
303
|
const parsed = parseHandlerOutput(run, eventName);
|
|
305
304
|
if (parsed.additionalContext) agg.additionalContext.push(parsed.additionalContext);
|
|
306
|
-
if (parsed.systemMessage && !agg.systemMessage) agg.systemMessage = parsed.systemMessage;
|
|
307
305
|
if (parsed.updatedInput && !agg.updatedInput) agg.updatedInput = parsed.updatedInput;
|
|
308
306
|
if (parsed.updatedToolName && !agg.updatedToolName) agg.updatedToolName = parsed.updatedToolName;
|
|
309
307
|
if (parsed.updatedToolOutput != null && agg.updatedToolOutput == null) agg.updatedToolOutput = parsed.updatedToolOutput;
|
|
@@ -509,7 +507,6 @@ export function createStandaloneHookBus({ maxEvents = 80, dataDir = null, prompt
|
|
|
509
507
|
blocked: agg.blocked || undefined,
|
|
510
508
|
reason: agg.reason || undefined,
|
|
511
509
|
additionalContext: agg.additionalContext.length ? agg.additionalContext : undefined,
|
|
512
|
-
systemMessage: agg.systemMessage || undefined,
|
|
513
510
|
updatedInput: agg.updatedInput || undefined,
|
|
514
511
|
updatedToolName: agg.updatedToolName || undefined,
|
|
515
512
|
handlersRun: agg.handlersRun || undefined,
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -3505,7 +3505,7 @@ function firstAgentResultLine(text) {
|
|
|
3505
3505
|
const trimmed = line.trim();
|
|
3506
3506
|
if (!trimmed) continue;
|
|
3507
3507
|
if (/^agent result\b/i.test(trimmed)) continue;
|
|
3508
|
-
if (/^<\/?(?:final-answer|task-
|
|
3508
|
+
if (/^<\/?(?:final-answer|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
|
|
3509
3509
|
if (/^(?:agent task|status|type|target|role|agent|preset|model|effort|fast|limits|session|task-id|task_id|notification|queueDepth|worker|worker_stage|last_progress|silent_for|watchdog|queued_followups|diagnostic|started|finished|elapsed|reused):\s*/i.test(trimmed)) continue;
|
|
3510
3510
|
if (/^\[[a-z-]+:\s*[^\]]*\]$/i.test(trimmed)) continue;
|
|
3511
3511
|
return truncateSingleLine(trimmed, AGENT_SURFACE_BRIEF_MAX);
|
|
@@ -11641,7 +11641,7 @@ function joinActionAgent(action, agent) {
|
|
|
11641
11641
|
function agentResponseTitle(args, count = 1) {
|
|
11642
11642
|
const total = Math.max(1, Number(count) || 1);
|
|
11643
11643
|
if (total > 1) return `Responses ${total} agents`;
|
|
11644
|
-
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || "");
|
|
11644
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || "") || "Agent";
|
|
11645
11645
|
return withModelAndTag(joinActionAgent("Response", name), args);
|
|
11646
11646
|
}
|
|
11647
11647
|
function agentActionTitle(args) {
|
|
@@ -11684,7 +11684,7 @@ function hasAgentResponseResult(value) {
|
|
|
11684
11684
|
}
|
|
11685
11685
|
if (/^agent result\b/i.test(trimmed)) continue;
|
|
11686
11686
|
if (/^(?:undefined|null)$/i.test(trimmed)) continue;
|
|
11687
|
-
if (/^<\/?(?:final-answer|task-
|
|
11687
|
+
if (/^<\/?(?:final-answer|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
|
|
11688
11688
|
if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|agent|preset|model|effort|fast|limits|started|finished|error|notification|queueDepth):?\s*/i.test(trimmed)) continue;
|
|
11689
11689
|
if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
|
|
11690
11690
|
if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
|
|
@@ -20,7 +20,7 @@ function stripSyntheticAgentTags(text) {
|
|
|
20
20
|
if (taskResult) return taskResult;
|
|
21
21
|
return value
|
|
22
22
|
.replace(/^agent result[^\n]*(?:\n|$)/i, '')
|
|
23
|
-
.replace(/<\/?(?:final-answer|task-
|
|
23
|
+
.replace(/<\/?(?:final-answer|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>/gi, '')
|
|
24
24
|
.trim();
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -153,8 +153,7 @@ export function parseModelVisibleCompletionWrapper(text) {
|
|
|
153
153
|
const split = /\n\nResult:\n/.exec(value);
|
|
154
154
|
if (!split) return null;
|
|
155
155
|
const preamble = value.slice(0, split.index).trim();
|
|
156
|
-
if (!/^
|
|
157
|
-
&& !/^Async \S+.* finished\.$/i.test(preamble)) {
|
|
156
|
+
if (!/^Async \S+.* finished\.$/i.test(preamble)) {
|
|
158
157
|
return null;
|
|
159
158
|
}
|
|
160
159
|
const quotedLines = value.slice(split.index + split[0].length).split(/\r?\n/);
|
|
@@ -243,37 +242,43 @@ export function parseSyntheticAgentMessage(text) {
|
|
|
243
242
|
isError: /^(failed|error|timeout|cancelled|canceled|killed|denied)$/i.test(label),
|
|
244
243
|
};
|
|
245
244
|
}
|
|
246
|
-
if (/<task-notification\b/i.test(value)) {
|
|
247
|
-
const status = textBetweenTag(value, 'status') || 'completed';
|
|
248
|
-
const summary = textBetweenTag(value, 'summary') || `Agent ${status}`;
|
|
249
|
-
const taskId = textBetweenTag(value, 'task-id');
|
|
250
|
-
const result = stripSyntheticAgentTags(value);
|
|
251
|
-
return {
|
|
252
|
-
name: 'agent',
|
|
253
|
-
label: status,
|
|
254
|
-
taskId,
|
|
255
|
-
summary,
|
|
256
|
-
result: result || summary,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
245
|
return null;
|
|
260
246
|
}
|
|
261
247
|
|
|
262
248
|
export function buildExecutionResponseToolItem(text, {
|
|
263
249
|
id,
|
|
264
250
|
responseKey = '',
|
|
251
|
+
executionSurface = '',
|
|
252
|
+
executionStatus = '',
|
|
265
253
|
now = Date.now(),
|
|
266
254
|
} = {}) {
|
|
267
|
-
const
|
|
255
|
+
const surface = String(executionSurface || '').trim().toLowerCase();
|
|
256
|
+
const status = String(executionStatus || '').trim().toLowerCase();
|
|
257
|
+
const explicitName = /^(agent|shell|search)$/.test(surface) ? surface : '';
|
|
258
|
+
const parsed = parseSyntheticAgentMessage(text);
|
|
259
|
+
const synthetic = parsed || (explicitName ? {
|
|
260
|
+
name: explicitName,
|
|
261
|
+
label: status || 'completed',
|
|
262
|
+
args: {
|
|
263
|
+
type: 'result',
|
|
264
|
+
status: status || undefined,
|
|
265
|
+
task_id: responseKey || undefined,
|
|
266
|
+
surface: explicitName,
|
|
267
|
+
},
|
|
268
|
+
result: String(text ?? '').trim(),
|
|
269
|
+
} : null);
|
|
268
270
|
if (!synthetic) return null;
|
|
269
271
|
const label = synthetic.label || 'notification';
|
|
270
|
-
const
|
|
272
|
+
const name = explicitName || synthetic.name || 'task';
|
|
273
|
+
const isAgent = name === 'agent';
|
|
271
274
|
const args = {
|
|
272
275
|
...(synthetic.args && typeof synthetic.args === 'object' ? synthetic.args : {
|
|
273
276
|
type: label,
|
|
274
277
|
task_id: synthetic.taskId || undefined,
|
|
275
278
|
description: synthetic.summary || 'execution notification',
|
|
276
279
|
}),
|
|
280
|
+
...(surface ? { surface } : {}),
|
|
281
|
+
...(status ? { status } : {}),
|
|
277
282
|
...(isAgent ? { type: 'result' } : {}),
|
|
278
283
|
};
|
|
279
284
|
const rawResult = synthetic.rawResult ?? text;
|
|
@@ -282,7 +287,7 @@ export function buildExecutionResponseToolItem(text, {
|
|
|
282
287
|
return {
|
|
283
288
|
kind: 'tool',
|
|
284
289
|
id,
|
|
285
|
-
name
|
|
290
|
+
name,
|
|
286
291
|
args,
|
|
287
292
|
result: synthetic.result,
|
|
288
293
|
rawResult,
|
|
@@ -326,11 +326,12 @@ export function createAgentJobFeed({
|
|
|
326
326
|
if (firstDelivery && !successfulPreview && !bodyAlreadyDisplayed) {
|
|
327
327
|
if (cardKey) rememberDisplayedExecutionNotificationKey(cardKey, terminal, executionId);
|
|
328
328
|
if (executionId) rememberDisplayedExecutionResponseState(executionId, hasBody ? 'body' : 'preview', terminal);
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
329
|
+
// Preserve the canonical execution surface through card construction;
|
|
330
|
+
// text parsing remains a fallback for restored/legacy envelopes.
|
|
331
|
+
(pushAsyncAgentResponse || pushUserOrSyntheticItem)(delivery.displayText, nextId(), 'injected', {
|
|
332
|
+
responseKey: executionId,
|
|
333
|
+
...(delivery.executionMeta || {}),
|
|
334
|
+
});
|
|
334
335
|
}
|
|
335
336
|
refreshAgentStatus(parsed);
|
|
336
337
|
const resumeBody = String(delivery.modelContent || '').trim();
|
|
@@ -9,7 +9,7 @@ import assert from 'node:assert/strict';
|
|
|
9
9
|
import { restoreTranscriptItems } from './session-api-ext.mjs';
|
|
10
10
|
|
|
11
11
|
const wrapper = (taskId, body) => [
|
|
12
|
-
`
|
|
12
|
+
`Async shell task ${taskId} (completed, exit 0) finished.`,
|
|
13
13
|
'',
|
|
14
14
|
'Result:',
|
|
15
15
|
...body.split('\n').map((line) => `> ${line}`),
|
|
@@ -45,7 +45,7 @@ test('completion wrapper user rows restore as tool cards, not dropped rows', ()
|
|
|
45
45
|
assert.equal(card.isError, false);
|
|
46
46
|
assert.match(String(card.result || ''), /8\/8 Mean: 1\.000/);
|
|
47
47
|
// The raw wrapper must never surface as a plain user bubble.
|
|
48
|
-
assert.ok(!items.some((it) => it?.kind === 'user' && /
|
|
48
|
+
assert.ok(!items.some((it) => it?.kind === 'user' && /Async shell task/.test(it.text || '')));
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
test('non-wrapper internal rows stay suppressed on restore', () => {
|
|
@@ -97,5 +97,9 @@ export function resolveTuiRuntimeNotificationDelivery(event, text) {
|
|
|
97
97
|
action: 'execution-ui',
|
|
98
98
|
displayText: trimmed,
|
|
99
99
|
modelContent,
|
|
100
|
+
executionMeta: {
|
|
101
|
+
executionSurface: String(meta.execution_surface || '').trim(),
|
|
102
|
+
executionStatus: String(meta.status || parsed?.status || '').trim(),
|
|
103
|
+
},
|
|
100
104
|
};
|
|
101
105
|
}
|
|
@@ -10,7 +10,10 @@ import { getVoiceStatus, toggleVoice } from '../lib/voice-setup.mjs';
|
|
|
10
10
|
import { createSessionOAuthFlowRegistry } from './oauth-flows.mjs';
|
|
11
11
|
import { aggregateToolCategoryEntries, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult, toolLoadingTargets } from '../../runtime/shared/tool-surface.mjs';
|
|
12
12
|
import { aggregateBucketForCategory, aggregateRawResult, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
isInternalTranscriptDisplayText,
|
|
15
|
+
isTranscriptCancelledStatusText,
|
|
16
|
+
} from '../../runtime/shared/tool-execution-contract.mjs';
|
|
14
17
|
import { toolResultTerminalStatus } from '../../runtime/shared/tool-status.mjs';
|
|
15
18
|
|
|
16
19
|
export function restoredTranscriptMetadata(message) {
|
|
@@ -253,7 +256,7 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
253
256
|
const text = (typeof message?.content === 'string'
|
|
254
257
|
? message.content
|
|
255
258
|
: toolResultText(message?.content)).trim();
|
|
256
|
-
// Persisted async-completion wrappers ("
|
|
259
|
+
// Persisted async-completion wrappers ("Async shell task ... finished. Result:
|
|
257
260
|
// > ...") are the only durable record of a background completion — the live
|
|
258
261
|
// Response card is an event-time push that does not survive a transcript
|
|
259
262
|
// rebuild. Restore them as tool cards instead of dropping them with the
|
|
@@ -288,10 +291,7 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
288
291
|
// Crash-recovery control row: keep the persisted marker for the next model
|
|
289
292
|
// step, but render it like a live cancel tail (◈ Cancelled) instead of a
|
|
290
293
|
// raw user bubble with bracketed internals.
|
|
291
|
-
if (
|
|
292
|
-
return [{ kind: 'turndone', id: nextId(), status: 'cancelled', elapsedMs: 0 }];
|
|
293
|
-
}
|
|
294
|
-
if (/^\[request interrupted\]$/i.test(text)) {
|
|
294
|
+
if (isTranscriptCancelledStatusText(text)) {
|
|
295
295
|
return [{ kind: 'turndone', id: nextId(), status: 'cancelled', elapsedMs: 0 }];
|
|
296
296
|
}
|
|
297
297
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
@@ -741,6 +741,8 @@ export async function createLocalSessionRuntime({
|
|
|
741
741
|
const responseItem = buildExecutionResponseToolItem(text, {
|
|
742
742
|
id,
|
|
743
743
|
responseKey: metadata.responseKey || metadata.executionId,
|
|
744
|
+
executionSurface: metadata.executionSurface,
|
|
745
|
+
executionStatus: metadata.executionStatus,
|
|
744
746
|
});
|
|
745
747
|
if (!responseItem) return pushUserOrSyntheticItem(text, id, origin);
|
|
746
748
|
if (responseItem.name !== 'agent') {
|