mixdog 0.9.39 → 0.9.41

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 (59) hide show
  1. package/package.json +2 -1
  2. package/scripts/agent-terminal-reap-test.mjs +6 -6
  3. package/scripts/anthropic-oauth-refresh-race-test.mjs +338 -0
  4. package/scripts/compact-pressure-test.mjs +128 -0
  5. package/scripts/compact-trigger-migration-smoke.mjs +8 -8
  6. package/scripts/execution-resume-esc-integration-test.mjs +4 -2
  7. package/scripts/internal-comms-smoke.mjs +66 -25
  8. package/scripts/max-output-recovery-persist-test.mjs +81 -0
  9. package/scripts/max-output-recovery-test.mjs +222 -0
  10. package/scripts/provider-toolcall-test.mjs +32 -0
  11. package/scripts/steering-drain-buckets-test.mjs +140 -5
  12. package/scripts/tui-transcript-perf-test.mjs +279 -0
  13. package/src/agents/reviewer/AGENT.md +4 -0
  14. package/src/rules/lead/lead-brief.md +11 -3
  15. package/src/rules/lead/lead-tool.md +0 -2
  16. package/src/rules/shared/01-tool.md +4 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +20 -2
  18. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +7 -0
  19. package/src/runtime/agent/orchestrator/context/collect.mjs +7 -3
  20. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +144 -7
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -2
  22. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +2 -2
  23. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -25
  24. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +54 -12
  25. package/src/runtime/agent/orchestrator/session/context-utils.mjs +4 -3
  26. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +106 -8
  27. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +12 -1
  28. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +22 -7
  29. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -1
  30. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +17 -1
  31. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +26 -0
  32. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +31 -4
  33. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +11 -2
  35. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +45 -0
  36. package/src/runtime/agent/orchestrator/session/store.mjs +9 -1
  37. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  38. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +49 -34
  39. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -10
  41. package/src/runtime/shared/buffered-appender.mjs +13 -2
  42. package/src/session-runtime/config-helpers.mjs +6 -6
  43. package/src/session-runtime/lifecycle-api.mjs +4 -0
  44. package/src/session-runtime/runtime-core.mjs +1 -0
  45. package/src/session-runtime/session-turn-api.mjs +12 -0
  46. package/src/standalone/agent-tool.mjs +8 -1
  47. package/src/tui/App.jsx +2 -1
  48. package/src/tui/app/transcript-window.mjs +9 -10
  49. package/src/tui/app/use-transcript-window.mjs +38 -56
  50. package/src/tui/dist/index.mjs +354 -163
  51. package/src/tui/engine/agent-job-feed.mjs +76 -6
  52. package/src/tui/engine/session-api.mjs +7 -1
  53. package/src/tui/engine/session-flow.mjs +3 -1
  54. package/src/tui/engine/tool-card-results.mjs +10 -5
  55. package/src/tui/engine/turn.mjs +96 -36
  56. package/src/tui/engine.mjs +136 -37
  57. package/src/tui/index.jsx +2 -2
  58. package/src/workflows/bench/WORKFLOW.md +51 -27
  59. package/src/workflows/default/WORKFLOW.md +29 -24
@@ -18,7 +18,6 @@ import {
18
18
  hasStreamingRowStateToPrune,
19
19
  transcriptItemVariantKey,
20
20
  buildTranscriptRowIndexIncremental,
21
- transcriptStructureSignature,
22
21
  estimateTranscriptItemRowsCached,
23
22
  setStreamingBottomPinned,
24
23
  transcriptRenderWindow,
@@ -32,7 +31,9 @@ import {
32
31
  import { shouldSuppressFullyFailedToolItem } from '../transcript-tool-failures.mjs';
33
32
 
34
33
  export function useTranscriptWindow({
35
- items,
34
+ items: settledItems,
35
+ structureRevision,
36
+ streamingTail,
36
37
  themeEpoch,
37
38
  frameColumns,
38
39
  toolOutputExpanded,
@@ -129,42 +130,12 @@ export function useTranscriptWindow({
129
130
  return fn;
130
131
  }, []);
131
132
 
132
- // Key the heavy O(n) row-index + windowing memos on a STRUCTURE signature
133
- // instead of the `items` array identity. The engine swaps `items`
134
- // for a new array on every streaming flush (~8ms) while only the final
135
- // assistant item's text grows; depending on array identity re-ran both memos
136
- // each delta frame and visibly throttled the stream. The signature changes
137
- // only when transcript structure or the streaming item's estimated height
138
- // changes, so steady per-character growth keeps both memos warm.
139
- //
140
- // The signature itself is memoized on `items` identity (+columns/
141
- // expanded) so re-renders that DO NOT touch items — drag motion, scroll,
142
- // input typing — skip the O(n) signature walk entirely. During streaming the
143
- // engine hands us a fresh `items` array each flush, so this memo
144
- // recomputes and still tracks the streaming item's height correctly.
145
- // Split the structure signature into prefix (all items except the trailing
146
- // streaming item) + tail. On a streaming flush only the tail item object is
147
- // replaced; every prefix item keeps identity, so the prefix sig is O(1)-
148
- const streamingTailItem = useMemo(() => {
149
- const last = (items || []).length > 0 ? items[items.length - 1] : null;
150
- return last && last.kind === 'assistant' && last.streaming ? last : null;
151
- }, [items]);
152
- const prefixLen = streamingTailItem ? (items || []).length - 1 : (items || []).length;
153
- // Key on `items` identity (engine swaps the array every flush — INCLUDING a
154
- // tool-card patch that replaces a MIDDLE item object), so a same-length
155
- // middle-item replacement is caught (new object → fresh WeakMap fragment →
156
- // different prefixSig string). The walk itself is cheap: transcript-
157
- // StructureSignature reads a WeakMap sigPart per item (no re-estimation for
158
- // unchanged objects) and joins — O(n) map lookups, not O(n) measurement. The
159
- // row-index memo stays keyed on the prefixSig STRING, so a tail-only flush
160
- // (identical prefix objects → identical string) still skips the row rebuild.
161
- const prefixSig = useMemo(
162
- () => transcriptStructureSignature(
163
- streamingTailItem ? (items || []).slice(0, prefixLen) : (items || []),
164
- frameColumns, toolOutputExpanded,
165
- ),
166
- [items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded],
167
- );
133
+ // The settled array no longer changes during streaming. Geometry is keyed by
134
+ // the engine revision plus the live tail's resolved height, so same-height
135
+ // text flushes do not copy/walk the settled prefix or rerun heavy memos.
136
+ const streamingTailItem = streamingTail?.kind === 'assistant' && streamingTail.streaming
137
+ ? streamingTail
138
+ : null;
168
139
  const scrolledUpRowsForPin = Math.max(0, Number(scrollTargetRef.current) || 0);
169
140
  const transcriptPinnedForStreaming = followingRef.current
170
141
  || scrolledUpRowsForPin <= transcriptBottomSlackRows;
@@ -173,13 +144,18 @@ export function useTranscriptWindow({
173
144
  // applies while bottom-pinned (right geometry on first commit, no judder) and
174
145
  // every tail-resolving path reads the same height (no sig/geometry divergence).
175
146
  setStreamingBottomPinned(transcriptPinnedForStreaming);
176
- const tailSig = streamingTailItem
177
- ? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}`
178
- : '_';
179
- const transcriptStructureSig = `${prefixSig}#${tailSig}`;
180
- const transcriptStreamingActive = (items || []).some(
181
- (item) => item?.kind === 'assistant' && item?.streaming,
147
+ const tailRows = streamingTailItem
148
+ ? estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)
149
+ : 0;
150
+ const tailSig = streamingTailItem ? `${streamingTailItem.id}:${tailRows}` : '_';
151
+ const revision = Math.max(0, Number(structureRevision) || 0);
152
+ const transcriptItems = useMemo(
153
+ () => streamingTailItem ? [...(settledItems || []), streamingTailItem] : (settledItems || []),
154
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- tail text at the same resolved height is injected into the bounded visible slice below
155
+ [settledItems, revision, streamingTailItem?.id, tailRows],
182
156
  );
157
+ const transcriptStructureSig = `${revision}#${tailSig}`;
158
+ const transcriptStreamingActive = !!streamingTailItem;
183
159
  const scrolledUpForStreamingMeasure = scrolledUpRowsForPin > transcriptBottomSlackRows;
184
160
  const prevEstimateGeometry = transcriptGeomRef.current?.suppressMeasuredRowHeights === true;
185
161
  const hasStreamingReadingAnchor = !!transcriptAnchorRef.current
@@ -199,15 +175,15 @@ export function useTranscriptWindow({
199
175
  // rebuild for the settled prefix. All those invalidators are folded into the
200
176
  // memo deps below (sig captures item/column/expanded structure; the rest are
201
177
  // listed explicitly), so the memo only recomputes when one of them changes.
202
- const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(items, {
178
+ const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(transcriptItems, {
203
179
  columns: frameColumns,
204
180
  toolOutputExpanded,
205
181
  suppressMeasuredRowHeights,
206
182
  measuredRowsVersion,
207
183
  cacheRef: incrementalRowIndexCacheRef,
208
- prefixSig,
209
- // 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
210
- }), [prefixSig, tailSig, measuredRowsVersion, suppressMeasuredRowHeights]);
184
+ prefixRevision: revision,
185
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- revision/tail height capture structural geometry; measuredRowsVersion folds in measured corrections
186
+ }), [revision, tailSig, frameColumns, toolOutputExpanded, measuredRowsVersion, suppressMeasuredRowHeights]);
211
187
  // ── Same-frame anchor lock ───────────────────────────────────────────────
212
188
  // While the user reads older transcript (anchor captured, not dirty), resolve
213
189
  // the scroll offset that keeps the anchored viewport-top row fixed for THIS
@@ -243,7 +219,7 @@ export function useTranscriptWindow({
243
219
  if (anchorLockActive) {
244
220
  const locked = resolveAnchorScrollOffset({
245
221
  anchor: transcriptAnchorRef.current,
246
- items: items,
222
+ items: transcriptItems,
247
223
  curPrefix: curPrefixForLock,
248
224
  totalRows: lockTotalRows,
249
225
  viewRows: lockViewRows,
@@ -288,7 +264,7 @@ export function useTranscriptWindow({
288
264
  const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
289
265
  const locked = resolveAnchorScrollOffset({
290
266
  anchor: captured,
291
- items: items,
267
+ items: transcriptItems,
292
268
  curPrefix: curPrefixForLock,
293
269
  totalRows: lockTotalRows,
294
270
  viewRows: lockViewRows,
@@ -343,7 +319,7 @@ export function useTranscriptWindow({
343
319
  const captured = { id: anchorItem.id, offset: Math.max(0, prevTopRow - (prevPrefix[idx] || 0)) };
344
320
  const locked = resolveAnchorScrollOffset({
345
321
  anchor: captured,
346
- items: items,
322
+ items: transcriptItems,
347
323
  curPrefix: curPrefixForLock,
348
324
  totalRows: lockTotalRows,
349
325
  viewRows: lockViewRows,
@@ -376,7 +352,7 @@ export function useTranscriptWindow({
376
352
  // measuredRowsVersion bumps, the row index absorbs the growth (idEntry.rows ==
377
353
  // the new measured height) and the live estimate matches it, so delta → 0.
378
354
  if (scrolledUp && !followingRef.current) {
379
- const growth = streamingTailMountedGrowth(items, frameColumns, toolOutputExpanded);
355
+ const growth = streamingTailMountedGrowth(transcriptItems, frameColumns, toolOutputExpanded);
380
356
  // Only compensate when the tail is actually mounted in the rendered slice
381
357
  // (viewport + overscan). Off-slice it is represented by a row-index-sized
382
358
  // bottom spacer that does NOT physically grow this frame, so shifting the
@@ -394,7 +370,7 @@ export function useTranscriptWindow({
394
370
  }
395
371
  }
396
372
  }
397
- const transcriptWindow = useMemo(() => transcriptRenderWindow(items, {
373
+ const transcriptWindow = useMemo(() => transcriptRenderWindow(transcriptItems, {
398
374
  scrollOffset: renderScrollOffset,
399
375
  viewportHeight: transcriptContentHeight,
400
376
  columns: frameColumns,
@@ -451,7 +427,7 @@ export function useTranscriptWindow({
451
427
  prefixRows: transcriptRowIndex?.prefixRows || null,
452
428
  totalRows: Math.max(0, Number(transcriptWindow.totalRows) || 0),
453
429
  viewRows: Math.max(1, Number(transcriptContentHeight) || 1),
454
- items: items || null,
430
+ items: transcriptItems || null,
455
431
  // The offset THIS frame actually rendered with. The same-frame anchor
456
432
  // CAPTURE for a missing/dirty anchor (above) reads this from the PREVIOUS
457
433
  // frame to reconstruct the exact top-edge row that was on screen, so the
@@ -469,10 +445,16 @@ export function useTranscriptWindow({
469
445
  // height changes. Re-slice the live `items` over the memo's stable
470
446
  // [startIndex, endIndex) bounds so the on-screen text is always current
471
447
  // while the expensive indexing/windowing stays warm.
472
- const transcriptVisibleItems = (items || []).slice(
448
+ const transcriptVisibleItems = (transcriptItems || []).slice(
473
449
  transcriptWindow.startIndex,
474
450
  transcriptWindow.endIndex,
475
451
  );
452
+ if (streamingTailItem && transcriptVisibleItems.length > 0) {
453
+ const last = transcriptVisibleItems.length - 1;
454
+ if (transcriptVisibleItems[last]?.id === streamingTailItem.id) {
455
+ transcriptVisibleItems[last] = streamingTailItem;
456
+ }
457
+ }
476
458
  // The bottom meta band is spinner-only, so nothing is pulled out of the
477
459
  // transcript for it. A finished turn's done row (turndone/statusdone) renders
478
460
  // inline in scrollback like any other item — no filtering, no double-paint.
@@ -703,7 +685,7 @@ export function useTranscriptWindow({
703
685
  // move the item's prefix start and are absorbed the same way. No deltas, no
704
686
  // fallback, no drift.
705
687
  const viewRows = Math.max(1, Number(transcriptContentHeight) || 1);
706
- const itemList = items || [];
688
+ const itemList = transcriptItems || [];
707
689
  let anchor = transcriptAnchorRef.current;
708
690
  // (Re)capture the anchor from the current viewport-top edge when missing or
709
691
  // invalidated by a manual scroll. anchorRow = absolute row at the top edge.