mixdog 0.9.41 → 0.9.43
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/scripts/agent-tag-reuse-smoke.mjs +124 -10
- package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
- package/scripts/arg-guard-test.mjs +16 -0
- package/scripts/compact-pressure-test.mjs +256 -1
- package/scripts/internal-comms-bench.mjs +0 -1
- package/scripts/internal-comms-smoke.mjs +14 -32
- package/scripts/internal-tools-normalization-test.mjs +52 -0
- package/scripts/max-output-recovery-test.mjs +49 -0
- package/scripts/mcp-client-normalization-test.mjs +45 -0
- package/scripts/provider-toolcall-test.mjs +23 -0
- package/scripts/result-classification-test.mjs +75 -0
- package/scripts/smoke.mjs +1 -1
- package/scripts/submit-commandbusy-race-test.mjs +99 -7
- package/scripts/tui-transcript-perf-test.mjs +56 -1
- package/src/agents/reviewer/AGENT.md +4 -0
- package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +4 -3
- package/src/runtime/shared/tool-surface.mjs +1 -2
- package/src/session-runtime/context-status.mjs +8 -2
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +202 -22
- package/src/tui/components/StatusLine.jsx +4 -26
- package/src/tui/dist/index.mjs +46 -31
- package/src/tui/engine/session-api.mjs +3 -0
- package/src/tui/engine/session-flow.mjs +19 -8
- package/src/tui/engine/turn.mjs +24 -2
- package/src/tui/engine.mjs +0 -1
- package/src/ui/statusline-agents.mjs +0 -8
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +29 -20
- package/src/workflows/solo-review/WORKFLOW.md +47 -0
- package/src/workflows/bench/WORKFLOW.md +0 -60
|
@@ -90,6 +90,8 @@ const DEFAULT_SPAWN_PREP_TIMEOUT_MS = envTimeoutMs('MIXDOG_AGENT_SPAWN_PREP_TIME
|
|
|
90
90
|
// startDeferredSpawnJob) so the agent tool call itself still returns task_id
|
|
91
91
|
// immediately.
|
|
92
92
|
const SPAWN_STAGGER_MS = envTimeoutMs('MIXDOG_SPAWN_STAGGER_MS', 0);
|
|
93
|
+
const TAG_TOMBSTONE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
94
|
+
const MAX_TAG_TOMBSTONES = 500;
|
|
93
95
|
let lastSpawnStartAt = 0;
|
|
94
96
|
async function waitForSpawnStagger() {
|
|
95
97
|
if (SPAWN_STAGGER_MS <= 0) return;
|
|
@@ -177,13 +179,48 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
177
179
|
.filter(keepWorkerRow);
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
function normalizeTagTombstones(value, { cap = true, priorityKeys = null } = {}) {
|
|
183
|
+
const source = Array.isArray(value?.tombstones)
|
|
184
|
+
? value.tombstones
|
|
185
|
+
: (value?.tombstones && typeof value.tombstones === 'object'
|
|
186
|
+
? Object.values(value.tombstones)
|
|
187
|
+
: []);
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const cutoff = now - TAG_TOMBSTONE_TTL_MS;
|
|
190
|
+
const rows = source
|
|
191
|
+
.filter((row) => row && typeof row === 'object')
|
|
192
|
+
.map((row) => {
|
|
193
|
+
const parsedReapedAt = Date.parse(clean(row.reapedAt)) || 0;
|
|
194
|
+
return {
|
|
195
|
+
tag: clean(row.tag),
|
|
196
|
+
agent: clean(row.agent) || null,
|
|
197
|
+
cwd: clean(row.cwd) || null,
|
|
198
|
+
clientHostPid: positiveInt(row.clientHostPid),
|
|
199
|
+
// A future clock must not outrank tombstones created by this process.
|
|
200
|
+
reapedAt: parsedReapedAt ? new Date(Math.min(parsedReapedAt, now)).toISOString() : null,
|
|
201
|
+
};
|
|
202
|
+
})
|
|
203
|
+
.filter((row) => row.tag && row.reapedAt && (Date.parse(row.reapedAt) || 0) >= cutoff)
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
const aPriority = priorityKeys?.has(tagTombstoneKey(a)) ? 1 : 0;
|
|
206
|
+
const bPriority = priorityKeys?.has(tagTombstoneKey(b)) ? 1 : 0;
|
|
207
|
+
return bPriority - aPriority
|
|
208
|
+
|| (Date.parse(b.reapedAt) || 0) - (Date.parse(a.reapedAt) || 0);
|
|
209
|
+
});
|
|
210
|
+
return cap ? rows.slice(0, MAX_TAG_TOMBSTONES) : rows;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function tagTombstoneKey(row = {}) {
|
|
214
|
+
return `${positiveInt(row.clientHostPid) || 0}\0${clean(row.tag)}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
180
217
|
// Mtime-keyed parse cache for the worker index. A single spawn calls
|
|
181
218
|
// refreshTagsFromSessions()/resolveTag()/nextTag() which each re-read and
|
|
182
219
|
// re-JSON.parse this file; across a parallel fanout that is O(spawns^2)
|
|
183
220
|
// synchronous reads of the same bytes on the event loop. Cache the parsed,
|
|
184
221
|
// normalized rows and reuse them while the file mtime+size is unchanged.
|
|
185
222
|
// Writes bump _workerRowsCacheDirty so the very next read re-parses.
|
|
186
|
-
let _workerRowsCache = null; // { mtimeMs, size, rows }
|
|
223
|
+
let _workerRowsCache = null; // { mtimeMs, size, rows, tombstones }
|
|
187
224
|
let _workerRowsCacheDirty = true;
|
|
188
225
|
function invalidateWorkerRowsCache() {
|
|
189
226
|
_workerRowsCacheDirty = true;
|
|
@@ -200,15 +237,26 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
200
237
|
return _workerRowsCache.rows;
|
|
201
238
|
}
|
|
202
239
|
let rows = [];
|
|
240
|
+
let tombstones = [];
|
|
203
241
|
try {
|
|
204
|
-
|
|
242
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
243
|
+
rows = normalizeWorkerRows(parsed);
|
|
244
|
+
tombstones = normalizeTagTombstones(parsed);
|
|
205
245
|
} catch {
|
|
206
246
|
rows = [];
|
|
247
|
+
tombstones = [];
|
|
207
248
|
}
|
|
208
|
-
_workerRowsCache = { mtimeMs: st.mtimeMs, size: st.size, rows };
|
|
249
|
+
_workerRowsCache = { mtimeMs: st.mtimeMs, size: st.size, rows, tombstones };
|
|
209
250
|
_workerRowsCacheDirty = false;
|
|
210
251
|
return rows;
|
|
211
252
|
}
|
|
253
|
+
function readAllTagTombstones() {
|
|
254
|
+
readAllWorkerRows();
|
|
255
|
+
return _workerRowsCache?.tombstones || [];
|
|
256
|
+
}
|
|
257
|
+
function readTagTombstones(context = {}) {
|
|
258
|
+
return readAllTagTombstones().filter((row) => rowMatchesContext(row, context));
|
|
259
|
+
}
|
|
212
260
|
function readWorkerRows(context = {}) {
|
|
213
261
|
const rows = readAllWorkerRows();
|
|
214
262
|
if (rows.length === 0) return rows;
|
|
@@ -229,13 +277,25 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
229
277
|
const key = workerRowKey(row);
|
|
230
278
|
if (key) byKey.set(key, row);
|
|
231
279
|
}
|
|
232
|
-
|
|
280
|
+
const tombstonesByKey = new Map();
|
|
281
|
+
for (const row of normalizeTagTombstones(cur, { cap: false })) {
|
|
282
|
+
tombstonesByKey.set(tagTombstoneKey(row), row);
|
|
283
|
+
}
|
|
284
|
+
const priorityTombstoneKeys = new Set();
|
|
285
|
+
mutator(byKey, tombstonesByKey, priorityTombstoneKeys);
|
|
233
286
|
const workers = {};
|
|
234
287
|
for (const row of [...byKey.values()].filter(keepWorkerRow)) {
|
|
235
288
|
const key = workerRowKey(row);
|
|
236
289
|
if (key) workers[key] = row;
|
|
237
290
|
}
|
|
238
|
-
|
|
291
|
+
const tombstones = {};
|
|
292
|
+
for (const row of normalizeTagTombstones(
|
|
293
|
+
{ tombstones: [...tombstonesByKey.values()] },
|
|
294
|
+
{ priorityKeys: priorityTombstoneKeys },
|
|
295
|
+
)) {
|
|
296
|
+
tombstones[tagTombstoneKey(row)] = row;
|
|
297
|
+
}
|
|
298
|
+
return { version: 2, updatedAt: new Date().toISOString(), workers, tombstones };
|
|
239
299
|
}, { lock: true });
|
|
240
300
|
// This process just rewrote the index; force the next read to re-parse
|
|
241
301
|
// even if the new mtime/size happen to collide with the cached stat.
|
|
@@ -504,6 +564,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
504
564
|
}
|
|
505
565
|
|
|
506
566
|
function refreshTagsFromSessions({ scanSessions = false, context = {} } = {}) {
|
|
567
|
+
transitionStaleNonterminalRows(context);
|
|
507
568
|
const indexedRows = refreshTagsFromIndex(context);
|
|
508
569
|
const indexedKeys = new Set(indexedRows.map((row) => `${row.tag}\0${row.sessionId}`));
|
|
509
570
|
for (const [tag, sessionId] of [...tags.entries()]) {
|
|
@@ -555,6 +616,50 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
555
616
|
if (id) removeWorkerRow({ sessionId: id });
|
|
556
617
|
}
|
|
557
618
|
|
|
619
|
+
function tombstoneTerminalSession(tag, sessionId, session = null) {
|
|
620
|
+
const value = clean(tag);
|
|
621
|
+
const id = clean(sessionId);
|
|
622
|
+
if (!value || !id) {
|
|
623
|
+
forgetTerminalSession(value, id);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (tags.get(value) === id) {
|
|
627
|
+
tags.delete(value);
|
|
628
|
+
tagAgents.delete(value);
|
|
629
|
+
tagCwds.delete(value);
|
|
630
|
+
}
|
|
631
|
+
const tombstone = {
|
|
632
|
+
tag: value,
|
|
633
|
+
agent: clean(session?.agent) || null,
|
|
634
|
+
cwd: clean(session?.cwd) || null,
|
|
635
|
+
clientHostPid: positiveInt(session?.clientHostPid),
|
|
636
|
+
reapedAt: new Date().toISOString(),
|
|
637
|
+
};
|
|
638
|
+
flushWorkerIndexMutations();
|
|
639
|
+
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
640
|
+
for (const [key, row] of [...byKey.entries()]) {
|
|
641
|
+
if (clean(row.sessionId) === id) byKey.delete(key);
|
|
642
|
+
}
|
|
643
|
+
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
644
|
+
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
645
|
+
priorityTombstoneKeys.add(tombstoneKey);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function tagTombstoneForTag(tag, context = {}) {
|
|
650
|
+
const value = clean(tag);
|
|
651
|
+
if (!value || value.startsWith('sess_')) return null;
|
|
652
|
+
return readTagTombstones(context).find((row) => row.tag === value) || null;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function consumeTagTombstone(tombstone) {
|
|
656
|
+
if (!tombstone?.tag) return false;
|
|
657
|
+
const key = tagTombstoneKey(tombstone);
|
|
658
|
+
flushWorkerIndexMutations();
|
|
659
|
+
writeWorkerRows((_byKey, tombstonesByKey) => tombstonesByKey.delete(key));
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
|
|
558
663
|
function cancelReap(sessionId) {
|
|
559
664
|
const handle = reapTimers.get(sessionId);
|
|
560
665
|
if (!handle) return false;
|
|
@@ -571,9 +676,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
571
676
|
if (reapMs == null) return;
|
|
572
677
|
const handle = setTimeout(() => {
|
|
573
678
|
reapTimers.delete(sessionId);
|
|
679
|
+
const session = getLiveSession(sessionId);
|
|
574
680
|
try { mgr.hideSessionFromList?.(sessionId); } catch {}
|
|
575
681
|
const tag = tagForSession(sessionId);
|
|
576
|
-
|
|
682
|
+
tombstoneTerminalSession(tag, sessionId, session);
|
|
577
683
|
clearAgentStatuslineRoute(sessionId);
|
|
578
684
|
try { mgr.closeSession(sessionId, 'terminal-reap'); } catch {}
|
|
579
685
|
}, reapMs);
|
|
@@ -581,6 +687,47 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
581
687
|
reapTimers.set(sessionId, handle);
|
|
582
688
|
}
|
|
583
689
|
|
|
690
|
+
function transitionStaleNonterminalRows(context = {}) {
|
|
691
|
+
const staleRows = readWorkerRows(context).filter((row) => {
|
|
692
|
+
if (isTerminalWorkerStatus(row.status || row.stage)) return false;
|
|
693
|
+
if (getLiveSession(clean(row.sessionId))) return false;
|
|
694
|
+
const rowTime = workerRowTime(row);
|
|
695
|
+
const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
|
|
696
|
+
// A row with no timestamp has no usable heartbeat at all. Explicitly
|
|
697
|
+
// disabled terminal reaping still gets the tombstone TTL as a finite
|
|
698
|
+
// stale-heartbeat bound, so malformed/running index rows cannot block a
|
|
699
|
+
// tag forever.
|
|
700
|
+
return rowTime <= 0 || Date.now() - rowTime >= (reapMs ?? TAG_TOMBSTONE_TTL_MS);
|
|
701
|
+
});
|
|
702
|
+
if (staleRows.length === 0) return false;
|
|
703
|
+
flushWorkerIndexMutations();
|
|
704
|
+
const nowIso = new Date().toISOString();
|
|
705
|
+
writeWorkerRows((byKey, tombstonesByKey, priorityTombstoneKeys) => {
|
|
706
|
+
for (const row of staleRows) {
|
|
707
|
+
const sessionId = clean(row.sessionId);
|
|
708
|
+
for (const [key, candidate] of [...byKey.entries()]) {
|
|
709
|
+
if (clean(candidate.sessionId) === sessionId) byKey.delete(key);
|
|
710
|
+
}
|
|
711
|
+
const tombstone = {
|
|
712
|
+
tag: clean(row.tag),
|
|
713
|
+
agent: clean(row.agent) || null,
|
|
714
|
+
cwd: clean(row.cwd) || null,
|
|
715
|
+
clientHostPid: positiveInt(row.clientHostPid),
|
|
716
|
+
reapedAt: nowIso,
|
|
717
|
+
};
|
|
718
|
+
const tombstoneKey = tagTombstoneKey(tombstone);
|
|
719
|
+
tombstonesByKey.set(tombstoneKey, tombstone);
|
|
720
|
+
priorityTombstoneKeys.add(tombstoneKey);
|
|
721
|
+
if (tags.get(tombstone.tag) === sessionId) {
|
|
722
|
+
tags.delete(tombstone.tag);
|
|
723
|
+
tagAgents.delete(tombstone.tag);
|
|
724
|
+
tagCwds.delete(tombstone.tag);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
|
|
584
731
|
function isSessionBusy(sessionId) {
|
|
585
732
|
const runtime = mgr.getSessionRuntime?.(sessionId);
|
|
586
733
|
if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return true;
|
|
@@ -1522,7 +1669,10 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1522
1669
|
tagAgents.clear();
|
|
1523
1670
|
tagCwds.clear();
|
|
1524
1671
|
flushWorkerIndexMutations();
|
|
1525
|
-
writeWorkerRows((byKey) =>
|
|
1672
|
+
writeWorkerRows((byKey, tombstonesByKey) => {
|
|
1673
|
+
byKey.clear();
|
|
1674
|
+
tombstonesByKey.clear();
|
|
1675
|
+
});
|
|
1526
1676
|
return { closed, failed };
|
|
1527
1677
|
}
|
|
1528
1678
|
|
|
@@ -1589,25 +1739,26 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1589
1739
|
if (!fallbackTag || fallbackTag.startsWith('sess_') || !isDeadTarget) throw err;
|
|
1590
1740
|
const prompt = clean(args.message || args.prompt);
|
|
1591
1741
|
if (!prompt) throw err;
|
|
1592
|
-
// A retained row
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1742
|
+
// A retained row or reap tombstone proves that this terminal owned
|
|
1743
|
+
// the tag. Unknown tags stay errors even when the caller supplies an
|
|
1744
|
+
// agent/cwd: typo absorption requires persisted same-tag evidence.
|
|
1745
|
+
// Absorption identity is always terminal-local, even when live
|
|
1746
|
+
// resolution was explicitly requested across all terminals.
|
|
1747
|
+
const ownershipContext = context;
|
|
1595
1748
|
let inheritedRow = null;
|
|
1596
1749
|
try {
|
|
1597
|
-
inheritedRow = readWorkerRows(
|
|
1750
|
+
inheritedRow = readWorkerRows(ownershipContext).find((row) => clean(row.tag) === fallbackTag) || null;
|
|
1598
1751
|
} catch { inheritedRow = null; }
|
|
1599
|
-
|
|
1600
|
-
// by another terminal. In-memory maps can contain peer entries, so
|
|
1601
|
-
// inspect the persisted index directly.
|
|
1602
|
-
const peerRow = readAllWorkerRows().find((row) => (
|
|
1603
|
-
clean(row.tag) === fallbackTag && !rowMatchesContext(row, scopedContext)
|
|
1604
|
-
));
|
|
1752
|
+
const inheritedTombstone = tagTombstoneForTag(fallbackTag, ownershipContext);
|
|
1605
1753
|
const explicitAgent = clean(args.agent);
|
|
1606
|
-
|
|
1607
|
-
|
|
1754
|
+
// A local proof wins even if another terminal also owns this tag.
|
|
1755
|
+
// With no local proof, foreign rows/tombstones and unknown tags are
|
|
1756
|
+
// both non-absorbable and retain the original not-found error.
|
|
1757
|
+
if (!inheritedRow && !inheritedTombstone) throw err;
|
|
1608
1758
|
const inheritedSessionId = clean(inheritedRow?.sessionId);
|
|
1609
|
-
const inheritedAgent = explicitAgent || clean(inheritedRow?.agent) ||
|
|
1610
|
-
const inheritedCwd = clean(args.cwd) || clean(inheritedRow?.cwd) || clean(callerCwd);
|
|
1759
|
+
const inheritedAgent = explicitAgent || clean(inheritedRow?.agent) || clean(inheritedTombstone?.agent);
|
|
1760
|
+
const inheritedCwd = clean(args.cwd) || clean(inheritedRow?.cwd) || clean(inheritedTombstone?.cwd) || clean(callerCwd);
|
|
1761
|
+
if (!inheritedAgent || !inheritedCwd) throw err;
|
|
1611
1762
|
// Drop this terminal's in-memory trace and remove ONLY the persisted
|
|
1612
1763
|
// row matching inheritedRow.sessionId. Do NOT call forgetTag here: it
|
|
1613
1764
|
// does a tag-wide removeWorkerRow({tag,sessionId}) (L556) whose OR
|
|
@@ -1618,6 +1769,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1618
1769
|
try { tags.delete(fallbackTag); tagAgents.delete(fallbackTag); tagCwds.delete(fallbackTag); } catch {}
|
|
1619
1770
|
}
|
|
1620
1771
|
if (inheritedSessionId) { try { removeWorkerRow({ sessionId: inheritedSessionId }); } catch {} }
|
|
1772
|
+
if (inheritedTombstone) consumeTagTombstone(inheritedTombstone);
|
|
1621
1773
|
const spawnArgs = {
|
|
1622
1774
|
...args,
|
|
1623
1775
|
type: 'spawn',
|
|
@@ -1638,6 +1790,8 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1638
1790
|
// 3) lingering terminal trace -> reap trace and fresh spawn under same tag
|
|
1639
1791
|
// 4) genuinely new tag -> fresh deferred spawn
|
|
1640
1792
|
const explicitTag = clean(args.tag);
|
|
1793
|
+
let respawned = false;
|
|
1794
|
+
let spawnArgs = args;
|
|
1641
1795
|
if (explicitTag) {
|
|
1642
1796
|
// Resolve a LIVE same-tag session in this terminal (busy or idle).
|
|
1643
1797
|
let liveSessionId = null;
|
|
@@ -1658,9 +1812,35 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1658
1812
|
}
|
|
1659
1813
|
if (hasTerminalTrace(explicitTag, scopedContext)) {
|
|
1660
1814
|
reapTerminalTraceForTag(explicitTag, scopedContext);
|
|
1815
|
+
respawned = true;
|
|
1816
|
+
} else {
|
|
1817
|
+
// Tombstone inheritance never honors allTerminals/global scope.
|
|
1818
|
+
const tombstone = tagTombstoneForTag(explicitTag, context);
|
|
1819
|
+
if (tombstone) {
|
|
1820
|
+
consumeTagTombstone(tombstone);
|
|
1821
|
+
spawnArgs = {
|
|
1822
|
+
...args,
|
|
1823
|
+
agent: clean(args.agent) || clean(tombstone.agent),
|
|
1824
|
+
...(clean(args.cwd) || !clean(tombstone.cwd) ? {} : { cwd: tombstone.cwd }),
|
|
1825
|
+
};
|
|
1826
|
+
respawned = true;
|
|
1827
|
+
} else {
|
|
1828
|
+
const foreignTombstone = readAllTagTombstones().find((row) => (
|
|
1829
|
+
clean(row.tag) === explicitTag && !rowMatchesContext(row, context)
|
|
1830
|
+
));
|
|
1831
|
+
if (foreignTombstone) {
|
|
1832
|
+
throw new Error(`agent spawn: tag "${explicitTag}" belongs to another terminal`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1661
1835
|
}
|
|
1662
1836
|
}
|
|
1663
|
-
const job = startDeferredSpawnJob(
|
|
1837
|
+
const job = startDeferredSpawnJob(
|
|
1838
|
+
spawnArgs,
|
|
1839
|
+
callerCwd,
|
|
1840
|
+
context,
|
|
1841
|
+
notifyContext,
|
|
1842
|
+
respawned ? { respawned: true } : {},
|
|
1843
|
+
);
|
|
1664
1844
|
return renderResult(renderJob(job, false));
|
|
1665
1845
|
}
|
|
1666
1846
|
throw new Error(`agent: unknown type "${type}"`);
|
|
@@ -250,26 +250,6 @@ function localRunningWorkerCount(agentWorkers = [], agentJobs = []) {
|
|
|
250
250
|
return seen.size;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
function localRunningWorkerTags(agentWorkers = [], agentJobs = [], limit = 3) {
|
|
254
|
-
const tags = [];
|
|
255
|
-
const seen = new Set();
|
|
256
|
-
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
257
|
-
const tag = String(worker?.tag || worker?.agent || worker?.name || '').trim();
|
|
258
|
-
if (!tag || isTerminalStatus(worker?.stage || worker?.status) || seen.has(tag)) continue;
|
|
259
|
-
seen.add(tag);
|
|
260
|
-
tags.push(tag);
|
|
261
|
-
}
|
|
262
|
-
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
263
|
-
if (!/running/i.test(String(job?.status || job?.stage || ''))) continue;
|
|
264
|
-
const tag = String(job?.tag || job?.agent || job?.type || job?.task_id || job?.taskId || '').trim();
|
|
265
|
-
if (!tag || seen.has(tag)) continue;
|
|
266
|
-
seen.add(tag);
|
|
267
|
-
tags.push(tag);
|
|
268
|
-
}
|
|
269
|
-
if (tags.length <= limit) return tags.join(', ');
|
|
270
|
-
return `${tags.slice(0, limit).join(', ')}, +${tags.length - limit}`;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
253
|
function localTimeMs(value) {
|
|
274
254
|
if (typeof value === 'number' && Number.isFinite(value) && value > 0) return value;
|
|
275
255
|
const n = Date.parse(String(value || ''));
|
|
@@ -311,11 +291,9 @@ function localStatusLineL2({
|
|
|
311
291
|
const runningCount = localRunningWorkerCount(agentWorkers, agentJobs);
|
|
312
292
|
if (runningCount > 0) {
|
|
313
293
|
const label = `Running ${runningCount} Agent${runningCount === 1 ? '' : 's'}`;
|
|
314
|
-
const tagSummary = localRunningWorkerTags(agentWorkers, agentJobs);
|
|
315
|
-
const tags = tagSummary ? ` ${SUBTLE}(${RESET}${STATUS}${tagSummary}${RESET}${SUBTLE})${RESET}` : '';
|
|
316
294
|
const oldestStart = localOldestWorkerStartMs(agentWorkers, agentJobs);
|
|
317
295
|
const elapsed = oldestStart > 0 ? localFormatElapsed(now - oldestStart) : '';
|
|
318
|
-
l2Parts.push(`${spin} ${STATUS}${label}${RESET}${
|
|
296
|
+
l2Parts.push(`${spin} ${STATUS}${label}${RESET}${elapsedSuffix(elapsed)}`);
|
|
319
297
|
}
|
|
320
298
|
const tools = activeTools && typeof activeTools === 'object' ? activeTools : {};
|
|
321
299
|
const exploreInfo = tools.explore || null;
|
|
@@ -369,9 +347,9 @@ function extractCachedShellSegments(cachedL2 = '', now = Date.now(), capturedAt
|
|
|
369
347
|
// Start-anchored so only a REAL standalone shell segment matches. A visible
|
|
370
348
|
// segment is `<spinner-glyph> Running N … [· elapsed]`, so allow the single
|
|
371
349
|
// leading spinner token via `^(?:\S+\s+)?` then pin `Running N Shell(s)` to
|
|
372
|
-
// that position. An Agents segment is `<glyph> Running N Agents
|
|
373
|
-
// `Shells?` cannot match `Agents`;
|
|
374
|
-
//
|
|
350
|
+
// that position. An Agents segment is `<glyph> Running N Agents · …` →
|
|
351
|
+
// `Shells?` cannot match `Agents`; any shell-like text after the Agents label
|
|
352
|
+
// is not at the pinned start, so it cannot produce a false match.
|
|
375
353
|
const m = /^(?:\S+\s+)?Running (\d+) Shells?\b(?:\s·\s(.+?))?\s*$/.exec(seg.trim());
|
|
376
354
|
if (!m) continue;
|
|
377
355
|
const n = Number(m[1]) || 0;
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -2896,7 +2896,7 @@ function toolWorkUnit(name, args = {}, category = "") {
|
|
|
2896
2896
|
case "memory": {
|
|
2897
2897
|
const action = String(a.action || "").toLowerCase();
|
|
2898
2898
|
const op = String(a.op || "").toLowerCase();
|
|
2899
|
-
const isMutation = op === "add" || op === "edit" || op === "delete" || op === "promote" || op === "dismiss"
|
|
2899
|
+
const isMutation = op === "add" || op === "edit" || op === "delete" || op === "promote" || op === "dismiss";
|
|
2900
2900
|
if (isMutation) return unitDescriptor("Memory", { count: queryCount(a, "entries", "items", "memories", "query", "text", "value") || 1, active: "Writing", done: "Wrote", noun: "memory item" });
|
|
2901
2901
|
return unitDescriptor("Memory", { count: queryCount(a, "entries", "items", "memories", "query", "text", "value") || 1, active: "Checking", done: "Checked", noun: "memory item" });
|
|
2902
2902
|
}
|
|
@@ -4023,25 +4023,6 @@ function localRunningWorkerCount(agentWorkers = [], agentJobs = []) {
|
|
|
4023
4023
|
}
|
|
4024
4024
|
return seen.size;
|
|
4025
4025
|
}
|
|
4026
|
-
function localRunningWorkerTags(agentWorkers = [], agentJobs = [], limit = 3) {
|
|
4027
|
-
const tags = [];
|
|
4028
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4029
|
-
for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
|
|
4030
|
-
const tag = String(worker?.tag || worker?.agent || worker?.name || "").trim();
|
|
4031
|
-
if (!tag || isTerminalStatus(worker?.stage || worker?.status) || seen.has(tag)) continue;
|
|
4032
|
-
seen.add(tag);
|
|
4033
|
-
tags.push(tag);
|
|
4034
|
-
}
|
|
4035
|
-
for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
|
|
4036
|
-
if (!/running/i.test(String(job?.status || job?.stage || ""))) continue;
|
|
4037
|
-
const tag = String(job?.tag || job?.agent || job?.type || job?.task_id || job?.taskId || "").trim();
|
|
4038
|
-
if (!tag || seen.has(tag)) continue;
|
|
4039
|
-
seen.add(tag);
|
|
4040
|
-
tags.push(tag);
|
|
4041
|
-
}
|
|
4042
|
-
if (tags.length <= limit) return tags.join(", ");
|
|
4043
|
-
return `${tags.slice(0, limit).join(", ")}, +${tags.length - limit}`;
|
|
4044
|
-
}
|
|
4045
4026
|
function localTimeMs(value) {
|
|
4046
4027
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
|
|
4047
4028
|
const n = Date.parse(String(value || ""));
|
|
@@ -4074,11 +4055,9 @@ function localStatusLineL2({
|
|
|
4074
4055
|
const runningCount = localRunningWorkerCount(agentWorkers, agentJobs);
|
|
4075
4056
|
if (runningCount > 0) {
|
|
4076
4057
|
const label = `Running ${runningCount} Agent${runningCount === 1 ? "" : "s"}`;
|
|
4077
|
-
const tagSummary = localRunningWorkerTags(agentWorkers, agentJobs);
|
|
4078
|
-
const tags = tagSummary ? ` ${SUBTLE}(${RESET2}${STATUS}${tagSummary}${RESET2}${SUBTLE})${RESET2}` : "";
|
|
4079
4058
|
const oldestStart = localOldestWorkerStartMs(agentWorkers, agentJobs);
|
|
4080
4059
|
const elapsed = oldestStart > 0 ? localFormatElapsed(now - oldestStart) : "";
|
|
4081
|
-
l2Parts.push(`${spin} ${STATUS}${label}${RESET2}${
|
|
4060
|
+
l2Parts.push(`${spin} ${STATUS}${label}${RESET2}${elapsedSuffix(elapsed)}`);
|
|
4082
4061
|
}
|
|
4083
4062
|
const tools = activeTools && typeof activeTools === "object" ? activeTools : {};
|
|
4084
4063
|
const exploreInfo = tools.explore || null;
|
|
@@ -24054,6 +24033,7 @@ function createSessionFlow(bag) {
|
|
|
24054
24033
|
pendingNotificationKeys,
|
|
24055
24034
|
displayedExecutionNotificationKeys,
|
|
24056
24035
|
clearExecutionDedupState,
|
|
24036
|
+
clearToastTimers,
|
|
24057
24037
|
getState,
|
|
24058
24038
|
set,
|
|
24059
24039
|
pushItem,
|
|
@@ -24356,7 +24336,14 @@ function createSessionFlow(bag) {
|
|
|
24356
24336
|
useCompaction: true
|
|
24357
24337
|
});
|
|
24358
24338
|
}
|
|
24359
|
-
async function performSessionClear({
|
|
24339
|
+
async function performSessionClear({
|
|
24340
|
+
verb,
|
|
24341
|
+
doneLabel,
|
|
24342
|
+
skipLabel,
|
|
24343
|
+
surface,
|
|
24344
|
+
useCompaction,
|
|
24345
|
+
compactTimeoutMs = AUTO_CLEAR_COMPACT_TIMEOUT_MS
|
|
24346
|
+
}) {
|
|
24360
24347
|
flags.autoClearRunning = true;
|
|
24361
24348
|
const startedAt = Date.now();
|
|
24362
24349
|
set({ commandBusy: true, commandStatus: { active: true, verb, startedAt, mode: "auto-clear" } });
|
|
@@ -24367,21 +24354,23 @@ function createSessionFlow(bag) {
|
|
|
24367
24354
|
const compaction = runtime.getCompactionSettings();
|
|
24368
24355
|
compactType = compaction.compactType || compaction.type || null;
|
|
24369
24356
|
}
|
|
24357
|
+
let clearResult;
|
|
24370
24358
|
if (compactType) {
|
|
24371
24359
|
const clearPromise = runtime.clear({ compactType, requireCompactSuccess: true });
|
|
24372
24360
|
let timer2 = null;
|
|
24373
24361
|
const timeout = new Promise((_, reject) => {
|
|
24374
24362
|
timer2 = setTimeout(
|
|
24375
|
-
() => reject(new Error(`compaction timed out after ${
|
|
24376
|
-
|
|
24363
|
+
() => reject(new Error(`compaction timed out after ${compactTimeoutMs}ms; auto-clear deferred to next idle`)),
|
|
24364
|
+
compactTimeoutMs
|
|
24377
24365
|
);
|
|
24378
24366
|
});
|
|
24379
24367
|
try {
|
|
24380
|
-
await Promise.race([clearPromise, timeout]);
|
|
24368
|
+
clearResult = await Promise.race([clearPromise, timeout]);
|
|
24381
24369
|
} catch (raceError) {
|
|
24382
24370
|
flags.autoClearInFlight = true;
|
|
24383
24371
|
clearPromise.then(
|
|
24384
|
-
() => {
|
|
24372
|
+
(lateResult) => {
|
|
24373
|
+
if (lateResult === false) return;
|
|
24385
24374
|
if (getState().busy) {
|
|
24386
24375
|
flags.pendingClearedSessionUi = { doneLabel, surface };
|
|
24387
24376
|
} else {
|
|
@@ -24399,7 +24388,10 @@ function createSessionFlow(bag) {
|
|
|
24399
24388
|
if (timer2) clearTimeout(timer2);
|
|
24400
24389
|
}
|
|
24401
24390
|
} else {
|
|
24402
|
-
await runtime.clear({});
|
|
24391
|
+
clearResult = await runtime.clear({});
|
|
24392
|
+
}
|
|
24393
|
+
if (clearResult === false) {
|
|
24394
|
+
throw new Error("runtime clear returned false");
|
|
24403
24395
|
}
|
|
24404
24396
|
applyClearedSessionUi(doneLabel);
|
|
24405
24397
|
return true;
|
|
@@ -24441,6 +24433,8 @@ function createSessionFlow(bag) {
|
|
|
24441
24433
|
return getState().stats;
|
|
24442
24434
|
};
|
|
24443
24435
|
const clearUiActivityBeforeContextSync = () => {
|
|
24436
|
+
clearToastTimers();
|
|
24437
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
24444
24438
|
getState().items = replaceItems([]);
|
|
24445
24439
|
getState().toasts = [];
|
|
24446
24440
|
getState().queued = [];
|
|
@@ -24546,6 +24540,7 @@ function createRunTurn(bag) {
|
|
|
24546
24540
|
set,
|
|
24547
24541
|
pushItem,
|
|
24548
24542
|
patchItem,
|
|
24543
|
+
replaceItems,
|
|
24549
24544
|
updateStreamingTail: updateStreamingTailFromStore,
|
|
24550
24545
|
settleStreamingTail: settleStreamingTailFromStore,
|
|
24551
24546
|
clearStreamingTail: clearStreamingTailFromStore,
|
|
@@ -24734,7 +24729,11 @@ function createRunTurn(bag) {
|
|
|
24734
24729
|
let cancelled = false;
|
|
24735
24730
|
let askResult = null;
|
|
24736
24731
|
let turnFinishedNormally = false;
|
|
24732
|
+
let transcriptCompactedThisTurn = false;
|
|
24737
24733
|
const itemsAtTurnStart = getState().items.length;
|
|
24734
|
+
const submittedIdSet = new Set(submittedIds.filter((id) => id != null));
|
|
24735
|
+
const firstSubmittedIndex = getState().items.findIndex((item) => submittedIdSet.has(item?.id));
|
|
24736
|
+
let currentTurnItemsStart = firstSubmittedIndex >= 0 ? firstSubmittedIndex : itemsAtTurnStart;
|
|
24738
24737
|
const cardByCallId = /* @__PURE__ */ new Map();
|
|
24739
24738
|
const toolCards = [];
|
|
24740
24739
|
const toolGroups = /* @__PURE__ */ new Map();
|
|
@@ -25301,6 +25300,13 @@ function createRunTurn(bag) {
|
|
|
25301
25300
|
if (!markTurnProgress("compact-event")) return;
|
|
25302
25301
|
flushStreamBatch();
|
|
25303
25302
|
clearAggregateContinuation();
|
|
25303
|
+
const compactStatus = String(event?.status || "").toLowerCase();
|
|
25304
|
+
if (!["failed", "skipped", "no_change"].includes(compactStatus)) {
|
|
25305
|
+
const currentTurnItems = getState().items.slice(currentTurnItemsStart);
|
|
25306
|
+
set({ items: replaceItems(currentTurnItems, { preserveStreamingTail: true }) });
|
|
25307
|
+
currentTurnItemsStart = 0;
|
|
25308
|
+
transcriptCompactedThisTurn = true;
|
|
25309
|
+
}
|
|
25304
25310
|
pushItem({
|
|
25305
25311
|
kind: "statusdone",
|
|
25306
25312
|
id: nextId2(),
|
|
@@ -25399,6 +25405,13 @@ function createRunTurn(bag) {
|
|
|
25399
25405
|
} else {
|
|
25400
25406
|
askResult = result;
|
|
25401
25407
|
markPromptCommitted();
|
|
25408
|
+
if (result?.terminationReason === "refusal") {
|
|
25409
|
+
pushNotice(
|
|
25410
|
+
"\uBAA8\uB378\uC774 \uC548\uC804 \uBD84\uB958\uAE30(refusal)\uB85C \uC751\uB2F5\uC744 \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uAC70\uB098 \uBB38\uAD6C\uB97C \uBC14\uAFD4\uC8FC\uC138\uC694.",
|
|
25411
|
+
"warn",
|
|
25412
|
+
{ transcript: true }
|
|
25413
|
+
);
|
|
25414
|
+
}
|
|
25402
25415
|
flushToolResults(session?.messages || [], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
|
|
25403
25416
|
finalizeToolHeaders();
|
|
25404
25417
|
flushStreamBatch();
|
|
@@ -25464,7 +25477,7 @@ function createRunTurn(bag) {
|
|
|
25464
25477
|
clearStreamingTail(currentAssistantId);
|
|
25465
25478
|
}
|
|
25466
25479
|
}
|
|
25467
|
-
const producedTranscriptItem = getState().items.length + closingItems.length > itemsAtTurnStart;
|
|
25480
|
+
const producedTranscriptItem = transcriptCompactedThisTurn || getState().items.length + closingItems.length > itemsAtTurnStart;
|
|
25468
25481
|
const reclaimed = cancelled && flags.activePromptRestore?.reclaimed === true;
|
|
25469
25482
|
flags.activePromptRestore = null;
|
|
25470
25483
|
const elapsedMs = Date.now() - startedAt;
|
|
@@ -26667,6 +26680,9 @@ function createEngineApiA(bag) {
|
|
|
26667
26680
|
syncContextStats({ allowEstimated: true });
|
|
26668
26681
|
set({ ...routeState(), stats: { ...getState().stats } });
|
|
26669
26682
|
if (result) {
|
|
26683
|
+
if (!result.error && result.changed !== false) {
|
|
26684
|
+
set({ items: replaceItems([]) });
|
|
26685
|
+
}
|
|
26670
26686
|
pushItem({
|
|
26671
26687
|
kind: "statusdone",
|
|
26672
26688
|
id: nextId2(),
|
|
@@ -27308,7 +27324,6 @@ async function createEngineSession({
|
|
|
27308
27324
|
requestToolApproval,
|
|
27309
27325
|
patchToolCardResult,
|
|
27310
27326
|
flushToolResults,
|
|
27311
|
-
clearExecutionDedupState,
|
|
27312
27327
|
kickExecutionPendingResume,
|
|
27313
27328
|
flushDeferredExecutionPendingResumeKick,
|
|
27314
27329
|
scheduleExecutionPendingResumeKick,
|
|
@@ -516,6 +516,9 @@ export function createEngineApiA(bag) {
|
|
|
516
516
|
syncContextStats({ allowEstimated: true });
|
|
517
517
|
set({ ...routeState(), stats: { ...getState().stats } });
|
|
518
518
|
if (result) {
|
|
519
|
+
if (!result.error && result.changed !== false) {
|
|
520
|
+
set({ items: replaceItems([]) });
|
|
521
|
+
}
|
|
519
522
|
pushItem({
|
|
520
523
|
kind: 'statusdone',
|
|
521
524
|
id: nextId(),
|