mixdog 0.9.39 → 0.9.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/scripts/agent-terminal-reap-test.mjs +6 -6
  3. package/scripts/execution-resume-esc-integration-test.mjs +4 -2
  4. package/scripts/steering-drain-buckets-test.mjs +140 -5
  5. package/scripts/tui-transcript-perf-test.mjs +279 -0
  6. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +7 -0
  7. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
  8. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +26 -0
  9. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  10. package/src/runtime/agent/orchestrator/session/store.mjs +9 -1
  11. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  12. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
  13. package/src/runtime/shared/buffered-appender.mjs +13 -2
  14. package/src/session-runtime/config-helpers.mjs +6 -6
  15. package/src/session-runtime/lifecycle-api.mjs +4 -0
  16. package/src/session-runtime/runtime-core.mjs +1 -0
  17. package/src/session-runtime/session-turn-api.mjs +12 -0
  18. package/src/standalone/agent-tool.mjs +8 -1
  19. package/src/tui/App.jsx +2 -1
  20. package/src/tui/app/transcript-window.mjs +9 -10
  21. package/src/tui/app/use-transcript-window.mjs +38 -56
  22. package/src/tui/dist/index.mjs +354 -163
  23. package/src/tui/engine/agent-job-feed.mjs +76 -6
  24. package/src/tui/engine/session-api.mjs +7 -1
  25. package/src/tui/engine/session-flow.mjs +3 -1
  26. package/src/tui/engine/tool-card-results.mjs +10 -5
  27. package/src/tui/engine/turn.mjs +96 -36
  28. package/src/tui/engine.mjs +136 -37
  29. package/src/tui/index.jsx +2 -2
@@ -10437,22 +10437,6 @@ function fnv1a32(str) {
10437
10437
  }
10438
10438
  return h >>> 0;
10439
10439
  }
10440
- function fnvStepA(hash, str) {
10441
- let h = hash >>> 0;
10442
- for (let i = 0; i < str.length; i++) {
10443
- h ^= str.charCodeAt(i);
10444
- h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
10445
- }
10446
- return h >>> 0;
10447
- }
10448
- function fnvStepB(hash, str) {
10449
- let h = hash >>> 0;
10450
- for (let i = 0; i < str.length; i++) {
10451
- h = Math.imul(h ^ str.charCodeAt(i), 2246822519) >>> 0;
10452
- h = (h ^ h >>> 13) >>> 0;
10453
- }
10454
- return h >>> 0;
10455
- }
10456
10440
  function textShapeFingerprint(value) {
10457
10441
  if (value == null) return "z";
10458
10442
  const text = String(value);
@@ -10637,7 +10621,7 @@ function buildTranscriptRowIndexIncremental(items, {
10637
10621
  suppressMeasuredRowHeights = false,
10638
10622
  measuredRowsVersion = 0,
10639
10623
  cacheRef = null,
10640
- prefixSig = null
10624
+ prefixRevision = null
10641
10625
  } = {}) {
10642
10626
  const allItems = Array.isArray(items) ? items : [];
10643
10627
  const tail = trailingStreamingItem(allItems);
@@ -10652,7 +10636,7 @@ function buildTranscriptRowIndexIncremental(items, {
10652
10636
  }
10653
10637
  const prefixLen = allItems.length - 1;
10654
10638
  const cache = holder.current;
10655
- if (cache && cache.columns === columns && cache.toolExpanded === (toolOutputExpanded ? 1 : 0) && cache.suppress === suppressMeasuredRowHeights && cache.version === measuredRowsVersion && cache.prefixLen === prefixLen && prefixSig != null && cache.prefixSig === prefixSig && cache.tailId === tail.id) {
10639
+ if (cache && cache.columns === columns && cache.toolExpanded === (toolOutputExpanded ? 1 : 0) && cache.suppress === suppressMeasuredRowHeights && cache.version === measuredRowsVersion && cache.prefixLen === prefixLen && prefixRevision != null && cache.prefixRevision === prefixRevision && cache.tailId === tail.id) {
10656
10640
  {
10657
10641
  const tailMeasured = suppressMeasuredRowHeights ? null : measuredTranscriptRows(tail, columns, toolOutputExpanded);
10658
10642
  const tailRows = tailMeasured != null ? tailMeasured : estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded);
@@ -10674,7 +10658,7 @@ function buildTranscriptRowIndexIncremental(items, {
10674
10658
  suppress: suppressMeasuredRowHeights,
10675
10659
  version: measuredRowsVersion,
10676
10660
  prefixLen,
10677
- prefixSig,
10661
+ prefixRevision,
10678
10662
  tailId: tail.id,
10679
10663
  prefixRowsArr: full.rows.slice(0, prefixLen),
10680
10664
  prefixPrefixRows: full.prefixRows.slice(0, prefixLen + 1),
@@ -10682,36 +10666,6 @@ function buildTranscriptRowIndexIncremental(items, {
10682
10666
  };
10683
10667
  return full;
10684
10668
  }
10685
- var transcriptSigPartCache = /* @__PURE__ */ new WeakMap();
10686
- function transcriptStructureSignature(items, columns, toolOutputExpanded) {
10687
- const list = Array.isArray(items) ? items : [];
10688
- let hA = fnvStepA(2166136261, `${list.length}|${columns}|${toolOutputExpanded ? 1 : 0}`);
10689
- let hB = fnvStepB(3421674724, `${list.length}|${columns}|${toolOutputExpanded ? 1 : 0}`);
10690
- for (let i = 0; i < list.length; i++) {
10691
- const it = list[i];
10692
- let sigPart;
10693
- if (!it) {
10694
- sigPart = "_";
10695
- } else if (it.kind === "assistant" && it.streaming) {
10696
- const resolvedRows = estimateTranscriptItemRowsCached(it, columns, toolOutputExpanded);
10697
- sigPart = `a${it.id}:${resolvedRows}`;
10698
- } else {
10699
- const variantKey = transcriptItemVariantKey(it);
10700
- const cached = transcriptSigPartCache.get(it);
10701
- if (cached && cached.variantKey === variantKey && cached.columns === columns && cached.id === it.id && cached.kind === it.kind) {
10702
- sigPart = cached.sigPart;
10703
- } else {
10704
- sigPart = `${it.kind?.[0] || "?"}${it.id}:${variantKey}`;
10705
- transcriptSigPartCache.set(it, { id: it.id, kind: it.kind, variantKey, columns, sigPart });
10706
- }
10707
- }
10708
- hA = fnvStepA(hA, `;${i};`);
10709
- hA = fnvStepA(hA, sigPart);
10710
- hB = fnvStepB(hB, `;${i};`);
10711
- hB = fnvStepB(hB, sigPart);
10712
- }
10713
- return `${hA.toString(36)}.${hB.toString(36)}`;
10714
- }
10715
10669
  function transcriptRenderWindow(items, { scrollOffset = 0, viewportHeight = 24, columns = 80, toolOutputExpanded = false, rowIndex = null } = {}) {
10716
10670
  const allItems = Array.isArray(items) ? items : [];
10717
10671
  const itemCount = allItems.length;
@@ -11705,7 +11659,9 @@ function useTranscriptScroll({
11705
11659
  // src/tui/app/use-transcript-window.mjs
11706
11660
  import { useCallback as useCallback4, useEffect as useEffect9, useLayoutEffect as useLayoutEffect3, useMemo, useRef as useRef9, useState as useState7 } from "react";
11707
11661
  function useTranscriptWindow({
11708
- items,
11662
+ items: settledItems,
11663
+ structureRevision,
11664
+ streamingTail,
11709
11665
  themeEpoch,
11710
11666
  frameColumns,
11711
11667
  toolOutputExpanded,
@@ -11763,41 +11719,34 @@ function useTranscriptWindow({
11763
11719
  }
11764
11720
  return fn;
11765
11721
  }, []);
11766
- const streamingTailItem = useMemo(() => {
11767
- const last = (items || []).length > 0 ? items[items.length - 1] : null;
11768
- return last && last.kind === "assistant" && last.streaming ? last : null;
11769
- }, [items]);
11770
- const prefixLen = streamingTailItem ? (items || []).length - 1 : (items || []).length;
11771
- const prefixSig = useMemo(
11772
- () => transcriptStructureSignature(
11773
- streamingTailItem ? (items || []).slice(0, prefixLen) : items || [],
11774
- frameColumns,
11775
- toolOutputExpanded
11776
- ),
11777
- [items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded]
11778
- );
11722
+ const streamingTailItem = streamingTail?.kind === "assistant" && streamingTail.streaming ? streamingTail : null;
11779
11723
  const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
11780
11724
  const transcriptPinnedForStreaming = followingRef.current || scrolledUpRowsForPin <= transcriptBottomSlackRows;
11781
11725
  setStreamingBottomPinned(transcriptPinnedForStreaming);
11782
- const tailSig = streamingTailItem ? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}` : "_";
11783
- const transcriptStructureSig = `${prefixSig}#${tailSig}`;
11784
- const transcriptStreamingActive = (items || []).some(
11785
- (item) => item?.kind === "assistant" && item?.streaming
11726
+ const tailRows = streamingTailItem ? estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded) : 0;
11727
+ const tailSig = streamingTailItem ? `${streamingTailItem.id}:${tailRows}` : "_";
11728
+ const revision = Math.max(0, Number(structureRevision) || 0);
11729
+ const transcriptItems = useMemo(
11730
+ () => streamingTailItem ? [...settledItems || [], streamingTailItem] : settledItems || [],
11731
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- tail text at the same resolved height is injected into the bounded visible slice below
11732
+ [settledItems, revision, streamingTailItem?.id, tailRows]
11786
11733
  );
11734
+ const transcriptStructureSig = `${revision}#${tailSig}`;
11735
+ const transcriptStreamingActive = !!streamingTailItem;
11787
11736
  const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
11788
11737
  const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
11789
11738
  const hasStreamingReadingAnchor = !!transcriptAnchorRef.current || transcriptAnchorDirtyRef.current;
11790
11739
  const bottomPinnedForMeasure = transcriptPinnedForStreaming;
11791
11740
  const suppressMeasuredRowHeights = bottomPinnedForMeasure || transcriptStreamingActive && (scrolledUpForStreamingMeasure && hasStreamingReadingAnchor || scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming);
11792
- const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(items, {
11741
+ const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(transcriptItems, {
11793
11742
  columns: frameColumns,
11794
11743
  toolOutputExpanded,
11795
11744
  suppressMeasuredRowHeights,
11796
11745
  measuredRowsVersion,
11797
11746
  cacheRef: incrementalRowIndexCacheRef,
11798
- prefixSig
11799
- // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: prefixSig/tailSig capture the relevant item changes (raw `items` dropped so a tail-only flush never re-enters the builder via array identity); measuredRowsVersion folds in app-level measured height corrections
11800
- }), [prefixSig, tailSig, measuredRowsVersion, suppressMeasuredRowHeights]);
11747
+ prefixRevision: revision
11748
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- revision/tail height capture structural geometry; measuredRowsVersion folds in measured corrections
11749
+ }), [revision, tailSig, frameColumns, toolOutputExpanded, measuredRowsVersion, suppressMeasuredRowHeights]);
11801
11750
  const hasReadingAnchor = !!transcriptAnchorRef.current && !transcriptAnchorDirtyRef.current;
11802
11751
  const scrolledUpRows = Math.max(0, Number(scrollTargetRef.current) || 0);
11803
11752
  const scrolledUp = scrolledUpRows > transcriptBottomSlackRows;
@@ -11812,7 +11761,7 @@ function useTranscriptWindow({
11812
11761
  if (anchorLockActive) {
11813
11762
  const locked = resolveAnchorScrollOffset({
11814
11763
  anchor: transcriptAnchorRef.current,
11815
- items,
11764
+ items: transcriptItems,
11816
11765
  curPrefix: curPrefixForLock,
11817
11766
  totalRows: lockTotalRows,
11818
11767
  viewRows: lockViewRows,
@@ -11836,7 +11785,7 @@ function useTranscriptWindow({
11836
11785
  const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
11837
11786
  const locked = resolveAnchorScrollOffset({
11838
11787
  anchor: captured,
11839
- items,
11788
+ items: transcriptItems,
11840
11789
  curPrefix: curPrefixForLock,
11841
11790
  totalRows: lockTotalRows,
11842
11791
  viewRows: lockViewRows,
@@ -11869,7 +11818,7 @@ function useTranscriptWindow({
11869
11818
  const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
11870
11819
  const locked = resolveAnchorScrollOffset({
11871
11820
  anchor: captured,
11872
- items,
11821
+ items: transcriptItems,
11873
11822
  curPrefix: curPrefixForLock,
11874
11823
  totalRows: lockTotalRows,
11875
11824
  viewRows: lockViewRows,
@@ -11888,7 +11837,7 @@ function useTranscriptWindow({
11888
11837
  floatingPanelRows: Number(floatingPanelRows) || 0
11889
11838
  };
11890
11839
  if (scrolledUp && !followingRef.current) {
11891
- const growth = streamingTailMountedGrowth(items, frameColumns, toolOutputExpanded);
11840
+ const growth = streamingTailMountedGrowth(transcriptItems, frameColumns, toolOutputExpanded);
11892
11841
  if (growth && growth.delta > 0) {
11893
11842
  const maxOffsetKeepingTailMounted = growth.tailRows + TRANSCRIPT_WINDOW_OVERSCAN_ROWS - 1;
11894
11843
  if (renderScrollOffset <= maxOffsetKeepingTailMounted) {
@@ -11896,7 +11845,7 @@ function useTranscriptWindow({
11896
11845
  }
11897
11846
  }
11898
11847
  }
11899
- const transcriptWindow = useMemo(() => transcriptRenderWindow(items, {
11848
+ const transcriptWindow = useMemo(() => transcriptRenderWindow(transcriptItems, {
11900
11849
  scrollOffset: renderScrollOffset,
11901
11850
  viewportHeight: transcriptContentHeight,
11902
11851
  columns: frameColumns,
@@ -11933,7 +11882,7 @@ function useTranscriptWindow({
11933
11882
  prefixRows: transcriptRowIndex?.prefixRows || null,
11934
11883
  totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
11935
11884
  viewRows: Math.max(1, Number(transcriptContentHeight) || 1),
11936
- items: items || null,
11885
+ items: transcriptItems || null,
11937
11886
  // The offset THIS frame actually rendered with. The same-frame anchor
11938
11887
  // CAPTURE for a missing/dirty anchor (above) reads this from the PREVIOUS
11939
11888
  // frame to reconstruct the exact top-edge row that was on screen, so the
@@ -11945,10 +11894,16 @@ function useTranscriptWindow({
11945
11894
  renderOffset: Math.max(0, Number(transcriptWindow.effectiveScrollOffset) || 0),
11946
11895
  suppressMeasuredRowHeights
11947
11896
  };
11948
- const transcriptVisibleItems = (items || []).slice(
11897
+ const transcriptVisibleItems = (transcriptItems || []).slice(
11949
11898
  transcriptWindow.startIndex,
11950
11899
  transcriptWindow.endIndex
11951
11900
  );
11901
+ if (streamingTailItem && transcriptVisibleItems.length > 0) {
11902
+ const last = transcriptVisibleItems.length - 1;
11903
+ if (transcriptVisibleItems[last]?.id === streamingTailItem.id) {
11904
+ transcriptVisibleItems[last] = streamingTailItem;
11905
+ }
11906
+ }
11952
11907
  const renderedTranscriptItems = transcriptVisibleItems;
11953
11908
  let overlayHintAttachItemIndex = -1;
11954
11909
  for (let i = renderedTranscriptItems.length - 1; i >= 0; i--) {
@@ -12068,7 +12023,7 @@ function useTranscriptWindow({
12068
12023
  return;
12069
12024
  }
12070
12025
  const viewRows = Math.max(1, Number(transcriptContentHeight) || 1);
12071
- const itemList = items || [];
12026
+ const itemList = transcriptItems || [];
12072
12027
  let anchor = transcriptAnchorRef.current;
12073
12028
  if (!followingRef.current && (!anchor || transcriptAnchorDirtyRef.current)) {
12074
12029
  if (curPrefix && curPrefix.length > 1) {
@@ -21475,6 +21430,8 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
21475
21430
  transcriptMeasureRef
21476
21431
  } = useTranscriptWindow({
21477
21432
  items: state.items,
21433
+ structureRevision: state.structureRevision,
21434
+ streamingTail: state.streamingTail,
21478
21435
  themeEpoch: state.themeEpoch,
21479
21436
  frameColumns,
21480
21437
  toolOutputExpanded,
@@ -23123,8 +23080,13 @@ function createToolCardResults({
23123
23080
  markToolCallDone,
23124
23081
  updateAgentJobCard,
23125
23082
  buildAgentJobCardPatch,
23126
- agentStatusState
23083
+ agentStatusState,
23084
+ itemIndexById
23127
23085
  }) {
23086
+ const itemById = (id) => {
23087
+ const index = itemIndexById?.get(id);
23088
+ return Number.isInteger(index) ? getState().items[index]?.id === id ? getState().items[index] : null : null;
23089
+ };
23128
23090
  function finalizedErrorFallbackBody(body, text, exitCode) {
23129
23091
  if (String(body || "").trim()) return body;
23130
23092
  if (String(text || "").trim()) return text;
@@ -23132,10 +23094,10 @@ function createToolCardResults({
23132
23094
  return "Failed";
23133
23095
  }
23134
23096
  function patchToolItem(id, patch) {
23135
- const prev = getState().items.find((it) => it.id === id);
23097
+ const prev = itemById(id);
23136
23098
  const ok = patchItem(id, patch);
23137
23099
  if (!ok || !prev) return ok;
23138
- const next = getState().items.find((it) => it.id === id);
23100
+ const next = itemById(id);
23139
23101
  if (next && next !== prev) carryTranscriptMeasuredRowsCache(prev, next);
23140
23102
  return ok;
23141
23103
  }
@@ -23173,7 +23135,7 @@ function createToolCardResults({
23173
23135
  const exitErrors = allCalls.filter((r) => r.isExitError).length;
23174
23136
  const succeeded = Math.max(0, completed - errors - exitErrors);
23175
23137
  const detailText = errors > 0 || exitErrors > 0 ? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode }) : formatAggregateDetail(aggregateSummaries(aggregate));
23176
- const currentItem = getState().items.find((it) => it.id === card.itemId);
23138
+ const currentItem = itemById(card.itemId);
23177
23139
  const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
23178
23140
  const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
23179
23141
  const rawResult = aggregateRawResult(allCalls);
@@ -23284,7 +23246,7 @@ function createToolCardResults({
23284
23246
  const rawResult = aggregateRawResult(allCalls);
23285
23247
  let displayDetail = errors > 0 || exitErrors > 0 ? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode }) : formatAggregateDetail(aggregateSummaries(aggregate));
23286
23248
  if (cancelled) {
23287
- const currentItem = getState().items.find((it) => it.id === card.itemId);
23249
+ const currentItem = itemById(card.itemId);
23288
23250
  displayDetail = withCancelledResultMarker(displayDetail, currentItem);
23289
23251
  }
23290
23252
  patchToolItem(card.itemId, {
@@ -23316,7 +23278,7 @@ function createToolCardResults({
23316
23278
  resultText = finalizedErrorFallbackBody(resultText, exitRec?.text, exitRec?.exitCode);
23317
23279
  }
23318
23280
  if (cancelled) {
23319
- const currentItem = getState().items.find((it) => it.id === card.itemId);
23281
+ const currentItem = itemById(card.itemId);
23320
23282
  resultText = withCancelledResultMarker(resultText, currentItem);
23321
23283
  }
23322
23284
  patchToolItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, callErrorCount: group.callErrors || 0, exitErrorCount: group.exitErrors || 0, count: group.count, completedCount: group.completed, completedAt: Date.now() });
@@ -23419,14 +23381,67 @@ function createAgentJobFeed({
23419
23381
  getPending,
23420
23382
  agentStatusState,
23421
23383
  displayedExecutionNotificationKeys,
23384
+ itemIndexById,
23422
23385
  pushNotice,
23423
23386
  now = () => Date.now(),
23424
23387
  executionResumeTombstoneTtlMs = 3e4,
23425
23388
  executionResumeTombstoneLimit = 128
23426
23389
  }) {
23390
+ const executionDedupLimit = 256;
23427
23391
  let executionResumeKickDeferred = false;
23428
23392
  const discardedExecutionResumeKeys = /* @__PURE__ */ new Map();
23429
23393
  const displayedExecutionResponseStates = /* @__PURE__ */ new Map();
23394
+ const terminalExecutionNotificationKeys = /* @__PURE__ */ new Set();
23395
+ const terminalExecutionResponseKeys = /* @__PURE__ */ new Set();
23396
+ const executionNotificationKeys = /* @__PURE__ */ new Map();
23397
+ const clearExecutionDedupState = () => {
23398
+ displayedExecutionNotificationKeys.clear();
23399
+ displayedExecutionResponseStates.clear();
23400
+ terminalExecutionNotificationKeys.clear();
23401
+ terminalExecutionResponseKeys.clear();
23402
+ executionNotificationKeys.clear();
23403
+ };
23404
+ const rememberDisplayedExecutionNotificationKey = (key, terminal = false, executionId = "") => {
23405
+ if (!key) return;
23406
+ displayedExecutionNotificationKeys.delete(key);
23407
+ if (executionId) executionNotificationKeys.set(key, executionId);
23408
+ if (terminal) terminalExecutionNotificationKeys.add(key);
23409
+ else terminalExecutionNotificationKeys.delete(key);
23410
+ while (displayedExecutionNotificationKeys.size >= executionDedupLimit) {
23411
+ const oldestTerminal = [...displayedExecutionNotificationKeys].find((candidate) => terminalExecutionNotificationKeys.has(candidate));
23412
+ if (oldestTerminal == null) break;
23413
+ displayedExecutionNotificationKeys.delete(oldestTerminal);
23414
+ terminalExecutionNotificationKeys.delete(oldestTerminal);
23415
+ executionNotificationKeys.delete(oldestTerminal);
23416
+ }
23417
+ displayedExecutionNotificationKeys.add(key);
23418
+ };
23419
+ const promoteExecutionNotificationKeys = (executionId) => {
23420
+ if (!executionId) return;
23421
+ for (const [key, keyExecutionId] of executionNotificationKeys) {
23422
+ if (keyExecutionId !== executionId || !displayedExecutionNotificationKeys.has(key)) continue;
23423
+ terminalExecutionNotificationKeys.add(key);
23424
+ }
23425
+ };
23426
+ const promoteExecutionDedupState = (executionId) => {
23427
+ if (!executionId) return;
23428
+ promoteExecutionNotificationKeys(executionId);
23429
+ const responseState = displayedExecutionResponseStates.get(executionId);
23430
+ if (responseState) rememberDisplayedExecutionResponseState(executionId, responseState, true);
23431
+ };
23432
+ const rememberDisplayedExecutionResponseState = (key, value, terminal = false) => {
23433
+ if (!key) return;
23434
+ displayedExecutionResponseStates.delete(key);
23435
+ if (terminal) terminalExecutionResponseKeys.add(key);
23436
+ else terminalExecutionResponseKeys.delete(key);
23437
+ while (displayedExecutionResponseStates.size >= executionDedupLimit) {
23438
+ const oldestTerminal = [...displayedExecutionResponseStates.keys()].find((candidate) => terminalExecutionResponseKeys.has(candidate));
23439
+ if (oldestTerminal == null) break;
23440
+ displayedExecutionResponseStates.delete(oldestTerminal);
23441
+ terminalExecutionResponseKeys.delete(oldestTerminal);
23442
+ }
23443
+ displayedExecutionResponseStates.set(key, value);
23444
+ };
23430
23445
  const executionResumeKickBodies = [];
23431
23446
  function executionResumeKey(body, completionKey = "") {
23432
23447
  if (completionKey && typeof completionKey === "object") {
@@ -23508,7 +23523,8 @@ function createAgentJobFeed({
23508
23523
  }
23509
23524
  function buildAgentJobCardPatch(itemId, text, isError = false) {
23510
23525
  const parsed = parseAgentJob(text);
23511
- const current = getState().items.find((it) => it.id === itemId);
23526
+ const index = itemIndexById?.get(itemId);
23527
+ const current = Number.isInteger(index) && getState().items[index]?.id === itemId ? getState().items[index] : null;
23512
23528
  const rawDisplayText = agentJobResultText(text, parsed) || String(text ?? "").trim();
23513
23529
  const displayText = isError ? toolErrorDisplay(rawDisplayText, "agent") : rawDisplayText;
23514
23530
  return {
@@ -23530,13 +23546,17 @@ function createAgentJobFeed({
23530
23546
  }
23531
23547
  function subscribeRuntimeNotifications() {
23532
23548
  if (typeof runtime.onNotification !== "function") return null;
23533
- return runtime.onNotification((event) => {
23549
+ const unsubscribe = runtime.onNotification((event) => {
23534
23550
  if (getDisposed()) return;
23535
23551
  const text = String(event?.content ?? event?.text ?? event ?? "").trim();
23536
23552
  if (!text) return;
23537
23553
  const parsed = parseAgentJob(text);
23538
23554
  const notificationKey = notificationQueueKey(event, text, parsed);
23539
23555
  const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
23556
+ const executionId = String(event?.meta?.execution_id || parsed?.taskId || "").trim();
23557
+ const status = String(event?.meta?.status || parsed?.status || "").toLowerCase();
23558
+ const terminalStatus = /^(completed|complete|done|success|succeeded|ok|failed|error|timeout|killed|cancelled|canceled|denied)$/.test(status);
23559
+ if (terminalStatus) promoteExecutionDedupState(executionId);
23540
23560
  if (delivery.action === "ignore") return;
23541
23561
  if (delivery.action === "notice") {
23542
23562
  pushNotice?.(delivery.displayText, delivery.tone || "info", { transcript: delivery.transcript === true });
@@ -23549,16 +23569,19 @@ function createAgentJobFeed({
23549
23569
  if (delivery.action === "execution-ui") {
23550
23570
  const cardKey = executionCardKey(event, text, parsed);
23551
23571
  const firstDelivery = !cardKey || !displayedExecutionNotificationKeys.has(cardKey);
23552
- const executionId = String(event?.meta?.execution_id || parsed?.taskId || "").trim();
23553
- const status = String(event?.meta?.status || parsed?.status || "").toLowerCase();
23554
23572
  const hasBody = /\n\s*\n[\s\S]*\S/.test(text);
23555
23573
  const isFailure = /^(failed|error|timeout|killed|cancelled|canceled|denied)$/.test(status);
23556
23574
  const successfulPreview = !hasBody && !isFailure && /^(completed|complete|done|success|succeeded|ok)$/.test(status);
23575
+ const terminal = terminalStatus;
23557
23576
  const responseState = executionId ? displayedExecutionResponseStates.get(executionId) : "";
23558
23577
  const bodyAlreadyDisplayed = responseState === "body";
23578
+ if (cardKey && terminal && displayedExecutionNotificationKeys.has(cardKey)) {
23579
+ rememberDisplayedExecutionNotificationKey(cardKey, true, executionId);
23580
+ }
23581
+ if (terminal) promoteExecutionDedupState(executionId);
23559
23582
  if (firstDelivery && !successfulPreview && !bodyAlreadyDisplayed) {
23560
- if (cardKey) displayedExecutionNotificationKeys.add(cardKey);
23561
- if (executionId) displayedExecutionResponseStates.set(executionId, hasBody ? "body" : "preview");
23583
+ if (cardKey) rememberDisplayedExecutionNotificationKey(cardKey, terminal, executionId);
23584
+ if (executionId) rememberDisplayedExecutionResponseState(executionId, hasBody ? "body" : "preview", terminal);
23562
23585
  (pushAsyncAgentResponse || pushUserOrSyntheticItem)(delivery.displayText, nextId2(), "injected", { responseKey: executionId });
23563
23586
  }
23564
23587
  refreshAgentStatus(parsed);
@@ -23627,6 +23650,13 @@ function createAgentJobFeed({
23627
23650
  enqueue(modelContent, enqueueOpts);
23628
23651
  return true;
23629
23652
  });
23653
+ return () => {
23654
+ try {
23655
+ unsubscribe?.();
23656
+ } finally {
23657
+ clearExecutionDedupState();
23658
+ }
23659
+ };
23630
23660
  }
23631
23661
  return {
23632
23662
  kickExecutionPendingResume,
@@ -23635,7 +23665,8 @@ function createAgentJobFeed({
23635
23665
  discardExecutionPendingResume,
23636
23666
  updateAgentJobCard,
23637
23667
  buildAgentJobCardPatch,
23638
- subscribeRuntimeNotifications
23668
+ subscribeRuntimeNotifications,
23669
+ clearExecutionDedupState
23639
23670
  };
23640
23671
  }
23641
23672
 
@@ -24022,6 +24053,7 @@ function createSessionFlow(bag) {
24022
24053
  pending,
24023
24054
  pendingNotificationKeys,
24024
24055
  displayedExecutionNotificationKeys,
24056
+ clearExecutionDedupState,
24025
24057
  getState,
24026
24058
  set,
24027
24059
  pushItem,
@@ -24120,6 +24152,7 @@ function createSessionFlow(bag) {
24120
24152
  if (predicate(entry) && (entry.mode || "prompt") === targetMode && queuePriorityValue(entry.priority) === bestPriority) {
24121
24153
  batch.push(entry);
24122
24154
  pending.splice(i, 1);
24155
+ if (entry.mode === "task-notification" && entry.key) pendingNotificationKeys.delete(entry.key);
24123
24156
  if (batch.length >= limit) break;
24124
24157
  } else {
24125
24158
  i += 1;
@@ -24417,6 +24450,7 @@ function createSessionFlow(bag) {
24417
24450
  getState().busy = false;
24418
24451
  pendingNotificationKeys.clear();
24419
24452
  displayedExecutionNotificationKeys.clear();
24453
+ clearExecutionDedupState?.();
24420
24454
  };
24421
24455
  const applyClearedSessionUi = (doneLabel) => {
24422
24456
  resetStats();
@@ -24512,6 +24546,9 @@ function createRunTurn(bag) {
24512
24546
  set,
24513
24547
  pushItem,
24514
24548
  patchItem,
24549
+ updateStreamingTail: updateStreamingTailFromStore,
24550
+ settleStreamingTail: settleStreamingTailFromStore,
24551
+ clearStreamingTail: clearStreamingTailFromStore,
24515
24552
  pushNotice,
24516
24553
  pushUserOrSyntheticItem,
24517
24554
  markToolCallActive,
@@ -24528,6 +24565,21 @@ function createRunTurn(bag) {
24528
24565
  drain,
24529
24566
  drainPendingSteering
24530
24567
  } = bag;
24568
+ const updateStreamingTail = updateStreamingTailFromStore || ((id, patch = {}) => {
24569
+ set({ streamingTail: { ...getState().streamingTail || {}, ...patch, kind: "assistant", id, streaming: true } });
24570
+ return true;
24571
+ });
24572
+ const settleStreamingTail = settleStreamingTailFromStore || ((id, patch = {}) => {
24573
+ const tail = getState().streamingTail;
24574
+ if (!tail || tail.id !== id) return false;
24575
+ pushItem({ ...tail, ...patch, kind: "assistant", id, streaming: false });
24576
+ set({ streamingTail: null });
24577
+ return true;
24578
+ });
24579
+ const clearStreamingTail = clearStreamingTailFromStore || ((id = null) => {
24580
+ if (id == null || getState().streamingTail?.id === id) set({ streamingTail: null });
24581
+ return true;
24582
+ });
24531
24583
  async function runTurn(userText, options = {}) {
24532
24584
  const turnIndex = getState().stats.turns || 0;
24533
24585
  const startedAt = Date.now();
@@ -24559,6 +24611,9 @@ function createRunTurn(bag) {
24559
24611
  let watchdogGraceTimer = null;
24560
24612
  let lastProgressAt = startedAt;
24561
24613
  let lastProgressLabel = "start";
24614
+ let watchdogDeferralCeilingAt = 0;
24615
+ const configuredLeadToolMaxMs = Number(process.env.MIXDOG_LEAD_TOOL_MAX_MS);
24616
+ const leadToolMaxMs = Number.isFinite(configuredLeadToolMaxMs) && configuredLeadToolMaxMs > 0 ? configuredLeadToolMaxMs : 30 * 60 * 1e3;
24562
24617
  const clearWatchdog = () => {
24563
24618
  if (watchdogTimer) {
24564
24619
  clearTimeout(watchdogTimer);
@@ -24569,21 +24624,57 @@ function createRunTurn(bag) {
24569
24624
  watchdogGraceTimer = null;
24570
24625
  }
24571
24626
  };
24627
+ const refreshWatchdogFromRuntimeLiveness = () => {
24628
+ let liveness;
24629
+ try {
24630
+ liveness = runtime.getTurnLiveness?.();
24631
+ } catch {
24632
+ return false;
24633
+ }
24634
+ if (liveness?.stage !== "tool_running") watchdogDeferralCeilingAt = 0;
24635
+ const progressAt = Number(liveness?.lastProgressAt);
24636
+ const now = Date.now();
24637
+ if (!liveness || !Number.isFinite(progressAt) || progressAt <= now - LEAD_TURN_TIMEOUT_MS2) return false;
24638
+ if (liveness.stage === "tool_running") {
24639
+ watchdogDeferralCeilingAt = 0;
24640
+ const toolStartedAt = Number(liveness.toolStartedAt);
24641
+ if (!Number.isFinite(toolStartedAt) || toolStartedAt <= 0) return false;
24642
+ const toolSelfDeadlineMs = Number(liveness.toolSelfDeadlineMs);
24643
+ const toolCeilingMs = Math.max(
24644
+ Number.isFinite(toolSelfDeadlineMs) && toolSelfDeadlineMs > 0 ? toolSelfDeadlineMs + 6e4 : 0,
24645
+ leadToolMaxMs
24646
+ );
24647
+ if (now - toolStartedAt >= toolCeilingMs) return false;
24648
+ watchdogDeferralCeilingAt = toolStartedAt + toolCeilingMs;
24649
+ }
24650
+ lastProgressAt = progressAt;
24651
+ lastProgressLabel = `orchestrator:${String(liveness.stage || "unknown")}`;
24652
+ armWatchdog();
24653
+ return true;
24654
+ };
24572
24655
  const armWatchdog = () => {
24573
24656
  if (watchdogTimer) {
24574
24657
  clearTimeout(watchdogTimer);
24575
24658
  watchdogTimer = null;
24576
24659
  }
24577
24660
  if (watchdogTripped) return;
24578
- const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS2 - Math.max(0, Date.now() - lastProgressAt));
24661
+ const now = Date.now();
24662
+ const remaining = Math.max(1, LEAD_TURN_TIMEOUT_MS2 - Math.max(0, now - lastProgressAt));
24663
+ const ceilingRemaining = watchdogDeferralCeilingAt > 0 ? Math.max(1, watchdogDeferralCeilingAt - now) : Infinity;
24664
+ const delay = Math.min(remaining, ceilingRemaining);
24579
24665
  watchdogTimer = setTimeout(() => {
24580
24666
  if (!isCurrentTurn()) return;
24581
24667
  if (watchdogTripped) return;
24582
- const idleMs = Date.now() - lastProgressAt;
24668
+ const now2 = Date.now();
24669
+ const idleMs = now2 - lastProgressAt;
24583
24670
  if (idleMs < LEAD_TURN_TIMEOUT_MS2) {
24584
- armWatchdog();
24585
- return;
24586
- }
24671
+ if (watchdogDeferralCeilingAt > 0 && now2 >= watchdogDeferralCeilingAt) {
24672
+ if (refreshWatchdogFromRuntimeLiveness()) return;
24673
+ } else {
24674
+ armWatchdog();
24675
+ return;
24676
+ }
24677
+ } else if (refreshWatchdogFromRuntimeLiveness()) return;
24587
24678
  watchdogTripped = true;
24588
24679
  if (_batchTimer !== null) {
24589
24680
  clearTimeout(_batchTimer);
@@ -24594,7 +24685,7 @@ function createRunTurn(bag) {
24594
24685
  _pendingThinkingLastEndedAt = 0;
24595
24686
  const elapsed = Date.now() - startedAt;
24596
24687
  tuiDebug2(`runTurn WATCHDOG TRIP turn=${turnIndex} elapsedMs=${elapsed} idleMs=${idleMs} lastProgress=${lastProgressLabel} \u2014 aborting stuck turn`);
24597
- pushNotice(`Turn timed out after ${Math.round(idleMs / 1e3)}s idle \u2014 aborting stuck request. Input will be released shortly if abort does not unwind.`, "warn", { transcript: true });
24688
+ pushNotice(`Turn timed out after ${Math.round(idleMs / 1e3)}s idle (last progress: ${lastProgressLabel}) \u2014 aborting stuck request. Input will be released shortly if abort does not unwind.`, "warn", { transcript: true });
24598
24689
  try {
24599
24690
  runtime.abort("cli-react-abort-watchdog");
24600
24691
  } catch {
@@ -24610,6 +24701,11 @@ function createRunTurn(bag) {
24610
24701
  finalizeToolHeaders();
24611
24702
  clearDeferredTimers();
24612
24703
  flags.flushDeferredBeforeImmediatePush = null;
24704
+ if (currentAssistantId && currentAssistantText.trim()) {
24705
+ settleStreamingTail(currentAssistantId, { text: currentAssistantText });
24706
+ } else {
24707
+ clearStreamingTail(currentAssistantId);
24708
+ }
24613
24709
  flags.leadTurnEpoch++;
24614
24710
  set({ busy: false, spinner: null, thinking: null, lastTurn: null });
24615
24711
  flags.activePromptRestore = null;
@@ -24618,7 +24714,7 @@ function createRunTurn(bag) {
24618
24714
  flushDeferredExecutionPendingResumeKick();
24619
24715
  }, 5e3);
24620
24716
  watchdogGraceTimer.unref?.();
24621
- }, remaining);
24717
+ }, delay);
24622
24718
  watchdogTimer.unref?.();
24623
24719
  };
24624
24720
  const markTurnProgress = (label) => {
@@ -24626,7 +24722,6 @@ function createRunTurn(bag) {
24626
24722
  if (watchdogTripped) return;
24627
24723
  lastProgressAt = Date.now();
24628
24724
  lastProgressLabel = String(label || "progress");
24629
- armWatchdog();
24630
24725
  return true;
24631
24726
  };
24632
24727
  armWatchdog();
@@ -24770,7 +24865,7 @@ function createRunTurn(bag) {
24770
24865
  const it = newItems[i];
24771
24866
  if (it?.id != null) itemIndexById.set(it.id, base + i);
24772
24867
  }
24773
- set({ items, ...extra });
24868
+ set({ items, structureRevision: (Number(getState().structureRevision) || 0) + 1, ...extra });
24774
24869
  };
24775
24870
  const markPromptCommitted = () => {
24776
24871
  if (flags.activePromptRestore) {
@@ -24805,7 +24900,7 @@ function createRunTurn(bag) {
24805
24900
  changed = true;
24806
24901
  return { ...item, headerFinalized: true };
24807
24902
  });
24808
- if (changed) set({ items });
24903
+ if (changed) set({ items, structureRevision: (Number(getState().structureRevision) || 0) + 1 });
24809
24904
  return changed;
24810
24905
  };
24811
24906
  const completeAggregateVisual = () => {
@@ -24896,7 +24991,7 @@ function createRunTurn(bag) {
24896
24991
  const ensureAssistant = (initialText = "") => {
24897
24992
  if (!currentAssistantId) {
24898
24993
  currentAssistantId = nextId2();
24899
- pushItem({ kind: "assistant", id: currentAssistantId, text: String(initialText || ""), streaming: true });
24994
+ updateStreamingTail(currentAssistantId, { text: String(initialText || "") });
24900
24995
  }
24901
24996
  return currentAssistantId;
24902
24997
  };
@@ -24907,7 +25002,6 @@ function createRunTurn(bag) {
24907
25002
  _lastNewlineIdx = -1;
24908
25003
  _emittedNewlineIdx = -2;
24909
25004
  _emittedVisibleText = "";
24910
- _cachedAssistantIndex = -1;
24911
25005
  };
24912
25006
  const commitAssistantSegment = ({ sealToolBlock = false } = {}) => {
24913
25007
  const text = currentAssistantText || "";
@@ -24917,7 +25011,7 @@ function createRunTurn(bag) {
24917
25011
  }
24918
25012
  if (sealToolBlock) clearAggregateContinuation();
24919
25013
  const id = currentAssistantId || ensureAssistant(text);
24920
- patchItem(id, { text, streaming: false });
25014
+ settleStreamingTail(id, { text });
24921
25015
  committedSegments.push(text);
24922
25016
  closeAssistantSegment();
24923
25017
  return true;
@@ -24945,7 +25039,6 @@ function createRunTurn(bag) {
24945
25039
  let _lastNewlineIdx = -1;
24946
25040
  let _emittedNewlineIdx = -2;
24947
25041
  let _emittedVisibleText = "";
24948
- let _cachedAssistantIndex = -1;
24949
25042
  let _publishedThinkingActive = false;
24950
25043
  const flushStreamBatch = () => {
24951
25044
  if (_batchTimer !== null) {
@@ -24980,22 +25073,13 @@ function createRunTurn(bag) {
24980
25073
  const patch = {};
24981
25074
  if (currentAssistantId || streamingVisibleText.trim()) {
24982
25075
  const id = ensureAssistant(streamingVisibleText);
24983
- let index = _cachedAssistantIndex;
24984
- if (index < 0 || getState().items[index]?.id !== id) {
24985
- index = getState().items.findIndex((it) => it.id === id);
24986
- _cachedAssistantIndex = index;
24987
- }
24988
- if (index >= 0) {
24989
- const current = getState().items[index];
24990
- if (!Object.is(current.text, streamingVisibleText) || current.streaming !== true) {
24991
- const items = getState().items.slice();
24992
- items[index] = { ...current, text: streamingVisibleText, streaming: true };
24993
- patch.items = items;
24994
- }
25076
+ const current = getState().streamingTail;
25077
+ if (!current || current.id !== id || !Object.is(current.text, streamingVisibleText)) {
25078
+ patch.streamingTail = { kind: "assistant", id, text: streamingVisibleText, streaming: true };
24995
25079
  }
24996
25080
  }
24997
25081
  const responseLengthVal = assistantText.length + thinkingText.length;
24998
- const visibleLineChanged = patch.items !== void 0;
25082
+ const visibleLineChanged = patch.streamingTail !== void 0;
24999
25083
  const thinkingTransition = _publishedThinkingActive === true;
25000
25084
  if (getState().spinner && (visibleLineChanged || thinkingTransition || _pendingThinkingLastEndedAt)) {
25001
25085
  patch.spinner = { ...getState().spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || getState().spinner.thinkingLastEndedAt, mode: compactingActive ? "compacting" : "responding" };
@@ -25054,7 +25138,8 @@ function createRunTurn(bag) {
25054
25138
  const succeeded = Math.max(0, completedCount - errors - exitErrors);
25055
25139
  const rawResult = aggregateRawResult(allCalls);
25056
25140
  const displayDetail = errors > 0 || exitErrors > 0 ? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: allCalls.find((r) => r.isExitError)?.exitCode }) : formatAggregateDetail(aggregateSummaries(aggregate));
25057
- const currentItem = getState().items.find((it) => it.id === card.itemId);
25141
+ const currentIndex = itemIndexById.get(card.itemId);
25142
+ const currentItem = Number.isInteger(currentIndex) && getState().items[currentIndex]?.id === card.itemId ? getState().items[currentIndex] : null;
25058
25143
  const visualCompleted = Math.max(
25059
25144
  completedCount,
25060
25145
  Math.min(allCalls.length, Number(currentItem?.completedCount || 0))
@@ -25087,6 +25172,9 @@ function createRunTurn(bag) {
25087
25172
  try {
25088
25173
  const { result, session } = await runtime.ask(userText, {
25089
25174
  drainSteering: (_sessionId, drainOptions) => isCurrentTurn() ? drainPendingSteering(drainOptions) : [],
25175
+ onStreamDelta: () => {
25176
+ markTurnProgress("stream-delta");
25177
+ },
25090
25178
  onSteerMessage: (text) => {
25091
25179
  if (!markTurnProgress("steer-message")) return;
25092
25180
  if (text === STEERING_SUPPRESSED_DISPLAY) return;
@@ -25327,10 +25415,10 @@ function createRunTurn(bag) {
25327
25415
  if (finalRemainder.trim()) {
25328
25416
  const id = currentAssistantId || ensureAssistant(finalRemainder);
25329
25417
  currentAssistantText = finalRemainder;
25330
- patchItem(id, { text: finalRemainder, streaming: false });
25418
+ settleStreamingTail(id, { text: finalRemainder });
25331
25419
  } else if (currentAssistantId && (currentAssistantText.trim() || assistantText.trim())) {
25332
25420
  const streamedText = currentAssistantText || assistantText;
25333
- patchItem(currentAssistantId, { text: streamedText, streaming: false });
25421
+ settleStreamingTail(currentAssistantId, { text: streamedText });
25334
25422
  }
25335
25423
  turnFinishedNormally = true;
25336
25424
  }
@@ -25343,7 +25431,7 @@ function createRunTurn(bag) {
25343
25431
  if (error?.name === "SessionClosedError") {
25344
25432
  cancelled = true;
25345
25433
  if (assistantText.trim() && currentAssistantId) {
25346
- patchItem(currentAssistantId, { text: currentAssistantText || assistantText, streaming: false });
25434
+ settleStreamingTail(currentAssistantId, { text: currentAssistantText || assistantText });
25347
25435
  }
25348
25436
  flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
25349
25437
  finalizeToolHeaders();
@@ -25369,6 +25457,13 @@ function createRunTurn(bag) {
25369
25457
  if (isStaleUnwind) {
25370
25458
  tuiDebug2(`runTurn STALE UNWIND turn=${turnIndex} \u2014 force-released; skipping shared UI/state writes`);
25371
25459
  } else {
25460
+ if (currentAssistantId && getState().streamingTail?.id === currentAssistantId) {
25461
+ if (currentAssistantText.trim()) {
25462
+ settleStreamingTail(currentAssistantId, { text: currentAssistantText });
25463
+ } else {
25464
+ clearStreamingTail(currentAssistantId);
25465
+ }
25466
+ }
25372
25467
  const producedTranscriptItem = getState().items.length + closingItems.length > itemsAtTurnStart;
25373
25468
  const reclaimed = cancelled && flags.activePromptRestore?.reclaimed === true;
25374
25469
  flags.activePromptRestore = null;
@@ -26105,6 +26200,8 @@ function createEngineApiA(bag) {
26105
26200
  pushItem,
26106
26201
  patchItem,
26107
26202
  replaceItems,
26203
+ settleStreamingTail,
26204
+ clearStreamingTail,
26108
26205
  pushNotice,
26109
26206
  autoClearState,
26110
26207
  agentStatusState,
@@ -26644,6 +26741,12 @@ function createEngineApiA(bag) {
26644
26741
  if (flags.disposed) return;
26645
26742
  if (!getState().busy) return;
26646
26743
  if (flags.leadTurnEpoch !== abortEpoch) return;
26744
+ const streamingTail = getState().streamingTail;
26745
+ if (streamingTail?.text?.trim()) {
26746
+ settleStreamingTail?.(streamingTail.id, {});
26747
+ } else {
26748
+ clearStreamingTail?.();
26749
+ }
26647
26750
  flags.leadTurnEpoch = (Number(flags.leadTurnEpoch) || 0) + 1;
26648
26751
  set({ busy: false, spinner: null, thinking: null, lastTurn: null });
26649
26752
  flags.activePromptRestore = null;
@@ -26680,6 +26783,79 @@ var tuiDebug = (msg) => {
26680
26783
  };
26681
26784
  var _idSeq = 0;
26682
26785
  var nextId = () => `it_${++_idSeq}`;
26786
+ function replaceEngineItemsState({
26787
+ state,
26788
+ items,
26789
+ itemIndexById,
26790
+ preserveStreamingTail = false,
26791
+ extra = {}
26792
+ }) {
26793
+ const nextItems = Array.isArray(items) ? items : [];
26794
+ itemIndexById.clear();
26795
+ for (let i = 0; i < nextItems.length; i++) {
26796
+ const id = nextItems[i]?.id;
26797
+ if (id != null) itemIndexById.set(id, i);
26798
+ }
26799
+ return {
26800
+ ...state,
26801
+ ...extra,
26802
+ items: nextItems,
26803
+ structureRevision: (Number(state.structureRevision) || 0) + 1,
26804
+ streamingTail: preserveStreamingTail ? state.streamingTail : null
26805
+ };
26806
+ }
26807
+ function createEngineItemMutators({ getState, set, itemIndexById }) {
26808
+ const patchItem = (id, patch) => {
26809
+ const state = getState();
26810
+ let index = itemIndexById.get(id);
26811
+ if (!Number.isInteger(index) || state.items[index]?.id !== id) {
26812
+ index = state.items.findIndex((it) => it.id === id);
26813
+ if (index >= 0) itemIndexById.set(id, index);
26814
+ }
26815
+ if (index < 0) return false;
26816
+ const current = state.items[index];
26817
+ let changed = false;
26818
+ for (const [key, value] of Object.entries(patch || {})) {
26819
+ if (!Object.is(current[key], value)) {
26820
+ changed = true;
26821
+ break;
26822
+ }
26823
+ }
26824
+ if (!changed) return false;
26825
+ const items = state.items.slice();
26826
+ items[index] = { ...current, ...patch };
26827
+ set({ items, structureRevision: (Number(state.structureRevision) || 0) + 1 });
26828
+ return true;
26829
+ };
26830
+ const settleStreamingTail = (id, patch = {}, extra = {}) => {
26831
+ const state = getState();
26832
+ const tail = state.streamingTail?.id === id ? state.streamingTail : null;
26833
+ if (!tail) return false;
26834
+ let existingIndex = itemIndexById.get(id);
26835
+ if (!Number.isInteger(existingIndex) || state.items[existingIndex]?.id !== id) {
26836
+ existingIndex = state.items.findIndex((item2) => item2?.id === id);
26837
+ }
26838
+ if (existingIndex >= 0) return false;
26839
+ const item = {
26840
+ ...tail || {},
26841
+ ...patch,
26842
+ kind: "assistant",
26843
+ id,
26844
+ streaming: false
26845
+ };
26846
+ const index = state.items.length;
26847
+ const items = [...state.items, item];
26848
+ itemIndexById.set(id, index);
26849
+ set({
26850
+ items,
26851
+ structureRevision: (Number(state.structureRevision) || 0) + 1,
26852
+ streamingTail: null,
26853
+ ...extra
26854
+ });
26855
+ return true;
26856
+ };
26857
+ return { patchItem, settleStreamingTail };
26858
+ }
26683
26859
  async function createEngineSession({
26684
26860
  provider: providerName,
26685
26861
  model,
@@ -26734,6 +26910,8 @@ async function createEngineSession({
26734
26910
  };
26735
26911
  let state = {
26736
26912
  items: [],
26913
+ structureRevision: 0,
26914
+ streamingTail: null,
26737
26915
  toasts: [],
26738
26916
  progressHint: null,
26739
26917
  busy: false,
@@ -26799,20 +26977,20 @@ async function createEngineSession({
26799
26977
  return true;
26800
26978
  };
26801
26979
  const itemIndexById = /* @__PURE__ */ new Map();
26802
- const replaceItems = (items) => {
26980
+ const replaceItems = (items, { preserveStreamingTail = false } = {}) => {
26803
26981
  const nextItems = Array.isArray(items) ? items : [];
26804
- itemIndexById.clear();
26805
- for (let i = 0; i < nextItems.length; i++) {
26806
- const id = nextItems[i]?.id;
26807
- if (id != null) itemIndexById.set(id, i);
26808
- }
26809
26982
  activeToolCalls.clear();
26810
- state = {
26811
- ...state,
26983
+ state = replaceEngineItemsState({
26984
+ state,
26812
26985
  items: nextItems,
26813
- promptHistoryList: buildMergedPromptHistory(recomputePromptHistory(nextItems), loadPromptHistory(state.cwd)),
26814
- activeToolSummary: null
26815
- };
26986
+ itemIndexById,
26987
+ preserveStreamingTail,
26988
+ extra: {
26989
+ promptHistoryList: buildMergedPromptHistory(recomputePromptHistory(nextItems), loadPromptHistory(state.cwd)),
26990
+ activeToolSummary: null
26991
+ }
26992
+ });
26993
+ emit();
26816
26994
  return nextItems;
26817
26995
  };
26818
26996
  const activeToolCalls = /* @__PURE__ */ new Map();
@@ -26858,11 +27036,37 @@ async function createEngineSession({
26858
27036
  if (item?.id != null) itemIndexById.set(item.id, index);
26859
27037
  if (item?.kind === "user") {
26860
27038
  const promptHistoryList = buildMergedPromptHistory(recomputePromptHistory(items), loadPromptHistory(state.cwd));
26861
- set({ items, promptHistoryList });
27039
+ set({ items, structureRevision: state.structureRevision + 1, promptHistoryList });
26862
27040
  } else {
26863
- set({ items });
27041
+ set({ items, structureRevision: state.structureRevision + 1 });
27042
+ }
27043
+ };
27044
+ const updateStreamingTail = (id, patch = {}, extra = {}) => {
27045
+ if (id == null) return false;
27046
+ const current = state.streamingTail?.id === id ? state.streamingTail : { kind: "assistant", id, text: "", streaming: true };
27047
+ const next = { ...current, ...patch, kind: "assistant", id, streaming: true };
27048
+ let changed = state.streamingTail !== current;
27049
+ if (!changed) {
27050
+ for (const [key, value] of Object.entries(next)) {
27051
+ if (!Object.is(current[key], value)) {
27052
+ changed = true;
27053
+ break;
27054
+ }
27055
+ }
26864
27056
  }
27057
+ return set(changed ? { streamingTail: next, ...extra } : extra);
26865
27058
  };
27059
+ const clearStreamingTail = (id = null, extra = {}) => {
27060
+ if (!state.streamingTail || id != null && state.streamingTail.id !== id) {
27061
+ return set(extra);
27062
+ }
27063
+ return set({ streamingTail: null, ...extra });
27064
+ };
27065
+ const { patchItem, settleStreamingTail } = createEngineItemMutators({
27066
+ getState: () => state,
27067
+ set,
27068
+ itemIndexById
27069
+ });
26866
27070
  const upsertSyntheticToolItem = (text, id = nextId(), parsed = null) => {
26867
27071
  const synthetic = parseSyntheticAgentMessage(text);
26868
27072
  if (!synthetic) return false;
@@ -26977,7 +27181,7 @@ async function createEngineSession({
26977
27181
  if (id == null) return false;
26978
27182
  const items = state.items.filter((it) => !(it?.kind === "notice" && it?.id === id));
26979
27183
  if (items.length === state.items.length) return false;
26980
- set({ items: replaceItems(items) });
27184
+ set({ items: replaceItems(items, { preserveStreamingTail: true }) });
26981
27185
  return true;
26982
27186
  };
26983
27187
  const setProgressHint = (text, tone = "info") => {
@@ -26996,27 +27200,6 @@ async function createEngineSession({
26996
27200
  getDisposed: () => flags.disposed,
26997
27201
  timeoutMs: TOOL_APPROVAL_TIMEOUT_MS
26998
27202
  });
26999
- const patchItem = (id, patch) => {
27000
- let index = itemIndexById.get(id);
27001
- if (!Number.isInteger(index) || state.items[index]?.id !== id) {
27002
- index = state.items.findIndex((it) => it.id === id);
27003
- if (index >= 0) itemIndexById.set(id, index);
27004
- }
27005
- if (index < 0) return false;
27006
- const current = state.items[index];
27007
- let changed = false;
27008
- for (const [key, value] of Object.entries(patch || {})) {
27009
- if (!Object.is(current[key], value)) {
27010
- changed = true;
27011
- break;
27012
- }
27013
- }
27014
- if (!changed) return false;
27015
- const items = state.items.slice();
27016
- items[index] = { ...current, ...patch };
27017
- set({ items });
27018
- return true;
27019
- };
27020
27203
  const toastTimers = /* @__PURE__ */ new Set();
27021
27204
  lifecycle.runtimePulseTimer = setInterval(() => {
27022
27205
  if (flags.disposed) return;
@@ -27042,7 +27225,8 @@ async function createEngineSession({
27042
27225
  discardExecutionPendingResume,
27043
27226
  updateAgentJobCard,
27044
27227
  buildAgentJobCardPatch,
27045
- subscribeRuntimeNotifications
27228
+ subscribeRuntimeNotifications,
27229
+ clearExecutionDedupState
27046
27230
  } = createAgentJobFeed({
27047
27231
  runtime,
27048
27232
  getState: () => state,
@@ -27058,7 +27242,8 @@ async function createEngineSession({
27058
27242
  getPending: () => pending,
27059
27243
  agentStatusState,
27060
27244
  displayedExecutionNotificationKeys,
27061
- pushNotice
27245
+ pushNotice,
27246
+ itemIndexById
27062
27247
  });
27063
27248
  lifecycle.unsubscribeRuntimeNotifications = subscribeRuntimeNotifications();
27064
27249
  if (typeof runtime.onRemoteStateChange === "function") {
@@ -27077,7 +27262,8 @@ async function createEngineSession({
27077
27262
  markToolCallDone,
27078
27263
  updateAgentJobCard,
27079
27264
  buildAgentJobCardPatch,
27080
- agentStatusState
27265
+ agentStatusState,
27266
+ itemIndexById
27081
27267
  });
27082
27268
  Object.assign(bag, {
27083
27269
  runtime,
@@ -27089,6 +27275,7 @@ async function createEngineSession({
27089
27275
  pending,
27090
27276
  pendingNotificationKeys,
27091
27277
  displayedExecutionNotificationKeys,
27278
+ clearExecutionDedupState,
27092
27279
  listeners,
27093
27280
  itemIndexById,
27094
27281
  getState: () => state,
@@ -27096,6 +27283,9 @@ async function createEngineSession({
27096
27283
  pushItem,
27097
27284
  patchItem,
27098
27285
  replaceItems,
27286
+ updateStreamingTail,
27287
+ settleStreamingTail,
27288
+ clearStreamingTail,
27099
27289
  pushToast,
27100
27290
  pushNotice,
27101
27291
  removeNotice,
@@ -27118,6 +27308,7 @@ async function createEngineSession({
27118
27308
  requestToolApproval,
27119
27309
  patchToolCardResult,
27120
27310
  flushToolResults,
27311
+ clearExecutionDedupState,
27121
27312
  kickExecutionPendingResume,
27122
27313
  flushDeferredExecutionPendingResumeKick,
27123
27314
  scheduleExecutionPendingResumeKick,
@@ -27511,7 +27702,7 @@ function paintBootSplash() {
27511
27702
  const rows = Math.max(1, Number(process.stdout.rows) || 24);
27512
27703
  const windowsLikeTerminal = process.platform === "win32" || Boolean(process.env.WT_SESSION);
27513
27704
  const frameCols = Math.max(1, cols - (windowsLikeTerminal ? 1 : 0));
27514
- const center = (s) => `${" ".repeat(Math.max(0, Math.floor((frameCols - displayWidth(s)) / 2)))}${s}`;
27705
+ const center = (s, reserve = 0) => `${" ".repeat(Math.max(0, Math.floor((frameCols - reserve - displayWidth(s)) / 2)))}${s}`;
27515
27706
  const logo = [
27516
27707
  "\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
27517
27708
  "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D ",
@@ -27531,7 +27722,7 @@ function paintBootSplash() {
27531
27722
  `;
27532
27723
  }
27533
27724
  out += "\r\n";
27534
- out += `${subtleFg}${center(`mixdog coding agent \xB7 v${localPackageVersion()} \xB7 ${process.cwd()}`)}${reset}`;
27725
+ out += `${subtleFg}${center(`mixdog coding agent \xB7 v${localPackageVersion()} \xB7 ${process.cwd()}`, 4)}${reset}`;
27535
27726
  out += "\x1B[H";
27536
27727
  process.stdout.write(out);
27537
27728
  return { stop: () => {