mixdog 0.8.0 → 0.8.1
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 +1 -1
- package/scripts/boot-smoke.mjs +4 -4
- package/scripts/session-context-bench.mjs +172 -0
- package/scripts/tool-smoke.mjs +7 -11
- package/src/mixdog-session-runtime.mjs +121 -12
- package/src/repl.mjs +8 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +3 -3
- package/src/runtime/agent/orchestrator/internal-roles.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +11 -62
- package/src/runtime/agent/orchestrator/session/manager.mjs +19 -7
- package/src/runtime/agent/orchestrator/session/store.mjs +230 -23
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/memory/index.mjs +116 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +10 -10
- package/src/runtime/memory/lib/memory-cycle2.mjs +2 -2
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/pg/adapter.mjs +14 -0
- package/src/runtime/shared/tool-surface.mjs +1 -4
- package/src/tui/App.jsx +10 -2
- package/src/tui/components/ContextPanel.jsx +76 -28
- package/src/tui/components/ToolExecution.jsx +26 -7
- package/src/tui/dist/index.mjs +106 -45
- package/src/ui/tool-card.mjs +1 -3
package/package.json
CHANGED
package/scripts/boot-smoke.mjs
CHANGED
|
@@ -54,15 +54,15 @@ const rows = [
|
|
|
54
54
|
try {
|
|
55
55
|
const status = runtime.toolsStatus();
|
|
56
56
|
const active = new Set(status.activeTools || []);
|
|
57
|
-
for (const name of ['read','code_graph','grep','glob','list','apply_patch','explore','bridge','tool_search']) {
|
|
57
|
+
for (const name of ['read','code_graph','grep','glob','list','apply_patch','explore','bridge','tool_search','shell']) {
|
|
58
58
|
if (!active.has(name)) throw new Error('missing ' + name + ' in ' + [...active].join(','));
|
|
59
59
|
}
|
|
60
|
-
for (const name of ['bash','
|
|
60
|
+
for (const name of ['bash','task']) {
|
|
61
61
|
if (active.has(name)) throw new Error('unexpected ' + name + ' in ' + [...active].join(','));
|
|
62
62
|
}
|
|
63
63
|
const result = runtime.selectTools('shell');
|
|
64
|
-
if (!result.
|
|
65
|
-
throw new Error('shell alias should add
|
|
64
|
+
if (!result.already.includes('shell') || !result.added.includes('task')) {
|
|
65
|
+
throw new Error('shell alias should keep shell active and add task: ' + JSON.stringify(result));
|
|
66
66
|
}
|
|
67
67
|
const nextActive = new Set(result.status.activeTools || []);
|
|
68
68
|
for (const name of ['shell','task']) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { loadConfig, getDefaultPreset, getPreset } from '../src/runtime/agent/orchestrator/config.mjs';
|
|
6
|
+
import { initProviders, getProvider } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
|
|
7
|
+
import { loadSession } from '../src/runtime/agent/orchestrator/session/store.mjs';
|
|
8
|
+
import { semanticCompactMessages } from '../src/runtime/agent/orchestrator/session/compact.mjs';
|
|
9
|
+
import { estimateMessagesTokens } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
|
|
10
|
+
import * as memory from '../src/runtime/memory/index.mjs';
|
|
11
|
+
|
|
12
|
+
const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
13
|
+
|
|
14
|
+
function arg(name, fallback = null) {
|
|
15
|
+
const prefix = `--${name}=`;
|
|
16
|
+
const found = process.argv.find((item) => item.startsWith(prefix));
|
|
17
|
+
if (found) return found.slice(prefix.length);
|
|
18
|
+
const idx = process.argv.indexOf(`--${name}`);
|
|
19
|
+
if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function flag(name) {
|
|
24
|
+
return process.argv.includes(`--${name}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textOfResult(result) {
|
|
28
|
+
if (result && typeof result === 'object' && Array.isArray(result.content)) {
|
|
29
|
+
return result.content.map((part) => part?.type === 'text' ? part.text || '' : JSON.stringify(part)).join('\n');
|
|
30
|
+
}
|
|
31
|
+
if (typeof result === 'string') return result;
|
|
32
|
+
return JSON.stringify(result ?? '', null, 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function messageText(content) {
|
|
36
|
+
if (typeof content === 'string') return content;
|
|
37
|
+
if (Array.isArray(content)) {
|
|
38
|
+
return content.map((part) => {
|
|
39
|
+
if (typeof part === 'string') return part;
|
|
40
|
+
if (part?.type === 'text') return part.text || '';
|
|
41
|
+
if (part?.type === 'image') return '[image]';
|
|
42
|
+
return part?.text || '';
|
|
43
|
+
}).filter(Boolean).join('\n');
|
|
44
|
+
}
|
|
45
|
+
try { return JSON.stringify(content ?? ''); } catch { return String(content ?? ''); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderMessages(messages) {
|
|
49
|
+
return (messages || [])
|
|
50
|
+
.map((m, i) => `# message ${i + 1} role=${m.role}\n${messageText(m.content)}`.trim())
|
|
51
|
+
.join('\n\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function coverageScore(sourceText, candidateText) {
|
|
55
|
+
const terms = [...new Set(String(sourceText || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{4,}/gu) || [])]
|
|
56
|
+
.filter((term) => !/^(https?|that|this|with|from|have|there|would|could|should)$/i.test(term))
|
|
57
|
+
.slice(0, 5000);
|
|
58
|
+
if (!terms.length) return 0;
|
|
59
|
+
const hay = String(candidateText || '').toLowerCase();
|
|
60
|
+
let hit = 0;
|
|
61
|
+
for (const term of terms) if (hay.includes(term)) hit += 1;
|
|
62
|
+
return Number((hit / terms.length).toFixed(4));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function providerFromConfig(config, { providerName, modelName }) {
|
|
66
|
+
await initProviders(config.providers || {});
|
|
67
|
+
let provider = providerName;
|
|
68
|
+
let model = modelName;
|
|
69
|
+
if (!provider || !model) {
|
|
70
|
+
const presetName = arg('preset', null);
|
|
71
|
+
const preset = presetName ? getPreset(config, presetName) : getDefaultPreset(config);
|
|
72
|
+
provider ||= preset?.provider;
|
|
73
|
+
model ||= preset?.model;
|
|
74
|
+
}
|
|
75
|
+
if (!provider || !model) throw new Error('provider/model required; pass --provider and --model or configure a default preset');
|
|
76
|
+
const impl = getProvider(provider);
|
|
77
|
+
if (!impl) throw new Error(`provider not available: ${provider}`);
|
|
78
|
+
return { provider, model, impl };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function judgePair(provider, model, sourceText, semanticText, recallText) {
|
|
82
|
+
const prompt = `You are judging two context-compression strategies for continuing a coding session.\n\nEvaluate which preserves actionable state better for the next assistant turn. Prefer factual coverage, exact constraints, file paths, decisions, and current task continuity.\n\nReturn strict JSON: {"winner":"semantic"|"recall_roots"|"tie","semantic_score":1-10,"recall_roots_score":1-10,"reason":"short"}.\n\n# Original session excerpt\n${sourceText.slice(0, 30000)}\n\n# Candidate A: semantic compact\n${semanticText.slice(0, 30000)}\n\n# Candidate B: recall root/raw chunks\n${recallText.slice(0, 30000)}`;
|
|
83
|
+
const response = await provider.send([
|
|
84
|
+
{ role: 'system', content: 'Judge compression quality. Output JSON only.' },
|
|
85
|
+
{ role: 'user', content: prompt },
|
|
86
|
+
], model, undefined, { effort: 'low', fast: true, maxOutputTokens: 800 });
|
|
87
|
+
const text = String(response?.content || '').trim();
|
|
88
|
+
try { return JSON.parse(text.replace(/^```json\s*/i, '').replace(/```$/i, '').trim()); }
|
|
89
|
+
catch { return { raw: text }; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
if (flag('help') || flag('h')) {
|
|
94
|
+
process.stdout.write('usage: node scripts/session-context-bench.mjs --session <sessionId> [--provider p --model m] [--preset name] [--budget n] [--judge]\n');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const sessionId = arg('session');
|
|
98
|
+
if (!sessionId) throw new Error('usage: node scripts/session-context-bench.mjs --session <sessionId> [--provider p --model m] [--judge]');
|
|
99
|
+
const session = loadSession(sessionId);
|
|
100
|
+
if (!session) throw new Error(`session not found: ${sessionId}`);
|
|
101
|
+
const messages = Array.isArray(session.messages) ? session.messages : [];
|
|
102
|
+
if (!messages.length) throw new Error(`session has no messages: ${sessionId}`);
|
|
103
|
+
|
|
104
|
+
const config = loadConfig();
|
|
105
|
+
const { provider, model, impl } = await providerFromConfig(config, {
|
|
106
|
+
providerName: arg('provider', session.provider || null),
|
|
107
|
+
modelName: arg('model', session.model || null),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await memory.init();
|
|
111
|
+
await memory.handleToolCall('memory', {
|
|
112
|
+
action: 'ingest_session',
|
|
113
|
+
sessionId,
|
|
114
|
+
cwd: session.cwd || ROOT,
|
|
115
|
+
messages,
|
|
116
|
+
limit: Number(arg('ingest-limit', 500)) || 500,
|
|
117
|
+
});
|
|
118
|
+
await memory.handleToolCall('memory', {
|
|
119
|
+
action: 'cycle1',
|
|
120
|
+
min_batch: 1,
|
|
121
|
+
session_cap: 1,
|
|
122
|
+
batch_size: Math.max(1, Math.min(200, Number(arg('batch-size', messages.length)) || messages.length)),
|
|
123
|
+
});
|
|
124
|
+
const recallDumpResult = await memory.handleToolCall('memory', {
|
|
125
|
+
action: 'dump_session_roots',
|
|
126
|
+
sessionId,
|
|
127
|
+
includeRaw: true,
|
|
128
|
+
limit: Number(arg('limit', 1000)) || 1000,
|
|
129
|
+
});
|
|
130
|
+
const recallText = textOfResult(recallDumpResult);
|
|
131
|
+
|
|
132
|
+
const budget = Number(arg('budget', 16000)) || 16000;
|
|
133
|
+
const semantic = await semanticCompactMessages(impl, messages, model, budget, {
|
|
134
|
+
force: true,
|
|
135
|
+
sessionId,
|
|
136
|
+
providerName: provider,
|
|
137
|
+
tailTurns: Number(arg('tail-turns', 2)) || 2,
|
|
138
|
+
timeoutMs: Number(arg('timeout-ms', 60000)) || 60000,
|
|
139
|
+
});
|
|
140
|
+
const semanticText = renderMessages(semantic.messages);
|
|
141
|
+
const sourceText = renderMessages(messages);
|
|
142
|
+
|
|
143
|
+
const report = {
|
|
144
|
+
sessionId,
|
|
145
|
+
provider,
|
|
146
|
+
model,
|
|
147
|
+
source: { messages: messages.length, tokens: estimateMessagesTokens(messages), chars: sourceText.length },
|
|
148
|
+
semantic: { messages: semantic.messages.length, tokens: estimateMessagesTokens(semantic.messages), chars: semanticText.length, lexicalCoverage: coverageScore(sourceText, semanticText), usage: semantic.usage || null },
|
|
149
|
+
recallRoots: { chars: recallText.length, lexicalCoverage: coverageScore(sourceText, recallText) },
|
|
150
|
+
judge: null,
|
|
151
|
+
};
|
|
152
|
+
if (flag('judge')) {
|
|
153
|
+
report.judge = await judgePair(impl, model, sourceText, semanticText, recallText);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const outDir = resolve(arg('out', join(ROOT, '.mixdog-bench', `session-context-${sessionId}`)));
|
|
157
|
+
mkdirSync(outDir, { recursive: true });
|
|
158
|
+
writeFileSync(join(outDir, 'source.txt'), sourceText, 'utf8');
|
|
159
|
+
writeFileSync(join(outDir, 'semantic.txt'), semanticText, 'utf8');
|
|
160
|
+
writeFileSync(join(outDir, 'recall-roots.txt'), recallText, 'utf8');
|
|
161
|
+
writeFileSync(join(outDir, 'report.json'), JSON.stringify(report, null, 2), 'utf8');
|
|
162
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\noutputs: ${outDir}\n`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
main()
|
|
166
|
+
.catch((err) => {
|
|
167
|
+
process.stderr.write(`${err?.stack || err?.message || err}\n`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
})
|
|
170
|
+
.finally(async () => {
|
|
171
|
+
try { await memory.stop?.(); } catch {}
|
|
172
|
+
});
|
package/scripts/tool-smoke.mjs
CHANGED
|
@@ -275,13 +275,13 @@ const smokeCatalog = [
|
|
|
275
275
|
].filter(Boolean);
|
|
276
276
|
|
|
277
277
|
const fullDefaults = defaultDeferredToolNames(smokeCatalog, 'full');
|
|
278
|
-
if (fullDefaults.size !==
|
|
279
|
-
throw new Error(`full default surface should stay
|
|
278
|
+
if (fullDefaults.size !== 13) {
|
|
279
|
+
throw new Error(`full default surface should stay 13 tools, got ${fullDefaults.size}: ${[...fullDefaults].join(', ')}`);
|
|
280
280
|
}
|
|
281
|
-
for (const name of ['read', 'code_graph', 'grep', 'glob', 'list', 'apply_patch', 'explore', 'bridge', 'recall', 'search', 'web_fetch', 'tool_search']) {
|
|
281
|
+
for (const name of ['read', 'code_graph', 'grep', 'glob', 'list', 'apply_patch', 'explore', 'bridge', 'shell', 'recall', 'search', 'web_fetch', 'tool_search']) {
|
|
282
282
|
assertHas(fullDefaults, name);
|
|
283
283
|
}
|
|
284
|
-
for (const name of ['
|
|
284
|
+
for (const name of ['task']) {
|
|
285
285
|
assertLacks(fullDefaults, name);
|
|
286
286
|
}
|
|
287
287
|
|
|
@@ -292,10 +292,6 @@ if (leadDefaults.size !== 14) {
|
|
|
292
292
|
for (const name of ['read', 'code_graph', 'grep', 'glob', 'list', 'shell', 'task', 'apply_patch', 'explore', 'bridge', 'recall', 'search', 'web_fetch', 'tool_search']) {
|
|
293
293
|
assertHas(leadDefaults, name);
|
|
294
294
|
}
|
|
295
|
-
for (const name of ['edit', 'write']) {
|
|
296
|
-
assertLacks(leadDefaults, name);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
295
|
function toolSchemaSize(tool) {
|
|
300
296
|
const desc = String(tool?.description || '');
|
|
301
297
|
const schema = JSON.stringify(tool?.input_schema || tool?.inputSchema || {});
|
|
@@ -306,8 +302,8 @@ const surfaceSize = [...fullDefaults].reduce((sum, name) => {
|
|
|
306
302
|
const tool = smokeCatalog.find((item) => item?.name === name);
|
|
307
303
|
return sum + toolSchemaSize(tool);
|
|
308
304
|
}, 0);
|
|
309
|
-
if (surfaceSize >
|
|
310
|
-
throw new Error(`full default tool surface too large: ${surfaceSize} chars (cap
|
|
305
|
+
if (surfaceSize > 17000) {
|
|
306
|
+
throw new Error(`full default tool surface too large: ${surfaceSize} chars (cap 17000)`);
|
|
311
307
|
}
|
|
312
308
|
for (const [name, cap] of [
|
|
313
309
|
['apply_patch', 1300],
|
|
@@ -330,7 +326,7 @@ if (readonlyDefaults.size !== 7) {
|
|
|
330
326
|
for (const name of ['read', 'code_graph', 'grep', 'glob', 'list', 'explore', 'tool_search']) {
|
|
331
327
|
assertHas(readonlyDefaults, name);
|
|
332
328
|
}
|
|
333
|
-
for (const name of ['apply_patch', 'bridge', 'shell'
|
|
329
|
+
for (const name of ['apply_patch', 'bridge', 'shell']) {
|
|
334
330
|
assertLacks(readonlyDefaults, name);
|
|
335
331
|
}
|
|
336
332
|
|
|
@@ -86,6 +86,40 @@ function messageContextText(message) {
|
|
|
86
86
|
return text;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function stripSystemReminder(text) {
|
|
90
|
+
return String(text || '')
|
|
91
|
+
.replace(/^\s*<system-reminder>\s*/i, '')
|
|
92
|
+
.replace(/\s*<\/system-reminder>\s*$/i, '')
|
|
93
|
+
.trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function splitMarkdownSections(text) {
|
|
97
|
+
const sections = [];
|
|
98
|
+
let current = [];
|
|
99
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
100
|
+
if (/^#\s+/.test(line) && current.length) {
|
|
101
|
+
const body = current.join('\n').trim();
|
|
102
|
+
if (body) sections.push(body);
|
|
103
|
+
current = [line];
|
|
104
|
+
} else {
|
|
105
|
+
current.push(line);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const tail = current.join('\n').trim();
|
|
109
|
+
if (tail) sections.push(tail);
|
|
110
|
+
return sections;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function reminderSectionBucket(section) {
|
|
114
|
+
const heading = String(section.match(/^#\s+([^\n]+)/)?.[1] || '').trim().toLowerCase();
|
|
115
|
+
if (heading.includes('core memory')) return 'memory';
|
|
116
|
+
if (heading.includes('mixdog-project-context') || heading.includes('project-context')) return 'project';
|
|
117
|
+
if (heading.includes('active workflow') || heading.includes('available agents') || heading.includes('workflow')) return 'workflow';
|
|
118
|
+
if (heading.includes('workspace') || heading === 'role' || heading.includes('role-identity') || heading.includes('task-brief')) return 'workspace';
|
|
119
|
+
if (heading.includes('environment')) return 'environment';
|
|
120
|
+
return 'other';
|
|
121
|
+
}
|
|
122
|
+
|
|
89
123
|
function summarizeContextMessages(messages) {
|
|
90
124
|
const rows = {
|
|
91
125
|
system: { count: 0, tokens: 0 },
|
|
@@ -94,15 +128,55 @@ function summarizeContextMessages(messages) {
|
|
|
94
128
|
tool: { count: 0, tokens: 0 },
|
|
95
129
|
other: { count: 0, tokens: 0 },
|
|
96
130
|
};
|
|
131
|
+
const semantic = {
|
|
132
|
+
system: { count: 0, tokens: 0 },
|
|
133
|
+
chat: { count: 0, tokens: 0 },
|
|
134
|
+
assistant: { count: 0, tokens: 0 },
|
|
135
|
+
toolResults: { count: 0, tokens: 0 },
|
|
136
|
+
reminders: { count: 0, tokens: 0, otherTokens: 0 },
|
|
137
|
+
project: { tokens: 0 },
|
|
138
|
+
workflow: { tokens: 0 },
|
|
139
|
+
memory: { tokens: 0 },
|
|
140
|
+
workspace: { tokens: 0 },
|
|
141
|
+
environment: { tokens: 0 },
|
|
142
|
+
other: { tokens: 0 },
|
|
143
|
+
};
|
|
97
144
|
let toolCallCount = 0;
|
|
98
145
|
let toolCallTokens = 0;
|
|
99
146
|
let toolResultCount = 0;
|
|
100
147
|
let toolResultTokens = 0;
|
|
101
148
|
for (const message of messages || []) {
|
|
102
149
|
const role = rows[message?.role] ? message.role : 'other';
|
|
103
|
-
const
|
|
150
|
+
const text = messageContextText(message);
|
|
151
|
+
const tokens = roughTokenCount(text) + 4;
|
|
104
152
|
rows[role].count += 1;
|
|
105
153
|
rows[role].tokens += tokens;
|
|
154
|
+
if (role === 'system') {
|
|
155
|
+
semantic.system.count += 1;
|
|
156
|
+
semantic.system.tokens += tokens;
|
|
157
|
+
} else if (role === 'user') {
|
|
158
|
+
if (String(text || '').trim().startsWith('<system-reminder>')) {
|
|
159
|
+
semantic.reminders.count += 1;
|
|
160
|
+
semantic.reminders.tokens += tokens;
|
|
161
|
+
let sectionTokens = 0;
|
|
162
|
+
for (const section of splitMarkdownSections(stripSystemReminder(text))) {
|
|
163
|
+
const bucket = reminderSectionBucket(section);
|
|
164
|
+
const sectionTokenCount = roughTokenCount(section);
|
|
165
|
+
semantic[bucket].tokens += sectionTokenCount;
|
|
166
|
+
sectionTokens += sectionTokenCount;
|
|
167
|
+
}
|
|
168
|
+
semantic.reminders.otherTokens += Math.max(0, tokens - sectionTokens);
|
|
169
|
+
} else {
|
|
170
|
+
semantic.chat.count += 1;
|
|
171
|
+
semantic.chat.tokens += tokens;
|
|
172
|
+
}
|
|
173
|
+
} else if (role === 'assistant') {
|
|
174
|
+
semantic.assistant.count += 1;
|
|
175
|
+
semantic.assistant.tokens += tokens;
|
|
176
|
+
} else if (role === 'tool') {
|
|
177
|
+
semantic.toolResults.count += 1;
|
|
178
|
+
semantic.toolResults.tokens += tokens;
|
|
179
|
+
}
|
|
106
180
|
if (message?.role === 'assistant' && Array.isArray(message.toolCalls) && message.toolCalls.length) {
|
|
107
181
|
toolCallCount += message.toolCalls.length;
|
|
108
182
|
try { toolCallTokens += roughTokenCount(JSON.stringify(message.toolCalls)); }
|
|
@@ -117,6 +191,7 @@ function summarizeContextMessages(messages) {
|
|
|
117
191
|
count: Array.isArray(messages) ? messages.length : 0,
|
|
118
192
|
estimatedTokens: Array.isArray(messages) ? estimateMessagesTokens(messages) : 0,
|
|
119
193
|
roles: rows,
|
|
194
|
+
semantic,
|
|
120
195
|
toolCallCount,
|
|
121
196
|
toolCallTokens,
|
|
122
197
|
toolResultCount,
|
|
@@ -254,12 +329,13 @@ export const TOOL_SEARCH_TOOL = {
|
|
|
254
329
|
destructiveHint: false,
|
|
255
330
|
idempotentHint: true,
|
|
256
331
|
openWorldHint: false,
|
|
332
|
+
bridgeHidden: true,
|
|
257
333
|
},
|
|
258
334
|
description: 'Search the current standalone tool surface and select deferred tools/skills for the task. Use before unfamiliar or currently inactive tools.',
|
|
259
335
|
inputSchema: {
|
|
260
336
|
type: 'object',
|
|
261
337
|
properties: {
|
|
262
|
-
query: { type: 'string', description: 'Optional search text, e.g.
|
|
338
|
+
query: { type: 'string', description: 'Optional search text, e.g. shell, bridge, memory, skill, mcp.' },
|
|
263
339
|
select: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Tool/skill names to activate, as comma-separated text or an array.' },
|
|
264
340
|
limit: { type: 'number', description: 'Maximum matches to return.' },
|
|
265
341
|
},
|
|
@@ -330,7 +406,7 @@ const DEFERRED_ALWAYS_ACTIVE_TOOLS = new Set([
|
|
|
330
406
|
'web_fetch',
|
|
331
407
|
]);
|
|
332
408
|
const LEAD_DISALLOWED_TOOLS = Object.freeze(['diagnostics', 'open_config']);
|
|
333
|
-
const DEFERRED_DEFAULT_FULL_LIMIT =
|
|
409
|
+
const DEFERRED_DEFAULT_FULL_LIMIT = 9;
|
|
334
410
|
const DEFERRED_DEFAULT_READONLY_TOOLS = Object.freeze([
|
|
335
411
|
'read',
|
|
336
412
|
'code_graph',
|
|
@@ -1168,10 +1244,37 @@ function toolKind(tool) {
|
|
|
1168
1244
|
if (name.startsWith('mcp__')) return 'mcp';
|
|
1169
1245
|
if (name.startsWith('skill_') || name === 'skills_list') return 'skill';
|
|
1170
1246
|
if (tool?.annotations?.bridgeHidden) return 'control';
|
|
1171
|
-
if (['
|
|
1247
|
+
if (['apply_patch', 'shell'].includes(name)) return 'mutation';
|
|
1172
1248
|
return 'tool';
|
|
1173
1249
|
}
|
|
1174
1250
|
|
|
1251
|
+
function toolSchemaBucket(tool) {
|
|
1252
|
+
const name = clean(tool?.name);
|
|
1253
|
+
const kind = toolKind(tool);
|
|
1254
|
+
if (kind === 'mcp') return 'mcp';
|
|
1255
|
+
if (kind === 'skill') return 'skills';
|
|
1256
|
+
if (name === 'memory' || name === 'recall' || name.includes('memory')) return 'memory';
|
|
1257
|
+
if (name === 'search' || name === 'web_fetch') return 'web';
|
|
1258
|
+
if (['read', 'grep', 'glob', 'list', 'code_graph', 'explore'].includes(name)) return 'code';
|
|
1259
|
+
if (['shell', 'apply_patch'].includes(name)) return 'mutation';
|
|
1260
|
+
if (name === 'bridge' || name === 'delegate' || name.includes('bridge')) return 'agents';
|
|
1261
|
+
if (name.includes('channel') || name.includes('discord') || name.includes('webhook')) return 'channels';
|
|
1262
|
+
if (name.includes('provider') || name === 'tool_search' || name === 'cwd') return 'setup';
|
|
1263
|
+
return 'other';
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function estimateToolSchemaBreakdown(tools) {
|
|
1267
|
+
const out = {};
|
|
1268
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
1269
|
+
const bucket = toolSchemaBucket(tool);
|
|
1270
|
+
const row = out[bucket] || { count: 0, tokens: 0 };
|
|
1271
|
+
row.count += 1;
|
|
1272
|
+
row.tokens += estimateToolSchemaTokens([tool]);
|
|
1273
|
+
out[bucket] = row;
|
|
1274
|
+
}
|
|
1275
|
+
return out;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1175
1278
|
function measuredToolUsage(name) {
|
|
1176
1279
|
return MEASURED_TOOL_USAGE[clean(name)] || 0;
|
|
1177
1280
|
}
|
|
@@ -2720,6 +2823,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2720
2823
|
|
|
2721
2824
|
const messageSummary = summarizeContextMessages(messages);
|
|
2722
2825
|
const toolSchemaTokens = estimateToolSchemaTokens(tools);
|
|
2826
|
+
const toolSchemaBreakdown = estimateToolSchemaBreakdown(tools);
|
|
2723
2827
|
const requestReserveTokens = estimateRequestReserveTokens(tools);
|
|
2724
2828
|
const requestOverheadTokens = Math.max(0, requestReserveTokens - toolSchemaTokens);
|
|
2725
2829
|
const rawWindow = Number(session?.rawContextWindow || session?.contextWindow || 0);
|
|
@@ -2776,6 +2880,7 @@ function parsedProviderModelVersion(id) {
|
|
|
2776
2880
|
messages: messageSummary,
|
|
2777
2881
|
request: {
|
|
2778
2882
|
toolSchemaTokens,
|
|
2883
|
+
toolSchemaBreakdown,
|
|
2779
2884
|
requestOverheadTokens,
|
|
2780
2885
|
reserveTokens: requestReserveTokens,
|
|
2781
2886
|
},
|
|
@@ -3647,21 +3752,25 @@ function parsedProviderModelVersion(id) {
|
|
|
3647
3752
|
|| (sourceType === 'cli' && (!sourceName || sourceName === 'main'))
|
|
3648
3753
|
|| (!sourceType && !sourceName && owner !== 'bridge');
|
|
3649
3754
|
if (!leadish) return null;
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
.
|
|
3654
|
-
|
|
3655
|
-
|
|
3755
|
+
let preview = cleanSessionPreview(s.preview || '');
|
|
3756
|
+
let messageCount = Math.max(0, Number(s.messageCount) || 0);
|
|
3757
|
+
if (!preview && Array.isArray(s.messages)) {
|
|
3758
|
+
const msgs = s.messages || [];
|
|
3759
|
+
const userPreviews = msgs
|
|
3760
|
+
.filter(m => m && m.role === 'user')
|
|
3761
|
+
.map(m => cleanSessionPreview(sessionMessageText(m.content)))
|
|
3762
|
+
.filter(text => !isSessionPreviewNoise(text));
|
|
3763
|
+
preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
|
|
3764
|
+
messageCount = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant')).length;
|
|
3765
|
+
}
|
|
3656
3766
|
if (!preview) return null;
|
|
3657
|
-
const userAsst = msgs.filter(m => m && (m.role === 'user' || m.role === 'assistant'));
|
|
3658
3767
|
return {
|
|
3659
3768
|
id: s.id,
|
|
3660
3769
|
updatedAt: s.updatedAt,
|
|
3661
3770
|
cwd: s.cwd || '',
|
|
3662
3771
|
model: s.model,
|
|
3663
3772
|
provider: s.provider,
|
|
3664
|
-
messageCount
|
|
3773
|
+
messageCount,
|
|
3665
3774
|
preview,
|
|
3666
3775
|
};
|
|
3667
3776
|
}).filter(Boolean);
|
package/src/repl.mjs
CHANGED
|
@@ -261,10 +261,18 @@ async function handleSlash(line, ctx) {
|
|
|
261
261
|
stdout.write(yellow('compact failed') + '\n');
|
|
262
262
|
return;
|
|
263
263
|
}
|
|
264
|
+
if (r.error) {
|
|
265
|
+
stdout.write(red('compact failed') + '\n');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
264
268
|
if (r.changed === false && r.reason) {
|
|
265
269
|
stdout.write(yellow(r.reason) + '\n');
|
|
266
270
|
return;
|
|
267
271
|
}
|
|
272
|
+
if (r.changed === false) {
|
|
273
|
+
stdout.write(yellow('nothing to compact') + '\n');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
268
276
|
stdout.write(green(`✓ compacted context: ${r.beforeMessages}→${r.afterMessages} messages, context ${r.beforeTokens}→${r.afterTokens}`) + '\n');
|
|
269
277
|
}
|
|
270
278
|
return;
|
|
@@ -683,11 +683,11 @@ export function composeSystemPrompt(opts) {
|
|
|
683
683
|
let permissionLabel = String(permission);
|
|
684
684
|
let allow =
|
|
685
685
|
permission === 'read'
|
|
686
|
-
? 'read-only;
|
|
686
|
+
? 'read-only; apply_patch/shell rejected'
|
|
687
687
|
: permission === 'read-write'
|
|
688
|
-
? 'read +
|
|
688
|
+
? 'read + apply_patch/shell'
|
|
689
689
|
: permission === 'mcp'
|
|
690
|
-
? 'MCP/internal retrieval tools only; file/shell
|
|
690
|
+
? 'MCP/internal retrieval tools only; file/shell mutation tools rejected'
|
|
691
691
|
: permission === 'full'
|
|
692
692
|
? 'full — all tools'
|
|
693
693
|
: 'unknown — treat as read-only';
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* webhook). Receive only their own self section in BP2.
|
|
29
29
|
*
|
|
30
30
|
* Permission classification:
|
|
31
|
-
* - 'read' : read-only —
|
|
31
|
+
* - 'read' : read-only — apply_patch/shell blocked at loop.mjs runtime
|
|
32
32
|
* guard with the same error string public read-only roles see.
|
|
33
33
|
* - 'read-write' : full tool surface — used by hidden roles that legitimately
|
|
34
34
|
* mutate state (scheduler-task launches commands,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// Post-
|
|
1
|
+
// Post-patch advisory marks: one-shot sidecar on the next read after a patch.
|
|
2
2
|
import { _normalizeAbs } from './util.mjs';
|
|
3
3
|
|
|
4
4
|
// sessionId -> Map<absPath, { ts, toolName }>
|
|
5
5
|
const _postEditBySession = new Map();
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Mark `path` as just-
|
|
9
|
-
* successful
|
|
8
|
+
* Mark `path` as just-patched for `sessionId`. Caller invokes this after a
|
|
9
|
+
* successful apply_patch so the next read on the same path can
|
|
10
10
|
* receive a one-shot advisory sidecar.
|
|
11
11
|
*/
|
|
12
12
|
export function markPostEdit({ sessionId, path, cwd, toolName }) {
|
|
@@ -15,7 +15,7 @@ export function markPostEdit({ sessionId, path, cwd, toolName }) {
|
|
|
15
15
|
if (!abs) return;
|
|
16
16
|
let m = _postEditBySession.get(sessionId);
|
|
17
17
|
if (!m) { m = new Map(); _postEditBySession.set(sessionId, m); }
|
|
18
|
-
m.set(abs, { ts: Date.now(), toolName: String(toolName || '
|
|
18
|
+
m.set(abs, { ts: Date.now(), toolName: String(toolName || 'apply_patch') });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -222,7 +222,7 @@ export function setReadCached({ sessionId, args, cwd, content, toolUseId }) {
|
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
224
|
* Invalidate all cache entries for `path` in the given session. Called when
|
|
225
|
-
* a
|
|
225
|
+
* a mutation tool (apply_patch) touches the path.
|
|
226
226
|
*/
|
|
227
227
|
export function invalidatePathForSession(sessionId, path, cwd) {
|
|
228
228
|
if (!sessionId) return;
|