mixdog 0.9.41 → 0.9.44
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 +3 -2
- package/scripts/agent-tag-reuse-smoke.mjs +124 -10
- package/scripts/ansi-color-capability-test.mjs +90 -0
- package/scripts/anthropic-oauth-refresh-race-test.mjs +59 -0
- package/scripts/arg-guard-test.mjs +16 -0
- package/scripts/compact-pressure-test.mjs +256 -1
- package/scripts/generate-runtime-manifest.mjs +39 -22
- package/scripts/grok-oauth-refresh-race-test.mjs +198 -0
- package/scripts/internal-comms-bench.mjs +0 -1
- package/scripts/internal-comms-smoke.mjs +38 -39
- package/scripts/internal-tools-normalization-test.mjs +52 -0
- package/scripts/maintenance-default-routes-test.mjs +164 -0
- package/scripts/max-output-recovery-test.mjs +49 -0
- package/scripts/mcp-client-normalization-test.mjs +45 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +123 -0
- package/scripts/provider-toolcall-test.mjs +23 -0
- package/scripts/result-classification-test.mjs +75 -0
- package/scripts/routing-corpus.mjs +1 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +164 -0
- package/scripts/smoke.mjs +1 -1
- package/scripts/submit-commandbusy-race-test.mjs +99 -7
- package/scripts/tui-transcript-jitter-harness-entry.jsx +302 -0
- package/scripts/tui-transcript-jitter-harness.mjs +58 -0
- package/scripts/tui-transcript-perf-test.mjs +56 -1
- package/src/agents/reviewer/AGENT.md +5 -3
- package/src/rules/lead/lead-brief.md +5 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +40 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -14
- package/src/runtime/agent/orchestrator/internal-tools.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +17 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +49 -6
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +46 -21
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +59 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +140 -81
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +23 -5
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +8 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -5
- package/src/runtime/memory/data/runtime-manifest.json +11 -11
- package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -2
- package/src/runtime/memory/tool-defs.mjs +4 -3
- package/src/runtime/shared/tool-surface.mjs +1 -2
- package/src/session-runtime/context-status.mjs +8 -2
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +267 -72
- package/src/standalone/explore-tool.mjs +17 -16
- package/src/tui/app/provider-setup-picker.mjs +1 -0
- package/src/tui/app/use-transcript-window.mjs +5 -1
- package/src/tui/components/StatusLine.jsx +11 -32
- package/src/tui/dist/index.mjs +257 -106
- package/src/tui/engine/session-api.mjs +3 -0
- package/src/tui/engine/session-flow.mjs +19 -8
- package/src/tui/engine/turn.mjs +24 -2
- package/src/tui/engine.mjs +0 -1
- package/src/tui/index.jsx +3 -2
- package/src/tui/markdown/streaming-markdown.mjs +35 -9
- package/src/tui/statusline-ansi-bridge.mjs +17 -7
- package/src/ui/ansi.mjs +85 -5
- package/src/ui/statusline-agents.mjs +0 -8
- package/src/ui/statusline-format.mjs +7 -7
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +29 -20
- package/src/workflows/solo-review/WORKFLOW.md +47 -0
- package/src/workflows/bench/WORKFLOW.md +0 -60
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { PassThrough } from 'node:stream';
|
|
3
|
+
import { Box, measureElement, render } from 'ink';
|
|
4
|
+
import { Item } from '../src/tui/components/TranscriptItem.jsx';
|
|
5
|
+
import { useTranscriptWindow } from '../src/tui/app/use-transcript-window.mjs';
|
|
6
|
+
import {
|
|
7
|
+
streamingMeasuredRowsById,
|
|
8
|
+
} from '../src/tui/app/transcript-window.mjs';
|
|
9
|
+
import {
|
|
10
|
+
resetAllStreamingMarkdownStablePrefixes,
|
|
11
|
+
resetStreamingMarkdownStablePrefix,
|
|
12
|
+
resolveStreamingMarkdownParts,
|
|
13
|
+
} from '../src/tui/markdown/streaming-markdown.mjs';
|
|
14
|
+
|
|
15
|
+
const COLUMNS = 42;
|
|
16
|
+
const VIEW_ROWS = 8;
|
|
17
|
+
const INITIAL_SCROLL = 8;
|
|
18
|
+
const STREAM_ID = 'jitter-fence-tail';
|
|
19
|
+
const HISTORY = Array.from({ length: 8 }, (_, index) => ({
|
|
20
|
+
id: `history-${index}`,
|
|
21
|
+
kind: 'notice',
|
|
22
|
+
tone: 'plain',
|
|
23
|
+
text: `H${index} stable history row`,
|
|
24
|
+
}));
|
|
25
|
+
const SCRIPT = [
|
|
26
|
+
'Here is the script:',
|
|
27
|
+
'',
|
|
28
|
+
'```js',
|
|
29
|
+
'const first = `',
|
|
30
|
+
'alpha',
|
|
31
|
+
'`;',
|
|
32
|
+
'const second = `',
|
|
33
|
+
'beta',
|
|
34
|
+
'`;',
|
|
35
|
+
'console.log(first, second);',
|
|
36
|
+
'```',
|
|
37
|
+
'',
|
|
38
|
+
'Done.',
|
|
39
|
+
].join('\n');
|
|
40
|
+
|
|
41
|
+
const frames = [];
|
|
42
|
+
globalThis.__mixdogTailGrowthProbe = null;
|
|
43
|
+
let commit = 0;
|
|
44
|
+
const identity = (value) => value;
|
|
45
|
+
const noop = () => {};
|
|
46
|
+
|
|
47
|
+
function Harness({ text, step }) {
|
|
48
|
+
const [scrollOffset, setScrollOffset] = React.useState(INITIAL_SCROLL);
|
|
49
|
+
const [measuredRowsVersion, setMeasuredRowsVersion] = React.useState(0);
|
|
50
|
+
const transcriptAnchorRef = React.useRef(null);
|
|
51
|
+
const transcriptAnchorDirtyRef = React.useRef(true);
|
|
52
|
+
const scrollTargetRef = React.useRef(INITIAL_SCROLL);
|
|
53
|
+
const scrollPositionRef = React.useRef(INITIAL_SCROLL);
|
|
54
|
+
const maxScrollRowsRef = React.useRef(0);
|
|
55
|
+
const transcriptGeomRef = React.useRef({});
|
|
56
|
+
const followingRef = React.useRef(false);
|
|
57
|
+
const dragRef = React.useRef({ active: false, rect: null });
|
|
58
|
+
const transcriptViewportRef = React.useRef({ top: 0 });
|
|
59
|
+
const selectionLayoutRef = React.useRef(null);
|
|
60
|
+
const contentRef = React.useRef(null);
|
|
61
|
+
const tailRef = React.useRef(null);
|
|
62
|
+
const streamingTail = React.useMemo(() => ({
|
|
63
|
+
id: STREAM_ID,
|
|
64
|
+
kind: 'assistant',
|
|
65
|
+
text,
|
|
66
|
+
streaming: true,
|
|
67
|
+
}), [text]);
|
|
68
|
+
const transcriptItems = React.useMemo(() => [...HISTORY, streamingTail], [streamingTail]);
|
|
69
|
+
|
|
70
|
+
const preHookEntry = streamingMeasuredRowsById.get(STREAM_ID);
|
|
71
|
+
const preHookMeasuredRows = preHookEntry?.rows ?? null;
|
|
72
|
+
const preHookEstimateRows = preHookEntry?.estimateRows ?? null;
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
transcriptWindow,
|
|
76
|
+
renderedTranscriptItems,
|
|
77
|
+
transcriptMeasureRef,
|
|
78
|
+
} = useTranscriptWindow({
|
|
79
|
+
items: HISTORY,
|
|
80
|
+
structureRevision: 1,
|
|
81
|
+
streamingTail,
|
|
82
|
+
themeEpoch: 0,
|
|
83
|
+
frameColumns: COLUMNS,
|
|
84
|
+
toolOutputExpanded: false,
|
|
85
|
+
transcriptContentHeight: VIEW_ROWS,
|
|
86
|
+
transcriptBottomSlackRows: 1,
|
|
87
|
+
transcriptGuardRows: 1,
|
|
88
|
+
floatingPanelRows: 0,
|
|
89
|
+
overlayHintRequested: false,
|
|
90
|
+
scrollOffset,
|
|
91
|
+
setScrollOffset,
|
|
92
|
+
transcriptAnchorRef,
|
|
93
|
+
transcriptAnchorDirtyRef,
|
|
94
|
+
scrollTargetRef,
|
|
95
|
+
scrollPositionRef,
|
|
96
|
+
maxScrollRowsRef,
|
|
97
|
+
transcriptGeomRef,
|
|
98
|
+
followingRef,
|
|
99
|
+
dragRef,
|
|
100
|
+
transcriptViewportRef,
|
|
101
|
+
selectionLayoutRef,
|
|
102
|
+
withSelectionClip: identity,
|
|
103
|
+
paintSelectionRect: noop,
|
|
104
|
+
stopSmoothScroll: noop,
|
|
105
|
+
measuredRowsVersion,
|
|
106
|
+
setMeasuredRowsVersion,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const growthProbe = globalThis.__mixdogTailGrowthProbe;
|
|
110
|
+
const tailHookRef = transcriptMeasureRef(streamingTail);
|
|
111
|
+
const combinedTailRef = React.useCallback((element) => {
|
|
112
|
+
tailHookRef?.(element);
|
|
113
|
+
tailRef.current = element;
|
|
114
|
+
}, [tailHookRef]);
|
|
115
|
+
|
|
116
|
+
React.useLayoutEffect(() => {
|
|
117
|
+
const geometry = transcriptGeomRef.current || {};
|
|
118
|
+
const prefix = geometry.prefixRows || [];
|
|
119
|
+
const physicalRows = measureElement(contentRef.current).height;
|
|
120
|
+
const tailYogaRows = measureElement(tailRef.current).height;
|
|
121
|
+
const renderScrollOffset = transcriptWindow.effectiveScrollOffset;
|
|
122
|
+
const visibleTopIndexed = transcriptWindow.totalRows - renderScrollOffset - VIEW_ROWS;
|
|
123
|
+
const visibleTopPhysical = physicalRows - renderScrollOffset - VIEW_ROWS;
|
|
124
|
+
frames.push({
|
|
125
|
+
commit: ++commit,
|
|
126
|
+
step,
|
|
127
|
+
char: text.at(-1) === '\n' ? '\\n' : (text.at(-1) || ''),
|
|
128
|
+
totalRows: transcriptWindow.totalRows,
|
|
129
|
+
renderScrollOffset,
|
|
130
|
+
visibleTopIndexed,
|
|
131
|
+
visibleTopPhysical,
|
|
132
|
+
physicalRows,
|
|
133
|
+
tailIndexedRows: prefix.length > 1 ? prefix.at(-1) - prefix.at(-2) : -1,
|
|
134
|
+
tailYogaRows,
|
|
135
|
+
mountedDelta: tailYogaRows - (prefix.length > 1 ? prefix.at(-1) - prefix.at(-2) : -1),
|
|
136
|
+
growthLive: growthProbe?.live ?? null,
|
|
137
|
+
growthBaseline: growthProbe?.baseline ?? null,
|
|
138
|
+
growthDelta: growthProbe?.delta ?? null,
|
|
139
|
+
suppressMeasured: geometry.suppressMeasuredRowHeights === true,
|
|
140
|
+
measuredRowsVersion,
|
|
141
|
+
preHookMeasuredRows,
|
|
142
|
+
preHookEstimateRows,
|
|
143
|
+
postHarvestMeasuredRows: streamingMeasuredRowsById.get(STREAM_ID)?.rows ?? null,
|
|
144
|
+
postHarvestEstimateRows: streamingMeasuredRowsById.get(STREAM_ID)?.estimateRows ?? null,
|
|
145
|
+
scrollTarget: scrollTargetRef.current,
|
|
146
|
+
following: followingRef.current,
|
|
147
|
+
anchor: transcriptAnchorRef.current?.id || '-',
|
|
148
|
+
});
|
|
149
|
+
}, [step, text, measuredRowsVersion, transcriptWindow.totalRows, transcriptWindow.effectiveScrollOffset,
|
|
150
|
+
transcriptAnchorRef, transcriptGeomRef]);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Box flexDirection="column" width={COLUMNS} height={VIEW_ROWS} overflow="hidden" justifyContent="flex-end">
|
|
154
|
+
<Box
|
|
155
|
+
ref={contentRef}
|
|
156
|
+
flexDirection="column"
|
|
157
|
+
width="100%"
|
|
158
|
+
flexShrink={0}
|
|
159
|
+
marginBottom={-transcriptWindow.effectiveScrollOffset}
|
|
160
|
+
>
|
|
161
|
+
{renderedTranscriptItems.map((item, index, all) => {
|
|
162
|
+
const hookRef = item.id === STREAM_ID ? combinedTailRef : transcriptMeasureRef(item);
|
|
163
|
+
return (
|
|
164
|
+
<Box key={item.id} ref={hookRef} flexDirection="column" flexShrink={0}>
|
|
165
|
+
<Item
|
|
166
|
+
item={item}
|
|
167
|
+
prevKind={index > 0 ? all[index - 1].kind : null}
|
|
168
|
+
columns={COLUMNS}
|
|
169
|
+
toolOutputExpanded={false}
|
|
170
|
+
/>
|
|
171
|
+
</Box>
|
|
172
|
+
);
|
|
173
|
+
})}
|
|
174
|
+
{transcriptWindow.bottomSpacerRows > 0
|
|
175
|
+
? <Box height={transcriptWindow.bottomSpacerRows} flexShrink={0} />
|
|
176
|
+
: null}
|
|
177
|
+
</Box>
|
|
178
|
+
</Box>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fakeTty(columns, rows) {
|
|
183
|
+
const stream = new PassThrough();
|
|
184
|
+
stream.columns = columns;
|
|
185
|
+
stream.rows = rows;
|
|
186
|
+
stream.isTTY = true;
|
|
187
|
+
stream.getColorDepth = () => 1;
|
|
188
|
+
stream.hasColors = () => false;
|
|
189
|
+
stream.setRawMode = () => stream;
|
|
190
|
+
stream.on('data', () => {});
|
|
191
|
+
return stream;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function settle(instance) {
|
|
195
|
+
await instance.waitUntilRenderFlush();
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
197
|
+
await instance.waitUntilRenderFlush();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertStreamingMarkdownPartsCache() {
|
|
201
|
+
const key = 'streaming-parts-cache-coverage';
|
|
202
|
+
const longText = 'Settled paragraph.\n\n```js\nconst value = 1;';
|
|
203
|
+
const initial = resolveStreamingMarkdownParts(longText, key);
|
|
204
|
+
const repeated = resolveStreamingMarkdownParts(`${longText}\n\n`, key);
|
|
205
|
+
if (repeated !== initial) {
|
|
206
|
+
throw new Error('normalized-equivalent stream text did not reuse its resolved parts');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const regressed = resolveStreamingMarkdownParts('plain text', key);
|
|
210
|
+
if (regressed === initial || regressed.stablePrefix || regressed.unstableSuffix !== 'plain text') {
|
|
211
|
+
throw new Error('text regression served a stale streaming-markdown split');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const recomputed = resolveStreamingMarkdownParts(longText, key);
|
|
215
|
+
if (recomputed === initial) {
|
|
216
|
+
throw new Error('text change did not evict the prior streaming-markdown snapshot');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const resetSeed = recomputed;
|
|
220
|
+
resetStreamingMarkdownStablePrefix(key);
|
|
221
|
+
if (resolveStreamingMarkdownParts(longText, key) === resetSeed) {
|
|
222
|
+
throw new Error('streaming-markdown reset did not clear its resolved-parts snapshot');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
assertStreamingMarkdownPartsCache();
|
|
227
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
228
|
+
streamingMeasuredRowsById.delete(STREAM_ID);
|
|
229
|
+
const stdout = fakeTty(COLUMNS, VIEW_ROWS);
|
|
230
|
+
const stderr = fakeTty(COLUMNS, VIEW_ROWS);
|
|
231
|
+
const stdin = fakeTty(COLUMNS, VIEW_ROWS);
|
|
232
|
+
const instance = render(<Harness text={SCRIPT.slice(0, 1)} step={1} />, {
|
|
233
|
+
stdout,
|
|
234
|
+
stderr,
|
|
235
|
+
stdin,
|
|
236
|
+
interactive: true,
|
|
237
|
+
patchConsole: false,
|
|
238
|
+
exitOnCtrlC: false,
|
|
239
|
+
maxFps: 1000,
|
|
240
|
+
});
|
|
241
|
+
await settle(instance);
|
|
242
|
+
for (let step = 2; step <= SCRIPT.length; step++) {
|
|
243
|
+
instance.rerender(<Harness text={SCRIPT.slice(0, step)} step={step} />);
|
|
244
|
+
await settle(instance);
|
|
245
|
+
}
|
|
246
|
+
instance.unmount();
|
|
247
|
+
await instance.waitUntilExit();
|
|
248
|
+
instance.cleanup();
|
|
249
|
+
|
|
250
|
+
const byStep = new Map();
|
|
251
|
+
for (const frame of frames) {
|
|
252
|
+
const list = byStep.get(frame.step) || [];
|
|
253
|
+
list.push(frame);
|
|
254
|
+
byStep.set(frame.step, list);
|
|
255
|
+
}
|
|
256
|
+
const dips = [];
|
|
257
|
+
let previousSettled = null;
|
|
258
|
+
for (const [step, list] of [...byStep.entries()].sort((a, b) => a[0] - b[0])) {
|
|
259
|
+
const first = list[0];
|
|
260
|
+
const settled = list.at(-1);
|
|
261
|
+
if (previousSettled
|
|
262
|
+
&& first.visibleTopPhysical !== previousSettled.visibleTopPhysical
|
|
263
|
+
&& settled.visibleTopPhysical === previousSettled.visibleTopPhysical) {
|
|
264
|
+
dips.push({ previous: previousSettled, transient: first, corrected: settled });
|
|
265
|
+
}
|
|
266
|
+
previousSettled = settled;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const print = (label, frame) => {
|
|
270
|
+
console.log(`${label} c${frame.commit} step=${frame.step} char=${JSON.stringify(frame.char)}`
|
|
271
|
+
+ ` totalRows=${frame.totalRows} renderScrollOffset=${frame.renderScrollOffset}`
|
|
272
|
+
+ ` visibleTop=${frame.visibleTopPhysical} indexedTop=${frame.visibleTopIndexed}`
|
|
273
|
+
+ ` physicalRows=${frame.physicalRows} tail(index/yoga)=${frame.tailIndexedRows}/${frame.tailYogaRows}`
|
|
274
|
+
+ ` mountedDelta=${frame.mountedDelta} helper(live/base/delta)=${frame.growthLive}/${frame.growthBaseline}/${frame.growthDelta}`
|
|
275
|
+
+ ` suppressMeasured=${frame.suppressMeasured ? 1 : 0}`
|
|
276
|
+
+ ` measuredVersion=${frame.measuredRowsVersion}`
|
|
277
|
+
+ ` baseline(measured/estimate)=${frame.preHookMeasuredRows}/${frame.preHookEstimateRows}`
|
|
278
|
+
+ ` harvest(measured/estimate)=${frame.postHarvestMeasuredRows}/${frame.postHarvestEstimateRows}`
|
|
279
|
+
+ ` target=${frame.scrollTarget} following=${frame.following ? 1 : 0} anchor=${frame.anchor}`);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
console.log(`# scrolled-up fenced-script frame repro columns=${COLUMNS} viewRows=${VIEW_ROWS} initialScroll=${INITIAL_SCROLL}`);
|
|
283
|
+
console.log(`# append-only characters=${SCRIPT.length} commits=${frames.length} dip-snap events=${dips.length}`);
|
|
284
|
+
for (const event of dips.slice(0, 4)) {
|
|
285
|
+
print('before ', event.previous);
|
|
286
|
+
print('transient', event.transient);
|
|
287
|
+
print('harvest ', event.corrected);
|
|
288
|
+
console.log('');
|
|
289
|
+
}
|
|
290
|
+
const suppressValues = new Set(frames.map((frame) => frame.suppressMeasured));
|
|
291
|
+
console.log(`# suppressMeasuredRowHeights values during repro: ${[...suppressValues].map(Number).join(',')}`);
|
|
292
|
+
if (dips.length > 0) {
|
|
293
|
+
throw new Error(`expected no visible-top dip/snap while fenced script streams; observed ${dips.length}`);
|
|
294
|
+
}
|
|
295
|
+
if (suppressValues.size !== 1 || !suppressValues.has(true)) {
|
|
296
|
+
throw new Error('repro unexpectedly toggled suppressMeasuredRowHeights');
|
|
297
|
+
}
|
|
298
|
+
console.log('tui-transcript-jitter-harness: ok');
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { build } from 'esbuild';
|
|
3
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const entry = join(ROOT, 'scripts', 'tui-transcript-jitter-harness-entry.jsx');
|
|
9
|
+
const outfile = join(ROOT, 'scripts', '.tui-transcript-jitter-harness.tmp.mjs');
|
|
10
|
+
|
|
11
|
+
const inkAlias = {
|
|
12
|
+
name: 'mixdog-ink-alias',
|
|
13
|
+
setup(ctx) {
|
|
14
|
+
ctx.onResolve({ filter: /^ink$/ }, () => ({
|
|
15
|
+
path: '../vendor/ink/build/index.js',
|
|
16
|
+
external: true,
|
|
17
|
+
}));
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Build-only probe: records the exact delta seen by the production helper
|
|
22
|
+
// without adding another stateful estimator call from the harness.
|
|
23
|
+
const growthProbe = {
|
|
24
|
+
name: 'streaming-tail-growth-probe',
|
|
25
|
+
setup(ctx) {
|
|
26
|
+
ctx.onLoad({ filter: /transcript-window\.mjs$/ }, async (args) => {
|
|
27
|
+
let source = (await readFile(args.path, 'utf8')).replace(/\r\n/g, '\n');
|
|
28
|
+
const target = ' return { tailRows: idEntry.rows, delta };';
|
|
29
|
+
const replacement = ` globalThis.__mixdogTailGrowthProbe = { live, baseline, delta, tailRows: idEntry.rows };\n${target}`;
|
|
30
|
+
if (!source.includes(target)) return null;
|
|
31
|
+
source = source.replace(target, replacement);
|
|
32
|
+
return { contents: source, loader: 'js' };
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await build({
|
|
39
|
+
entryPoints: [entry],
|
|
40
|
+
outfile,
|
|
41
|
+
bundle: true,
|
|
42
|
+
format: 'esm',
|
|
43
|
+
platform: 'node',
|
|
44
|
+
target: 'node22',
|
|
45
|
+
jsx: 'automatic',
|
|
46
|
+
packages: 'external',
|
|
47
|
+
plugins: [inkAlias, growthProbe],
|
|
48
|
+
banner: {
|
|
49
|
+
js: "import { createRequire as __mixdogCreateRequire } from 'node:module';\nconst require = __mixdogCreateRequire(import.meta.url);",
|
|
50
|
+
},
|
|
51
|
+
logLevel: 'silent',
|
|
52
|
+
});
|
|
53
|
+
await import(`${pathToFileURL(outfile).href}?run=${Date.now()}`);
|
|
54
|
+
} finally {
|
|
55
|
+
await rm(outfile, { force: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
@@ -8,7 +8,7 @@ import { buildTranscriptRowIndexIncremental } from '../src/tui/app/transcript-wi
|
|
|
8
8
|
|
|
9
9
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
10
|
|
|
11
|
-
function makeTurnHarness(ask) {
|
|
11
|
+
function makeTurnHarness(ask, stateOverrides = {}) {
|
|
12
12
|
let seq = 0;
|
|
13
13
|
let state = {
|
|
14
14
|
items: [],
|
|
@@ -18,6 +18,7 @@ function makeTurnHarness(ask) {
|
|
|
18
18
|
busy: false,
|
|
19
19
|
spinner: null,
|
|
20
20
|
thinking: null,
|
|
21
|
+
...stateOverrides,
|
|
21
22
|
};
|
|
22
23
|
const itemIndexById = new Map();
|
|
23
24
|
const set = (patch) => { state = { ...state, ...patch }; return true; };
|
|
@@ -43,6 +44,15 @@ function makeTurnHarness(ask) {
|
|
|
43
44
|
const clearStreamingTail = (id = null) => {
|
|
44
45
|
if (id == null || state.streamingTail?.id === id) set({ streamingTail: null });
|
|
45
46
|
};
|
|
47
|
+
const replaceItems = (items, options = {}) => {
|
|
48
|
+
state = replaceEngineItemsState({
|
|
49
|
+
state,
|
|
50
|
+
items,
|
|
51
|
+
itemIndexById,
|
|
52
|
+
preserveStreamingTail: options.preserveStreamingTail === true,
|
|
53
|
+
});
|
|
54
|
+
return items;
|
|
55
|
+
};
|
|
46
56
|
const bag = {
|
|
47
57
|
runtime: { id: null, toolMode: 'auto', ask, abort: () => true },
|
|
48
58
|
nextId: () => `id_${++seq}`,
|
|
@@ -55,6 +65,7 @@ function makeTurnHarness(ask) {
|
|
|
55
65
|
set,
|
|
56
66
|
pushItem,
|
|
57
67
|
patchItem,
|
|
68
|
+
replaceItems,
|
|
58
69
|
updateStreamingTail,
|
|
59
70
|
settleStreamingTail,
|
|
60
71
|
clearStreamingTail,
|
|
@@ -77,6 +88,50 @@ function makeTurnHarness(ask) {
|
|
|
77
88
|
return { runTurn: createRunTurn(bag), getState: () => state };
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
test('successful mid-turn compact trims history but preserves live turn references', async () => {
|
|
92
|
+
let preservedTail = false;
|
|
93
|
+
const harness = makeTurnHarness(async (_text, options) => {
|
|
94
|
+
options.onTextDelta('before compact\n');
|
|
95
|
+
await wait(30);
|
|
96
|
+
options.onCompactEvent({ status: 'compacted', trigger: 'reactive' });
|
|
97
|
+
preservedTail = harness.getState().streamingTail?.text === 'before compact\n';
|
|
98
|
+
options.onCompactEvent({ status: 'compacted', trigger: 'reactive' });
|
|
99
|
+
options.onTextDelta('after compact\n');
|
|
100
|
+
return { result: { content: 'before compact\nafter compact\n' }, session: { messages: [] } };
|
|
101
|
+
}, {
|
|
102
|
+
items: [
|
|
103
|
+
{ id: 'old', kind: 'assistant', text: 'old history' },
|
|
104
|
+
{ id: 'current-user', kind: 'user', text: 'go' },
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assert.equal(await harness.runTurn('go', { submittedIds: ['current-user'] }), 'done');
|
|
109
|
+
assert.equal(preservedTail, true);
|
|
110
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'old'), false);
|
|
111
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'current-user'), true);
|
|
112
|
+
assert.equal(
|
|
113
|
+
harness.getState().items.filter((item) => item.label === 'Compact complete (overflow recovery)').length,
|
|
114
|
+
2,
|
|
115
|
+
);
|
|
116
|
+
assert.equal(
|
|
117
|
+
harness.getState().items.find((item) => item.kind === 'assistant')?.text,
|
|
118
|
+
'before compact\nafter compact\n',
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('failed mid-turn compact leaves prior transcript items untouched', async () => {
|
|
123
|
+
const harness = makeTurnHarness(async (_text, options) => {
|
|
124
|
+
options.onCompactEvent({ status: 'failed' });
|
|
125
|
+
return { result: { content: '' }, session: { messages: [] } };
|
|
126
|
+
}, {
|
|
127
|
+
items: [{ id: 'old', kind: 'assistant', text: 'old history' }],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
assert.equal(await harness.runTurn('go'), 'done');
|
|
131
|
+
assert.equal(harness.getState().items.some((item) => item.id === 'old'), true);
|
|
132
|
+
assert.equal(harness.getState().items.some((item) => item.label === 'Compact failed'), true);
|
|
133
|
+
});
|
|
134
|
+
|
|
80
135
|
test('stream flush keeps settled items identity and finalize appends one assistant', async () => {
|
|
81
136
|
let identityStable = false;
|
|
82
137
|
const harness = makeTurnHarness(async (_text, options) => {
|
|
@@ -6,9 +6,11 @@ permission: read
|
|
|
6
6
|
|
|
7
7
|
Independent regression/risk review agent.
|
|
8
8
|
|
|
9
|
-
Review the
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Review the diff and tests with independent judgment. Prioritize actionable
|
|
10
|
+
correctness, regression, security, and verification risks; inspect affected
|
|
11
|
+
boundaries. Do not reimplement the change or report non-risky nits. Independently
|
|
12
|
+
evaluate the final deliverable with a critical lens, actively seeking errors,
|
|
13
|
+
unsupported assumptions, and counterexamples before confirming.
|
|
12
14
|
Report findings first, severity-ordered, with one line per `file:line`. If clean,
|
|
13
15
|
say so in one line and include only material residual risk.
|
|
14
16
|
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
`Task:`. Never infer exactness from a task name, file count, or perceived
|
|
9
9
|
difficulty.
|
|
10
10
|
- All other fields are optional task-specific deltas: `Anchors:`
|
|
11
|
-
`Allow/Forbid:` `Deliver
|
|
11
|
+
`Allow/Forbid:` `Deliver:`. Omit any that add no information.
|
|
12
12
|
- Anchors: `file:line` + one-line conclusion; never log/code bodies. Specify
|
|
13
13
|
outcome, not method unless required. `Deliver:` gives shape/size, never a long
|
|
14
|
-
handoff.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
handoff. The original request and official spec/test acceptance criteria beat
|
|
15
|
+
their brief summary.
|
|
16
|
+
- Each role independently constructs a role-appropriate, lossless `Task:` from
|
|
17
|
+
the original request and official spec/test acceptance criteria.
|
|
17
18
|
- Full brief only for fresh spawn/`respawned: true`; live follow-ups are delta.
|
|
18
19
|
Dead-tag send is cold: re-supply anchors.
|
|
19
20
|
- Never `send` mid-run; batch one follow-up after completion; interrupt only to
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { loadConfig } from '../config.mjs';
|
|
25
25
|
import { resolveRuntimeSpec } from '../config.mjs';
|
|
26
26
|
import { getHiddenAgent, resolveAgentSessionPermission } from '../internal-agents.mjs';
|
|
27
|
+
import { isKnownProvider } from '../../../../standalone/provider-admin.mjs';
|
|
27
28
|
import { prepareAgentSession } from './session-builder.mjs';
|
|
28
29
|
import {
|
|
29
30
|
askSession,
|
|
@@ -163,9 +164,30 @@ export function resolveHiddenRoleSchemaAllowedTools(hidden) {
|
|
|
163
164
|
* against config.presets for backward compatibility.
|
|
164
165
|
* - null — unresolved.
|
|
165
166
|
*
|
|
166
|
-
*
|
|
167
|
-
* agents
|
|
167
|
+
* Explore and memory hidden roles mirror public spawning precedence:
|
|
168
|
+
* `agents.<role>` (including legacy `agents.maintenance`) → workflow route →
|
|
169
|
+
* maintenance route → Main. The cycle1/2/3 agents share the memory knob via
|
|
170
|
+
* their `maintKey: 'memory'` override. Scheduler and webhook are unchanged.
|
|
168
171
|
*/
|
|
172
|
+
const DEFAULT_AGENT_ROUTE_PROVIDER = 'anthropic-oauth';
|
|
173
|
+
|
|
174
|
+
function normalizeMaintenanceCandidate(candidate, config) {
|
|
175
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) return candidate || null;
|
|
176
|
+
const configuredProvider = String(config?.defaultProvider || '').trim();
|
|
177
|
+
const fallbackProvider = isKnownProvider(configuredProvider)
|
|
178
|
+
? configuredProvider
|
|
179
|
+
: DEFAULT_AGENT_ROUTE_PROVIDER;
|
|
180
|
+
const provider = String(candidate.provider || fallbackProvider).trim();
|
|
181
|
+
const model = String(candidate.model || '').trim();
|
|
182
|
+
if (!provider || !model) return null;
|
|
183
|
+
return {
|
|
184
|
+
provider,
|
|
185
|
+
model,
|
|
186
|
+
effort: String(candidate.effort || '').trim() || undefined,
|
|
187
|
+
fast: candidate.fast === true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
169
191
|
export function resolveMaintenanceRoute({ preset, optsPreset, agent, config: cfgIn = null }) {
|
|
170
192
|
if (preset) return preset;
|
|
171
193
|
if (optsPreset) return optsPreset;
|
|
@@ -175,7 +197,22 @@ export function resolveMaintenanceRoute({ preset, optsPreset, agent, config: cfg
|
|
|
175
197
|
try {
|
|
176
198
|
const config = cfgIn || loadConfig({ secrets: false });
|
|
177
199
|
const maint = config?.maintenance || {};
|
|
178
|
-
|
|
200
|
+
const key = hidden.maintKey || hidden.slot;
|
|
201
|
+
const role = key === 'explore' ? 'explore' : (key === 'memory' ? 'maintainer' : '');
|
|
202
|
+
const workflowSlot = key === 'explore' ? 'explorer' : (key === 'memory' ? 'memory' : '');
|
|
203
|
+
if (!role) return maint[key] ?? null;
|
|
204
|
+
const candidates = [
|
|
205
|
+
role ? config?.agents?.[role] : null,
|
|
206
|
+
key === 'memory' ? config?.agents?.maintenance : null,
|
|
207
|
+
workflowSlot ? config?.workflowRoutes?.[workflowSlot] : null,
|
|
208
|
+
maint[key],
|
|
209
|
+
role ? config?.default : null,
|
|
210
|
+
];
|
|
211
|
+
for (const candidate of candidates) {
|
|
212
|
+
const route = normalizeMaintenanceCandidate(candidate, config);
|
|
213
|
+
if (route) return route;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
179
216
|
} catch { return null; }
|
|
180
217
|
}
|
|
181
218
|
return null;
|
|
@@ -7,6 +7,20 @@ import {
|
|
|
7
7
|
hasGrokOAuthCredentials,
|
|
8
8
|
} from './providers/oauth-credential-probes.mjs';
|
|
9
9
|
|
|
10
|
+
// Keep config loading free of standalone provider-admin and its provider
|
|
11
|
+
// runtimes. These identifiers mirror that module's static catalog, assembled
|
|
12
|
+
// solely from the lightweight config/provider constants already imported here.
|
|
13
|
+
const CONFIG_PROVIDER_IDS = new Set([
|
|
14
|
+
...Object.keys(AGENT_PROVIDER_ENV),
|
|
15
|
+
...Object.keys(OPENAI_COMPAT_PRESETS),
|
|
16
|
+
'openai-oauth',
|
|
17
|
+
'anthropic-oauth',
|
|
18
|
+
'grok-oauth',
|
|
19
|
+
]);
|
|
20
|
+
function isConfiguredProviderId(provider) {
|
|
21
|
+
return CONFIG_PROVIDER_IDS.has(String(provider || '').trim());
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
// Thin wrapper around resolvePluginData so callers in this orchestrator tree
|
|
11
25
|
// can import a single helper without reaching into shared/.
|
|
12
26
|
export function getPluginData() {
|
|
@@ -18,15 +32,12 @@ export function getPluginData() {
|
|
|
18
32
|
// Canonical maintenance defaults. Single source of truth — imported by
|
|
19
33
|
// llm/index.mjs and setup-server.mjs so UI/runtime cannot drift from config.
|
|
20
34
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// memory
|
|
27
|
-
// preset knob — the cycle agents stay separate (cycle1/2/3-agent, distinct
|
|
28
|
-
// slots and invokedBy) but resolve their model from `maint.memory` via the
|
|
29
|
-
// `maintKey` override on their hidden-role entries.
|
|
35
|
+
// Explore and Maintainer start without a route so they dynamically inherit the
|
|
36
|
+
// Main route. Explicit `maintenance.explore` / `maintenance.memory` choices
|
|
37
|
+
// remain supported. The memory cycles (chunker / re-scorer / core reviewer)
|
|
38
|
+
// share the optional `memory` preset knob — the cycle agents stay separate
|
|
39
|
+
// (cycle1/2/3-agent, distinct slots and invokedBy) but resolve their model from
|
|
40
|
+
// `maint.memory` via the `maintKey` override on their hidden-role entries.
|
|
30
41
|
// scheduler/webhook still let a per-entry config.json model win first (the
|
|
31
42
|
// caller passes it explicitly via opts.preset); the haiku default below only
|
|
32
43
|
// applies when an entry omits its own model.
|
|
@@ -129,8 +140,6 @@ const _HAIKU_ROUTE = Object.freeze({
|
|
|
129
140
|
model: resolveAnthropicFamilyModel('haiku'),
|
|
130
141
|
});
|
|
131
142
|
export const DEFAULT_MAINTENANCE = Object.freeze({
|
|
132
|
-
explore: { ..._HAIKU_ROUTE },
|
|
133
|
-
memory: { ..._HAIKU_ROUTE },
|
|
134
143
|
scheduler: { ..._HAIKU_ROUTE },
|
|
135
144
|
webhook: { ..._HAIKU_ROUTE },
|
|
136
145
|
});
|
|
@@ -201,12 +210,18 @@ function normalizeSearchRoute(route) {
|
|
|
201
210
|
// is resolved against the config.presets array (the legacy lookup) and rewritten
|
|
202
211
|
// to a route. Unresolvable strings are dropped so the DEFAULT_MAINTENANCE route
|
|
203
212
|
// fills the slot. `presets` is the normalized preset array for legacy lookup.
|
|
204
|
-
function migrateMaintenanceRoutes(rawMaint, presets) {
|
|
213
|
+
function migrateMaintenanceRoutes(rawMaint, presets, defaultProvider) {
|
|
205
214
|
const out = {};
|
|
206
215
|
const list = Array.isArray(presets) ? presets : [];
|
|
216
|
+
const configuredProvider = String(defaultProvider || '').trim();
|
|
217
|
+
const fallbackProvider = isConfiguredProviderId(configuredProvider)
|
|
218
|
+
? configuredProvider
|
|
219
|
+
: 'anthropic-oauth';
|
|
207
220
|
for (const [slot, value] of Object.entries(rawMaint || {})) {
|
|
208
221
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
209
|
-
const provider = normalizeAgentProviderId(
|
|
222
|
+
const provider = normalizeAgentProviderId(
|
|
223
|
+
value.provider || ((slot === 'explore' || slot === 'memory') ? fallbackProvider : ''),
|
|
224
|
+
);
|
|
210
225
|
const model = String(value.model || '').trim();
|
|
211
226
|
if (provider && model) {
|
|
212
227
|
const route = { provider, model };
|
|
@@ -437,7 +452,7 @@ export function loadConfig(options = {}) {
|
|
|
437
452
|
delete workflowRoutes.search;
|
|
438
453
|
// Migrate legacy preset-name maintenance slots to direct routes,
|
|
439
454
|
// then overlay onto the route-shaped defaults.
|
|
440
|
-
const migratedMaint = migrateMaintenanceRoutes(rawMaint, normalizedPresets);
|
|
455
|
+
const migratedMaint = migrateMaintenanceRoutes(rawMaint, normalizedPresets, raw.defaultProvider);
|
|
441
456
|
return {
|
|
442
457
|
providers: mergedProviders,
|
|
443
458
|
mcpServers,
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* no-tool role guard) — not a permission check. No gating is needed here.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { isToolEnvelope, makeToolEnvelope } from './session/tool-envelope.mjs';
|
|
18
|
+
import { isToolEnvelope, makeToolEnvelope, normalizeToolEnvelope } from './session/tool-envelope.mjs';
|
|
19
|
+
import { classifyResultKind } from './session/result-classification.mjs';
|
|
19
20
|
|
|
20
21
|
let _executor = null;
|
|
21
22
|
let _tools = [];
|
|
@@ -62,12 +63,24 @@ function _normalize(result) {
|
|
|
62
63
|
// JSON.stringify below and the loop would see the stringified envelope as
|
|
63
64
|
// the tool_result (body inlined + duplicated, no newMessages).
|
|
64
65
|
if (isToolEnvelope(result)) {
|
|
65
|
-
|
|
66
|
+
const normalized = normalizeToolEnvelope(_normalize(result.result));
|
|
67
|
+
return makeToolEnvelope(normalized.result, result.newMessages, {
|
|
68
|
+
explicitSuccess: normalized.explicitSuccess || result.explicitSuccess === true,
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
68
|
-
|
|
72
|
+
const text = result.content
|
|
69
73
|
.map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
|
|
70
74
|
.join('\n');
|
|
75
|
+
// Preserve MCP-style handler outcome metadata across the object→string
|
|
76
|
+
// boundary. Explicit failures retain the canonical Error: convention;
|
|
77
|
+
// explicit successes use a transient envelope so legitimate output
|
|
78
|
+
// beginning with Error: is not mistaken for a failed execution.
|
|
79
|
+
if (result.isError === true) return !text.startsWith('Error:') ? `Error: ${text}` : text;
|
|
80
|
+
if (result.isError === false && classifyResultKind(text) === 'error') {
|
|
81
|
+
return makeToolEnvelope(text, [], { explicitSuccess: true });
|
|
82
|
+
}
|
|
83
|
+
return text;
|
|
71
84
|
}
|
|
72
85
|
if (typeof result === 'string') return result;
|
|
73
86
|
return JSON.stringify(result);
|