mixdog 0.9.97 → 0.9.99

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.
Files changed (48) hide show
  1. package/package.json +9 -2
  2. package/src/rules/agent/30-explorer.md +43 -32
  3. package/src/runtime/agent/orchestrator/agent-trace.mjs +19 -5
  4. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +7 -0
  5. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -3
  6. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +5 -3
  7. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +23 -1
  8. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -1
  9. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +44 -6
  10. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -1
  11. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +3 -6
  12. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +103 -12
  13. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +10 -30
  14. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -84
  15. package/src/runtime/attachments/pdf-extract.mjs +83 -0
  16. package/src/runtime/attachments/store.mjs +515 -0
  17. package/src/runtime/attachments/store.test.mjs +188 -0
  18. package/src/runtime/channels/lib/interaction-handlers.mjs +1 -191
  19. package/src/runtime/channels/lib/runtime-paths.mjs +1 -4
  20. package/src/runtime/channels/lib/worker-ipc.mjs +0 -66
  21. package/src/runtime/channels/lib/worker-main.mjs +1 -20
  22. package/src/runtime/memory/lib/http-wire.mjs +39 -4
  23. package/src/runtime/shared/json-metrics.mjs +94 -0
  24. package/src/runtime/shared/turn-snapshot.mjs +13 -0
  25. package/src/runtime/shared/turn-worktree-snapshot.mjs +19 -0
  26. package/src/session-runtime/provider-models.mjs +11 -1
  27. package/src/session-runtime/quick-model-rows.mjs +38 -7
  28. package/src/session-runtime/quick-search-models.mjs +15 -17
  29. package/src/session-runtime/runtime-core.mjs +9 -4
  30. package/src/session-runtime/runtime-tunables.mjs +9 -6
  31. package/src/session-runtime/session-title.mjs +27 -3
  32. package/src/standalone/channel-transport.mjs +28 -12
  33. package/src/standalone/daemon.mjs +3 -0
  34. package/src/standalone/session-protocol.mjs +1 -0
  35. package/src/standalone/session-service.mjs +23 -1
  36. package/src/standalone/session-transport.mjs +3 -2
  37. package/src/tui/app/prompt-submit.mjs +7 -9
  38. package/src/tui/app/use-prompt-handlers.mjs +7 -3
  39. package/src/tui/dist/index.mjs +249 -43
  40. package/src/tui/paste-attachments.mjs +13 -7
  41. package/src/tui/prompt-history-store.mjs +52 -4
  42. package/src/tui/session/agent-job-feed.mjs +5 -1
  43. package/src/tui/session/live-share.mjs +21 -7
  44. package/src/tui/session/queue-helpers.mjs +6 -6
  45. package/src/tui/session/session-api-ext.mjs +12 -4
  46. package/src/tui/session/session-api.mjs +10 -1
  47. package/src/tui/session/session-flow.mjs +4 -2
  48. package/src/tui/session-local.mjs +15 -5
@@ -62,6 +62,40 @@ var init_plugin_paths = __esm({
62
62
  });
63
63
 
64
64
  // src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs
65
+ import { createHash } from "node:crypto";
66
+ function imageProfileForProvider(provider) {
67
+ const value = String(provider || "").trim().toLowerCase();
68
+ return /^(?:openai|xai|grok|deepseek|opencode-go|ollama|lmstudio)(?:-|$)/.test(value) ? "openai" : "anthropic";
69
+ }
70
+ function openAIImagePatchCount(width, height) {
71
+ return Math.ceil(Math.max(1, Number(width) || 1) / OPENAI_IMAGE_PATCH_SIZE) * Math.ceil(Math.max(1, Number(height) || 1) / OPENAI_IMAGE_PATCH_SIZE);
72
+ }
73
+ function resizeCacheKey(buffer, ext, maxTokens, profile) {
74
+ return createHash("sha256").update(String(ext || "")).update("\0").update(String(maxTokens || 0)).update("\0").update(String(profile || "")).update("\0").update(buffer).digest("hex");
75
+ }
76
+ function cloneResizeResult(result) {
77
+ return {
78
+ ...result,
79
+ ...result?.dimensions ? { dimensions: { ...result.dimensions } } : {}
80
+ };
81
+ }
82
+ function rememberResizeResult(key, result) {
83
+ const bytes = Buffer.byteLength(String(result?.data || ""), "base64");
84
+ if (bytes <= 0 || bytes > IMAGE_RESIZE_CACHE_MAX_BYTES) return;
85
+ const existing = imageResizeCache.get(key);
86
+ if (existing) {
87
+ imageResizeCacheBytes -= existing.bytes;
88
+ imageResizeCache.delete(key);
89
+ }
90
+ imageResizeCache.set(key, { result: cloneResizeResult(result), bytes });
91
+ imageResizeCacheBytes += bytes;
92
+ while (imageResizeCacheBytes > IMAGE_RESIZE_CACHE_MAX_BYTES && imageResizeCache.size > 0) {
93
+ const oldest = imageResizeCache.keys().next().value;
94
+ const evicted = imageResizeCache.get(oldest);
95
+ imageResizeCache.delete(oldest);
96
+ imageResizeCacheBytes -= evicted?.bytes || 0;
97
+ }
98
+ }
65
99
  async function loadSharp() {
66
100
  if (_sharpPromise === void 0) {
67
101
  _sharpPromise = (async () => {
@@ -101,8 +135,21 @@ function imageMetadataText(dims, sourcePath) {
101
135
  }
102
136
  return `[Image: ${parts.join(", ")}]`;
103
137
  }
104
- async function resizeImageBuffer(buffer, ext, { maxTokens = DEFAULT_IMAGE_MAX_TOKENS } = {}) {
138
+ async function resizeImageBuffer(buffer, ext, {
139
+ maxTokens = DEFAULT_IMAGE_MAX_TOKENS,
140
+ profile = "anthropic"
141
+ } = {}) {
105
142
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) return null;
143
+ const normalizedProfile = profile === "openai" ? "openai" : "anthropic";
144
+ const cacheKey = resizeCacheKey(buffer, ext, maxTokens, normalizedProfile);
145
+ const cached = imageResizeCache.get(cacheKey);
146
+ if (cached) {
147
+ imageResizeCacheHits += 1;
148
+ imageResizeCache.delete(cacheKey);
149
+ imageResizeCache.set(cacheKey, cached);
150
+ return cloneResizeResult(cached.result);
151
+ }
152
+ imageResizeCacheMisses += 1;
106
153
  const sharp = await loadSharp();
107
154
  if (!sharp) return null;
108
155
  try {
@@ -118,13 +165,20 @@ async function resizeImageBuffer(buffer, ext, { maxTokens = DEFAULT_IMAGE_MAX_TO
118
165
  if (originalWidth && originalHeight) {
119
166
  let width = originalWidth;
120
167
  let height = originalHeight;
121
- if (width > IMAGE_MAX_WIDTH) {
122
- height = Math.round(height * IMAGE_MAX_WIDTH / width);
123
- width = IMAGE_MAX_WIDTH;
168
+ const maxWidth = normalizedProfile === "openai" ? OPENAI_IMAGE_MAX_DIMENSION : IMAGE_MAX_WIDTH;
169
+ const maxHeight = normalizedProfile === "openai" ? OPENAI_IMAGE_MAX_DIMENSION : IMAGE_MAX_HEIGHT;
170
+ let scale = Math.min(1, maxWidth / width, maxHeight / height);
171
+ if (normalizedProfile === "openai") {
172
+ scale = Math.min(
173
+ scale,
174
+ Math.sqrt(OPENAI_IMAGE_MAX_PATCHES * OPENAI_IMAGE_PATCH_SIZE ** 2 / (width * height))
175
+ );
124
176
  }
125
- if (height > IMAGE_MAX_HEIGHT) {
126
- width = Math.round(width * IMAGE_MAX_HEIGHT / height);
127
- height = IMAGE_MAX_HEIGHT;
177
+ width = Math.max(1, Math.floor(width * scale));
178
+ height = Math.max(1, Math.floor(height * scale));
179
+ while (normalizedProfile === "openai" && openAIImagePatchCount(width, height) > OPENAI_IMAGE_MAX_PATCHES) {
180
+ if (width >= height) width -= 1;
181
+ else height -= 1;
128
182
  }
129
183
  const needsResize = width !== originalWidth || height !== originalHeight;
130
184
  if (needsResize || originalSize > IMAGE_TARGET_RAW_SIZE) {
@@ -161,16 +215,18 @@ async function resizeImageBuffer(buffer, ext, { maxTokens = DEFAULT_IMAGE_MAX_TO
161
215
  }
162
216
  }
163
217
  }
164
- return {
218
+ const result = {
165
219
  data: base64,
166
220
  mimeType: `image/${mediaType}`,
167
221
  dimensions: { originalWidth, originalHeight, displayWidth: displayWidth2, displayHeight }
168
222
  };
223
+ rememberResizeResult(cacheKey, result);
224
+ return result;
169
225
  } catch {
170
226
  return null;
171
227
  }
172
228
  }
173
- var API_IMAGE_MAX_BASE64_SIZE, IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, DEFAULT_IMAGE_MAX_TOKENS, _sharpPromise;
229
+ var API_IMAGE_MAX_BASE64_SIZE, IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, DEFAULT_IMAGE_MAX_TOKENS, OPENAI_IMAGE_MAX_DIMENSION, OPENAI_IMAGE_PATCH_SIZE, OPENAI_IMAGE_MAX_PATCHES, IMAGE_RESIZE_CACHE_MAX_BYTES, imageResizeCache, imageResizeCacheBytes, imageResizeCacheHits, imageResizeCacheMisses, _sharpPromise;
174
230
  var init_read_image_resize = __esm({
175
231
  "src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs"() {
176
232
  API_IMAGE_MAX_BASE64_SIZE = 5 * 1024 * 1024;
@@ -178,6 +234,14 @@ var init_read_image_resize = __esm({
178
234
  IMAGE_MAX_WIDTH = 2e3;
179
235
  IMAGE_MAX_HEIGHT = 2e3;
180
236
  DEFAULT_IMAGE_MAX_TOKENS = Math.ceil(API_IMAGE_MAX_BASE64_SIZE * 0.125);
237
+ OPENAI_IMAGE_MAX_DIMENSION = 2048;
238
+ OPENAI_IMAGE_PATCH_SIZE = 32;
239
+ OPENAI_IMAGE_MAX_PATCHES = 1536;
240
+ IMAGE_RESIZE_CACHE_MAX_BYTES = 64 * 1024 * 1024;
241
+ imageResizeCache = /* @__PURE__ */ new Map();
242
+ imageResizeCacheBytes = 0;
243
+ imageResizeCacheHits = 0;
244
+ imageResizeCacheMisses = 0;
181
245
  }
182
246
  });
183
247
 
@@ -246,17 +310,6 @@ function pastedTextReferenceIds(input) {
246
310
  const re = /\[Pasted text #(\d+) \+\d+ lines\]/g;
247
311
  return new Set([...String(input || "").matchAll(re)].map((m) => Number(m[1]) || 0).filter(Boolean));
248
312
  }
249
- function expandPastedTextTokens(value, pastedTexts = {}) {
250
- let out = String(value ?? "");
251
- if (!pastedTexts || typeof pastedTexts !== "object") return out;
252
- const re = /\[Pasted text #(\d+) \+\d+ lines\]/g;
253
- out = out.replace(re, (match, idRaw) => {
254
- const entry = pastedTexts[Number(idRaw)] || pastedTexts[idRaw];
255
- if (entry && typeof entry.text === "string") return entry.text;
256
- return match;
257
- });
258
- return out;
259
- }
260
313
  function buildPromptContentWithImages(text, pastedImages = {}) {
261
314
  const value = String(text ?? "");
262
315
  const refs = imageReferenceIds(value);
@@ -290,10 +343,14 @@ function splitPastedImagePathCandidates(text) {
290
343
  }
291
344
  return out;
292
345
  }
293
- async function imageAttachmentFromBuffer(buffer, mimeType, { filename = "Pasted image", sourcePath = "" } = {}) {
346
+ async function imageAttachmentFromBuffer(buffer, mimeType, {
347
+ filename = "Pasted image",
348
+ sourcePath = "",
349
+ provider = ""
350
+ } = {}) {
294
351
  if (!Buffer2.isBuffer(buffer) || buffer.length === 0) throw new Error("image is empty");
295
352
  const ext = (mimeType || "image/png").split("/")[1] || "png";
296
- const resized = await resizeImageBuffer(buffer, ext);
353
+ const resized = await resizeImageBuffer(buffer, ext, { profile: imageProfileForProvider(provider) });
297
354
  if (resized) {
298
355
  return {
299
356
  type: "image",
@@ -313,7 +370,7 @@ async function imageAttachmentFromBuffer(buffer, mimeType, { filename = "Pasted
313
370
  }
314
371
  return { type: "image", content: data, mediaType: mimeType || "image/png", filename, sourcePath };
315
372
  }
316
- async function readImageAttachmentFromPath(rawPath, cwd = process.cwd()) {
373
+ async function readImageAttachmentFromPath(rawPath, cwd = process.cwd(), { provider = "" } = {}) {
317
374
  const cleaned = cleanPathText(rawPath);
318
375
  const mimeType = mimeForPath(cleaned);
319
376
  if (!mimeType) return null;
@@ -328,7 +385,8 @@ async function readImageAttachmentFromPath(rawPath, cwd = process.cwd()) {
328
385
  const buffer = await readFileAsync(fullPath);
329
386
  return imageAttachmentFromBuffer(buffer, mimeType, {
330
387
  filename: basename2(fullPath),
331
- sourcePath: fullPath
388
+ sourcePath: fullPath,
389
+ provider
332
390
  });
333
391
  }
334
392
  async function readClipboardImageToTempFile() {
@@ -350,19 +408,19 @@ close access fp`;
350
408
  }
351
409
  return null;
352
410
  }
353
- async function readClipboardImageAttachment() {
411
+ async function readClipboardImageAttachment({ provider = "" } = {}) {
354
412
  if (process.platform === "linux") {
355
413
  const wl = await execFileBuffer("wl-paste", ["--type", "image/png"], { timeout: 3e3 });
356
- if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, "image/png");
414
+ if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, "image/png", { provider });
357
415
  const xc = await execFileBuffer("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"], { timeout: 3e3 });
358
- if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, "image/png");
416
+ if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, "image/png", { provider });
359
417
  return null;
360
418
  }
361
419
  const file = await readClipboardImageToTempFile();
362
420
  if (!file) return null;
363
421
  try {
364
422
  const buffer = await readFileAsync(file);
365
- return await imageAttachmentFromBuffer(buffer, "image/png");
423
+ return await imageAttachmentFromBuffer(buffer, "image/png", { provider });
366
424
  } finally {
367
425
  try {
368
426
  unlinkSync(file);
@@ -879,7 +937,7 @@ import {
879
937
  } from "fs/promises";
880
938
  import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
881
939
  import { homedir as homedir2 } from "os";
882
- import { createHash } from "crypto";
940
+ import { createHash as createHash2 } from "crypto";
883
941
  function mixdogConfigBaseDir() {
884
942
  return process.env.MIXDOG_CONFIG_DIR || join4(homedir2(), ".mixdog");
885
943
  }
@@ -893,7 +951,7 @@ function safeReason(reason) {
893
951
  return String(reason || "snapshot").replace(/[^a-z0-9_.-]+/gi, "-").slice(0, 48) || "snapshot";
894
952
  }
895
953
  function initMarkerPath(dataDir) {
896
- const id = createHash("sha256").update(String(dataDir || "unknown")).digest("hex").slice(0, 16);
954
+ const id = createHash2("sha256").update(String(dataDir || "unknown")).digest("hex").slice(0, 16);
897
955
  return join4(getBackupRoot(), `.initialized-${id}.json`);
898
956
  }
899
957
  function isPlainObject(value) {
@@ -1497,7 +1555,7 @@ import React22 from "react";
1497
1555
  import { render } from "../../../vendor/ink/build/index.js";
1498
1556
  import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync6, openSync as openSync2, readSync } from "node:fs";
1499
1557
  import { tmpdir as tmpdir2 } from "node:os";
1500
- import { dirname as dirname7, join as join8 } from "node:path";
1558
+ import { dirname as dirname7, join as join9 } from "node:path";
1501
1559
  import { performance as performance3 } from "node:perf_hooks";
1502
1560
  import { format } from "node:util";
1503
1561
 
@@ -14316,15 +14374,14 @@ function createPromptSubmit({
14316
14374
  const textRefs = pastedTextReferenceIds(text);
14317
14375
  const textSnapshot = Object.fromEntries(Object.entries(pastedTextsRef.current || {}).filter(([id]) => textRefs.has(Number(id))));
14318
14376
  const hasTextSnapshot = Object.keys(textSnapshot).length > 0;
14319
- const expandedText = hasTextSnapshot ? expandPastedTextTokens(text, textSnapshot) : text;
14320
- const content = buildPromptContentWithImages(expandedText, imageSnapshot);
14377
+ const content = buildPromptContentWithImages(text, imageSnapshot);
14378
+ const imageRestoreMeta = Object.fromEntries(Object.entries(imageSnapshot).map(([id, image]) => {
14379
+ const { content: _content, ...metadata } = image;
14380
+ return [id, { ...metadata, sizeBytes: Math.floor(String(image.content || "").length * 3 / 4) }];
14381
+ }));
14321
14382
  const accepted = submitPrompt(content, {
14322
- // Store the EXPANDED text in the transcript/history so a later prompt-
14323
- // history recall resubmits the real content, not the literal token
14324
- // (pastedTexts entries are cleared on accept). History recall therefore
14325
- // shows the full original text instead of the token — acceptable.
14326
- displayText: expandedText,
14327
- pastedImages: imageSnapshot,
14383
+ ...hasImageSnapshot || hasTextSnapshot ? { displayText: text } : {},
14384
+ pastedImages: imageRestoreMeta,
14328
14385
  pastedTexts: textSnapshot,
14329
14386
  onCommitted: hasImageSnapshot || hasTextSnapshot ? () => {
14330
14387
  clearPastedImagesSnapshot(imageSnapshot);
@@ -22091,6 +22148,10 @@ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFile
22091
22148
  init_plugin_paths();
22092
22149
  import { dirname as dirname6, join as join6, resolve as resolve3 } from "node:path";
22093
22150
  var PROMPT_HISTORY_LIMIT2 = 50;
22151
+ var PROMPT_HISTORY_CACHE_LIMIT = Math.max(
22152
+ 8,
22153
+ Number(process.env.MIXDOG_PROMPT_HISTORY_CACHE_LIMIT) || 128
22154
+ );
22094
22155
  function promptHistoryKey2(value) {
22095
22156
  return String(value || "").trim().replace(/\s+/g, " ");
22096
22157
  }
@@ -22209,7 +22270,11 @@ function usePromptHandlers({
22209
22270
  return Promise.all(chunks.map(async (chunk) => {
22210
22271
  if (!chunk.imagePath) return chunk.text;
22211
22272
  try {
22212
- const image = await readImageAttachmentFromPath(chunk.text, state.cwd || process.cwd());
22273
+ const image = await readImageAttachmentFromPath(
22274
+ chunk.text,
22275
+ state.cwd || process.cwd(),
22276
+ { provider: state.provider || "" }
22277
+ );
22213
22278
  if (!image) return chunk.text;
22214
22279
  const ref = registerPastedImage(image);
22215
22280
  showPromptHint(`attached ${image.filename || "image"}`, "plain");
@@ -22242,7 +22307,7 @@ function usePromptHandlers({
22242
22307
  return readClipboardText().then((clip) => {
22243
22308
  const normalized = String(clip ?? "").replace(/\r\n?/g, "\n");
22244
22309
  if (normalized) return processText(normalized, true);
22245
- return readClipboardImageAttachment().then((image) => {
22310
+ return readClipboardImageAttachment({ provider: state.provider || "" }).then((image) => {
22246
22311
  if (!image) {
22247
22312
  showPromptHint("no text or image found on clipboard", "plain");
22248
22313
  return false;
@@ -22257,7 +22322,7 @@ function usePromptHandlers({
22257
22322
  });
22258
22323
  }
22259
22324
  return processText(value);
22260
- }, [registerPastedImage, registerPastedText, showPromptHint, state.cwd]);
22325
+ }, [registerPastedImage, registerPastedText, showPromptHint, state.cwd, state.provider]);
22261
22326
  const handlePromptHistoryNavigate = useCallback12((direction, currentText = "", meta = {}) => {
22262
22327
  const currentValue = String(currentText || "");
22263
22328
  const currentKey = promptHistoryKey2(currentValue);
@@ -23401,6 +23466,147 @@ import { performance as performance2 } from "node:perf_hooks";
23401
23466
  var BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ""));
23402
23467
  var BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance2.now());
23403
23468
 
23469
+ // src/runtime/attachments/store.mjs
23470
+ init_plugin_paths();
23471
+ import {
23472
+ readdir,
23473
+ readFile as readFileAsync3,
23474
+ stat,
23475
+ unlink
23476
+ } from "node:fs/promises";
23477
+ import { join as join8 } from "node:path";
23478
+
23479
+ // src/runtime/attachments/pdf-extract.mjs
23480
+ var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
23481
+
23482
+ // src/runtime/attachments/store.mjs
23483
+ var ATTACHMENT_REF_RE = /^[a-f0-9]{64}$/;
23484
+ var MAX_PROMPT_TEXT_BYTES = 1024 * 1024;
23485
+ var MAX_PROMPT_PDF_BYTES = 20 * 1024 * 1024;
23486
+ var ATTACHMENT_CACHE_MAX_BYTES = 64 * 1024 * 1024;
23487
+ var ATTACHMENT_GC_MIN_AGE_MS = Math.max(
23488
+ 6e4,
23489
+ Number(process.env.MIXDOG_ATTACHMENT_GC_MIN_AGE_MS) || 7 * 24 * 60 * 60 * 1e3
23490
+ );
23491
+ var ATTACHMENT_GC_INTERVAL_MS = 6 * 60 * 60 * 1e3;
23492
+ var ATTACHMENT_GC_START_DELAY_MS = 5e3;
23493
+ var bufferCache = /* @__PURE__ */ new Map();
23494
+ var bufferCacheBytes = 0;
23495
+ var attachmentGcTimer = null;
23496
+ var lastAttachmentGcAt = 0;
23497
+ function attachmentsDir() {
23498
+ return join8(resolvePluginData(), "prompt-attachments", "sha256");
23499
+ }
23500
+ function dropCachedAttachment(path2) {
23501
+ const cached = bufferCache.get(path2);
23502
+ if (!cached) return;
23503
+ bufferCache.delete(path2);
23504
+ bufferCacheBytes -= cached.length;
23505
+ }
23506
+ async function persistedAttachmentReferencePaths() {
23507
+ const root = resolvePluginData();
23508
+ const paths = [];
23509
+ for (const dirname8 of ["sessions", "turn-checkpoints"]) {
23510
+ const dir = join8(root, dirname8);
23511
+ let entries;
23512
+ try {
23513
+ entries = await readdir(dir, { withFileTypes: true });
23514
+ } catch (error) {
23515
+ if (error?.code === "ENOENT") continue;
23516
+ throw error;
23517
+ }
23518
+ for (const entry of entries) {
23519
+ if (entry.isFile() && entry.name.endsWith(".json")) paths.push(join8(dir, entry.name));
23520
+ }
23521
+ }
23522
+ paths.push(join8(root, "session-pending-messages.json"));
23523
+ return paths;
23524
+ }
23525
+ async function collectPromptAttachments({
23526
+ now = Date.now(),
23527
+ minAgeMs = ATTACHMENT_GC_MIN_AGE_MS
23528
+ } = {}) {
23529
+ const referenced = /* @__PURE__ */ new Set();
23530
+ let referenceFiles = 0;
23531
+ for (const path2 of await persistedAttachmentReferencePaths()) {
23532
+ let raw;
23533
+ try {
23534
+ raw = await readFileAsync3(path2, "utf8");
23535
+ } catch (error) {
23536
+ if (error?.code === "ENOENT") continue;
23537
+ return { deleted: 0, referenced: 0, fresh: 0, scanned: 0, incomplete: true };
23538
+ }
23539
+ referenceFiles += 1;
23540
+ const regex = /"attachmentRef"\s*:\s*"([a-f0-9]{64})"/g;
23541
+ let match;
23542
+ while ((match = regex.exec(raw)) !== null) referenced.add(match[1]);
23543
+ }
23544
+ const cutoff = Number(now) - Math.max(0, Number(minAgeMs) || 0);
23545
+ let prefixes;
23546
+ try {
23547
+ prefixes = await readdir(attachmentsDir(), { withFileTypes: true });
23548
+ } catch (error) {
23549
+ if (error?.code === "ENOENT") {
23550
+ return { deleted: 0, referenced: referenced.size, fresh: 0, scanned: 0, referenceFiles };
23551
+ }
23552
+ return { deleted: 0, referenced: referenced.size, fresh: 0, scanned: 0, referenceFiles, incomplete: true };
23553
+ }
23554
+ let deleted = 0;
23555
+ let fresh = 0;
23556
+ let scanned = 0;
23557
+ for (const prefix of prefixes) {
23558
+ if (!prefix.isDirectory() || !/^[a-f0-9]{2}$/.test(prefix.name)) continue;
23559
+ const dir = join8(attachmentsDir(), prefix.name);
23560
+ let entries;
23561
+ try {
23562
+ entries = await readdir(dir, { withFileTypes: true });
23563
+ } catch {
23564
+ continue;
23565
+ }
23566
+ for (const entry of entries) {
23567
+ if (!entry.isFile() || !ATTACHMENT_REF_RE.test(entry.name)) continue;
23568
+ scanned += 1;
23569
+ if (referenced.has(entry.name)) continue;
23570
+ const path2 = join8(dir, entry.name);
23571
+ try {
23572
+ const first = await stat(path2);
23573
+ if (first.mtimeMs > cutoff) {
23574
+ fresh += 1;
23575
+ continue;
23576
+ }
23577
+ const latest = await stat(path2);
23578
+ if (latest.mtimeMs > cutoff || latest.mtimeMs !== first.mtimeMs) {
23579
+ fresh += 1;
23580
+ continue;
23581
+ }
23582
+ await unlink(path2);
23583
+ dropCachedAttachment(path2);
23584
+ deleted += 1;
23585
+ } catch (error) {
23586
+ if (error?.code !== "ENOENT") fresh += 1;
23587
+ }
23588
+ }
23589
+ }
23590
+ return { deleted, referenced: referenced.size, fresh, scanned, referenceFiles };
23591
+ }
23592
+ function armAttachmentGc(delayMs) {
23593
+ if (attachmentGcTimer) return;
23594
+ attachmentGcTimer = setTimeout(() => {
23595
+ attachmentGcTimer = null;
23596
+ void collectPromptAttachments().catch(() => void 0).finally(() => {
23597
+ lastAttachmentGcAt = Date.now();
23598
+ armAttachmentGc(ATTACHMENT_GC_INTERVAL_MS);
23599
+ });
23600
+ }, Math.max(ATTACHMENT_GC_START_DELAY_MS, Number(delayMs) || 0));
23601
+ attachmentGcTimer.unref?.();
23602
+ }
23603
+ function scheduleAttachmentGc() {
23604
+ if (attachmentGcTimer) return;
23605
+ const elapsed = Date.now() - lastAttachmentGcAt;
23606
+ armAttachmentGc(lastAttachmentGcAt === 0 ? ATTACHMENT_GC_START_DELAY_MS : ATTACHMENT_GC_INTERVAL_MS - elapsed);
23607
+ }
23608
+ scheduleAttachmentGc();
23609
+
23404
23610
  // src/tui/session/agent-job-feed.mjs
23405
23611
  init_paste_attachments();
23406
23612
 
@@ -23656,7 +23862,7 @@ function scheduleHardExit(code = 0) {
23656
23862
  timer2.unref?.();
23657
23863
  }
23658
23864
  function resolveTuiStderrLogPath() {
23659
- return process.env.MIXDOG_TUI_STDERR_LOG || join8(process.env.MIXDOG_RUNTIME_ROOT || join8(tmpdir2(), "mixdog"), "mixdog-tui.stderr.log");
23865
+ return process.env.MIXDOG_TUI_STDERR_LOG || join9(process.env.MIXDOG_RUNTIME_ROOT || join9(tmpdir2(), "mixdog"), "mixdog-tui.stderr.log");
23660
23866
  }
23661
23867
  function ansiFg(rgb) {
23662
23868
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
@@ -8,6 +8,7 @@ import { basename, extname, isAbsolute, resolve } from 'node:path';
8
8
  import {
9
9
  API_IMAGE_MAX_BASE64_SIZE,
10
10
  IMAGE_TARGET_RAW_SIZE,
11
+ imageProfileForProvider,
11
12
  imageMetadataText,
12
13
  resizeImageBuffer,
13
14
  } from '../runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs';
@@ -148,10 +149,14 @@ export function splitPastedImagePathCandidates(text) {
148
149
  // Exported for the desktop engine capability surface (resizeImage): the GUI
149
150
  // composer routes its attachments through this SAME pipeline so desktop and
150
151
  // TUI submit byte-identical image payloads.
151
- export async function imageAttachmentFromBuffer(buffer, mimeType, { filename = 'Pasted image', sourcePath = '' } = {}) {
152
+ export async function imageAttachmentFromBuffer(buffer, mimeType, {
153
+ filename = 'Pasted image',
154
+ sourcePath = '',
155
+ provider = '',
156
+ } = {}) {
152
157
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) throw new Error('image is empty');
153
158
  const ext = (mimeType || 'image/png').split('/')[1] || 'png';
154
- const resized = await resizeImageBuffer(buffer, ext);
159
+ const resized = await resizeImageBuffer(buffer, ext, { profile: imageProfileForProvider(provider) });
155
160
  if (resized) {
156
161
  return {
157
162
  type: 'image',
@@ -172,7 +177,7 @@ export async function imageAttachmentFromBuffer(buffer, mimeType, { filename = '
172
177
  return { type: 'image', content: data, mediaType: mimeType || 'image/png', filename, sourcePath };
173
178
  }
174
179
 
175
- export async function readImageAttachmentFromPath(rawPath, cwd = process.cwd()) {
180
+ export async function readImageAttachmentFromPath(rawPath, cwd = process.cwd(), { provider = '' } = {}) {
176
181
  const cleaned = cleanPathText(rawPath);
177
182
  const mimeType = mimeForPath(cleaned);
178
183
  if (!mimeType) return null;
@@ -190,6 +195,7 @@ export async function readImageAttachmentFromPath(rawPath, cwd = process.cwd())
190
195
  return imageAttachmentFromBuffer(buffer, mimeType, {
191
196
  filename: basename(fullPath),
192
197
  sourcePath: fullPath,
198
+ provider,
193
199
  });
194
200
  }
195
201
 
@@ -210,19 +216,19 @@ async function readClipboardImageToTempFile() {
210
216
  return null;
211
217
  }
212
218
 
213
- export async function readClipboardImageAttachment() {
219
+ export async function readClipboardImageAttachment({ provider = '' } = {}) {
214
220
  if (process.platform === 'linux') {
215
221
  const wl = await execFileBuffer('wl-paste', ['--type', 'image/png'], { timeout: 3000 });
216
- if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, 'image/png');
222
+ if (wl.ok && wl.stdout?.length) return imageAttachmentFromBuffer(wl.stdout, 'image/png', { provider });
217
223
  const xc = await execFileBuffer('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], { timeout: 3000 });
218
- if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, 'image/png');
224
+ if (xc.ok && xc.stdout?.length) return imageAttachmentFromBuffer(xc.stdout, 'image/png', { provider });
219
225
  return null;
220
226
  }
221
227
  const file = await readClipboardImageToTempFile();
222
228
  if (!file) return null;
223
229
  try {
224
230
  const buffer = await readFileAsync(file);
225
- return await imageAttachmentFromBuffer(buffer, 'image/png');
231
+ return await imageAttachmentFromBuffer(buffer, 'image/png', { provider });
226
232
  } finally {
227
233
  try { unlinkSync(file); } catch {}
228
234
  }
@@ -9,6 +9,10 @@ import { dirname, join, resolve } from 'node:path';
9
9
  import { resolvePluginData } from '../runtime/shared/plugin-paths.mjs';
10
10
 
11
11
  export const PROMPT_HISTORY_LIMIT = 50;
12
+ export const PROMPT_HISTORY_CACHE_LIMIT = Math.max(
13
+ 8,
14
+ Number(process.env.MIXDOG_PROMPT_HISTORY_CACHE_LIMIT) || 128,
15
+ );
12
16
 
13
17
  export function promptHistoryKey(value) {
14
18
  return String(value || '').trim().replace(/\s+/g, ' ');
@@ -101,10 +105,43 @@ const memCache = new Map(); // filePath -> entries[] (optimistic in-memory view)
101
105
  const pendingAppends = new Map(); // filePath -> string[] appends since last flush
102
106
  const pendingTimers = new Map(); // filePath -> timeout handle
103
107
 
108
+ function flushPendingSyncForEviction(filePath) {
109
+ const timer = pendingTimers.get(filePath);
110
+ if (timer) clearTimeout(timer);
111
+ pendingTimers.delete(filePath);
112
+ const pend = pendingAppends.get(filePath);
113
+ pendingAppends.delete(filePath);
114
+ if (!pend?.length) return;
115
+ const merged = reconcileWithDisk(filePath, pend);
116
+ writeEntriesSync(filePath, merged);
117
+ }
118
+
119
+ function trimMemCache() {
120
+ while (memCache.size > PROMPT_HISTORY_CACHE_LIMIT) {
121
+ const oldest = memCache.keys().next().value;
122
+ if (oldest === undefined) break;
123
+ // Never drop the only optimistic copy of an unflushed prompt. Cwd churn is
124
+ // rare; once the cache is full, synchronously landing the oldest dirty
125
+ // bucket gives all three maps a deterministic process-lifetime bound.
126
+ flushPendingSyncForEviction(oldest);
127
+ memCache.delete(oldest);
128
+ }
129
+ }
130
+
131
+ function rememberCachedEntries(filePath, entries) {
132
+ memCache.delete(filePath);
133
+ memCache.set(filePath, entries);
134
+ trimMemCache();
135
+ }
136
+
104
137
  function cachedEntries(filePath) {
105
- if (memCache.has(filePath)) return memCache.get(filePath);
138
+ if (memCache.has(filePath)) {
139
+ const entries = memCache.get(filePath);
140
+ rememberCachedEntries(filePath, entries);
141
+ return entries;
142
+ }
106
143
  const entries = readEntries(filePath);
107
- memCache.set(filePath, entries);
144
+ rememberCachedEntries(filePath, entries);
108
145
  return entries;
109
146
  }
110
147
 
@@ -141,13 +178,15 @@ async function writeBehindFlush(filePath) {
141
178
  // and reconcile against the file we are about to write.
142
179
  pendingAppends.set(filePath, []);
143
180
  const merged = reconcileWithDisk(filePath, pend);
144
- memCache.set(filePath, merged);
181
+ rememberCachedEntries(filePath, merged);
145
182
  const ok = await writeEntriesAsync(filePath, merged);
146
183
  if (!ok) {
147
184
  // Retry: re-queue our appends ahead of any newer ones (oldest → newest).
148
185
  const cur = pendingAppends.get(filePath) || [];
149
186
  pendingAppends.set(filePath, pend.concat(cur));
150
187
  scheduleWriteBehind(filePath);
188
+ } else if (!(pendingAppends.get(filePath)?.length)) {
189
+ pendingAppends.delete(filePath);
151
190
  }
152
191
  }
153
192
 
@@ -229,14 +268,23 @@ export function appendPromptHistory(cwd, value) {
229
268
  if (!filePath) return null;
230
269
  // Optimistic in-memory update for immediate return/reads.
231
270
  const trimmed = applyAppend(cachedEntries(filePath), value);
232
- memCache.set(filePath, trimmed);
233
271
  // Queue the append for a durable, disk-reconciled flush (async, coalesced).
234
272
  const pend = pendingAppends.get(filePath) || [];
235
273
  pend.push(String(value).trim());
236
274
  pendingAppends.set(filePath, pend);
275
+ rememberCachedEntries(filePath, trimmed);
237
276
  // Bound crash-loss to FLUSH_CAP appends; otherwise coalesce over the window.
238
277
  if (pend.length >= FLUSH_CAP) void writeBehindFlush(filePath);
239
278
  else scheduleWriteBehind(filePath);
240
279
  return trimmed;
241
280
  }
242
281
 
282
+ export function promptHistoryStoreStatsForTest() {
283
+ return {
284
+ cacheEntries: memCache.size,
285
+ pendingEntries: pendingAppends.size,
286
+ timerEntries: pendingTimers.size,
287
+ cacheLimit: PROMPT_HISTORY_CACHE_LIMIT,
288
+ };
289
+ }
290
+
@@ -395,7 +395,11 @@ export function createAgentJobFeed({
395
395
  if (modelContent) parts.push({ type: 'text', text: modelContent });
396
396
  for (const p of imagePaths) {
397
397
  let att = null;
398
- try { att = await readImageAttachmentFromPath(p); } catch { att = null; }
398
+ try {
399
+ att = await readImageAttachmentFromPath(p, process.cwd(), {
400
+ provider: getState()?.provider || '',
401
+ });
402
+ } catch { att = null; }
399
403
  if (!att) continue;
400
404
  if (att.metadataText) parts.push({ type: 'text', text: att.metadataText });
401
405
  parts.push({ type: 'image', data: att.content, mimeType: att.mediaType || 'image/png' });