mixdog 0.9.15 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/atomic-lock-tryonce-test.mjs +66 -0
- package/scripts/explore-bench.mjs +5 -4
- package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
- package/scripts/output-style-smoke.mjs +3 -3
- package/scripts/provider-toolcall-test.mjs +79 -2
- package/scripts/termio-input-smoke.mjs +199 -0
- package/scripts/tool-efficiency-diag.mjs +88 -0
- package/scripts/tool-smoke.mjs +40 -24
- package/scripts/tui-frame-harness-shim.mjs +32 -0
- package/scripts/tui-frame-harness.mjs +306 -0
- package/src/agents/heavy-worker/AGENT.md +6 -7
- package/src/agents/worker/AGENT.md +6 -7
- package/src/lib/keychain-cjs.cjs +28 -11
- package/src/lib/rules-builder.cjs +6 -10
- package/src/mixdog-session-runtime.mjs +102 -20
- package/src/output-styles/simple.md +22 -24
- package/src/rules/agent/00-core.md +12 -11
- package/src/rules/agent/30-explorer.md +22 -21
- package/src/rules/lead/01-general.md +8 -8
- package/src/rules/lead/lead-brief.md +11 -12
- package/src/rules/shared/01-tool.md +25 -14
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +31 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +30 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +37 -19
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
- package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +118 -22
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -25
- package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
- package/src/runtime/channels/backends/telegram.mjs +29 -9
- package/src/runtime/channels/lib/event-queue.mjs +6 -2
- package/src/runtime/channels/lib/memory-client.mjs +66 -1
- package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
- package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
- package/src/runtime/memory/index.mjs +132 -8
- package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
- package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
- package/src/runtime/memory/lib/memory-embed.mjs +48 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
- package/src/runtime/memory/lib/memory.mjs +88 -1
- package/src/runtime/memory/lib/session-ingest.mjs +34 -3
- package/src/runtime/memory/lib/trace-store.mjs +52 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
- package/src/runtime/shared/atomic-file.mjs +110 -0
- package/src/runtime/shared/child-guardian.mjs +4 -2
- package/src/runtime/shared/open-url.mjs +2 -1
- package/src/runtime/shared/singleton-owner.mjs +26 -0
- package/src/runtime/shared/tool-execution-contract.mjs +25 -0
- package/src/runtime/shared/transcript-writer.mjs +49 -5
- package/src/session-runtime/config-helpers.mjs +7 -2
- package/src/session-runtime/cwd-plugins.mjs +6 -1
- package/src/session-runtime/effort.mjs +6 -1
- package/src/session-runtime/plugin-mcp.mjs +116 -12
- package/src/session-runtime/provider-models.mjs +47 -8
- package/src/session-runtime/settings-api.mjs +7 -1
- package/src/standalone/channel-worker.mjs +25 -3
- package/src/standalone/explore-tool.mjs +5 -5
- package/src/standalone/hook-bus/config.mjs +38 -2
- package/src/standalone/hook-bus/handlers.mjs +103 -11
- package/src/standalone/hook-bus.mjs +20 -6
- package/src/standalone/memory-runtime-proxy.mjs +40 -5
- package/src/tui/App.jsx +214 -30
- package/src/tui/app/core-memory-picker.mjs +6 -6
- package/src/tui/app/model-options.mjs +10 -4
- package/src/tui/app/settings-picker.mjs +5 -30
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/text-layout.mjs +57 -0
- package/src/tui/app/transcript-window.mjs +162 -2
- package/src/tui/app/use-mouse-input.mjs +60 -47
- package/src/tui/app/use-transcript-window.mjs +96 -17
- package/src/tui/components/PromptInput.jsx +18 -1
- package/src/tui/components/StatusLine.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +97 -33
- package/src/tui/dist/index.mjs +856 -288
- package/src/tui/engine/tool-card-results.mjs +22 -5
- package/src/tui/engine/tui-steering-persist.mjs +66 -35
- package/src/tui/engine.mjs +41 -9
- package/src/tui/index.jsx +101 -7
- package/src/ui/statusline-segments.mjs +54 -36
- package/src/ui/statusline.mjs +141 -95
- package/src/workflows/default/WORKFLOW.md +27 -31
- package/vendor/ink/build/components/App.js +62 -17
- package/vendor/ink/build/ink.js +78 -3
- package/vendor/ink/build/input-parser.d.ts +9 -4
- package/vendor/ink/build/input-parser.js +45 -176
- package/vendor/ink/build/log-update.js +47 -2
- package/vendor/ink/build/termio-keypress.js +240 -0
- package/vendor/ink/build/termio-tokenize.js +253 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.17",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"smoke:freevars": "node scripts/freevar-smoke.mjs",
|
|
47
47
|
"test:toolcall": "node --test scripts/toolcall-args-test.mjs",
|
|
48
48
|
"test:providers": "node --test scripts/provider-toolcall-test.mjs",
|
|
49
|
+
"test:atomiclock": "node --test scripts/atomic-lock-tryonce-test.mjs",
|
|
49
50
|
"failures": "node scripts/tool-failures.mjs",
|
|
50
51
|
"trace:llm": "node scripts/llm-trace-summary.mjs",
|
|
51
52
|
"diag:sessions": "node scripts/session-diag.mjs",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Proves try-once (timeoutMs:0) lock behavior: when the lock is already held,
|
|
2
|
+
// withFileLockSync/withFileLock return IMMEDIATELY with ELOCKCONTENDED and
|
|
3
|
+
// never sleep (no Atomics.wait / setTimeout backoff). Also asserts sync+async
|
|
4
|
+
// lock interop: neither can enter the critical section while the other holds.
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
withFileLockSync,
|
|
12
|
+
withFileLock,
|
|
13
|
+
} from '../src/runtime/shared/atomic-file.mjs';
|
|
14
|
+
|
|
15
|
+
function tmpLock() {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixlock-'));
|
|
17
|
+
return { dir, lockPath: join(dir, 't.lock') };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('try-once sync throws ELOCKCONTENDED without sleeping when held', () => {
|
|
21
|
+
const { dir, lockPath } = tmpLock();
|
|
22
|
+
try {
|
|
23
|
+
withFileLockSync(lockPath, () => {
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
assert.throws(
|
|
26
|
+
() => withFileLockSync(lockPath, () => 'unreachable', { timeoutMs: 0 }),
|
|
27
|
+
(e) => e?.code === 'ELOCKCONTENDED',
|
|
28
|
+
);
|
|
29
|
+
assert.ok(Date.now() - started < 20, `try-once slept ${Date.now() - started}ms`);
|
|
30
|
+
});
|
|
31
|
+
} finally {
|
|
32
|
+
rmSync(dir, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('try-once async rejects ELOCKCONTENDED without sleeping when held', async () => {
|
|
37
|
+
const { dir, lockPath } = tmpLock();
|
|
38
|
+
try {
|
|
39
|
+
await withFileLockSync(lockPath, async () => {
|
|
40
|
+
const started = Date.now();
|
|
41
|
+
await assert.rejects(
|
|
42
|
+
withFileLock(lockPath, () => 'unreachable', { timeoutMs: 0 }),
|
|
43
|
+
(e) => e?.code === 'ELOCKCONTENDED',
|
|
44
|
+
);
|
|
45
|
+
assert.ok(Date.now() - started < 20, `try-once slept ${Date.now() - started}ms`);
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('async holder blocks sync try-once, then sync acquires after release', async () => {
|
|
53
|
+
const { dir, lockPath } = tmpLock();
|
|
54
|
+
try {
|
|
55
|
+
await withFileLock(lockPath, () => {
|
|
56
|
+
assert.throws(
|
|
57
|
+
() => withFileLockSync(lockPath, () => 'unreachable', { timeoutMs: 0 }),
|
|
58
|
+
(e) => e?.code === 'ELOCKCONTENDED',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
const val = withFileLockSync(lockPath, () => 7, { timeoutMs: 0 });
|
|
62
|
+
assert.equal(val, 7);
|
|
63
|
+
} finally {
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -88,8 +88,9 @@ function readTraceSince(ts) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const t0 = Date.now();
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// Parallel round: all queries fire at once. Per-session trace attribution is
|
|
92
|
+
// by session_id so counts stay correct; per-query ms includes queueing.
|
|
93
|
+
const results = await Promise.all(QUERIES.map(async ({ q, expect, expectFail }) => {
|
|
93
94
|
const qt0 = Date.now();
|
|
94
95
|
const res = await runExplore({ query: q, cwd: CWD }, { callerCwd: CWD });
|
|
95
96
|
const text = res?.content?.[0]?.text || '';
|
|
@@ -99,8 +100,8 @@ for (const { q, expect, expectFail } of QUERIES) {
|
|
|
99
100
|
const hit = expectFail
|
|
100
101
|
? (failed && anchors === 0)
|
|
101
102
|
: (!failed && (expect || []).some((e) => lower.includes(e.toLowerCase())));
|
|
102
|
-
|
|
103
|
-
}
|
|
103
|
+
return { q: q.slice(0, 48), ms: Date.now() - qt0, anchors, failed, hit, bytes: text.length };
|
|
104
|
+
}));
|
|
104
105
|
|
|
105
106
|
// Attribute tool calls per explorer session spawned during this run.
|
|
106
107
|
const rows = readTraceSince(t0);
|
|
@@ -56,6 +56,25 @@ const summaryRow = { role: 'user', content: `${SUMMARY_PREFIX}\nmessages=10 sha2
|
|
|
56
56
|
const ackRow = { role: 'assistant', content: '.' }
|
|
57
57
|
// (vi) internal runtime nudge user row.
|
|
58
58
|
const nudgeRow = { role: 'user', content: '[mixdog-runtime] Your previous response was empty. Continue.' }
|
|
59
|
+
// (vi-b) model-visible tool-completion wrapper row (mgr.enqueuePendingMessage
|
|
60
|
+
// mirror of modelVisibleToolCompletionMessage — the transcript-watcher bug
|
|
61
|
+
// this change fixes: these were previously persisted verbatim).
|
|
62
|
+
const completionWrapperRow = {
|
|
63
|
+
role: 'user',
|
|
64
|
+
content:
|
|
65
|
+
'The async shell task task_123 has finished (completed) - review this result in your next step.\n\n' +
|
|
66
|
+
'Result:\n> background task\n> task_id: task_123\n> status: completed',
|
|
67
|
+
}
|
|
68
|
+
// (vi-c) UNQUOTED persisted form — taken from a real DB row. No `> ` prefix
|
|
69
|
+
// on the Result body; isModelVisibleToolCompletionWrapper alone misses this
|
|
70
|
+
// shape, so shouldExcludeIngestMessage falls back to the instruction-head
|
|
71
|
+
// regex (isUnquotedToolCompletionHead).
|
|
72
|
+
const unquotedCompletionRow = {
|
|
73
|
+
role: 'user',
|
|
74
|
+
content:
|
|
75
|
+
'The async shell task task_456 has finished (completed, exit 0) - review this result in your next step.\n' +
|
|
76
|
+
'Result:\nbackground task\ntask_id: task_456\nstatus: completed',
|
|
77
|
+
}
|
|
59
78
|
// (vii) normal human + normal assistant.
|
|
60
79
|
const normalUser = { role: 'user', content: '안녕' }
|
|
61
80
|
const normalAsst = { role: 'assistant', content: '네 안녕하세요' }
|
|
@@ -67,6 +86,8 @@ const before = {
|
|
|
67
86
|
summaryRow: summaryRow.content,
|
|
68
87
|
ackRow: ackRow.content,
|
|
69
88
|
nudgeRow: nudgeRow.content,
|
|
89
|
+
completionWrapperRow: completionWrapperRow.content,
|
|
90
|
+
unquotedCompletionRow: unquotedCompletionRow.content,
|
|
70
91
|
normalUser: normalUser.content,
|
|
71
92
|
normalAsst: normalAsst.content,
|
|
72
93
|
}
|
|
@@ -78,6 +99,8 @@ const after = {
|
|
|
78
99
|
summaryRow: ingestRow(summaryRow),
|
|
79
100
|
ackRow: ingestRow(ackRow),
|
|
80
101
|
nudgeRow: ingestRow(nudgeRow),
|
|
102
|
+
completionWrapperRow: ingestRow(completionWrapperRow),
|
|
103
|
+
unquotedCompletionRow: ingestRow(unquotedCompletionRow),
|
|
81
104
|
normalUser: ingestRow(normalUser),
|
|
82
105
|
normalAsst: ingestRow(normalAsst),
|
|
83
106
|
}
|
|
@@ -96,6 +119,8 @@ assert(after.referenceRow === null, 'Reference files row must be excluded')
|
|
|
96
119
|
assert(after.summaryRow === null, 'SUMMARY_PREFIX row must be excluded')
|
|
97
120
|
assert(after.ackRow === null, '`.` ack row must be excluded')
|
|
98
121
|
assert(after.nudgeRow === null, '[mixdog-runtime] nudge row must be excluded')
|
|
122
|
+
assert(after.completionWrapperRow === null, 'model-visible tool-completion wrapper row must be excluded')
|
|
123
|
+
assert(after.unquotedCompletionRow === null, 'unquoted tool-completion wrapper row (real DB shape) must be excluded')
|
|
99
124
|
|
|
100
125
|
// (vii) normal conversation survives intact.
|
|
101
126
|
assert(after.normalUser === '안녕', 'normal human text must survive intact')
|
|
@@ -136,6 +161,8 @@ const rows = [
|
|
|
136
161
|
['(iv) SUMMARY_PREFIX', before.summaryRow, after.summaryRow],
|
|
137
162
|
['(v) `.` ack', before.ackRow, after.ackRow],
|
|
138
163
|
['(vi) [mixdog-runtime] nudge', before.nudgeRow, after.nudgeRow],
|
|
164
|
+
['(vi-b) tool-completion wrapper', before.completionWrapperRow, after.completionWrapperRow],
|
|
165
|
+
['(vi-c) unquoted tool-completion (real DB shape)', before.unquotedCompletionRow, after.unquotedCompletionRow],
|
|
139
166
|
['(vii) normal human', before.normalUser, after.normalUser],
|
|
140
167
|
['(vii) normal assistant', before.normalAsst, after.normalAsst],
|
|
141
168
|
]
|
|
@@ -69,7 +69,7 @@ for (const required of [
|
|
|
69
69
|
'Outcome-first concise handoffs for coding work',
|
|
70
70
|
'Practical concise — outcome-first handoffs',
|
|
71
71
|
'file_path:line_number',
|
|
72
|
-
'
|
|
72
|
+
'Controlled detail',
|
|
73
73
|
'Synthesize agent or retrieval results',
|
|
74
74
|
'Do not hide blockers',
|
|
75
75
|
'verification was not run, say so once',
|
|
@@ -106,9 +106,9 @@ try {
|
|
|
106
106
|
assert(!customRules.includes('Mixdog default — the most detailed of the three styles'), 'custom outputStyle should not append default style');
|
|
107
107
|
const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
|
|
108
108
|
assert(profileMeta.includes('Use "재영님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
|
|
109
|
-
assert(profileMeta.includes('
|
|
109
|
+
assert(profileMeta.includes('do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
|
|
110
110
|
assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
|
|
111
|
-
assert(profileMeta.includes('
|
|
111
|
+
assert(profileMeta.includes('pre-tool preambles (even single-line)'), 'profile language must cover pre-tool preambles');
|
|
112
112
|
} finally {
|
|
113
113
|
rmSync(dataDir, { recursive: true, force: true });
|
|
114
114
|
}
|
|
@@ -366,6 +366,81 @@ test('anthropic(-oauth): text-only stream → no toolCalls', async () => {
|
|
|
366
366
|
assert.equal(result.content, 'hello');
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
+
test('anthropic(-oauth): thinking + signature deltas → ordered thinkingBlocks before tool_use', async () => {
|
|
370
|
+
const events = [
|
|
371
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
372
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' } },
|
|
373
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'step ' } },
|
|
374
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'one' } },
|
|
375
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sig123' } },
|
|
376
|
+
{ type: 'content_block_stop', index: 0 },
|
|
377
|
+
{ type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_9', name: 'shell' } },
|
|
378
|
+
{ type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
|
|
379
|
+
{ type: 'content_block_stop', index: 1 },
|
|
380
|
+
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
381
|
+
{ type: 'message_stop' },
|
|
382
|
+
];
|
|
383
|
+
const result = await anthropicParseSSEStream(
|
|
384
|
+
anthropicSseResponse(events),
|
|
385
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
386
|
+
);
|
|
387
|
+
assert.equal(result.hasThinkingContent, true);
|
|
388
|
+
assert.deepEqual(result.thinkingBlocks, [
|
|
389
|
+
{ type: 'thinking', thinking: 'step one', signature: 'sig123' },
|
|
390
|
+
]);
|
|
391
|
+
assert.deepEqual(result.toolCalls, [{ id: 'toolu_9', name: 'shell', arguments: { command: 'ls' } }]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('anthropic(-oauth): signature-only block (display omitted) → empty thinking kept with signature', async () => {
|
|
395
|
+
const events = [
|
|
396
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
397
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking' } },
|
|
398
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'signature_delta', signature: 'sigABC' } },
|
|
399
|
+
{ type: 'content_block_stop', index: 0 },
|
|
400
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
401
|
+
{ type: 'message_stop' },
|
|
402
|
+
];
|
|
403
|
+
const result = await anthropicParseSSEStream(
|
|
404
|
+
anthropicSseResponse(events),
|
|
405
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
406
|
+
);
|
|
407
|
+
assert.deepEqual(result.thinkingBlocks, [
|
|
408
|
+
{ type: 'thinking', thinking: '', signature: 'sigABC' },
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('anthropic(-oauth): redacted_thinking round-trips exactly as {type,data} (no extra fields)', async () => {
|
|
413
|
+
const events = [
|
|
414
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
415
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'redacted_thinking', data: 'ENCRYPTED_PAYLOAD' } },
|
|
416
|
+
{ type: 'content_block_stop', index: 0 },
|
|
417
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
418
|
+
{ type: 'message_stop' },
|
|
419
|
+
];
|
|
420
|
+
const result = await anthropicParseSSEStream(
|
|
421
|
+
anthropicSseResponse(events),
|
|
422
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
423
|
+
);
|
|
424
|
+
assert.deepEqual(result.thinkingBlocks, [
|
|
425
|
+
{ type: 'redacted_thinking', data: 'ENCRYPTED_PAYLOAD' },
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('anthropic effort: legacy claude-3-7-sonnet gets NO adaptive thinking / effort beta', () => {
|
|
430
|
+
const model = 'claude-3-7-sonnet-20250219';
|
|
431
|
+
assert.equal(modelSupportsEffort(model), false);
|
|
432
|
+
const body = _buildRequestBodyForCacheSmoke(
|
|
433
|
+
[{ role: 'user', content: 'hi' }],
|
|
434
|
+
model,
|
|
435
|
+
[],
|
|
436
|
+
{ effort: 'high' },
|
|
437
|
+
);
|
|
438
|
+
assert.equal(body.output_config, undefined);
|
|
439
|
+
// Legacy path uses the budget_tokens shape, never thinking:adaptive.
|
|
440
|
+
assert.notEqual(body.thinking?.type, 'adaptive');
|
|
441
|
+
assert.equal(shouldIncludeEffortBeta(model, { effort: 'high' }), false);
|
|
442
|
+
});
|
|
443
|
+
|
|
369
444
|
// --- Leaked tool-call recovery (shared parseSSEStream guard) ----------------
|
|
370
445
|
// The model sometimes emits a tool call as plain text tags inside text_delta
|
|
371
446
|
// instead of a native tool_use block. The guard (8th arg = known tool names)
|
|
@@ -933,7 +1008,9 @@ test('anthropic effort: sonnet-4-6 uses output_config + effort beta, not thinkin
|
|
|
933
1008
|
{ effort: 'high' },
|
|
934
1009
|
);
|
|
935
1010
|
assert.deepEqual(body.output_config, { effort: 'high' });
|
|
936
|
-
|
|
1011
|
+
// Adaptive-thinking models also carry thinking:{type:'adaptive'} — the
|
|
1012
|
+
// legacy budget_tokens shape 400s on these models.
|
|
1013
|
+
assert.deepEqual(body.thinking, { type: 'adaptive', display: 'summarized' });
|
|
937
1014
|
assert.equal(shouldIncludeEffortBeta(model, { effort: 'high' }), true);
|
|
938
1015
|
const beta = buildAnthropicBetaHeaders({ effort: true });
|
|
939
1016
|
assert.ok(beta.includes(EFFORT_BETA_HEADER));
|
|
@@ -967,7 +1044,7 @@ test('anthropic effort: xhigh on opus-4-8 is a first-class level (not downgraded
|
|
|
967
1044
|
{ effort: 'xhigh' },
|
|
968
1045
|
);
|
|
969
1046
|
assert.deepEqual(body.output_config, { effort: 'xhigh' });
|
|
970
|
-
assert.
|
|
1047
|
+
assert.deepEqual(body.thinking, { type: 'adaptive', display: 'summarized' });
|
|
971
1048
|
});
|
|
972
1049
|
|
|
973
1050
|
test('anthropic effort: explicit thinkingBudgetTokens wins over effort', () => {
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* termio-input-smoke.mjs — throwaway smoke for the vendored termio input
|
|
4
|
+
* pipeline. Feeds synthetic chunk sequences through createInputParser() and
|
|
5
|
+
* asserts the typed event kinds / channel routing decisions.
|
|
6
|
+
*
|
|
7
|
+
* Run: node scripts/termio-input-smoke.mjs
|
|
8
|
+
*/
|
|
9
|
+
import { createInputParser } from '../vendor/ink/build/input-parser.js';
|
|
10
|
+
|
|
11
|
+
let failures = 0;
|
|
12
|
+
function check(name, cond, detail) {
|
|
13
|
+
if (cond) {
|
|
14
|
+
console.log(`ok - ${name}`);
|
|
15
|
+
} else {
|
|
16
|
+
failures++;
|
|
17
|
+
console.log(`FAIL - ${name}${detail ? ' :: ' + detail : ''}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Helper: push a list of chunks, collect all emitted events.
|
|
22
|
+
function run(chunks) {
|
|
23
|
+
const p = createInputParser();
|
|
24
|
+
const events = [];
|
|
25
|
+
for (const c of chunks) events.push(...p.push(c));
|
|
26
|
+
return { p, events };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 1. ESC split across two chunks -> single arrow-up key, no lone escape.
|
|
30
|
+
{
|
|
31
|
+
const { events } = run(['\x1b', '[A']);
|
|
32
|
+
check('split ESC -> one up key',
|
|
33
|
+
events.length === 1 && events[0].kind === 'key' && events[0].name === 'up',
|
|
34
|
+
JSON.stringify(events));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. SGR mouse click (button 0 press) -> ParsedMouse.
|
|
38
|
+
{
|
|
39
|
+
const { events } = run(['\x1b[<0;12;34M']);
|
|
40
|
+
const m = events[0];
|
|
41
|
+
check('SGR click -> mouse press',
|
|
42
|
+
events.length === 1 && m.kind === 'mouse' && m.action === 'press' &&
|
|
43
|
+
m.col === 12 && m.row === 34 && m.button === 0,
|
|
44
|
+
JSON.stringify(events));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. SGR wheel-up (button 64) -> key wheelup (stays a key, routes to mouse chan).
|
|
48
|
+
{
|
|
49
|
+
const { events } = run(['\x1b[<64;5;5M']);
|
|
50
|
+
const k = events[0];
|
|
51
|
+
check('SGR wheel -> wheelup key',
|
|
52
|
+
events.length === 1 && k.kind === 'key' && k.name === 'wheelup',
|
|
53
|
+
JSON.stringify(events));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 4. X10 mouse: CSI M + 3 payload bytes (wheel-up: Cb = 0x40+32 = 96 = '`').
|
|
57
|
+
{
|
|
58
|
+
const { events } = run(['\x1b[M\x60\x30\x30']);
|
|
59
|
+
const k = events[0];
|
|
60
|
+
check('X10 wheel -> wheelup key',
|
|
61
|
+
events.length === 1 && k.kind === 'key' && k.name === 'wheelup',
|
|
62
|
+
JSON.stringify(events));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 5. Bracketed paste split across 3 chunks -> one pasted key with full body.
|
|
66
|
+
{
|
|
67
|
+
const { events } = run(['\x1b[200~hel', 'lo wor', 'ld\x1b[201~']);
|
|
68
|
+
const paste = events.find((e) => e.isPasted);
|
|
69
|
+
check('split paste -> one pasted key "hello world"',
|
|
70
|
+
events.length === 1 && paste && paste.sequence === 'hello world',
|
|
71
|
+
JSON.stringify(events));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 6. Paste whose body ends with an X10 mouse tail `\x1b[M` — the ESC in the
|
|
75
|
+
// payload slot must NOT be consumed as X10 mouse; it stays paste content and
|
|
76
|
+
// PASTE_END still terminates the paste.
|
|
77
|
+
{
|
|
78
|
+
const { events } = run(['\x1b[200~abc\x1b[M12\x1b[201~']);
|
|
79
|
+
const paste = events.find((e) => e.isPasted);
|
|
80
|
+
check('paste with \\x1b[M tail stays paste',
|
|
81
|
+
events.length === 1 && paste && paste.sequence === 'abc\x1b[M12',
|
|
82
|
+
JSON.stringify(events));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 7. Terminal response (DA1) -> ParsedResponse.
|
|
86
|
+
{
|
|
87
|
+
const { events } = run(['\x1b[?62;1c']);
|
|
88
|
+
const r = events[0];
|
|
89
|
+
check('DA1 -> response',
|
|
90
|
+
events.length === 1 && r.kind === 'response' && r.response.type === 'da1',
|
|
91
|
+
JSON.stringify(events));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 8. Lone ESC buffered -> hasPendingEscape true; flushPendingEscape -> escape key.
|
|
95
|
+
{
|
|
96
|
+
const p = createInputParser();
|
|
97
|
+
const first = p.push('\x1b');
|
|
98
|
+
check('lone ESC buffers (no immediate events)', first.length === 0, JSON.stringify(first));
|
|
99
|
+
check('hasPendingEscape true for lone ESC', p.hasPendingEscape() === true);
|
|
100
|
+
const flushed = p.flushPendingEscape();
|
|
101
|
+
check('flushPendingEscape -> escape key',
|
|
102
|
+
Array.isArray(flushed) && flushed.length === 1 && flushed[0].name === 'escape',
|
|
103
|
+
JSON.stringify(flushed));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 9. Incomplete paste-start marker must NOT arm the escape flush timer.
|
|
107
|
+
{
|
|
108
|
+
const p = createInputParser();
|
|
109
|
+
p.push('\x1b[200');
|
|
110
|
+
check('partial paste-start does not arm flush', p.hasPendingEscape() === false);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 9b. Generic partial CSI (`\x1b[`, `\x1b[2`, `\x1b[20`) — none of these are
|
|
114
|
+
// paste-start prefixes/penultimate, so hasPendingEscape MUST arm to avoid
|
|
115
|
+
// buffering indefinitely.
|
|
116
|
+
{
|
|
117
|
+
for (const partial of ['\x1b[', '\x1b[2', '\x1b[20']) {
|
|
118
|
+
const p = createInputParser();
|
|
119
|
+
p.push(partial);
|
|
120
|
+
check(`generic partial CSI ${JSON.stringify(partial)} arms flush`,
|
|
121
|
+
p.hasPendingEscape() === true);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 9c. `\x1b[200` and `\x1b[200~`-prefixed pending must NOT arm the flush
|
|
126
|
+
// (paste-start marker / its penultimate prefix complete on their own).
|
|
127
|
+
{
|
|
128
|
+
const p1 = createInputParser();
|
|
129
|
+
p1.push('\x1b[200');
|
|
130
|
+
check('"\\x1b[200" does not arm flush', p1.hasPendingEscape() === false);
|
|
131
|
+
|
|
132
|
+
const p2 = createInputParser();
|
|
133
|
+
p2.push('\x1b[200~hel'); // still inside paste body once PASTE_START token completes
|
|
134
|
+
check('paste-start-prefixed pending does not arm flush', p2.hasPendingEscape() === false);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 9d. Held-backspace chunk splits into individual key events; `\r`/`\t` in
|
|
138
|
+
// text are NOT split.
|
|
139
|
+
{
|
|
140
|
+
const { events } = run(['abc\x7f\x7f\x7f']);
|
|
141
|
+
const names = events.map((e) => e.name || e.sequence);
|
|
142
|
+
check('abc + 3x backspace -> text key(s) + 3 separate backspace keys',
|
|
143
|
+
events.length === 4 &&
|
|
144
|
+
events[0].sequence === 'abc' &&
|
|
145
|
+
events.slice(1).every((e) => e.name === 'backspace' || e.sequence === '\x7f'),
|
|
146
|
+
JSON.stringify(names));
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
const { events } = run(['a\rb\tc']);
|
|
150
|
+
check('\\r and \\t inside text are not split (single key for whole run)',
|
|
151
|
+
events.length === 1 && events[0].sequence === 'a\rb\tc',
|
|
152
|
+
JSON.stringify(events));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 10. Plain text -> key events, sequence carries the character.
|
|
156
|
+
{
|
|
157
|
+
const { events } = run(['a']);
|
|
158
|
+
check('plain char -> key "a"',
|
|
159
|
+
events.length === 1 && events[0].kind === 'key' && events[0].sequence === 'a',
|
|
160
|
+
JSON.stringify(events));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 11. Consumer-level routing: synthetic 'mouse' channel events reach the
|
|
164
|
+
// use-mouse-input dispatch decisions exactly once each. We replicate the
|
|
165
|
+
// hook's branch keys (kind:'key' wheel with ctrl-mask from sequence vs.
|
|
166
|
+
// kind:'mouse' press) to guard against double-handling / mis-routing.
|
|
167
|
+
// MOUSE_CTRL_MASK = 16; wheel ctrl bit is read from the SGR button in
|
|
168
|
+
// the ParsedKey.sequence via /\x1b\[<(\d+);/ (no button field on wheel).
|
|
169
|
+
{
|
|
170
|
+
const MOUSE_CTRL_MASK = 16;
|
|
171
|
+
const WHEEL_SGR = /\x1b\[<(\d+);/;
|
|
172
|
+
// Feed real parser output so field shapes match production.
|
|
173
|
+
const wheelPlain = run(['\x1b[<64;5;5M']).events[0]; // wheelup, no ctrl
|
|
174
|
+
const wheelCtrl = run(['\x1b[<80;5;5M']).events[0]; // 64|16 -> ctrl wheelup
|
|
175
|
+
const click = run(['\x1b[<0;12;34M']).events[0]; // left press
|
|
176
|
+
|
|
177
|
+
const routes = { zoom: 0, scroll: 0, press: 0, ignored: 0 };
|
|
178
|
+
function route(event) {
|
|
179
|
+
if (!event || typeof event !== 'object') return;
|
|
180
|
+
if (event.kind === 'key') {
|
|
181
|
+
if (event.name !== 'wheelup' && event.name !== 'wheeldown') { routes.ignored++; return; }
|
|
182
|
+
const wm = WHEEL_SGR.exec(typeof event.sequence === 'string' ? event.sequence : '');
|
|
183
|
+
const ctrl = wm ? ((Number(wm[1]) & MOUSE_CTRL_MASK) !== 0) : false;
|
|
184
|
+
if (ctrl) routes.zoom++; else routes.scroll++;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (event.kind !== 'mouse') { routes.ignored++; return; }
|
|
188
|
+
const button = Number(event.button);
|
|
189
|
+
if ((button & 3) === 0 && event.action === 'press' && (button & 32) === 0) routes.press++;
|
|
190
|
+
}
|
|
191
|
+
for (const e of [wheelPlain, wheelCtrl, click]) route(e);
|
|
192
|
+
check('mouse routing: plain wheel -> scroll x1', routes.scroll === 1, JSON.stringify(routes));
|
|
193
|
+
check('mouse routing: ctrl wheel -> zoom x1', routes.zoom === 1, JSON.stringify(routes));
|
|
194
|
+
check('mouse routing: left press -> press x1 (no double)', routes.press === 1, JSON.stringify(routes));
|
|
195
|
+
check('mouse routing: nothing mis-routed', routes.ignored === 0, JSON.stringify(routes));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILURE(S)`);
|
|
199
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tool-usage efficiency diag for worker/heavy-worker (before/after rule changes).
|
|
3
|
+
// Usage: node scripts/tool-efficiency-diag.mjs [--since 24h|<epoch-ms>|ISO]
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
function sinceArg() {
|
|
9
|
+
const i = process.argv.indexOf('--since');
|
|
10
|
+
const raw = i >= 0 ? process.argv[i + 1] : '24h';
|
|
11
|
+
const rel = String(raw).match(/^(\d+(?:\.\d+)?)(h|m|d)$/i);
|
|
12
|
+
if (rel) {
|
|
13
|
+
const mult = { m: 60_000, h: 3_600_000, d: 86_400_000 }[rel[2].toLowerCase()];
|
|
14
|
+
return Date.now() - Number(rel[1]) * mult;
|
|
15
|
+
}
|
|
16
|
+
if (/^\d+$/.test(raw)) return Number(raw);
|
|
17
|
+
const p = Date.parse(raw);
|
|
18
|
+
return Number.isFinite(p) ? p : Date.now() - 86_400_000;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dataDir = process.env.MIXDOG_DATA_DIR || resolve(homedir(), '.mixdog', 'data');
|
|
22
|
+
const files = [resolve(dataDir, 'history', 'agent-trace.jsonl.1'), resolve(dataDir, 'history', 'agent-trace.jsonl')];
|
|
23
|
+
const since = sinceArg();
|
|
24
|
+
const rows = [];
|
|
25
|
+
for (const f of files) {
|
|
26
|
+
if (!existsSync(f)) continue;
|
|
27
|
+
for (const line of readFileSync(f, 'utf8').split(/\r?\n/)) {
|
|
28
|
+
if (!line) continue;
|
|
29
|
+
try { const r = JSON.parse(line); if ((r.ts || 0) >= since) rows.push(r); } catch {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const F = (r, n) => (r[n] != null ? r[n] : (r.payload && r.payload[n] != null ? r.payload[n] : null));
|
|
33
|
+
const bySess = new Map();
|
|
34
|
+
for (const r of rows) {
|
|
35
|
+
const k = r.sessionId || r.session_id || '?';
|
|
36
|
+
if (!bySess.has(k)) bySess.set(k, []);
|
|
37
|
+
bySess.get(k).push(r);
|
|
38
|
+
}
|
|
39
|
+
console.log(`window since ${new Date(since).toISOString()}`);
|
|
40
|
+
for (const target of ['heavy-worker', 'worker']) {
|
|
41
|
+
let batch1 = 0, batchTot = 0, llmCalls = 0, sessions = 0, toolCalls = 0;
|
|
42
|
+
let grepContent = 0, grepContentWithCtx = 0, grepThenRead = 0, rereads = 0, outTok = 0;
|
|
43
|
+
for (const [, rs] of bySess) {
|
|
44
|
+
let agent = null;
|
|
45
|
+
for (const r of rs) { const x = F(r, 'agent'); if (x) { agent = x; break; } }
|
|
46
|
+
if (agent !== target) continue;
|
|
47
|
+
sessions++;
|
|
48
|
+
rs.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
49
|
+
for (const b of rs.filter((r) => r.kind === 'batch')) {
|
|
50
|
+
const c = Number(F(b, 'tool_call_count')) || 0;
|
|
51
|
+
batchTot++; if (c === 1) batch1++;
|
|
52
|
+
}
|
|
53
|
+
const usage = rs.filter((r) => r.kind === 'usage_raw');
|
|
54
|
+
llmCalls += usage.length;
|
|
55
|
+
outTok += usage.reduce((s, r) => s + (Number(F(r, 'output_tokens')) || 0), 0);
|
|
56
|
+
const tools = rs.filter((r) => r.kind === 'tool');
|
|
57
|
+
toolCalls += tools.length;
|
|
58
|
+
const seenRead = new Map();
|
|
59
|
+
for (let i = 0; i < tools.length; i++) {
|
|
60
|
+
const t = tools[i];
|
|
61
|
+
const n = String(F(t, 'tool_name'));
|
|
62
|
+
const a = F(t, 'tool_args_summary') || {};
|
|
63
|
+
if (n === 'read') {
|
|
64
|
+
const p = typeof a.path === 'string' ? a.path : JSON.stringify(a.path);
|
|
65
|
+
seenRead.set(p, (seenRead.get(p) || 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
if (n === 'grep' && ['content', 'content_with_context'].includes(String(a.output_mode || ''))) {
|
|
68
|
+
grepContent++;
|
|
69
|
+
if (a['-C'] != null || a['-A'] != null || a['-B'] != null) grepContentWithCtx++;
|
|
70
|
+
const gpath = typeof a.path === 'string' ? a.path : JSON.stringify(a.path || '');
|
|
71
|
+
for (let j = i + 1; j < Math.min(i + 4, tools.length); j++) {
|
|
72
|
+
const u = tools[j];
|
|
73
|
+
if (String(F(u, 'tool_name')) !== 'read') continue;
|
|
74
|
+
const ra = F(u, 'tool_args_summary') || {};
|
|
75
|
+
const rpath = typeof ra.path === 'string' ? ra.path : JSON.stringify(ra.path || '');
|
|
76
|
+
if (rpath && gpath && (rpath.includes(gpath) || gpath.includes(rpath))) { grepThenRead++; break; }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const [, c] of seenRead) if (c >= 3) rereads++;
|
|
81
|
+
}
|
|
82
|
+
const pct = (a, b) => (b ? Math.round((a / b) * 100) : 0);
|
|
83
|
+
console.log(`\n${target}: sessions=${sessions} llmCalls=${llmCalls} toolCalls=${toolCalls} outTok=${outTok}`);
|
|
84
|
+
console.log(` llmCalls/session=${sessions ? Math.round(llmCalls / sessions) : 0} tools/llmCall=${llmCalls ? (toolCalls / llmCalls).toFixed(2) : '-'}`);
|
|
85
|
+
console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
|
|
86
|
+
console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
|
|
87
|
+
console.log(` same-path re-reads >=3x: ${rereads}`);
|
|
88
|
+
}
|