mixdog 0.9.46 → 0.9.49
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 +14 -4
- package/scripts/agent-parallel-smoke.mjs +4 -1
- package/scripts/agent-route-batch-test.mjs +40 -0
- package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
- package/scripts/code-graph-description-contract.mjs +185 -0
- package/scripts/code-graph-disk-hit-test.mjs +55 -0
- package/scripts/code-graph-dispatch-test.mjs +96 -0
- package/scripts/compact-pressure-test.mjs +40 -0
- package/scripts/deferred-tool-loading-test.mjs +233 -0
- package/scripts/execution-completion-dedup-test.mjs +48 -0
- package/scripts/explore-prompt-policy-test.mjs +88 -3
- package/scripts/live-worker-smoke.mjs +68 -16
- package/scripts/memory-core-input-test.mjs +33 -13
- package/scripts/native-edit-wire-test.mjs +152 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
- package/scripts/patch-binary-cache-test.mjs +181 -0
- package/scripts/prompt-immediate-render-test.mjs +89 -0
- package/scripts/provider-toolcall-test.mjs +280 -15
- package/scripts/shell-failure-diagnostics-test.mjs +211 -0
- package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
- package/scripts/streaming-tail-window-test.mjs +29 -0
- package/scripts/tool-failures.mjs +21 -3
- package/scripts/tool-smoke.mjs +263 -38
- package/scripts/tool-tui-presentation-test.mjs +17 -1
- package/scripts/tui-perf-run.ps1 +26 -0
- package/scripts/tui-transcript-perf-test.mjs +7 -1
- package/scripts/verify-release-assets-test.mjs +647 -0
- package/scripts/verify-release-assets.mjs +293 -0
- package/scripts/windows-hide-spawn-options-test.mjs +19 -0
- package/src/cli.mjs +1 -0
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
- package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +2 -3
- package/src/session-runtime/context-status.mjs +25 -16
- package/src/session-runtime/model-route-api.mjs +2 -0
- package/src/session-runtime/runtime-core.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +4 -1
- package/src/session-runtime/tool-catalog.mjs +113 -19
- package/src/session-runtime/tool-defs.mjs +1 -0
- package/src/standalone/agent-tool/helpers.mjs +25 -6
- package/src/standalone/agent-tool.mjs +75 -41
- package/src/tui/App.jsx +4 -0
- package/src/tui/app/input-parsers.mjs +8 -9
- package/src/tui/components/Markdown.jsx +6 -1
- package/src/tui/components/Message.jsx +11 -2
- package/src/tui/components/PromptInput.jsx +19 -21
- package/src/tui/components/Spinner.jsx +4 -4
- package/src/tui/components/StatusLine.jsx +6 -6
- package/src/tui/components/TranscriptItem.jsx +2 -2
- package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
- package/src/tui/dist/index.mjs +130 -45
- package/src/tui/engine/agent-job-feed.mjs +21 -2
- package/src/tui/engine/turn.mjs +12 -1
- package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +20 -0
- package/src/ui/statusline.mjs +5 -5
- package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { classifyToolFailure } from '../src/runtime/agent/orchestrator/agent-trace-format.mjs';
|
|
8
|
+
import { ExecResult, execShellCommand } from '../src/runtime/agent/orchestrator/tools/shell-command.mjs';
|
|
9
|
+
import { _composeShellFailure, _shellFailureStatus } from '../src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs';
|
|
10
|
+
|
|
11
|
+
test('shell trace classification uses only the leading status marker', () => {
|
|
12
|
+
assert.equal(classifyToolFailure(
|
|
13
|
+
'Error: [shell-run-failed] [exit code: 1]\n\ncommand timed out while parsing an aborted field',
|
|
14
|
+
'shell',
|
|
15
|
+
), 'command-exit');
|
|
16
|
+
assert.equal(classifyToolFailure(
|
|
17
|
+
'Error: [shell-run-failed] [signal: SIGKILL]\n\n(no output)',
|
|
18
|
+
'shell',
|
|
19
|
+
), 'process/signal');
|
|
20
|
+
assert.equal(classifyToolFailure(
|
|
21
|
+
'Error: [shell-run-failed] [timeout: 500ms signal: SIGTERM cause: timeout]',
|
|
22
|
+
'shell',
|
|
23
|
+
), 'timeout/abort');
|
|
24
|
+
assert.equal(classifyToolFailure(
|
|
25
|
+
'Error: [shell-run-failed] [signal: SIGTERM cause: cancellation]',
|
|
26
|
+
'shell',
|
|
27
|
+
), 'timeout/abort');
|
|
28
|
+
assert.equal(classifyToolFailure(
|
|
29
|
+
'Error: [shell-run-failed] [signal: SIGKILL cause: output-limit]',
|
|
30
|
+
'shell',
|
|
31
|
+
), 'runtime/failure');
|
|
32
|
+
assert.equal(classifyToolFailure(
|
|
33
|
+
'⚠️ destructive command warning\nError: [shell-run-failed] [signal: SIGKILL]',
|
|
34
|
+
'shell',
|
|
35
|
+
), 'process/signal');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('shell failure rendering preserves actual signals and runtime kill causes', () => {
|
|
39
|
+
const status = (opts) => _shellFailureStatus(new ExecResult({
|
|
40
|
+
stdout: '', stderr: '', exitCode: null, taskId: 'test', ...opts,
|
|
41
|
+
}), 500).statusDetail;
|
|
42
|
+
assert.match(status({ signal: 'SIGKILL' }), /^\[signal: SIGKILL\]$/);
|
|
43
|
+
assert.match(status({ signal: 'SIGTERM', killed: true, killCause: 'cancellation' }),
|
|
44
|
+
/^\[signal: SIGTERM cause: cancellation\]$/);
|
|
45
|
+
assert.match(status({ signal: 'SIGTERM', killed: true, timedOut: true, killCause: 'timeout' }),
|
|
46
|
+
/^\[timeout: 500ms signal: SIGTERM cause: timeout\]/);
|
|
47
|
+
assert.match(status({
|
|
48
|
+
killed: true,
|
|
49
|
+
killCause: 'output-capture-error',
|
|
50
|
+
outputCaptureError: new Error('disk full'),
|
|
51
|
+
}), /^\[output capture failed cause: output-capture-error signal: SIGKILL\]$/);
|
|
52
|
+
assert.match(status({ signal: 'SIGKILL', killed: true, killCause: 'output-limit' }),
|
|
53
|
+
/^\[signal: SIGKILL cause: output-limit\]$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('WMIC rewrite note follows the leading shell failure marker', () => {
|
|
57
|
+
const rendered = _composeShellFailure(
|
|
58
|
+
'[shell-run-failed] [exit code: 1]',
|
|
59
|
+
'Error: ',
|
|
60
|
+
'[auto-rewrite: deprecated wmic process query -> PowerShell; timeout capped at 30000ms]',
|
|
61
|
+
'(no output)',
|
|
62
|
+
);
|
|
63
|
+
assert.match(rendered, /^Error: \[shell-run-failed\] \[exit code: 1\]\n\[auto-rewrite:/);
|
|
64
|
+
assert.equal(classifyToolFailure(rendered, 'shell'), 'command-exit');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function withoutUnhandledProcessFailure(run) {
|
|
68
|
+
const uncaught = [];
|
|
69
|
+
const rejected = [];
|
|
70
|
+
const onUncaught = (err) => uncaught.push(err);
|
|
71
|
+
const onRejected = (err) => rejected.push(err);
|
|
72
|
+
process.on('uncaughtException', onUncaught);
|
|
73
|
+
process.on('unhandledRejection', onRejected);
|
|
74
|
+
try {
|
|
75
|
+
const result = await run();
|
|
76
|
+
await new Promise((resolveTurn) => setImmediate(resolveTurn));
|
|
77
|
+
assert.deepEqual(uncaught, [], `unexpected uncaught error: ${uncaught[0]?.stack || uncaught[0]}`);
|
|
78
|
+
assert.deepEqual(rejected, [], `unexpected unhandled rejection: ${rejected[0]?.stack || rejected[0]}`);
|
|
79
|
+
return result;
|
|
80
|
+
} finally {
|
|
81
|
+
process.removeListener('uncaughtException', onUncaught);
|
|
82
|
+
process.removeListener('unhandledRejection', onRejected);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertSpawnToolFailure(result) {
|
|
87
|
+
assert.equal(result.failurePhase, 'tool');
|
|
88
|
+
assert.equal(result.failureReason, 'spawn failed');
|
|
89
|
+
const status = _shellFailureStatus(result, 1000);
|
|
90
|
+
assert.equal(status.shellToolFailed, true);
|
|
91
|
+
const rendered = _composeShellFailure(
|
|
92
|
+
`[shell-tool-failed] ${status.statusDetail}`,
|
|
93
|
+
'Error: ',
|
|
94
|
+
'',
|
|
95
|
+
result.stderr,
|
|
96
|
+
);
|
|
97
|
+
assert.match(rendered, /^Error: \[shell-tool-failed\] \[spawn failed\]/);
|
|
98
|
+
assert.equal(classifyToolFailure(rendered, 'shell'), 'tool-call/failure');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test('asynchronous ENOENT spawn errors remain shell tool failures', async () => {
|
|
102
|
+
const missing = await withoutUnhandledProcessFailure(() => execShellCommand({
|
|
103
|
+
shell: join(tmpdir(), `mixdog-missing-shell-${process.pid}`),
|
|
104
|
+
shellArg: '-c',
|
|
105
|
+
command: 'echo unreachable',
|
|
106
|
+
env: process.env,
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
timeoutMs: 1000,
|
|
109
|
+
}));
|
|
110
|
+
assertSpawnToolFailure(missing);
|
|
111
|
+
assert.match(missing.stderr, /ENOENT|not found/i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('asynchronous EACCES spawn errors remain shell tool failures', async (t) => {
|
|
115
|
+
if (process.platform === 'win32') return t.skip('executable-bit case is POSIX-only');
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixdog-eacces-shell-'));
|
|
117
|
+
try {
|
|
118
|
+
const denied = join(dir, 'denied.sh');
|
|
119
|
+
writeFileSync(denied, '#!/bin/sh\necho unreachable\n');
|
|
120
|
+
chmodSync(denied, 0o600);
|
|
121
|
+
const result = await withoutUnhandledProcessFailure(() => execShellCommand({
|
|
122
|
+
shell: denied,
|
|
123
|
+
shellArg: '-c',
|
|
124
|
+
command: 'echo unreachable',
|
|
125
|
+
env: process.env,
|
|
126
|
+
cwd: process.cwd(),
|
|
127
|
+
timeoutMs: 1000,
|
|
128
|
+
}));
|
|
129
|
+
assertSpawnToolFailure(result);
|
|
130
|
+
assert.match(result.stderr, /EACCES|permission denied/i);
|
|
131
|
+
} finally {
|
|
132
|
+
rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('execShellCommand carries cancellation cause alongside process signal', async () => {
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const isWindows = process.platform === 'win32';
|
|
139
|
+
const promise = execShellCommand({
|
|
140
|
+
shell: isWindows ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh',
|
|
141
|
+
shellArg: isWindows ? '/c' : '-c',
|
|
142
|
+
command: isWindows ? 'ping 127.0.0.1 -n 20 > nul' : 'sleep 10',
|
|
143
|
+
env: process.env,
|
|
144
|
+
cwd: process.cwd(),
|
|
145
|
+
timeoutMs: 5000,
|
|
146
|
+
abortSignal: controller.signal,
|
|
147
|
+
backgroundOnTimeout: false,
|
|
148
|
+
});
|
|
149
|
+
setTimeout(() => controller.abort(), 100);
|
|
150
|
+
const result = await promise;
|
|
151
|
+
assert.equal(result.killed, true);
|
|
152
|
+
assert.equal(result.killCause, 'cancellation');
|
|
153
|
+
assert.ok(result.signal || process.platform === 'win32');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('cancellation racing with auto-background adoption is returned as cancelled', async () => {
|
|
157
|
+
let abortReads = 0;
|
|
158
|
+
const racingSignal = {
|
|
159
|
+
get aborted() { abortReads += 1; return abortReads >= 2; },
|
|
160
|
+
addEventListener() {},
|
|
161
|
+
removeEventListener() {},
|
|
162
|
+
};
|
|
163
|
+
const isWindows = process.platform === 'win32';
|
|
164
|
+
const result = await execShellCommand({
|
|
165
|
+
shell: isWindows ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh',
|
|
166
|
+
shellArg: isWindows ? '/c' : '-c',
|
|
167
|
+
command: isWindows ? 'ping 127.0.0.1 -n 20 > nul' : 'sleep 10',
|
|
168
|
+
env: process.env,
|
|
169
|
+
cwd: process.cwd(),
|
|
170
|
+
timeoutMs: 5000,
|
|
171
|
+
abortSignal: racingSignal,
|
|
172
|
+
autoBackgroundMs: 25,
|
|
173
|
+
backgroundOnTimeout: false,
|
|
174
|
+
});
|
|
175
|
+
assert.equal(result.backgrounded, false);
|
|
176
|
+
assert.equal(result.killed, true);
|
|
177
|
+
assert.equal(result.killCause, 'cancellation');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('tool-failures separates actionable totals while retaining command-exit rows', () => {
|
|
181
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixdog-tool-failures-test-'));
|
|
182
|
+
try {
|
|
183
|
+
const history = join(dir, 'history');
|
|
184
|
+
mkdirSync(history);
|
|
185
|
+
const rows = [
|
|
186
|
+
{ ts: 1, tool_name: 'shell', category: 'process/signal', error_first_line: 'SIGKILL' },
|
|
187
|
+
{ ts: 2, tool_name: 'shell', category: 'runtime/failure', error_first_line: 'capture guard' },
|
|
188
|
+
...Array.from({ length: 45 }, (_, index) => ({
|
|
189
|
+
ts: index + 3,
|
|
190
|
+
tool_name: 'shell',
|
|
191
|
+
category: 'command-exit',
|
|
192
|
+
error_first_line: `exit ${index}`,
|
|
193
|
+
})),
|
|
194
|
+
];
|
|
195
|
+
writeFileSync(join(history, 'tool-failures.jsonl'), `${rows.map(JSON.stringify).join('\n')}\n`);
|
|
196
|
+
const script = resolve('scripts/tool-failures.mjs');
|
|
197
|
+
const text = spawnSync(process.execPath, [script, '--data-dir', dir, '--limit', '2'], { encoding: 'utf8' });
|
|
198
|
+
assert.equal(text.status, 0, text.stderr);
|
|
199
|
+
assert.match(text.stdout, /actionable failures: 2\/2 shown/);
|
|
200
|
+
assert.match(text.stdout, /command exits: 2\/45 shown \(retained\)/);
|
|
201
|
+
assert.equal((text.stdout.match(/^- /gm) || []).length, 4);
|
|
202
|
+
const json = spawnSync(process.execPath, [script, '--data-dir', dir, '--limit', '2', '--json'], { encoding: 'utf8' });
|
|
203
|
+
assert.equal(json.status, 0, json.stderr);
|
|
204
|
+
const report = JSON.parse(json.stdout);
|
|
205
|
+
assert.deepEqual(report.actionable_failures, { shown: 2, matched: 2 });
|
|
206
|
+
assert.deepEqual(report.command_exits, { shown: 2, matched: 45 });
|
|
207
|
+
assert.equal(report.rows.length, 4);
|
|
208
|
+
} finally {
|
|
209
|
+
rmSync(dir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
acceptQuotaSnapshot,
|
|
5
|
+
fallbackStatusline,
|
|
6
|
+
resolveContextUsedPct,
|
|
7
|
+
} from '../src/ui/statusline.mjs';
|
|
4
8
|
|
|
5
9
|
// Monotonic hysteresis for the 5H/7D usage segment: once a value has rendered,
|
|
6
10
|
// an OLDER shared-cache snapshot (written by another mixdog instance) must not
|
|
@@ -35,3 +39,24 @@ test('own-instance live data always wins; empty timestamps preserve prior behavi
|
|
|
35
39
|
// Shared→shared same asOf → accept (idempotent refresh).
|
|
36
40
|
assert.equal(acceptQuotaSnapshot({ asOf: 2000, owned: false }, { asOf: 2000, owned: false }), true);
|
|
37
41
|
});
|
|
42
|
+
|
|
43
|
+
test('context percentage uses compaction pressure over trigger and ignores gateway boundary percent', () => {
|
|
44
|
+
const args = {
|
|
45
|
+
stats: {
|
|
46
|
+
currentEstimatedContextTokens: 75_000,
|
|
47
|
+
currentContextSource: 'estimated',
|
|
48
|
+
},
|
|
49
|
+
contextWindow: 100_000,
|
|
50
|
+
compactBoundaryTokens: 100_000,
|
|
51
|
+
autoCompactTokenLimit: 75_000,
|
|
52
|
+
};
|
|
53
|
+
assert.equal(resolveContextUsedPct({
|
|
54
|
+
...args,
|
|
55
|
+
gatewayStatus: { contextUsedPct: 75 },
|
|
56
|
+
}), 100);
|
|
57
|
+
assert.match(
|
|
58
|
+
fallbackStatusline({ provider: 'openai', model: 'test', ...args })
|
|
59
|
+
.replace(/\x1b\[[0-9;]*m/g, ''),
|
|
60
|
+
/\b100%/,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
windowPlainStreamingText,
|
|
6
|
+
} from '../src/tui/markdown/streaming-markdown.mjs';
|
|
7
|
+
import {
|
|
8
|
+
measureStreamingMarkdownRenderedRows,
|
|
9
|
+
} from '../src/tui/markdown/measure-rendered-rows.mjs';
|
|
10
|
+
|
|
11
|
+
test('bottom-pinned plain streaming text keeps only the visible suffix', () => {
|
|
12
|
+
const lines = Array.from({ length: 100 }, (_, index) => `plain line ${index}`);
|
|
13
|
+
const full = lines.join('\n');
|
|
14
|
+
const windowed = windowPlainStreamingText(full, 80, 12);
|
|
15
|
+
|
|
16
|
+
assert.equal(windowed, lines.slice(-12).join('\n'));
|
|
17
|
+
assert.ok(measureStreamingMarkdownRenderedRows(full, 83, 'plain-window-full') > 12);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('markdown streaming text is never internally windowed', () => {
|
|
21
|
+
const markdown = `${'plain history\n'.repeat(100)}**live markdown**`;
|
|
22
|
+
assert.equal(windowPlainStreamingText(markdown, 80, 12), markdown.trim());
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('wrapped plain lines consume the streaming row budget', () => {
|
|
26
|
+
const lines = ['old', 'x'.repeat(160), 'new'];
|
|
27
|
+
assert.equal(windowPlainStreamingText(lines.join('\n'), 80, 2), lines.slice(-1).join('\n'));
|
|
28
|
+
assert.equal(windowPlainStreamingText(lines.join('\n'), 80, 3), lines.slice(-2).join('\n'));
|
|
29
|
+
});
|
|
@@ -94,20 +94,33 @@ const rows = files.flatMap(readRows)
|
|
|
94
94
|
.filter((row) => !agentFilter || String(row.agent || '-') === agentFilter)
|
|
95
95
|
.filter((row) => !categoryFilter || rowCategory(row) === categoryFilter)
|
|
96
96
|
.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
|
|
97
|
-
const
|
|
97
|
+
const isCommandExit = (row) => rowCategory(row) === 'command-exit';
|
|
98
|
+
const actionableRows = rows.filter((row) => !isCommandExit(row));
|
|
99
|
+
const commandExitRows = rows.filter(isCommandExit);
|
|
100
|
+
// Limit each partition independently so a burst of ordinary command exits
|
|
101
|
+
// cannot crowd runtime/actionable failures out of the displayed report.
|
|
102
|
+
const actionableRecent = actionableRows.slice(-limit);
|
|
103
|
+
const commandExitRecent = commandExitRows.slice(-limit);
|
|
104
|
+
const recent = [...actionableRecent, ...commandExitRecent]
|
|
105
|
+
.sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
|
|
98
106
|
const byTool = new Map();
|
|
99
107
|
const byCategory = new Map();
|
|
108
|
+
const actionableByTool = new Map();
|
|
109
|
+
const commandExitByTool = new Map();
|
|
100
110
|
for (const row of recent) {
|
|
101
111
|
const tool = rowTool(row);
|
|
102
112
|
const category = rowCategory(row);
|
|
103
113
|
inc(byTool, tool);
|
|
104
114
|
inc(byCategory, `${tool} / ${category}`);
|
|
115
|
+
inc(isCommandExit(row) ? commandExitByTool : actionableByTool, tool);
|
|
105
116
|
}
|
|
106
117
|
|
|
107
118
|
if (jsonMode) {
|
|
108
119
|
console.log(JSON.stringify({
|
|
109
120
|
shown: recent.length,
|
|
110
121
|
matched: rows.length,
|
|
122
|
+
actionable_failures: { shown: actionableRecent.length, matched: actionableRows.length },
|
|
123
|
+
command_exits: { shown: commandExitRecent.length, matched: commandExitRows.length },
|
|
111
124
|
since: sinceTs ? new Date(sinceTs).toISOString() : null,
|
|
112
125
|
filters: {
|
|
113
126
|
tool: toolFilter,
|
|
@@ -116,13 +129,17 @@ if (jsonMode) {
|
|
|
116
129
|
},
|
|
117
130
|
sources: files.filter(existsSync),
|
|
118
131
|
tools: Object.fromEntries([...byTool.entries()].sort((a, b) => b[1] - a[1])),
|
|
132
|
+
actionable_tools: Object.fromEntries([...actionableByTool.entries()].sort((a, b) => b[1] - a[1])),
|
|
133
|
+
command_exit_tools: Object.fromEntries([...commandExitByTool.entries()].sort((a, b) => b[1] - a[1])),
|
|
119
134
|
categories: Object.fromEntries([...byCategory.entries()].sort((a, b) => b[1] - a[1])),
|
|
120
135
|
rows: recent,
|
|
121
136
|
}, null, 2));
|
|
122
137
|
process.exit(0);
|
|
123
138
|
}
|
|
124
139
|
|
|
125
|
-
console.log(`
|
|
140
|
+
console.log(`actionable failures: ${actionableRecent.length}/${actionableRows.length} shown`);
|
|
141
|
+
console.log(`command exits: ${commandExitRecent.length}/${commandExitRows.length} shown (retained)`);
|
|
142
|
+
console.log(`rows: ${recent.length}/${rows.length} shown`);
|
|
126
143
|
if (sinceTs) console.log(`since: ${new Date(sinceTs).toISOString()}`);
|
|
127
144
|
const filterParts = [
|
|
128
145
|
toolFilter ? `tool=${toolFilter}` : '',
|
|
@@ -131,7 +148,8 @@ const filterParts = [
|
|
|
131
148
|
].filter(Boolean);
|
|
132
149
|
if (filterParts.length) console.log(`filters: ${filterParts.join(', ')}`);
|
|
133
150
|
if (files.length > 0) console.log(`sources: ${files.filter(existsSync).join(', ') || '(none)'}`);
|
|
134
|
-
console.log(`tools: ${[...
|
|
151
|
+
console.log(`actionable tools: ${[...actionableByTool.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(', ') || '(none)'}`);
|
|
152
|
+
console.log(`command-exit tools: ${[...commandExitByTool.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(', ') || '(none)'}`);
|
|
135
153
|
console.log(`categories: ${[...byCategory.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(', ') || '(none)'}`);
|
|
136
154
|
for (const row of recent) {
|
|
137
155
|
const tool = rowTool(row);
|