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
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
estimateMessagesTokens,
|
|
5
|
+
estimateRequestReserveTokens,
|
|
6
|
+
estimateToolSchemaTokens,
|
|
7
|
+
estimateTokens,
|
|
8
|
+
IMAGE_VISUAL_TOKEN_ALLOWANCE,
|
|
9
|
+
summarizeContextMessages,
|
|
10
|
+
} from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
|
|
11
|
+
import { createContextStatus } from '../src/session-runtime/context-status.mjs';
|
|
4
12
|
import {
|
|
5
13
|
compactionTelemetryPressureTokens,
|
|
6
14
|
recordProviderContextBaseline,
|
|
15
|
+
resolveCompactionPressureTokens,
|
|
7
16
|
rememberCompactTelemetry,
|
|
8
17
|
resolveWorkerCompactPolicy,
|
|
9
18
|
shouldCompactForSession,
|
|
@@ -26,6 +35,252 @@ test('Lead/main auto-compaction triggers at 95% of the effective boundary', () =
|
|
|
26
35
|
assert.equal(agentPolicy.triggerTokens, 90_000, 'agent default 10% headroom must remain unchanged');
|
|
27
36
|
});
|
|
28
37
|
|
|
38
|
+
test('image payload bytes do not inflate live gauge or fallback compaction estimates', () => {
|
|
39
|
+
const text = 'Please describe the attached image.';
|
|
40
|
+
const imageData = 'A'.repeat(400_000);
|
|
41
|
+
const imagePart = { type: 'image', data: imageData, mimeType: 'image/png' };
|
|
42
|
+
const messages = [{
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: [{ type: 'text', text }, imagePart],
|
|
45
|
+
}];
|
|
46
|
+
const plainTextEstimate = estimateTokens(text) + 4;
|
|
47
|
+
const fallbackEstimate = estimateMessagesTokens(messages);
|
|
48
|
+
const summaryEstimate = summarizeContextMessages(messages).estimatedTokens;
|
|
49
|
+
|
|
50
|
+
assert.equal(estimateMessagesTokens([{ role: 'user', content: text }]), plainTextEstimate,
|
|
51
|
+
'ordinary string estimates must remain unchanged');
|
|
52
|
+
assert.equal(fallbackEstimate, plainTextEstimate + IMAGE_VISUAL_TOKEN_ALLOWANCE,
|
|
53
|
+
'fallback compaction estimate must count text plus a visual image allowance');
|
|
54
|
+
const largerPayloadEstimate = estimateMessagesTokens([{
|
|
55
|
+
role: 'user',
|
|
56
|
+
content: [{ type: 'text', text }, { ...imagePart, data: imageData.repeat(2) }],
|
|
57
|
+
}]);
|
|
58
|
+
assert.equal(largerPayloadEstimate, fallbackEstimate,
|
|
59
|
+
'raw image byte length must not affect the visual allowance');
|
|
60
|
+
assert.equal(summaryEstimate, fallbackEstimate,
|
|
61
|
+
'display and fallback compaction must share the image-aware message estimate');
|
|
62
|
+
|
|
63
|
+
const session = {
|
|
64
|
+
id: 'image-context-test',
|
|
65
|
+
provider: 'openai',
|
|
66
|
+
contextWindow: 100_000,
|
|
67
|
+
rawContextWindow: 100_000,
|
|
68
|
+
compactBoundaryTokens: 100_000,
|
|
69
|
+
messages: [],
|
|
70
|
+
liveTurnMessages: messages,
|
|
71
|
+
tools: [],
|
|
72
|
+
compaction: {},
|
|
73
|
+
};
|
|
74
|
+
const { contextStatus } = createContextStatus({
|
|
75
|
+
getSession: () => session,
|
|
76
|
+
getRoute: () => ({ provider: 'openai', model: 'test-model' }),
|
|
77
|
+
getCurrentCwd: () => '',
|
|
78
|
+
getMode: () => 'default',
|
|
79
|
+
});
|
|
80
|
+
const status = contextStatus();
|
|
81
|
+
assert.ok(status.usedTokens < status.compaction.triggerTokens,
|
|
82
|
+
`image-aware live gauge must remain below compaction threshold, got ${status.usedTokens}`);
|
|
83
|
+
assert.ok(status.usedTokens < status.contextWindow,
|
|
84
|
+
`raw base64 must not pin the live gauge at 100%, got ${status.usedTokens}/${status.contextWindow}`);
|
|
85
|
+
|
|
86
|
+
recordProviderContextBaseline(session, messages, { inputTokens: 80_000, outputTokens: 0 });
|
|
87
|
+
const policy = { ...policyFor(session), reserveTokens: 0 };
|
|
88
|
+
assert.equal(
|
|
89
|
+
shouldCompactForSession(fallbackEstimate, policy, { messages, sessionRef: session }),
|
|
90
|
+
false,
|
|
91
|
+
'provider-backed pressure below threshold must remain below threshold',
|
|
92
|
+
);
|
|
93
|
+
assert.equal(imagePart.data, imageData,
|
|
94
|
+
'estimating live context must not sanitize or remove image content');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('dense ASCII, encoded, and minified payloads do not retain the prose chars/4 estimate', () => {
|
|
98
|
+
for (const text of [
|
|
99
|
+
'A1b2C3d4E5f6G7h8'.repeat(100),
|
|
100
|
+
'%7B%22path%22%3A%22very%2Flong%2Fencoded%2Fvalue%22%7D'.repeat(40),
|
|
101
|
+
JSON.stringify({ rows: Array.from({ length: 100 }, (_, i) => ({ id: i, value: `item_${i}_abcdef` })) }),
|
|
102
|
+
]) {
|
|
103
|
+
assert.ok(estimateTokens(text) >= Math.ceil(text.length * 0.7),
|
|
104
|
+
`dense ASCII estimate must be conservative (${estimateTokens(text)}/${text.length})`);
|
|
105
|
+
}
|
|
106
|
+
const shortJsonl = Array.from({ length: 200 }, (_, i) => `{"i":${i}}`).join('\n');
|
|
107
|
+
assert.ok(estimateTokens(shortJsonl) >= shortJsonl.replace(/\s/g, '').length * 0.7,
|
|
108
|
+
'short JSONL records must receive the structured multiline floor');
|
|
109
|
+
const spacedShortIdentifiers = 'A1b2C3d4E5 F6G7H8I9J0 '.repeat(100);
|
|
110
|
+
assert.ok(estimateTokens(spacedShortIdentifiers) >= spacedShortIdentifiers.length * 0.7,
|
|
111
|
+
'space-separated encoded identifiers below the long-run threshold must receive the dense floor');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('assistantBlocks and reasoningItems count provider-visible data but not image bytes', () => {
|
|
115
|
+
const base = { role: 'assistant', content: '' };
|
|
116
|
+
const assistantBlocks = [
|
|
117
|
+
{ type: 'text', text: 'native assistant replay' },
|
|
118
|
+
{ type: 'tool_use', id: 'call_native', name: 'read', input: '{"path":"x"}' },
|
|
119
|
+
{ type: 'image', data: 'Z'.repeat(300_000), mimeType: 'image/png' },
|
|
120
|
+
];
|
|
121
|
+
const reasoningItems = [{ type: 'reasoning', encrypted_content: 'enc_'.repeat(2_000) }];
|
|
122
|
+
const estimated = estimateMessagesTokens([{ ...base, assistantBlocks, reasoningItems }]);
|
|
123
|
+
const noNativeReplay = estimateMessagesTokens([base]);
|
|
124
|
+
assert.ok(estimated > noNativeReplay + IMAGE_VISUAL_TOKEN_ALLOWANCE,
|
|
125
|
+
'native assistant blocks and opaque reasoning must contribute tokens');
|
|
126
|
+
assert.equal(
|
|
127
|
+
estimateMessagesTokens([{ ...base, assistantBlocks: assistantBlocks.map(block => (
|
|
128
|
+
block.type === 'image' ? { ...block, data: 'Z'.repeat(600_000) } : block
|
|
129
|
+
)), reasoningItems }]),
|
|
130
|
+
estimated,
|
|
131
|
+
'native replay image bytes must not be tokenized',
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('image allowance handles identity, detail, dimensions, and multiple accepted forms', () => {
|
|
136
|
+
const low = { type: 'image_url', image_url: { url: 'https://example.test/a.png', detail: 'low' } };
|
|
137
|
+
const tiled = {
|
|
138
|
+
type: 'input_image',
|
|
139
|
+
image_url: { url: 'https://example.test/b.png', detail: 'high' },
|
|
140
|
+
dimensions: { width: 2_048, height: 1_024 },
|
|
141
|
+
};
|
|
142
|
+
const inline = { inlineData: { data: 'A'.repeat(1_000), mimeType: 'image/png' }, width: 512, height: 512 };
|
|
143
|
+
const lowEstimate = estimateMessagesTokens([{ role: 'user', content: [low] }]);
|
|
144
|
+
const tiledEstimate = estimateMessagesTokens([{ role: 'user', content: [tiled] }]);
|
|
145
|
+
const multiEstimate = estimateMessagesTokens([{ role: 'user', content: [low, tiled, inline] }]);
|
|
146
|
+
assert.ok(tiledEstimate > lowEstimate, 'high-detail multi-tile image must reserve more than low detail');
|
|
147
|
+
assert.ok(lowEstimate >= IMAGE_VISUAL_TOKEN_ALLOWANCE + 4,
|
|
148
|
+
'unverified low-detail metadata must not reduce the conservative image fallback');
|
|
149
|
+
const unverifiedSmallDimensions = estimateMessagesTokens([{
|
|
150
|
+
role: 'user',
|
|
151
|
+
content: [{ type: 'image', data: 'A'.repeat(1_000), width: 1, height: 1, detail: 'low' }],
|
|
152
|
+
}]);
|
|
153
|
+
assert.ok(unverifiedSmallDimensions >= IMAGE_VISUAL_TOKEN_ALLOWANCE + 4,
|
|
154
|
+
'unverified tiny dimensions must not reduce the conservative image fallback');
|
|
155
|
+
assert.ok(multiEstimate > tiledEstimate + lowEstimate, 'every accepted image form must add visual allowance');
|
|
156
|
+
|
|
157
|
+
const session = { provider: 'openai', model: 'm', tools: [], contextWindow: 100_000, compaction: {} };
|
|
158
|
+
const messages = [{ role: 'user', content: [{ type: 'image', data: 'A'.repeat(1_000), mimeType: 'image/png' }] }];
|
|
159
|
+
const policy = policyFor(session);
|
|
160
|
+
recordProviderContextBaseline(session, messages, { inputTokens: 80_000 }, { sendTools: [] });
|
|
161
|
+
messages[0].content[0].data = 'B'.repeat(1_000);
|
|
162
|
+
assert.equal(
|
|
163
|
+
resolveCompactionPressureTokens(estimateMessagesTokens(messages), policy, { messages, sessionRef: session }),
|
|
164
|
+
estimateMessagesTokens(messages) + policy.reserveTokens,
|
|
165
|
+
'same-size image replacement must invalidate the measured prefix',
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('tool reserve and status cache detect nested schema mutation', () => {
|
|
170
|
+
const tools = [{ name: 'read', parameters: { type: 'object', properties: { path: { type: 'string' } } } }];
|
|
171
|
+
const beforeSchema = estimateToolSchemaTokens(tools);
|
|
172
|
+
const beforeReserve = estimateRequestReserveTokens(tools);
|
|
173
|
+
tools[0].parameters.properties.path.description = 'dense_nested_schema_description_'.repeat(200);
|
|
174
|
+
assert.ok(estimateToolSchemaTokens(tools) > beforeSchema);
|
|
175
|
+
assert.ok(estimateRequestReserveTokens(tools) > beforeReserve);
|
|
176
|
+
|
|
177
|
+
const session = { id: 'tool-cache', contextWindow: 100_000, messages: [{ role: 'user', content: 'x' }], tools, compaction: {} };
|
|
178
|
+
const { contextStatus } = createContextStatus({
|
|
179
|
+
getSession: () => session,
|
|
180
|
+
getRoute: () => ({ provider: 'openai', model: 'm' }),
|
|
181
|
+
getCurrentCwd: () => '',
|
|
182
|
+
getMode: () => 'default',
|
|
183
|
+
});
|
|
184
|
+
const statusBefore = contextStatus();
|
|
185
|
+
tools[0].parameters.properties.path.description += 'more_'.repeat(500);
|
|
186
|
+
const statusAfter = contextStatus();
|
|
187
|
+
assert.notEqual(statusAfter, statusBefore);
|
|
188
|
+
assert.ok(statusAfter.request.toolSchemaTokens > statusBefore.request.toolSchemaTokens);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('native replay fingerprint detects encrypted reasoning and assistant metadata mutation', () => {
|
|
192
|
+
const message = {
|
|
193
|
+
role: 'assistant',
|
|
194
|
+
content: '',
|
|
195
|
+
reasoningItems: [{ type: 'reasoning', encrypted_content: 'short' }],
|
|
196
|
+
assistantBlocks: [{ type: 'tool_use', id: 'c1', name: 'read', input: { path: 'a' }, cache_control: { type: 'ephemeral' } }],
|
|
197
|
+
};
|
|
198
|
+
const messages = [message];
|
|
199
|
+
const first = summarizeContextMessages(messages).estimatedTokens;
|
|
200
|
+
message.reasoningItems[0].encrypted_content = 'encrypted_'.repeat(1_000);
|
|
201
|
+
const second = summarizeContextMessages(messages).estimatedTokens;
|
|
202
|
+
assert.ok(second > first, 'in-place encrypted_content mutation must invalidate the message memo');
|
|
203
|
+
message.assistantBlocks[0].cache_control.mode = 'metadata_'.repeat(1_000);
|
|
204
|
+
const third = summarizeContextMessages(messages).estimatedTokens;
|
|
205
|
+
assert.ok(third > second, 'in-place assistant-block metadata mutation must invalidate the message memo');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('provider baseline pressure includes only the configured extra reserve', () => {
|
|
209
|
+
const session = { provider: 'openai', model: 'm', tools: [], contextWindow: 100_000, compaction: {} };
|
|
210
|
+
const messages = [{ role: 'user', content: 'baseline' }];
|
|
211
|
+
recordProviderContextBaseline(session, messages, { inputTokens: 80_000, outputTokens: 0 });
|
|
212
|
+
const policy = { ...policyFor(session), reserveTokens: 7_512, requestReserveTokens: 512, configuredReserveTokens: 7_000 };
|
|
213
|
+
assert.equal(resolveCompactionPressureTokens(1, policy, { messages, sessionRef: session }), 87_000);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('provider baselines fingerprint actual sendTools and reject provider, model, tool-schema, and prefix changes', () => {
|
|
217
|
+
const make = () => ({
|
|
218
|
+
provider: 'openai',
|
|
219
|
+
model: 'm1',
|
|
220
|
+
tools: [{ name: 'read', parameters: { type: 'object' } }],
|
|
221
|
+
contextWindow: 100_000,
|
|
222
|
+
compaction: {},
|
|
223
|
+
});
|
|
224
|
+
const fallbackForMutation = (mutate) => {
|
|
225
|
+
const session = make();
|
|
226
|
+
const messages = [{ role: 'user', content: 'original prefix' }];
|
|
227
|
+
recordProviderContextBaseline(session, messages, { inputTokens: 80_000, outputTokens: 0 }, { sendTools: session.tools });
|
|
228
|
+
mutate(session, messages);
|
|
229
|
+
const policy = { ...resolveWorkerCompactPolicy(session, session.tools), reserveTokens: 0 };
|
|
230
|
+
return {
|
|
231
|
+
pressure: resolveCompactionPressureTokens(estimateMessagesTokens(messages), policy, { messages, sessionRef: session }),
|
|
232
|
+
fallback: estimateMessagesTokens(messages),
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
for (const mutate of [
|
|
236
|
+
session => { session.provider = 'anthropic'; },
|
|
237
|
+
session => { session.model = 'm2'; },
|
|
238
|
+
session => { session.tools[0].parameters.properties = { path: { type: 'string' } }; },
|
|
239
|
+
(_session, messages) => { messages[0].content = 'mutated earlier prefix'; },
|
|
240
|
+
]) {
|
|
241
|
+
const result = fallbackForMutation(mutate);
|
|
242
|
+
assert.equal(result.pressure, result.fallback);
|
|
243
|
+
}
|
|
244
|
+
const session = make();
|
|
245
|
+
const messages = [{ role: 'user', content: 'forced tool request' }];
|
|
246
|
+
const actualSendTools = [session.tools[0]];
|
|
247
|
+
const matchingPolicy = resolveWorkerCompactPolicy(session, actualSendTools);
|
|
248
|
+
recordProviderContextBaseline(session, messages, { inputTokens: 80_000 }, { sendTools: actualSendTools });
|
|
249
|
+
session.tools = [{ name: 'different-session-tool', parameters: { type: 'object' } }];
|
|
250
|
+
assert.equal(
|
|
251
|
+
resolveCompactionPressureTokens(1, matchingPolicy, { messages, sessionRef: session }),
|
|
252
|
+
80_000,
|
|
253
|
+
'baseline must remain aligned to actual sendTools rather than mutable sessionRef.tools',
|
|
254
|
+
);
|
|
255
|
+
const changedSendPolicy = resolveWorkerCompactPolicy(session, session.tools);
|
|
256
|
+
assert.notEqual(
|
|
257
|
+
resolveCompactionPressureTokens(1, changedSendPolicy, { messages, sessionRef: session }),
|
|
258
|
+
80_000,
|
|
259
|
+
'a different next-send schema must invalidate the baseline',
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('context status cache notices mutation of an earlier message', () => {
|
|
264
|
+
const session = {
|
|
265
|
+
id: 'status-mutation',
|
|
266
|
+
contextWindow: 100_000,
|
|
267
|
+
messages: [{ role: 'user', content: 'short' }, { role: 'assistant', content: 'tail' }],
|
|
268
|
+
tools: [],
|
|
269
|
+
compaction: {},
|
|
270
|
+
};
|
|
271
|
+
const { contextStatus } = createContextStatus({
|
|
272
|
+
getSession: () => session,
|
|
273
|
+
getRoute: () => ({ provider: 'openai', model: 'm' }),
|
|
274
|
+
getCurrentCwd: () => '',
|
|
275
|
+
getMode: () => 'default',
|
|
276
|
+
});
|
|
277
|
+
const before = contextStatus();
|
|
278
|
+
session.messages[0].content = 'dense_earlier_message_'.repeat(1_000);
|
|
279
|
+
const after = contextStatus();
|
|
280
|
+
assert.notEqual(after, before);
|
|
281
|
+
assert.ok(after.usedTokens > before.usedTokens);
|
|
282
|
+
});
|
|
283
|
+
|
|
29
284
|
test('provider callback usage counts assistant output once and estimates only later tool results', () => {
|
|
30
285
|
const session = { provider: 'anthropic', contextWindow: 100_000, compaction: {} };
|
|
31
286
|
const policy = { ...policyFor(session), reserveTokens: 0 };
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// Aggregator step run after all matrix build jobs succeed.
|
|
4
4
|
// Reads release assets via gh CLI, filters mixdog-runtime-*.tar.gz,
|
|
5
5
|
// fetches sha256 from .sha256 sidecar files, and emits runtime-manifest.json
|
|
6
|
-
// to both dist/ and src/runtime/memory/data/ (for the
|
|
6
|
+
// to both dist/ and src/runtime/memory/data/ (for the workflow branch sync).
|
|
7
7
|
//
|
|
8
8
|
// Environment:
|
|
9
9
|
// GITHUB_TOKEN — PAT or GITHUB_TOKEN secret with contents:read
|
|
10
10
|
// RELEASE_TAG — e.g. "runtime-v0.4.0" (falls back to process.argv[2])
|
|
11
11
|
// GITHUB_REPOSITORY — auto-set by Actions (e.g. "owner/repo")
|
|
12
|
-
// RUNTIME_RELEASE_REPOSITORY — override repo for gh commands
|
|
12
|
+
// RUNTIME_RELEASE_REPOSITORY — override repo for gh commands
|
|
13
13
|
|
|
14
14
|
import { execSync } from 'node:child_process';
|
|
15
15
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -77,23 +77,23 @@ for (const t of tarballs) {
|
|
|
77
77
|
async function fetchSha256(sha256AssetName, _downloadUrl) {
|
|
78
78
|
// Try matching .sha256 sidecar from the asset list first
|
|
79
79
|
const sidecar = assets.find(a => a.name === sha256AssetName);
|
|
80
|
-
if (sidecar) {
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const tmpDir = resolve(tmpdir(), 'mixdog-manifest-sha256');
|
|
84
|
-
mkdirSync(tmpDir, { recursive: true });
|
|
85
|
-
execSync(
|
|
86
|
-
`gh release download ${RELEASE_TAG} --repo ${REPO} --pattern ${sha256AssetName} --dir "${tmpDir}" --clobber`,
|
|
87
|
-
{ encoding: 'utf8', env: process.env }
|
|
88
|
-
);
|
|
89
|
-
const content = readFileSync(resolve(tmpDir, sha256AssetName), 'utf8').trim();
|
|
90
|
-
// Format: "<hex> <filename>"
|
|
91
|
-
return content.split(/\s+/)[0];
|
|
92
|
-
} catch (e) {
|
|
93
|
-
console.warn(`Could not download sidecar ${sha256AssetName}: ${e.message}`);
|
|
94
|
-
}
|
|
80
|
+
if (!sidecar) {
|
|
81
|
+
throw new Error(`Missing required checksum sidecar: ${sha256AssetName}`);
|
|
95
82
|
}
|
|
96
|
-
|
|
83
|
+
|
|
84
|
+
const tmpDir = resolve(tmpdir(), 'mixdog-manifest-sha256');
|
|
85
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
86
|
+
execSync(
|
|
87
|
+
`gh release download ${RELEASE_TAG} --repo ${REPO} --pattern ${sha256AssetName} --dir "${tmpDir}" --clobber`,
|
|
88
|
+
{ encoding: 'utf8', env: process.env }
|
|
89
|
+
);
|
|
90
|
+
const content = readFileSync(resolve(tmpDir, sha256AssetName), 'utf8').trim();
|
|
91
|
+
// Format: "<hex> <filename>"
|
|
92
|
+
const checksum = content.split(/\s+/)[0];
|
|
93
|
+
if (!/^[a-f0-9]{64}$/i.test(checksum)) {
|
|
94
|
+
throw new Error(`Invalid checksum in sidecar ${sha256AssetName}: ${checksum}`);
|
|
95
|
+
}
|
|
96
|
+
return checksum.toLowerCase();
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
// ---------------------------------------------------------------------------
|
|
@@ -118,12 +118,29 @@ for (const tarball of tarballs) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
//
|
|
121
|
+
// A partial release must never produce a publishable or bundled manifest.
|
|
122
122
|
const EXPECTED = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'win32-x64'];
|
|
123
|
+
const missing = EXPECTED.filter(p => !assetsMap[p]);
|
|
124
|
+
if (missing.length > 0) {
|
|
125
|
+
console.error(`Missing required runtime asset(s): ${missing.join(', ')}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
for (const p of EXPECTED) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
const asset = assetsMap[p];
|
|
131
|
+
let url;
|
|
132
|
+
try {
|
|
133
|
+
url = new URL(asset.url);
|
|
134
|
+
} catch {
|
|
135
|
+
console.error(`Invalid asset URL for ${p}: ${asset.url}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
if (url.protocol !== 'https:' ||
|
|
139
|
+
!/^[a-f0-9]{64}$/.test(asset.sha256) ||
|
|
140
|
+
!Number.isSafeInteger(asset.size) ||
|
|
141
|
+
asset.size <= 0) {
|
|
142
|
+
console.error(`Invalid release metadata for ${p}: ${JSON.stringify(asset)}`);
|
|
143
|
+
process.exit(1);
|
|
127
144
|
}
|
|
128
145
|
}
|
|
129
146
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import test, { after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
13
|
+
import { spawn } from 'node:child_process';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { writeJsonAtomicSync } from '../src/runtime/shared/atomic-file.mjs';
|
|
17
|
+
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-grok-race-'));
|
|
19
|
+
const tokenPath = join(root, 'grok-oauth.json');
|
|
20
|
+
const moduleUrl = new URL(
|
|
21
|
+
'../src/runtime/agent/orchestrator/providers/grok-oauth.mjs',
|
|
22
|
+
import.meta.url,
|
|
23
|
+
).href;
|
|
24
|
+
|
|
25
|
+
after(() => {
|
|
26
|
+
rmSync(root, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function writeTokens(accessToken, refreshToken, expiresAt) {
|
|
30
|
+
writeJsonAtomicSync(tokenPath, {
|
|
31
|
+
access_token: accessToken,
|
|
32
|
+
refresh_token: refreshToken,
|
|
33
|
+
expires_at: expiresAt,
|
|
34
|
+
token_endpoint: 'https://auth.x.ai/oauth/token',
|
|
35
|
+
}, { lock: true, fsyncDir: true, mode: 0o600, secret: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function waitForExit(child, label) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let stderr = '';
|
|
41
|
+
child.stderr.setEncoding('utf8');
|
|
42
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
43
|
+
child.on('error', reject);
|
|
44
|
+
child.on('exit', (code) => {
|
|
45
|
+
if (code === 0) resolve();
|
|
46
|
+
else reject(new Error(`${label} exited ${code}: ${stderr}`));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function waitForFiles(dir, count) {
|
|
52
|
+
const deadline = Date.now() + 30_000;
|
|
53
|
+
while (readdirSync(dir).length < count) {
|
|
54
|
+
if (Date.now() >= deadline) throw new Error('Grok race children did not become ready');
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test('contending Grok processes adopt a refresh-only token rotation', async () => {
|
|
60
|
+
const readyDir = join(root, 'ready');
|
|
61
|
+
const exchangeDir = join(root, 'exchanges');
|
|
62
|
+
const resultDir = join(root, 'results');
|
|
63
|
+
const startPath = join(root, 'start');
|
|
64
|
+
mkdirSync(readyDir);
|
|
65
|
+
mkdirSync(exchangeDir);
|
|
66
|
+
mkdirSync(resultDir);
|
|
67
|
+
writeTokens('fixture-old-access', 'fixture-old-refresh', Date.now() + 1_000);
|
|
68
|
+
|
|
69
|
+
const childSource = `
|
|
70
|
+
import { writeFileSync } from 'node:fs';
|
|
71
|
+
const { GrokOAuthProvider } = await import(${JSON.stringify(moduleUrl)});
|
|
72
|
+
const provider = Object.create(GrokOAuthProvider.prototype);
|
|
73
|
+
provider.config = {};
|
|
74
|
+
provider.tokens = {
|
|
75
|
+
...JSON.parse(await (await import('node:fs/promises')).readFile(process.env.TOKEN_PATH, 'utf8')),
|
|
76
|
+
mtimeMs: (await (await import('node:fs/promises')).stat(process.env.TOKEN_PATH)).mtimeMs,
|
|
77
|
+
source: 'own'
|
|
78
|
+
};
|
|
79
|
+
provider._lastDiskScan = provider.tokens.mtimeMs;
|
|
80
|
+
provider._refreshFallbackUntil = 0;
|
|
81
|
+
writeFileSync(process.env.READY_PATH, 'ready');
|
|
82
|
+
while (!(await (await import('node:fs/promises')).stat(process.env.START_PATH).catch(() => null))) {
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
84
|
+
}
|
|
85
|
+
globalThis.fetch = async () => {
|
|
86
|
+
const owner = String(process.pid);
|
|
87
|
+
writeFileSync(process.env.EXCHANGE_DIR + '/' + owner, 'exchange', { flag: 'wx' });
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 75));
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
status: 200,
|
|
92
|
+
async text() {
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
access_token: 'fixture-old-access',
|
|
95
|
+
refresh_token: 'fixture-new-refresh-' + owner,
|
|
96
|
+
expires_in: 600
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
const tokens = await provider.ensureAuth();
|
|
102
|
+
writeFileSync(process.env.RESULT_PATH, tokens.refresh_token);
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const exits = [];
|
|
106
|
+
for (let index = 0; index < 10; index += 1) {
|
|
107
|
+
const child = spawn(process.execPath, ['--input-type=module', '--eval', childSource], {
|
|
108
|
+
env: {
|
|
109
|
+
...process.env,
|
|
110
|
+
MIXDOG_DATA_DIR: root,
|
|
111
|
+
TOKEN_PATH: tokenPath,
|
|
112
|
+
READY_PATH: join(readyDir, String(index)),
|
|
113
|
+
START_PATH: startPath,
|
|
114
|
+
EXCHANGE_DIR: exchangeDir,
|
|
115
|
+
RESULT_PATH: join(resultDir, String(index)),
|
|
116
|
+
},
|
|
117
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
118
|
+
});
|
|
119
|
+
exits.push(waitForExit(child, `contention child ${index}`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await waitForFiles(readyDir, 10);
|
|
123
|
+
writeFileSync(startPath, 'start');
|
|
124
|
+
await Promise.all(exits);
|
|
125
|
+
|
|
126
|
+
const exchanges = readdirSync(exchangeDir);
|
|
127
|
+
assert.equal(exchanges.length, 1);
|
|
128
|
+
const expectedRefresh = `fixture-new-refresh-${exchanges[0]}`;
|
|
129
|
+
const persisted = JSON.parse(readFileSync(tokenPath, 'utf8'));
|
|
130
|
+
assert.equal(persisted.access_token, 'fixture-old-access');
|
|
131
|
+
assert.equal(persisted.refresh_token, expectedRefresh);
|
|
132
|
+
for (const result of readdirSync(resultDir)) {
|
|
133
|
+
assert.equal(readFileSync(join(resultDir, result), 'utf8'), expectedRefresh);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('invalid_grant adopts a valid token written by a peer without rotating it', async () => {
|
|
138
|
+
const requestStarted = join(root, 'invalid-request-started');
|
|
139
|
+
const peerWritten = join(root, 'invalid-peer-written');
|
|
140
|
+
const resultPath = join(root, 'invalid-result');
|
|
141
|
+
writeTokens('fixture-invalid-old-access', 'fixture-invalid-old-refresh', Date.now() + 1_000);
|
|
142
|
+
|
|
143
|
+
const childSource = `
|
|
144
|
+
import { writeFileSync } from 'node:fs';
|
|
145
|
+
const { GrokOAuthProvider } = await import(${JSON.stringify(moduleUrl)});
|
|
146
|
+
const provider = Object.create(GrokOAuthProvider.prototype);
|
|
147
|
+
provider.config = {};
|
|
148
|
+
provider.tokens = {
|
|
149
|
+
...JSON.parse(await (await import('node:fs/promises')).readFile(process.env.TOKEN_PATH, 'utf8')),
|
|
150
|
+
mtimeMs: (await (await import('node:fs/promises')).stat(process.env.TOKEN_PATH)).mtimeMs,
|
|
151
|
+
source: 'own'
|
|
152
|
+
};
|
|
153
|
+
provider._lastDiskScan = provider.tokens.mtimeMs;
|
|
154
|
+
provider._refreshFallbackUntil = 0;
|
|
155
|
+
let calls = 0;
|
|
156
|
+
globalThis.fetch = async () => {
|
|
157
|
+
calls += 1;
|
|
158
|
+
writeFileSync(process.env.REQUEST_STARTED, 'started');
|
|
159
|
+
while (!(await (await import('node:fs/promises')).stat(process.env.PEER_WRITTEN).catch(() => null))) {
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
status: 400,
|
|
165
|
+
async text() { return JSON.stringify({ error: 'invalid_grant' }); }
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
const tokens = await provider.ensureAuth();
|
|
169
|
+
writeFileSync(process.env.RESULT_PATH, JSON.stringify({
|
|
170
|
+
adopted: tokens.access_token === 'fixture-peer-access',
|
|
171
|
+
calls
|
|
172
|
+
}));
|
|
173
|
+
`;
|
|
174
|
+
const child = spawn(process.execPath, ['--input-type=module', '--eval', childSource], {
|
|
175
|
+
env: {
|
|
176
|
+
...process.env,
|
|
177
|
+
MIXDOG_DATA_DIR: root,
|
|
178
|
+
TOKEN_PATH: tokenPath,
|
|
179
|
+
REQUEST_STARTED: requestStarted,
|
|
180
|
+
PEER_WRITTEN: peerWritten,
|
|
181
|
+
RESULT_PATH: resultPath,
|
|
182
|
+
},
|
|
183
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
184
|
+
});
|
|
185
|
+
const exit = waitForExit(child, 'invalid_grant child');
|
|
186
|
+
const deadline = Date.now() + 30_000;
|
|
187
|
+
while (!existsSync(requestStarted)) {
|
|
188
|
+
if (Date.now() >= deadline) throw new Error('invalid_grant request did not start');
|
|
189
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
writeTokens('fixture-peer-access', 'fixture-peer-refresh', Date.now() + 10 * 60_000);
|
|
193
|
+
writeFileSync(peerWritten, 'written');
|
|
194
|
+
await exit;
|
|
195
|
+
|
|
196
|
+
assert.deepEqual(JSON.parse(readFileSync(resultPath, 'utf8')), { adopted: true, calls: 1 });
|
|
197
|
+
assert.ok(statSync(tokenPath).size > 0);
|
|
198
|
+
});
|