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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-tag-reuse-smoke.mjs +124 -10
  3. package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
  4. package/scripts/arg-guard-test.mjs +16 -0
  5. package/scripts/compact-pressure-test.mjs +256 -1
  6. package/scripts/internal-comms-bench.mjs +0 -1
  7. package/scripts/internal-comms-smoke.mjs +14 -32
  8. package/scripts/internal-tools-normalization-test.mjs +52 -0
  9. package/scripts/max-output-recovery-test.mjs +49 -0
  10. package/scripts/mcp-client-normalization-test.mjs +45 -0
  11. package/scripts/provider-toolcall-test.mjs +23 -0
  12. package/scripts/result-classification-test.mjs +75 -0
  13. package/scripts/smoke.mjs +1 -1
  14. package/scripts/submit-commandbusy-race-test.mjs +99 -7
  15. package/scripts/tui-transcript-perf-test.mjs +56 -1
  16. package/src/agents/reviewer/AGENT.md +4 -0
  17. package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
  18. package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
  19. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
  20. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
  21. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
  22. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  23. package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
  24. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
  25. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
  26. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
  27. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
  28. package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
  29. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
  30. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
  31. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
  33. package/src/runtime/memory/tool-defs.mjs +4 -3
  34. package/src/runtime/shared/tool-surface.mjs +1 -2
  35. package/src/session-runtime/context-status.mjs +8 -2
  36. package/src/standalone/agent-tool/render.mjs +2 -0
  37. package/src/standalone/agent-tool.mjs +202 -22
  38. package/src/tui/components/StatusLine.jsx +4 -26
  39. package/src/tui/dist/index.mjs +46 -31
  40. package/src/tui/engine/session-api.mjs +3 -0
  41. package/src/tui/engine/session-flow.mjs +19 -8
  42. package/src/tui/engine/turn.mjs +24 -2
  43. package/src/tui/engine.mjs +0 -1
  44. package/src/ui/statusline-agents.mjs +0 -8
  45. package/src/ui/statusline.mjs +2 -4
  46. package/src/workflows/default/WORKFLOW.md +29 -20
  47. package/src/workflows/solo-review/WORKFLOW.md +47 -0
  48. 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
- rows = normalizeWorkerRows(JSON.parse(readFileSync(file, 'utf8')));
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
- mutator(byKey);
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
- return { version: 1, updatedAt: new Date().toISOString(), workers };
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
- forgetTerminalSession(tag, sessionId);
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) => byKey.clear());
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 is proof that this terminal owned the tag. Once the
1593
- // reaper removes that row, an explicit agent plus usable cwd is enough
1594
- // to cold-spawn a replacement without retaining closed sessions.
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(scopedContext).find((row) => clean(row.tag) === fallbackTag) || null;
1750
+ inheritedRow = readWorkerRows(ownershipContext).find((row) => clean(row.tag) === fallbackTag) || null;
1598
1751
  } catch { inheritedRow = null; }
1599
- // Never use explicit cold-spawn identity to take over a tag retained
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
- const explicitCwd = clean(args.cwd) || clean(callerCwd);
1607
- if (peerRow || (!inheritedRow && (!explicitAgent || !explicitCwd))) throw err;
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) || 'worker';
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(args, callerCwd, context, notifyContext);
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}${tags}${elapsedSuffix(elapsed)}`);
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 (tags) …` →
373
- // `Shells?` cannot match `Agents`; a tag-injected `Running 3 Shells · 9s`
374
- // sits AFTER `Running N Agents (` (not at the pinned start) no false match.
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;
@@ -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" || action === "core" && !op;
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}${tags}${elapsedSuffix(elapsed)}`);
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({ verb, doneLabel, skipLabel, surface, useCompaction }) {
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 ${AUTO_CLEAR_COMPACT_TIMEOUT_MS}ms; auto-clear deferred to next idle`)),
24376
- AUTO_CLEAR_COMPACT_TIMEOUT_MS
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(),