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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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','edit','write']) {
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.added.includes('shell') || !result.added.includes('task')) {
65
- throw new Error('shell alias should add shell/task: ' + JSON.stringify(result));
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
+ });
@@ -275,13 +275,13 @@ const smokeCatalog = [
275
275
  ].filter(Boolean);
276
276
 
277
277
  const fullDefaults = defaultDeferredToolNames(smokeCatalog, 'full');
278
- if (fullDefaults.size !== 12) {
279
- throw new Error(`full default surface should stay 12 tools, got ${fullDefaults.size}: ${[...fullDefaults].join(', ')}`);
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 ['shell', 'edit', 'write']) {
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 > 14000) {
310
- throw new Error(`full default tool surface too large: ${surfaceSize} chars (cap 14000)`);
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', 'edit', 'write']) {
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 tokens = roughTokenCount(messageContextText(message)) + 4;
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. edit, bridge, memory, skill, mcp.' },
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 = 8;
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 (['edit', 'write', 'apply_patch', 'shell'].includes(name)) return 'write';
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
- const msgs = s.messages || [];
3651
- const userPreviews = msgs
3652
- .filter(m => m && m.role === 'user')
3653
- .map(m => cleanSessionPreview(sessionMessageText(m.content)))
3654
- .filter(text => !isSessionPreviewNoise(text));
3655
- const preview = userPreviews[userPreviews.length - 1] || userPreviews[0] || '';
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: userAsst.length,
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; write/edit/shell rejected'
686
+ ? 'read-only; apply_patch/shell rejected'
687
687
  : permission === 'read-write'
688
- ? 'read + write/edit/shell'
688
+ ? 'read + apply_patch/shell'
689
689
  : permission === 'mcp'
690
- ? 'MCP/internal retrieval tools only; file/shell/edit tools rejected'
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 — write/edit/bash blocked at loop.mjs runtime
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-edit advisory marks: one-shot sidecar on the next read after a write.
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-edited for `sessionId`. Caller invokes this after a
9
- * successful write/edit/apply_patch so the next read on the same path can
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 || 'edit') });
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 write-class tool (write/edit/apply_patch) touches the path.
225
+ * a mutation tool (apply_patch) touches the path.
226
226
  */
227
227
  export function invalidatePathForSession(sessionId, path, cwd) {
228
228
  if (!sessionId) return;