mixdog 0.9.55 → 0.9.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/scripts/_smoke_wd.mjs +7 -0
- package/scripts/agent-terminal-reap-test.mjs +127 -2
- package/scripts/compact-file-reattach-test.mjs +88 -0
- package/scripts/compact-pressure-test.mjs +45 -21
- package/scripts/compact-recall-digest-test.mjs +57 -0
- package/scripts/compact-smoke.mjs +35 -39
- package/scripts/desktop-session-bridge-test.mjs +271 -9
- package/scripts/generate-oc-icons.mjs +11 -0
- package/scripts/live-share-test.mjs +148 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/max-output-recovery-test.mjs +12 -1
- package/scripts/mouse-tracking-restore-smoke.mjs +107 -0
- package/scripts/provider-admission-scheduler-test.mjs +100 -0
- package/scripts/provider-contract-test.mjs +49 -0
- package/scripts/resource-admission-test.mjs +5 -2
- package/scripts/session-ingest-compaction-smoke.mjs +38 -0
- package/scripts/steering-drain-buckets-test.mjs +15 -0
- package/scripts/submit-commandbusy-race-test.mjs +54 -3
- package/scripts/tool-smoke.mjs +2 -3
- package/scripts/tui-ambiguous-width-test.mjs +113 -0
- package/scripts/tui-runtime-load-bench.mjs +5 -0
- package/scripts/tui-transcript-perf-test.mjs +167 -0
- package/src/app.mjs +23 -0
- package/src/cli.mjs +6 -0
- package/src/lib/keychain-cjs.cjs +70 -20
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +18 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +111 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +61 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -4
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +43 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +203 -49
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -15
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +109 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -7
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +39 -39
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +108 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -11
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +132 -24
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +14 -33
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +169 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +474 -42
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +36 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +249 -11
- package/src/runtime/channels/data/voice-runtime-manifest.json +21 -5
- package/src/runtime/channels/lib/scheduler.mjs +8 -6
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +71 -8
- package/src/runtime/channels/lib/voice-transcription.mjs +2 -2
- package/src/runtime/channels/lib/whisper-language.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +7 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +20 -0
- package/src/runtime/memory/lib/query-handlers.mjs +10 -4
- package/src/runtime/memory/lib/recall-format.mjs +43 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +90 -64
- package/src/runtime/shared/err-text.mjs +4 -1
- package/src/runtime/shared/resource-admission.mjs +11 -0
- package/src/runtime/shared/schedule-model-ref.mjs +28 -0
- package/src/runtime/shared/schedule-session-run.mjs +63 -0
- package/src/runtime/shared/schedules-db.mjs +8 -3
- package/src/runtime/shared/tool-result-summary.mjs +4 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/transcript-writer.mjs +17 -0
- package/src/session-runtime/channel-config-api.mjs +11 -0
- package/src/session-runtime/config-helpers.mjs +9 -6
- package/src/session-runtime/context-status.mjs +80 -3
- package/src/session-runtime/lifecycle-api.mjs +154 -40
- package/src/session-runtime/provider-auth-api.mjs +21 -2
- package/src/session-runtime/provider-usage.mjs +4 -1
- package/src/session-runtime/resource-api.mjs +12 -11
- package/src/session-runtime/runtime-core.mjs +45 -11
- package/src/session-runtime/session-text.mjs +56 -6
- package/src/session-runtime/session-turn-api.mjs +70 -2
- package/src/session-runtime/tool-catalog.mjs +2 -1
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/render.mjs +5 -1
- package/src/standalone/agent-tool.mjs +63 -3
- package/src/standalone/channel-admin.mjs +19 -3
- package/src/standalone/channel-daemon.mjs +40 -0
- package/src/tui/app/resume-picker.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +19 -0
- package/src/tui/app/transcript-window.mjs +45 -8
- package/src/tui/app/use-mouse-input.mjs +101 -31
- package/src/tui/app/use-transcript-window.mjs +53 -9
- package/src/tui/components/ToolExecution.jsx +3 -4
- package/src/tui/components/TranscriptItem.jsx +3 -0
- package/src/tui/dist/index.mjs +17391 -14920
- package/src/tui/engine/context-state.mjs +10 -1
- package/src/tui/engine/live-share.mjs +390 -0
- package/src/tui/engine/queue-helpers.mjs +23 -0
- package/src/tui/engine/session-api-ext.mjs +439 -40
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +21 -7
- package/src/tui/engine/tool-card-results.mjs +3 -1
- package/src/tui/engine/turn.mjs +57 -4
- package/src/tui/engine.mjs +211 -7
- package/src/tui/index.jsx +2 -0
- package/src/tui/lib/voice-setup.mjs +10 -4
- package/src/tui/paste-attachments.mjs +4 -1
- package/src/vendor/statusline/src/gateway/session-routes.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +3 -0
- package/vendor/ink/build/display-width.js +14 -0
- package/vendor/ink/build/output.js +24 -3
- package/vendor/ink/build/wrap-text.d.ts +2 -0
- package/vendor/ink/build/wrap-text.js +45 -4
|
@@ -461,7 +461,7 @@ export class AnthropicOAuthProvider {
|
|
|
461
461
|
const fresh = loadCredentials();
|
|
462
462
|
if (fresh?.accessToken) {
|
|
463
463
|
this.credentials = fresh;
|
|
464
|
-
process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk (mtime change)\n`);
|
|
464
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk (mtime change)\n`);
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
|
|
@@ -493,7 +493,7 @@ export class AnthropicOAuthProvider {
|
|
|
493
493
|
if (disk?.accessToken && disk.accessToken !== currentToken
|
|
494
494
|
&& (!disk.expiresAt || disk.expiresAt >= validAfter)) {
|
|
495
495
|
this.credentials = disk;
|
|
496
|
-
process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk\n`);
|
|
496
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk\n`);
|
|
497
497
|
return disk;
|
|
498
498
|
}
|
|
499
499
|
if (!this.credentials && disk) this.credentials = disk;
|
|
@@ -510,13 +510,13 @@ export class AnthropicOAuthProvider {
|
|
|
510
510
|
const latestValidAfter = Date.now() + (force ? 0 : TOKEN_REFRESH_SKEW_MS);
|
|
511
511
|
if (latest?.accessToken && latest.accessToken !== currentToken
|
|
512
512
|
&& (!latest.expiresAt || latest.expiresAt >= latestValidAfter)) {
|
|
513
|
-
process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk\n`);
|
|
513
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Credentials reloaded from disk\n`);
|
|
514
514
|
return latest;
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
if (!latest?.refreshToken) {
|
|
518
518
|
if (!force && latest?.accessToken && (!latest.expiresAt || latest.expiresAt > Date.now())) {
|
|
519
|
-
process.stderr.write(`[anthropic-oauth] WARNING: token expiring but no refresh token; using current token until expiry\n`);
|
|
519
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] WARNING: token expiring but no refresh token; using current token until expiry\n`);
|
|
520
520
|
return latest;
|
|
521
521
|
}
|
|
522
522
|
throw new Error('Anthropic OAuth refresh token not available. Open /providers in mixdog to sign in again.');
|
|
@@ -525,12 +525,12 @@ export class AnthropicOAuthProvider {
|
|
|
525
525
|
try {
|
|
526
526
|
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Token ${reason}, refreshing...\n`);
|
|
527
527
|
const refreshed = await refreshOAuthCredentials(latest);
|
|
528
|
-
process.stderr.write(`[anthropic-oauth] Token refreshed, expires in ${Math.round(((refreshed.expiresAt || Date.now()) - Date.now()) / 1000)}s\n`);
|
|
528
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Token refreshed, expires in ${Math.round(((refreshed.expiresAt || Date.now()) - Date.now()) / 1000)}s\n`);
|
|
529
529
|
return refreshed;
|
|
530
530
|
} catch (err) {
|
|
531
531
|
if (!force && latest?.accessToken && (!latest.expiresAt || latest.expiresAt > Date.now())) {
|
|
532
532
|
const msg = err instanceof Error ? err.message : String(err);
|
|
533
|
-
process.stderr.write(`[anthropic-oauth] Refresh failed (${msg}); using still-valid current token\n`);
|
|
533
|
+
if (!process.env.MIXDOG_QUIET_PROVIDER_LOG) process.stderr.write(`[anthropic-oauth] Refresh failed (${msg}); using still-valid current token\n`);
|
|
534
534
|
return latest;
|
|
535
535
|
}
|
|
536
536
|
throw err;
|
|
@@ -27,6 +27,17 @@ function geminiInlineInfo(block) {
|
|
|
27
27
|
return { data, mimeType: cleanMimeType(inline.mimeType || inline.mime_type || inline.mediaType || inline.media_type) };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Inline document/file part ({ type: 'file', data, mimeType, filename? }) as
|
|
31
|
+
// submitted by the desktop composer (PDF attachments). Providers receive it
|
|
32
|
+
// as their native document shape; estimators must never serialize its base64.
|
|
33
|
+
function fileInfo(block) {
|
|
34
|
+
if (!block || typeof block !== 'object' || block.type !== 'file') return null;
|
|
35
|
+
if (typeof block.data !== 'string' || !block.data) return null;
|
|
36
|
+
const mimeType = String(block.mimeType || block.mediaType || 'application/pdf').trim().toLowerCase() || 'application/pdf';
|
|
37
|
+
const filename = typeof block.filename === 'string' && block.filename ? block.filename : '';
|
|
38
|
+
return { data: block.data, mimeType, filename };
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
function imageUrlFromPart(block) {
|
|
31
42
|
if (!block || typeof block !== 'object') return null;
|
|
32
43
|
if (block.type === 'image_url') {
|
|
@@ -100,7 +111,8 @@ function contentParts(content) {
|
|
|
100
111
|
}
|
|
101
112
|
if (content && typeof content === 'object'
|
|
102
113
|
&& (imageUrlFromPart(content) || imageFileIdFromPart(content)
|
|
103
|
-
|| imageFileUriFromPart(content) || geminiInlineInfo(content)
|
|
114
|
+
|| imageFileUriFromPart(content) || geminiInlineInfo(content)
|
|
115
|
+
|| fileInfo(content))) return [content];
|
|
104
116
|
return null;
|
|
105
117
|
}
|
|
106
118
|
|
|
@@ -108,7 +120,7 @@ function jsonFallbackFromPart(block) {
|
|
|
108
120
|
const text = textFromPart(block);
|
|
109
121
|
if (text) return text;
|
|
110
122
|
if (!block || typeof block !== 'object') return block == null ? '' : String(block);
|
|
111
|
-
if (imageUrlFromPart(block) || imageFileIdFromPart(block) || imageFileUriFromPart(block) || geminiInlineInfo(block)) return '';
|
|
123
|
+
if (imageUrlFromPart(block) || imageFileIdFromPart(block) || imageFileUriFromPart(block) || geminiInlineInfo(block) || fileInfo(block)) return '';
|
|
112
124
|
return stringifyFallback(block);
|
|
113
125
|
}
|
|
114
126
|
|
|
@@ -170,6 +182,17 @@ export function contentImageDescriptors(content) {
|
|
|
170
182
|
return parts.map(imageDescriptor).filter(Boolean);
|
|
171
183
|
}
|
|
172
184
|
|
|
185
|
+
// Byte-free descriptors for inline file/document parts (context estimation).
|
|
186
|
+
export function contentFileDescriptors(content) {
|
|
187
|
+
const parts = contentParts(content);
|
|
188
|
+
if (!parts) return [];
|
|
189
|
+
return parts.flatMap((part) => {
|
|
190
|
+
const file = fileInfo(part);
|
|
191
|
+
if (!file) return [];
|
|
192
|
+
return [{ mimeType: file.mimeType, sizeBytes: Math.floor((file.data.length * 3) / 4) }];
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
173
196
|
export function contentToText(content, fallback = '') {
|
|
174
197
|
if (typeof content === 'string') return content;
|
|
175
198
|
const parts = contentParts(content);
|
|
@@ -192,6 +215,10 @@ function sanitizePartForStoredHistory(part) {
|
|
|
192
215
|
if (part.type === 'image' || part.type === 'image_url' || part.type === 'input_image' || imageUrlFromPart(part) || imageFileIdFromPart(part) || imageFileUriFromPart(part) || geminiInlineInfo(part)) {
|
|
193
216
|
return { type: 'text', text: storedHistoryImagePlaceholder(part) };
|
|
194
217
|
}
|
|
218
|
+
const file = fileInfo(part);
|
|
219
|
+
if (file) {
|
|
220
|
+
return { type: 'text', text: `[File omitted from stored history: ${file.filename || file.mimeType}]` };
|
|
221
|
+
}
|
|
195
222
|
if (Array.isArray(part.content)) {
|
|
196
223
|
const nextContent = sanitizeContentForStoredHistory(part.content);
|
|
197
224
|
if (nextContent !== part.content) return { ...part, content: nextContent };
|
|
@@ -217,6 +244,16 @@ export function normalizeContentForAnthropic(content) {
|
|
|
217
244
|
const parts = contentParts(content);
|
|
218
245
|
if (!parts) return content;
|
|
219
246
|
return parts.map((part) => {
|
|
247
|
+
const file = fileInfo(part);
|
|
248
|
+
if (file) {
|
|
249
|
+
const out = {
|
|
250
|
+
type: 'document',
|
|
251
|
+
source: { type: 'base64', media_type: file.mimeType, data: file.data },
|
|
252
|
+
...(file.filename ? { title: file.filename } : {}),
|
|
253
|
+
};
|
|
254
|
+
if (part.cache_control) out.cache_control = part.cache_control;
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
220
257
|
const info = imageInfo(part);
|
|
221
258
|
if (info) {
|
|
222
259
|
const out = {
|
|
@@ -276,6 +313,14 @@ export function normalizeContentForOpenAIChat(content, { role = 'user' } = {}) {
|
|
|
276
313
|
if (!parts) return content;
|
|
277
314
|
const out = [];
|
|
278
315
|
for (const part of parts) {
|
|
316
|
+
const file = fileInfo(part);
|
|
317
|
+
if (file) {
|
|
318
|
+
out.push({
|
|
319
|
+
type: 'file',
|
|
320
|
+
file: { filename: file.filename || 'document.pdf', file_data: `data:${file.mimeType};base64,${file.data}` },
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
279
324
|
const fileId = imageFileIdFromPart(part);
|
|
280
325
|
if (fileId) {
|
|
281
326
|
out.push({ type: 'text', text: `[unsupported image file_id for OpenAI Chat-compatible request: ${fileId}]` });
|
|
@@ -308,6 +353,15 @@ export function normalizeContentForOpenAIResponses(content, { role = 'user' } =
|
|
|
308
353
|
}
|
|
309
354
|
const out = [];
|
|
310
355
|
for (const part of parts) {
|
|
356
|
+
const file = fileInfo(part);
|
|
357
|
+
if (file) {
|
|
358
|
+
out.push({
|
|
359
|
+
type: 'input_file',
|
|
360
|
+
filename: file.filename || 'document.pdf',
|
|
361
|
+
file_data: `data:${file.mimeType};base64,${file.data}`,
|
|
362
|
+
});
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
311
365
|
const fileId = imageFileIdFromPart(part);
|
|
312
366
|
if (fileId) {
|
|
313
367
|
out.push({ type: 'input_image', file_id: fileId });
|
|
@@ -338,6 +392,11 @@ export function normalizeContentForGeminiParts(content) {
|
|
|
338
392
|
}
|
|
339
393
|
const out = [];
|
|
340
394
|
for (const part of parts) {
|
|
395
|
+
const file = fileInfo(part);
|
|
396
|
+
if (file) {
|
|
397
|
+
out.push({ inlineData: { mimeType: file.mimeType, data: file.data } });
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
341
400
|
const inlineInfo = geminiInlineInfo(part);
|
|
342
401
|
if (inlineInfo) {
|
|
343
402
|
out.push({ inlineData: { mimeType: inlineInfo.mimeType, data: inlineInfo.data } });
|
|
@@ -144,6 +144,15 @@ const MAX_OUTPUT_EXHAUSTED_NOTICE = '[mixdog-runtime] Output remained truncated
|
|
|
144
144
|
// was not done — re-prompt instead of accepting empty as final.
|
|
145
145
|
// Covers Anthropic (pause_turn, max_tokens), OpenAI (length), Gemini
|
|
146
146
|
// (MAX_TOKENS, OTHER), and case variants.
|
|
147
|
+
export function attachAssistantTranscriptMetadata(message, opts = {}) {
|
|
148
|
+
const transcript = typeof opts.takeAssistantTranscriptMetadata === 'function'
|
|
149
|
+
? opts.takeAssistantTranscriptMetadata()
|
|
150
|
+
: null;
|
|
151
|
+
if (!transcript) return message;
|
|
152
|
+
const meta = message?.meta && typeof message.meta === 'object' ? message.meta : {};
|
|
153
|
+
return { ...message, meta: { ...meta, transcript } };
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
|
|
148
157
|
let iterations = 0;
|
|
149
158
|
let toolCallsTotal = 0;
|
|
@@ -253,7 +262,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
253
262
|
? resp.providerMetadata
|
|
254
263
|
: null;
|
|
255
264
|
if (!content && !reasoningContent && !reasoningItems && !thinkingBlocks) return false;
|
|
256
|
-
const message = {
|
|
265
|
+
const message = attachAssistantTranscriptMetadata({
|
|
257
266
|
role: 'assistant',
|
|
258
267
|
content,
|
|
259
268
|
// Anthropic adaptive-thinking signatures must be replayed verbatim
|
|
@@ -262,7 +271,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
262
271
|
...(reasoningItems ? { reasoningItems } : {}),
|
|
263
272
|
...(reasoningContent ? { reasoningContent } : {}),
|
|
264
273
|
...(providerMetadata ? { providerMetadata } : {}),
|
|
265
|
-
};
|
|
274
|
+
}, opts);
|
|
266
275
|
messages.push(message);
|
|
267
276
|
try { opts.onAssistantMessageCommitted?.(message); } catch {}
|
|
268
277
|
return true;
|
|
@@ -781,7 +790,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
781
790
|
// openai-oauth.convertMessagesToResponsesInput emits matching
|
|
782
791
|
// type:'reasoning' input items on the next turn to keep the openai-oauth
|
|
783
792
|
// server-side cache prefix stable.
|
|
784
|
-
const _assistantTurnMsg = {
|
|
793
|
+
const _assistantTurnMsg = attachAssistantTranscriptMetadata({
|
|
785
794
|
role: 'assistant',
|
|
786
795
|
// Sub-agent tool-call turns carry only mid-turn preamble in
|
|
787
796
|
// response.content (the real result rides the later final-answer
|
|
@@ -805,7 +814,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
805
814
|
...(response.providerMetadata && typeof response.providerMetadata === 'object'
|
|
806
815
|
? { providerMetadata: response.providerMetadata }
|
|
807
816
|
: {}),
|
|
808
|
-
};
|
|
817
|
+
}, opts);
|
|
809
818
|
messages.push(_assistantTurnMsg);
|
|
810
819
|
try { opts.onAssistantMessageCommitted?.(_assistantTurnMsg); } catch {}
|
|
811
820
|
// Hard-cap final turn: tools are disabled but the model still emitted
|
|
@@ -48,6 +48,37 @@ import {
|
|
|
48
48
|
RECALL_TAIL_TRUNCATION_MARKER,
|
|
49
49
|
RECALL_TAIL_SHORT_TRUNCATION_MARKER,
|
|
50
50
|
} from './summary.mjs';
|
|
51
|
+
import { buildPostCompactFileAttachment } from './file-reattach.mjs';
|
|
52
|
+
|
|
53
|
+
// Post-compact file re-attachment (claude-code parity): re-inject fresh reads
|
|
54
|
+
// of files the summarized-away head was working with, when they still fit the
|
|
55
|
+
// budget. Applied identically by the semantic and recall-fasttrack paths.
|
|
56
|
+
// Follows the `Reference files:` + assistant `.` ack convention from
|
|
57
|
+
// session-lifecycle.mjs so provider turn alternation and ingest exclusion
|
|
58
|
+
// both hold. Best-effort: on any failure the plain result stands.
|
|
59
|
+
function withFileReattachment(result, finalTokens, budget, headMessages, tailMessages, cwd) {
|
|
60
|
+
try {
|
|
61
|
+
const attachment = buildPostCompactFileAttachment(
|
|
62
|
+
headMessages,
|
|
63
|
+
tailMessages,
|
|
64
|
+
budget - finalTokens,
|
|
65
|
+
{ cwd },
|
|
66
|
+
);
|
|
67
|
+
if (!attachment) return { result, finalTokens, reattached: false };
|
|
68
|
+
const summaryIdx = result.length - tailMessages.length;
|
|
69
|
+
const augmented = reconcileDedupStubs(dedupToolResultBodies(sanitizeToolPairs([
|
|
70
|
+
...result.slice(0, summaryIdx),
|
|
71
|
+
attachment,
|
|
72
|
+
{ role: 'assistant', content: '.' },
|
|
73
|
+
...result.slice(summaryIdx),
|
|
74
|
+
])));
|
|
75
|
+
const augmentedTokens = estimateMessagesTokens(augmented);
|
|
76
|
+
if (augmentedTokens > budget) return { result, finalTokens, reattached: false };
|
|
77
|
+
return { result: augmented, finalTokens: augmentedTokens, reattached: true };
|
|
78
|
+
} catch {
|
|
79
|
+
return { result, finalTokens, reattached: false };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
51
82
|
|
|
52
83
|
const DEFAULT_TAIL_TURNS = 2;
|
|
53
84
|
const COMPACTION_PROMPT_HEADROOM = 0.85;
|
|
@@ -433,10 +464,15 @@ export async function semanticCompactMessages(provider, messages, model, budgetT
|
|
|
433
464
|
// is both measured and emitted in redacted form.
|
|
434
465
|
let result = sanitizeToolPairs([...selected.system, summaryMessage, ...selected.tail]);
|
|
435
466
|
result = reconcileDedupStubs(dedupToolResultBodies(result));
|
|
436
|
-
|
|
467
|
+
let finalTokens = estimateMessagesTokens(result);
|
|
437
468
|
if (finalTokens > budget) {
|
|
438
469
|
throw new Error(`semanticCompactMessages: compacted result exceeds budget=${budget} (result=${finalTokens})`);
|
|
439
470
|
}
|
|
471
|
+
// Re-attach fresh reads of head files. Uses the RAW (pre-ingest-filter)
|
|
472
|
+
// head so tool_call read paths are visible even on the /compact path.
|
|
473
|
+
const reattach = withFileReattachment(result, finalTokens, budget, selectedRaw.head, selected.tail, opts.cwd);
|
|
474
|
+
result = reattach.result;
|
|
475
|
+
finalTokens = reattach.finalTokens;
|
|
440
476
|
const diagnostics = {
|
|
441
477
|
noOp: false,
|
|
442
478
|
inputMessages: Array.isArray(messages) ? messages.length : 0,
|
|
@@ -471,6 +507,7 @@ export async function semanticCompactMessages(provider, messages, model, budgetT
|
|
|
471
507
|
summaryChars: String(summary || '').length,
|
|
472
508
|
rawSummaryChars: String(rawSummary || '').length,
|
|
473
509
|
summaryRepaired: enforced.repaired === true,
|
|
510
|
+
fileReattached: reattach.reattached,
|
|
474
511
|
previousSummary: !!selected.previousSummary,
|
|
475
512
|
durationMs: Date.now() - startedAt,
|
|
476
513
|
};
|
|
@@ -767,10 +804,13 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
767
804
|
|
|
768
805
|
let result = sanitizeToolPairs([...safeSystem, summaryMessage, ...recallTail]);
|
|
769
806
|
result = reconcileDedupStubs(dedupToolResultBodies(result));
|
|
770
|
-
|
|
807
|
+
let finalTokens = estimateMessagesTokens(result);
|
|
771
808
|
if (finalTokens > budget) {
|
|
772
809
|
throw new Error(`recallFastTrackCompactMessages: compacted result exceeds budget=${budget} (result=${finalTokens})`);
|
|
773
810
|
}
|
|
811
|
+
const reattach = withFileReattachment(result, finalTokens, budget, recallHead, recallTail, opts.cwd);
|
|
812
|
+
result = reattach.result;
|
|
813
|
+
finalTokens = reattach.finalTokens;
|
|
774
814
|
const summaryContent = String(summaryMessage?.content || '');
|
|
775
815
|
const diagnostics = {
|
|
776
816
|
noOp: false,
|
|
@@ -807,6 +847,7 @@ function _recallFastTrackCompactMessages(messages, budgetTokens, opts = {}) {
|
|
|
807
847
|
recallTruncatedInSummary: !!recallFit.recall && !summaryContent.includes(recallFit.recall),
|
|
808
848
|
priorTruncatedInSummary: !!recallFit.prior && !summaryContent.includes(recallFit.prior),
|
|
809
849
|
tailTruncated: recallTail.some((m) => messageContentHasMarker(m, RECALL_TAIL_TRUNCATION_MARKER) || messageContentHasMarker(m, RECALL_TAIL_SHORT_TRUNCATION_MARKER)),
|
|
850
|
+
fileReattached: reattach.reattached,
|
|
810
851
|
tailOptions: recallTailOpts,
|
|
811
852
|
previousSummary: !!previousSummary,
|
|
812
853
|
durationMs: Date.now() - startedAt,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Post-compact file re-attachment (claude-code createPostCompactFileAttachments
|
|
2
|
+
// parity). Files the summarized-away head had `read` are re-read FRESH from
|
|
3
|
+
// disk and re-injected right after the summary message, so the model does not
|
|
4
|
+
// burn a turn (and tokens) re-reading files it was actively working with.
|
|
5
|
+
// - newest-first, capped at MAX_REATTACH_FILES and per-file/total token caps
|
|
6
|
+
// - files whose `read` tool_call survives in the preserved tail are skipped
|
|
7
|
+
// (the model can already see that content — re-injecting is pure waste)
|
|
8
|
+
// - message shape reuses the existing `Reference files:` user-row convention
|
|
9
|
+
// (session-lifecycle.mjs), which memory ingest already excludes.
|
|
10
|
+
// Best-effort: any fs/parse failure skips the file, never fails the compact.
|
|
11
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
12
|
+
import { isAbsolute, resolve as resolvePath } from 'node:path';
|
|
13
|
+
import { estimateTokens } from '../context-utils.mjs';
|
|
14
|
+
|
|
15
|
+
export const MAX_REATTACH_FILES = 3;
|
|
16
|
+
export const REATTACH_MAX_TOKENS_PER_FILE = 5_000;
|
|
17
|
+
export const REATTACH_MAX_TOTAL_TOKENS = 8_000;
|
|
18
|
+
const REATTACH_MIN_ROOM_TOKENS = 1_024;
|
|
19
|
+
const REATTACH_MAX_FILE_BYTES = 512 * 1024;
|
|
20
|
+
|
|
21
|
+
function reattachDisabled() {
|
|
22
|
+
return String(process.env.MIXDOG_COMPACT_FILE_REATTACH || '').trim() === '0';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pathsFromReadArgs(rawArgs) {
|
|
26
|
+
let args = rawArgs;
|
|
27
|
+
if (typeof args === 'string') {
|
|
28
|
+
try { args = JSON.parse(args); } catch { return []; }
|
|
29
|
+
}
|
|
30
|
+
if (!args || typeof args !== 'object') return [];
|
|
31
|
+
const value = args.path;
|
|
32
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
if (typeof entry === 'string' && entry.trim()) out.push(entry.trim());
|
|
36
|
+
else if (entry && typeof entry === 'object' && typeof entry.path === 'string' && entry.path.trim()) {
|
|
37
|
+
out.push(entry.path.trim());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Ordered (oldest -> newest) file paths read via the `read` tool in `messages`.
|
|
44
|
+
export function collectReadToolPaths(messages) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const m of Array.isArray(messages) ? messages : []) {
|
|
47
|
+
if (m?.role !== 'assistant' || !Array.isArray(m.toolCalls)) continue;
|
|
48
|
+
for (const tc of m.toolCalls) {
|
|
49
|
+
const name = String(tc?.name || tc?.function?.name || '').toLowerCase();
|
|
50
|
+
if (name !== 'read') continue;
|
|
51
|
+
out.push(...pathsFromReadArgs(tc?.arguments ?? tc?.function?.arguments));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function truncateToTokenCap(content, cap) {
|
|
58
|
+
if (estimateTokens(content) <= cap) return content;
|
|
59
|
+
const marker = '\n[truncated after compaction — Read the full file if needed]';
|
|
60
|
+
// ~4 chars/token head-keep; iterate down until the estimate fits.
|
|
61
|
+
let chars = Math.max(0, cap * 4);
|
|
62
|
+
let out = content.slice(0, chars) + marker;
|
|
63
|
+
while (chars > 256 && estimateTokens(out) > cap) {
|
|
64
|
+
chars = Math.floor(chars * 0.8);
|
|
65
|
+
out = content.slice(0, chars) + marker;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build ONE `Reference files:` user message re-attaching the freshest files the
|
|
72
|
+
* compacted-away head had read, or null when nothing qualifies/fits.
|
|
73
|
+
* `roomTokens` is the remaining post-compact budget the attachment may use.
|
|
74
|
+
*/
|
|
75
|
+
export function buildPostCompactFileAttachment(headMessages, tailMessages, roomTokens, { cwd } = {}) {
|
|
76
|
+
if (reattachDisabled()) return null;
|
|
77
|
+
const room = Math.min(Number(roomTokens) || 0, REATTACH_MAX_TOTAL_TOKENS);
|
|
78
|
+
if (room < REATTACH_MIN_ROOM_TOKENS) return null;
|
|
79
|
+
const headPaths = collectReadToolPaths(headMessages);
|
|
80
|
+
if (headPaths.length === 0) return null;
|
|
81
|
+
const tailPaths = new Set(collectReadToolPaths(tailMessages));
|
|
82
|
+
// newest-first unique selection
|
|
83
|
+
const selected = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (let i = headPaths.length - 1; i >= 0 && selected.length < MAX_REATTACH_FILES; i -= 1) {
|
|
86
|
+
const p = headPaths[i];
|
|
87
|
+
if (seen.has(p) || tailPaths.has(p)) continue;
|
|
88
|
+
seen.add(p);
|
|
89
|
+
selected.push(p);
|
|
90
|
+
}
|
|
91
|
+
if (selected.length === 0) return null;
|
|
92
|
+
const sections = [];
|
|
93
|
+
const prefix = 'Reference files:\n\nRe-attached after compaction (fresh reads of files the summarized history was working with):\n\n';
|
|
94
|
+
for (const p of selected) {
|
|
95
|
+
try {
|
|
96
|
+
const abs = isAbsolute(p) ? p : (cwd ? resolvePath(cwd, p) : null);
|
|
97
|
+
if (!abs) continue;
|
|
98
|
+
const stat = statSync(abs);
|
|
99
|
+
if (!stat.isFile() || stat.size > REATTACH_MAX_FILE_BYTES) continue;
|
|
100
|
+
const body = truncateToTokenCap(readFileSync(abs, 'utf8'), REATTACH_MAX_TOKENS_PER_FILE);
|
|
101
|
+
const section = `### ${p}\n\`\`\`\n${body}\n\`\`\``;
|
|
102
|
+
const candidate = `${prefix}${[...sections, section].join('\n\n')}`;
|
|
103
|
+
if (estimateTokens(candidate) > room) continue;
|
|
104
|
+
sections.push(section);
|
|
105
|
+
} catch { /* unreadable/missing file — skip, never fail the compact */ }
|
|
106
|
+
}
|
|
107
|
+
if (sections.length === 0) return null;
|
|
108
|
+
return {
|
|
109
|
+
role: 'user',
|
|
110
|
+
content: `${prefix}${sections.join('\n\n')}`,
|
|
111
|
+
};
|
|
112
|
+
}
|