mixdog 0.9.16 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/atomic-lock-tryonce-test.mjs +66 -0
- package/scripts/bench/cache-probe-tasks.json +1 -1
- package/scripts/bench/lead-review-tasks-r3.json +1 -1
- package/scripts/bench/r4-mixed-tasks.json +1 -1
- package/scripts/bench/review-tasks.json +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/provider-toolcall-test.mjs +79 -2
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/src/mixdog-session-runtime.mjs +12 -0
- package/src/rules/lead/02-channels.md +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +32 -18
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +108 -30
- package/src/runtime/agent/orchestrator/session/store.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +15 -15
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
- package/src/runtime/memory/index.mjs +37 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -0
- package/src/runtime/shared/atomic-file.mjs +110 -0
- package/src/runtime/shared/transcript-writer.mjs +46 -4
- package/src/session-runtime/provider-models.mjs +47 -8
- package/src/tui/app/transcript-window.mjs +115 -6
- package/src/tui/app/use-transcript-window.mjs +58 -7
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/dist/index.mjs +373 -81
- package/src/tui/engine/tui-steering-persist.mjs +66 -35
- package/src/tui/engine.mjs +6 -4
- package/src/tui/index.jsx +97 -6
- package/src/ui/statusline-segments.mjs +54 -36
- package/src/ui/statusline.mjs +141 -95
|
@@ -573,20 +573,31 @@ function streamingEstimateRows(item, columns, toolOutputExpanded) {
|
|
|
573
573
|
return Math.ceil(raw / STREAMING_ROW_QUANTUM) * STREAMING_ROW_QUANTUM;
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
-
function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
|
|
576
|
+
export function estimateTranscriptItemRowsCached(item, columns, toolOutputExpanded) {
|
|
577
577
|
if (!item) return Math.max(1, Math.ceil(estimateTranscriptItemRows(item, columns, toolOutputExpanded)));
|
|
578
578
|
if (shouldSuppressFullyFailedToolItem(item)) return 0;
|
|
579
579
|
if (item.kind === 'assistant' && item.streaming) {
|
|
580
|
-
const estimate = streamingEstimateRows(item, columns, toolOutputExpanded);
|
|
581
580
|
const toolExpanded = toolOutputExpanded ? 1 : 0;
|
|
582
581
|
const idEntry = streamingMeasuredRowsById.get(item.id);
|
|
583
582
|
if (idEntry && idEntry.rows > 0 && idEntry.columns === columns && idEntry.toolExpanded === toolExpanded) {
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
583
|
+
// Defer-growth: while a confirmed (post-commit Yoga) measurement exists
|
|
584
|
+
// for this streaming id, THIS render keeps that value instead of
|
|
585
|
+
// folding in the freshly-grown estimate. Combining
|
|
586
|
+
// max(measuredFloor, liveEstimate) let per-flush row growth reach
|
|
587
|
+
// totalRows/scrollOffset a full frame BEFORE Yoga confirmed the real
|
|
588
|
+
// wrap — the estimate could over/undercount vs the real layout, and
|
|
589
|
+
// the next commit's harvest then corrected it, bouncing an anchored or
|
|
590
|
+
// bottom-pinned offset by the mismatch. Freezing at the last confirmed
|
|
591
|
+
// height means growth only reaches the row index on the SAME frame the
|
|
592
|
+
// harvest (use-transcript-window.mjs) writes the new measured value and
|
|
593
|
+
// bumps measuredRowsVersion — the "measured frame" consumes it, not the
|
|
594
|
+
// estimate frame.
|
|
595
|
+
return idEntry.rows;
|
|
587
596
|
}
|
|
588
597
|
if (idEntry) streamingMeasuredRowsById.delete(item.id);
|
|
589
|
-
|
|
598
|
+
// No confirmed measurement yet for this id (item just started streaming):
|
|
599
|
+
// nothing to defer against, so the first frame falls back to the estimate.
|
|
600
|
+
return streamingEstimateRows(item, columns, toolOutputExpanded);
|
|
590
601
|
}
|
|
591
602
|
if (item.kind === 'assistant' && streamingMeasuredRowsById.has(item.id)) {
|
|
592
603
|
// Item settled (no longer streaming): the id-keyed floor is no longer
|
|
@@ -632,6 +643,104 @@ export function buildTranscriptRowIndex(items, {
|
|
|
632
643
|
return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
|
|
633
644
|
}
|
|
634
645
|
|
|
646
|
+
// ── Incremental streaming-tail row-index cache ─────────────────────────────
|
|
647
|
+
// On a streaming flush the engine swaps `items` for a fresh array in which
|
|
648
|
+
// ONLY the trailing assistant item's text has grown; every settled item ahead
|
|
649
|
+
// of it is byte-identical geometry. buildTranscriptRowIndex re-walks all N
|
|
650
|
+
// items each flush (O(n) per ~16ms frame) even though only the last row's
|
|
651
|
+
// height can change. This cache holds the SETTLED-PREFIX row-index (all items
|
|
652
|
+
// except the trailing streaming assistant item) keyed on a stable signature of
|
|
653
|
+
// that prefix + columns + expanded + suppress + measuredRowsVersion. When the
|
|
654
|
+
// only difference since last flush is the trailing streaming item, we recompute
|
|
655
|
+
// just that one tail row and append it to the cached prefixRows. Any structural
|
|
656
|
+
// change (item count, non-tail change, columns, expanded, suppress, version)
|
|
657
|
+
// invalidates the cache and falls back to a full buildTranscriptRowIndex.
|
|
658
|
+
// The cache is per-hook-instance now (passed in as `cacheRef`); there is no
|
|
659
|
+
// module-level mutable state to leak across hook instances.
|
|
660
|
+
|
|
661
|
+
/** True when `items` ends with a streaming assistant item (the growing tail). */
|
|
662
|
+
function trailingStreamingItem(allItems) {
|
|
663
|
+
const last = allItems.length > 0 ? allItems[allItems.length - 1] : null;
|
|
664
|
+
return last && last.kind === 'assistant' && last.streaming ? last : null;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function buildTranscriptRowIndexIncremental(items, {
|
|
668
|
+
columns = 80,
|
|
669
|
+
toolOutputExpanded = false,
|
|
670
|
+
suppressMeasuredRowHeights = false,
|
|
671
|
+
measuredRowsVersion = 0,
|
|
672
|
+
cacheRef = null,
|
|
673
|
+
prefixSig = null,
|
|
674
|
+
} = {}) {
|
|
675
|
+
const allItems = Array.isArray(items) ? items : [];
|
|
676
|
+
const tail = trailingStreamingItem(allItems);
|
|
677
|
+
// Per-hook-instance cache holder (useRef object). Fall back to a throwaway
|
|
678
|
+
// holder if none supplied so the builder still works in isolation.
|
|
679
|
+
const holder = cacheRef || { current: null };
|
|
680
|
+
// No streaming tail → nothing incremental to exploit; drop any stale cache and
|
|
681
|
+
// do the normal full build. (The memo layer already skips recompute when the
|
|
682
|
+
// structure signature is unchanged, so a settled transcript pays this once.)
|
|
683
|
+
if (!tail) {
|
|
684
|
+
holder.current = null;
|
|
685
|
+
return buildTranscriptRowIndex(allItems, {
|
|
686
|
+
columns, toolOutputExpanded, suppressMeasuredRowHeights,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const prefixLen = allItems.length - 1;
|
|
690
|
+
const cache = holder.current;
|
|
691
|
+
// Prefix identity is carried by `prefixSig` (a hash of every prefix item's
|
|
692
|
+
// WeakMap sigPart, computed once in the hook). String equality catches a
|
|
693
|
+
// same-length MIDDLE-item replacement (tool-card patch swaps a middle object →
|
|
694
|
+
// fresh fragment → different sig), which a last-item ref check would miss.
|
|
695
|
+
// Fast path: same prefix length + tail id + prefixSig, and columns/expanded/
|
|
696
|
+
// suppress/version all match. Only the tail row can differ → recompute + append.
|
|
697
|
+
if (cache
|
|
698
|
+
&& cache.columns === columns
|
|
699
|
+
&& cache.toolExpanded === (toolOutputExpanded ? 1 : 0)
|
|
700
|
+
&& cache.suppress === suppressMeasuredRowHeights
|
|
701
|
+
&& cache.version === measuredRowsVersion
|
|
702
|
+
&& cache.prefixLen === prefixLen
|
|
703
|
+
&& prefixSig != null
|
|
704
|
+
&& cache.prefixSig === prefixSig
|
|
705
|
+
&& cache.tailId === tail.id) {
|
|
706
|
+
// prefixSig matches → prefix rows provably unchanged; NO prefix walk / no
|
|
707
|
+
// re-estimation. Only the tail row can differ, recompute it.
|
|
708
|
+
{
|
|
709
|
+
const tailMeasured = suppressMeasuredRowHeights
|
|
710
|
+
? null
|
|
711
|
+
: measuredTranscriptRows(tail, columns, toolOutputExpanded);
|
|
712
|
+
const tailRows = tailMeasured != null
|
|
713
|
+
? tailMeasured
|
|
714
|
+
: estimateTranscriptItemRowsCached(tail, columns, toolOutputExpanded);
|
|
715
|
+
const rows = cache.prefixRowsArr.slice();
|
|
716
|
+
rows.push(tailRows);
|
|
717
|
+
const prefixRows = cache.prefixPrefixRows.slice();
|
|
718
|
+
prefixRows.push(cache.prefixTotal + tailRows);
|
|
719
|
+
return { rows, prefixRows, totalRows: prefixRows[allItems.length] || 0 };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Cache miss: full build, then repopulate the settled-prefix cache so the NEXT
|
|
723
|
+
// flush (only the tail grown) takes the fast path. The prefix arrays are the
|
|
724
|
+
// full-build outputs truncated before the tail row — byte-identical to a
|
|
725
|
+
// full rebuild's prefix by construction.
|
|
726
|
+
const full = buildTranscriptRowIndex(allItems, {
|
|
727
|
+
columns, toolOutputExpanded, suppressMeasuredRowHeights,
|
|
728
|
+
});
|
|
729
|
+
holder.current = {
|
|
730
|
+
columns,
|
|
731
|
+
toolExpanded: toolOutputExpanded ? 1 : 0,
|
|
732
|
+
suppress: suppressMeasuredRowHeights,
|
|
733
|
+
version: measuredRowsVersion,
|
|
734
|
+
prefixLen,
|
|
735
|
+
prefixSig,
|
|
736
|
+
tailId: tail.id,
|
|
737
|
+
prefixRowsArr: full.rows.slice(0, prefixLen),
|
|
738
|
+
prefixPrefixRows: full.prefixRows.slice(0, prefixLen + 1),
|
|
739
|
+
prefixTotal: full.prefixRows[prefixLen] || 0,
|
|
740
|
+
};
|
|
741
|
+
return full;
|
|
742
|
+
}
|
|
743
|
+
|
|
635
744
|
// Stable signature for the transcript row-index / window memos. Changes only
|
|
636
745
|
// when transcript STRUCTURE changes or the streaming item's estimated height
|
|
637
746
|
// changes — not on every character. Per-item sigParts are identity-memoized.
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
streamingMeasuredRowsById,
|
|
17
17
|
pruneStreamingMeasuredRowsById,
|
|
18
18
|
transcriptItemVariantKey,
|
|
19
|
-
|
|
19
|
+
buildTranscriptRowIndexIncremental,
|
|
20
20
|
transcriptStructureSignature,
|
|
21
|
+
estimateTranscriptItemRowsCached,
|
|
21
22
|
transcriptRenderWindow,
|
|
22
23
|
resolveAnchorScrollOffset,
|
|
23
24
|
upperBound,
|
|
@@ -55,6 +56,10 @@ export function useTranscriptWindow({
|
|
|
55
56
|
}) {
|
|
56
57
|
const transcriptTotalRowsRef = useRef(0);
|
|
57
58
|
const preservedScrollDeltaRef = useRef(0);
|
|
59
|
+
// Per-hook-instance settled-prefix row-index cache for the incremental
|
|
60
|
+
// builder. Was module-level (leaked across hook instances); now local so
|
|
61
|
+
// each transcript window owns its own tail-flush cache.
|
|
62
|
+
const incrementalRowIndexCacheRef = useRef(null);
|
|
58
63
|
// Previous frame's viewport-only geometry (content height + floating-panel
|
|
59
64
|
// reservation). A floating panel / view (picker/context/usage/text-entry)
|
|
60
65
|
// open-close changes transcriptContentHeight WITHOUT changing `items`, so the
|
|
@@ -120,10 +125,33 @@ export function useTranscriptWindow({
|
|
|
120
125
|
// input typing — skip the O(n) signature walk entirely. During streaming the
|
|
121
126
|
// engine hands us a fresh `items` array each flush, so this memo
|
|
122
127
|
// recomputes and still tracks the streaming item's height correctly.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
// Split the structure signature into prefix (all items except the trailing
|
|
129
|
+
// streaming item) + tail. On a streaming flush only the tail item object is
|
|
130
|
+
// replaced; every prefix item keeps identity, so the prefix sig is O(1)-
|
|
131
|
+
const streamingTailItem = useMemo(() => {
|
|
132
|
+
const last = (items || []).length > 0 ? items[items.length - 1] : null;
|
|
133
|
+
return last && last.kind === 'assistant' && last.streaming ? last : null;
|
|
134
|
+
}, [items]);
|
|
135
|
+
const prefixLen = streamingTailItem ? (items || []).length - 1 : (items || []).length;
|
|
136
|
+
// Key on `items` identity (engine swaps the array every flush — INCLUDING a
|
|
137
|
+
// tool-card patch that replaces a MIDDLE item object), so a same-length
|
|
138
|
+
// middle-item replacement is caught (new object → fresh WeakMap fragment →
|
|
139
|
+
// different prefixSig string). The walk itself is cheap: transcript-
|
|
140
|
+
// StructureSignature reads a WeakMap sigPart per item (no re-estimation for
|
|
141
|
+
// unchanged objects) and joins — O(n) map lookups, not O(n) measurement. The
|
|
142
|
+
// row-index memo stays keyed on the prefixSig STRING, so a tail-only flush
|
|
143
|
+
// (identical prefix objects → identical string) still skips the row rebuild.
|
|
144
|
+
const prefixSig = useMemo(
|
|
145
|
+
() => transcriptStructureSignature(
|
|
146
|
+
streamingTailItem ? (items || []).slice(0, prefixLen) : (items || []),
|
|
147
|
+
frameColumns, toolOutputExpanded,
|
|
148
|
+
),
|
|
149
|
+
[items, streamingTailItem, prefixLen, frameColumns, toolOutputExpanded],
|
|
126
150
|
);
|
|
151
|
+
const tailSig = streamingTailItem
|
|
152
|
+
? `a${streamingTailItem.id}:${estimateTranscriptItemRowsCached(streamingTailItem, frameColumns, toolOutputExpanded)}`
|
|
153
|
+
: '_';
|
|
154
|
+
const transcriptStructureSig = `${prefixSig}#${tailSig}`;
|
|
127
155
|
const transcriptStreamingActive = (items || []).some(
|
|
128
156
|
(item) => item?.kind === 'assistant' && item?.streaming,
|
|
129
157
|
);
|
|
@@ -140,12 +168,24 @@ export function useTranscriptWindow({
|
|
|
140
168
|
(scrolledUpForStreamingMeasure && hasStreamingReadingAnchor)
|
|
141
169
|
|| (scrolledUpForStreamingMeasure && prevEstimateGeometry && !transcriptPinnedForStreaming)
|
|
142
170
|
));
|
|
143
|
-
|
|
171
|
+
// Incremental builder: on a streaming flush where only the trailing assistant
|
|
172
|
+
// item's text grew, it recomputes just the tail row and appends to a cached
|
|
173
|
+
// settled-prefix row-index (O(1) prefix) instead of re-walking all N items.
|
|
174
|
+
// Any structural change (item count, non-tail change, columns, expanded,
|
|
175
|
+
// suppress, measuredRowsVersion) misses the cache and falls back to a full
|
|
176
|
+
// buildTranscriptRowIndex — so the prefix table is byte-identical to a full
|
|
177
|
+
// rebuild for the settled prefix. All those invalidators are folded into the
|
|
178
|
+
// memo deps below (sig captures item/column/expanded structure; the rest are
|
|
179
|
+
// listed explicitly), so the memo only recomputes when one of them changes.
|
|
180
|
+
const transcriptRowIndex = useMemo(() => buildTranscriptRowIndexIncremental(items, {
|
|
144
181
|
columns: frameColumns,
|
|
145
182
|
toolOutputExpanded,
|
|
146
183
|
suppressMeasuredRowHeights,
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
measuredRowsVersion,
|
|
185
|
+
cacheRef: incrementalRowIndexCacheRef,
|
|
186
|
+
prefixSig,
|
|
187
|
+
// 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
|
|
188
|
+
}), [prefixSig, tailSig, measuredRowsVersion, suppressMeasuredRowHeights]);
|
|
149
189
|
// ── Same-frame anchor lock ───────────────────────────────────────────────
|
|
150
190
|
// While the user reads older transcript (anchor captured, not dirty), resolve
|
|
151
191
|
// the scroll offset that keeps the anchored viewport-top row fixed for THIS
|
|
@@ -412,6 +452,17 @@ export function useTranscriptWindow({
|
|
|
412
452
|
const idPrev = streamingMeasuredRowsById.get(item.id);
|
|
413
453
|
if (!idPrev || idPrev.rows !== measured || idPrev.columns !== frameColumns || idPrev.toolExpanded !== toolExpandedFlag) {
|
|
414
454
|
streamingMeasuredRowsById.set(item.id, { rows: measured, columns: frameColumns, toolExpanded: toolExpandedFlag });
|
|
455
|
+
// ALWAYS bump on a real id-store change, bottom-pinned or not.
|
|
456
|
+
// estimateTranscriptItemRowsCached (transcript-window.mjs) now
|
|
457
|
+
// returns ONLY this id-store floor for a streaming item (defer-
|
|
458
|
+
// growth: no live-estimate blend), so it is the SOLE path that ever
|
|
459
|
+
// feeds this item's grown height into transcriptStructureSig /
|
|
460
|
+
// buildTranscriptRowIndex. Skipping the bump while bottom-pinned
|
|
461
|
+
// used to be safe because the blended estimate kept the signature
|
|
462
|
+
// moving on its own each flush; with the blend gone, skipping it
|
|
463
|
+
// here would freeze totalRows/prefixRows for this item until the
|
|
464
|
+
// stream settles — invisible growth for anything reading rowIndex
|
|
465
|
+
// (windowing, maxScrollRows, an anchor captured mid-stream).
|
|
415
466
|
changed = true;
|
|
416
467
|
}
|
|
417
468
|
}
|
|
@@ -84,7 +84,7 @@ function hasActiveStatuslineWork(line, agentWorkers = [], agentJobs = [], active
|
|
|
84
84
|
return hasRunningStatuslineWorkers(agentWorkers, agentJobs)
|
|
85
85
|
|| hasActiveStatuslineTools(activeTools)
|
|
86
86
|
|| /\bRunning \d+ (?:Agents?|Shells?)\b/.test(stripAnsi(line))
|
|
87
|
-
|| /\b(?:Exploring|Searching)\b/.test(stripAnsi(line));
|
|
87
|
+
|| /\b(?:Exploring|Searching|Memory)\b/.test(stripAnsi(line));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function bootFullRenderEligible(mountAtMs, line, agentWorkers = [], agentJobs = [], activeTools = null) {
|