@mindstudio-ai/remy 0.1.274 → 0.1.276

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/dist/headless.js CHANGED
@@ -289,7 +289,7 @@ function isRetryableError(error) {
289
289
  /Unable to download/i.test(error);
290
290
  }
291
291
  function sleep(ms) {
292
- return new Promise((resolve3) => setTimeout(resolve3, ms));
292
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
293
293
  }
294
294
  async function* streamChatWithRetry(params, options) {
295
295
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -433,7 +433,7 @@ var MODEL_SURFACES = {
433
433
  userPickable: true
434
434
  },
435
435
  imageGeneration: {
436
- default: "seedream-4.5",
436
+ default: "gpt-image-2",
437
437
  label: "Image Generation",
438
438
  description: "Creates images for your product \u2014 icons, illustrations, photos, and any other visual assets.",
439
439
  modelType: "image_generation",
@@ -692,9 +692,9 @@ function parseFrontmatter(content) {
692
692
  }
693
693
  const fields = {};
694
694
  for (const line of match[1].split("\n")) {
695
- const sep = line.indexOf(":");
696
- if (sep > 0) {
697
- fields[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
695
+ const sep2 = line.indexOf(":");
696
+ if (sep2 > 0) {
697
+ fields[line.slice(0, sep2).trim()] = line.slice(sep2 + 1).trim();
698
698
  }
699
699
  }
700
700
  return fields;
@@ -1663,7 +1663,7 @@ function formatCliResult(r) {
1663
1663
  return logBlock + body + truncNote;
1664
1664
  }
1665
1665
  function runCli(command, args, options) {
1666
- return new Promise((resolve3) => {
1666
+ return new Promise((resolve4) => {
1667
1667
  const timeout = options?.timeout ?? 6e4;
1668
1668
  const maxBuffer = options?.maxBuffer ?? 1024 * 1024;
1669
1669
  let finalArgs = args;
@@ -1692,7 +1692,7 @@ function runCli(command, args, options) {
1692
1692
  if (killTimer) {
1693
1693
  clearTimeout(killTimer);
1694
1694
  }
1695
- resolve3(result);
1695
+ resolve4(result);
1696
1696
  };
1697
1697
  const child = spawn(command, finalArgs, {
1698
1698
  stdio: [options?.stdin ? "pipe" : "ignore", "pipe", "pipe"]
@@ -2389,7 +2389,7 @@ var bashTool = {
2389
2389
  async execute(input, context) {
2390
2390
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
2391
2391
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2392
- return new Promise((resolve3) => {
2392
+ return new Promise((resolve4) => {
2393
2393
  const child = spawn2("sh", ["-c", input.command], {
2394
2394
  // Pinned rather than inherited. `undefined` here means "wherever the
2395
2395
  // process happens to be", which is the project root only by luck.
@@ -2414,9 +2414,9 @@ var bashTool = {
2414
2414
  clearTimeout(timer);
2415
2415
  if (!output) {
2416
2416
  if (code && code !== 0) {
2417
- resolve3(`Error: process exited with code ${code}`);
2417
+ resolve4(`Error: process exited with code ${code}`);
2418
2418
  } else {
2419
- resolve3("(no output)");
2419
+ resolve4("(no output)");
2420
2420
  }
2421
2421
  return;
2422
2422
  }
@@ -2442,18 +2442,18 @@ var bashTool = {
2442
2442
  `${(MAX_OUTPUT_BYTES / 1024).toFixed(0)}KB of ${(totalBytes / 1024).toFixed(0)}KB`
2443
2443
  );
2444
2444
  }
2445
- resolve3(
2445
+ resolve4(
2446
2446
  truncated + `
2447
2447
 
2448
2448
  (truncated at ${reasons.join(" / ")} \u2014 narrow the command (grep, head/tail, smaller paths) instead of increasing limits)`
2449
2449
  );
2450
2450
  } else {
2451
- resolve3(output);
2451
+ resolve4(output);
2452
2452
  }
2453
2453
  });
2454
2454
  child.on("error", (err) => {
2455
2455
  clearTimeout(timer);
2456
- resolve3(`Error: ${err.message}`);
2456
+ resolve4(`Error: ${err.message}`);
2457
2457
  });
2458
2458
  });
2459
2459
  }
@@ -2570,17 +2570,17 @@ var grepTool = {
2570
2570
  }
2571
2571
  const rgCmd = `rg ${rgFlags}${globFlag} '${escaped}' ${searchPath}`;
2572
2572
  const grepCmd = `grep ${grepFlags} '${escaped}' ${searchPath} --include='*.ts' --include='*.tsx' --include='*.js' --include='*.json' --include='*.md'`;
2573
- return new Promise((resolve3) => {
2573
+ return new Promise((resolve4) => {
2574
2574
  exec(rgCmd, { maxBuffer: 512 * 1024 }, (err, stdout) => {
2575
2575
  if (stdout?.trim()) {
2576
- resolve3(formatResults(stdout, max, mode));
2576
+ resolve4(formatResults(stdout, max, mode));
2577
2577
  return;
2578
2578
  }
2579
2579
  exec(grepCmd, { maxBuffer: 512 * 1024 }, (_err, grepStdout) => {
2580
2580
  if (grepStdout?.trim()) {
2581
- resolve3(formatResults(grepStdout, max, mode));
2581
+ resolve4(formatResults(grepStdout, max, mode));
2582
2582
  } else {
2583
- resolve3("No matches found.");
2583
+ resolve4("No matches found.");
2584
2584
  }
2585
2585
  });
2586
2586
  });
@@ -2896,7 +2896,7 @@ var restartProcessTool = {
2896
2896
  async execute(input) {
2897
2897
  const data = await lspRequest("/restart-process", { name: input.name });
2898
2898
  if (data.ok) {
2899
- await new Promise((resolve3) => setTimeout(resolve3, 5e3));
2899
+ await new Promise((resolve4) => setTimeout(resolve4, 5e3));
2900
2900
  return `Restarted ${input.name}.`;
2901
2901
  }
2902
2902
  return `Error: unexpected response: ${JSON.stringify(data)}`;
@@ -3123,6 +3123,26 @@ ${opts.styleMap}
3123
3123
  ${ANALYSIS_RESPONSE_FORMAT}`;
3124
3124
  return p;
3125
3125
  }
3126
+ async function renderHtmlViaSidecar(opts) {
3127
+ const result = await sidecarRequest(
3128
+ "/render-html",
3129
+ {
3130
+ html: opts.html,
3131
+ width: opts.width,
3132
+ height: opts.height,
3133
+ ...opts.transparent ? { transparent: true } : {},
3134
+ ...opts.scale != null ? { scale: opts.scale } : {}
3135
+ },
3136
+ { timeout: VIEWPORT_CAPTURE_TIMEOUT_MS }
3137
+ );
3138
+ const url = result?.url;
3139
+ if (!url) {
3140
+ throw new Error(
3141
+ `No URL in sidecar render response. The browser may not be ready yet. Response: ${JSON.stringify(result)}`
3142
+ );
3143
+ }
3144
+ return { url, width: result.width, height: result.height };
3145
+ }
3126
3146
  async function streamScreenshotAnalysis(opts) {
3127
3147
  const { image, prompt, styleMap, onLog, model, apiConfig } = opts;
3128
3148
  const url = await resolveImageRef(image, apiConfig);
@@ -3301,18 +3321,70 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3301
3321
  }
3302
3322
  };
3303
3323
 
3304
- // src/toolResultCap.ts
3324
+ // src/historyLimits.ts
3305
3325
  var MAX_TOOL_RESULT_BYTES = 256 * 1024;
3306
- function capToolResult(result) {
3326
+ function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
3307
3327
  const total = Buffer.byteLength(result, "utf-8");
3308
- if (total <= MAX_TOOL_RESULT_BYTES) {
3328
+ if (total <= maxBytes) {
3309
3329
  return result;
3310
3330
  }
3311
- const head = Buffer.from(result, "utf-8").subarray(0, MAX_TOOL_RESULT_BYTES).toString("utf-8");
3331
+ const head = Buffer.from(result, "utf-8").subarray(0, maxBytes).toString("utf-8");
3312
3332
  return head + `
3313
3333
 
3314
- (tool result truncated at ${(MAX_TOOL_RESULT_BYTES / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 too large to keep in context. Narrow the call (select fewer fields, paginate, or query a subset) instead of fetching everything.)`;
3334
+ (tool result truncated at ${(maxBytes / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 too large to keep in context. Narrow the call (select fewer fields, paginate, or query a subset) instead of fetching everything.)`;
3335
+ }
3336
+ var MAX_SUBAGENT_RESULT_BYTES = 32 * 1024;
3337
+ var MAX_SUBAGENT_TRANSCRIPT_BYTES = 512 * 1024;
3338
+ function capSubAgentTranscript(messages) {
3339
+ for (const msg of messages) {
3340
+ capMessageForHistory(msg, MAX_SUBAGENT_RESULT_BYTES);
3341
+ }
3342
+ const sizes = messages.map(
3343
+ (m) => Buffer.byteLength(JSON.stringify(m), "utf-8") + 1
3344
+ );
3345
+ let total = sizes.reduce((a, b) => a + b, 0);
3346
+ if (total <= MAX_SUBAGENT_TRANSCRIPT_BYTES) {
3347
+ return messages;
3348
+ }
3349
+ let start = 0;
3350
+ while (start < messages.length - 1 && total > MAX_SUBAGENT_TRANSCRIPT_BYTES) {
3351
+ total -= sizes[start];
3352
+ start++;
3353
+ }
3354
+ while (start < messages.length - 1 && messages[start].role === "user" && messages[start].toolCallId) {
3355
+ start++;
3356
+ }
3357
+ return messages.slice(start);
3315
3358
  }
3359
+ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
3360
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
3361
+ for (const block of msg.content) {
3362
+ if (block.type !== "tool") {
3363
+ continue;
3364
+ }
3365
+ if (typeof block.result === "string") {
3366
+ block.result = capToolResult(block.result, maxBytes);
3367
+ }
3368
+ if (typeof block.backgroundResult === "string") {
3369
+ block.backgroundResult = capToolResult(
3370
+ block.backgroundResult,
3371
+ maxBytes
3372
+ );
3373
+ }
3374
+ if (Array.isArray(block.subAgentMessages)) {
3375
+ block.subAgentMessages = capSubAgentTranscript(block.subAgentMessages);
3376
+ }
3377
+ }
3378
+ } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
3379
+ msg.content = capToolResult(msg.content, maxBytes);
3380
+ }
3381
+ }
3382
+ var HISTORY_PAGE_MAX_BYTES = 2 * 1024 * 1024;
3383
+ var HISTORY_DEFAULT_LIMIT = 500;
3384
+ var HISTORY_MAX_LIMIT = 2e3;
3385
+ var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
3386
+ var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
3387
+ var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
3316
3388
 
3317
3389
  // src/statusWatcher.ts
3318
3390
  var INTERNAL_PAYLOAD_MARKERS = [
@@ -3944,7 +4016,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3944
4016
  block.completedAt = Date.now();
3945
4017
  const innerMsgs = subAgentMessages.get(r.id);
3946
4018
  if (innerMsgs) {
3947
- block.subAgentMessages = innerMsgs;
4019
+ block.subAgentMessages = capSubAgentTranscript(innerMsgs);
3948
4020
  }
3949
4021
  if (captureArtifacts?.includes(block.name) && !r.isError) {
3950
4022
  try {
@@ -4210,10 +4282,10 @@ function parseFrontmatter2(filePath) {
4210
4282
  }
4211
4283
  const fm = {};
4212
4284
  for (const line of match[1].split("\n")) {
4213
- const sep = line.indexOf(":");
4214
- if (sep > 0) {
4215
- const key = line.slice(0, sep).trim();
4216
- const val = line.slice(sep + 1).trim();
4285
+ const sep2 = line.indexOf(":");
4286
+ if (sep2 > 0) {
4287
+ const key = line.slice(0, sep2).trim();
4288
+ const val = line.slice(sep2 + 1).trim();
4217
4289
  fm[key] = val;
4218
4290
  }
4219
4291
  }
@@ -4517,7 +4589,7 @@ var browserAutomationTool = {
4517
4589
  // src/tools/code/screenshot.ts
4518
4590
  var screenshotDefinition = {
4519
4591
  name: "screenshot",
4520
- description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To render a fixed-size image such as an Open Graph share card, set `width` and `height` (e.g. 1200 \xD7 630) and `format: 'png'`: the tool navigates to `path`, clips to exactly those pixel dimensions, and returns the image URL.",
4592
+ description: "Capture a screenshot of the app preview and get a description of what's on screen. Choose `fullPage`: `false` captures just the visible viewport (fast \u2014 for a specific section the page is scrolled to), `true` captures the entire page top-to-bottom (slower \u2014 for overall composition or content past the fold). Captures the settled page state \u2014 it cannot catch animations, transitions, or transient state. The analysis is not precise about every detail \u2014 for example it cannot reliably identify specific fonts by name, only describe what the letterforms look like. Optionally provide specific questions about what you're looking for. Use a bulleted list to ask many questions at once. To ask additional questions about a screenshot you have already captured, pass its URL as `imageUrl` to skip recapture; `imageUrl` also accepts the disk path of an image file (a user upload, a saved asset) to analyze that instead of the preview. If the screenshot requires interaction first (logging in, clicking a tab, dismissing a modal, scrolling to a section), use the instructions param to describe the steps. To capture at exact pixel dimensions, set `width` and `height`: the tool clips to exactly that viewport and returns the image URL.",
4521
4593
  inputSchema: {
4522
4594
  type: "object",
4523
4595
  properties: {
@@ -4548,7 +4620,7 @@ var screenshotDefinition = {
4548
4620
  format: {
4549
4621
  type: "string",
4550
4622
  enum: ["png", "jpeg"],
4551
- description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics like share cards, where JPEG artifacts show on sharp type and edges."
4623
+ description: "Output image format. Defaults to 'jpeg'. Use 'png' for crisp flat graphics where JPEG artifacts show on sharp type and edges."
4552
4624
  },
4553
4625
  instructions: {
4554
4626
  type: "string",
@@ -4870,7 +4942,36 @@ ${brief}
4870
4942
  }
4871
4943
 
4872
4944
  // src/subagents/designExpert/tools/images/imageGenerator.ts
4873
- var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4945
+ var ANALYZE_PROMPT = 'You are reviewing this image for a visual designer sourcing assets for a project. Describe: what the image depicts, the mood and color palette, how the lighting and composition work, any text present in the image, whether there are any issues (artifacts, distortions), whether the artwork extends fully to all four edges of the canvas or sits inset (call out any baked-in border, frame, rounded-corner mask, drop-shadow margin, or mockup presentation such as an icon rendered on a background or device), and how it could be used in a layout for an app or website. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Asset Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
4946
+ var OPENAI_IMAGE_SIZES = [
4947
+ [768, 1024],
4948
+ [1024, 768],
4949
+ [1024, 1024],
4950
+ [1024, 1536],
4951
+ [1536, 1024],
4952
+ [1280, 2560],
4953
+ [2560, 1280],
4954
+ [1440, 2560],
4955
+ [2560, 1440],
4956
+ [1792, 2400],
4957
+ [2400, 1792],
4958
+ [2400, 2880],
4959
+ [2560, 2560]
4960
+ ];
4961
+ function snapToOpenAiImageSize(width, height) {
4962
+ let best = OPENAI_IMAGE_SIZES[0];
4963
+ let bestScore = Infinity;
4964
+ for (const [w, h] of OPENAI_IMAGE_SIZES) {
4965
+ const aspectDiff = Math.abs(Math.log(w / h / (width / height)));
4966
+ const areaDiff = Math.abs(Math.log(w * h / (width * height)));
4967
+ const score = aspectDiff * 8 + areaDiff;
4968
+ if (score < bestScore) {
4969
+ bestScore = score;
4970
+ best = [w, h];
4971
+ }
4972
+ }
4973
+ return `${best[0]}x${best[1]}`;
4974
+ }
4874
4975
  async function generateImageAssets(opts) {
4875
4976
  const {
4876
4977
  prompts,
@@ -4885,7 +4986,11 @@ async function generateImageAssets(opts) {
4885
4986
  const sourceImages = opts.sourceImages?.length ? await resolveImageRefs(opts.sourceImages, apiConfig) : void 0;
4886
4987
  const width = opts.width || 2048;
4887
4988
  const height = opts.height || 2048;
4888
- const config = { width, height };
4989
+ const config = {
4990
+ width,
4991
+ height,
4992
+ size: snapToOpenAiImageSize(width, height)
4993
+ };
4889
4994
  if (sourceImages?.length) {
4890
4995
  const [firstImage] = sourceImages;
4891
4996
  config.images = sourceImages;
@@ -5137,11 +5242,143 @@ async function execute6(input, onLog, context) {
5137
5242
  });
5138
5243
  }
5139
5244
 
5245
+ // src/subagents/designExpert/tools/images/renderImage.ts
5246
+ var renderImage_exports = {};
5247
+ __export(renderImage_exports, {
5248
+ definition: () => definition7,
5249
+ execute: () => execute7
5250
+ });
5251
+ import { mkdir, unlink, writeFile } from "fs/promises";
5252
+ import { tmpdir } from "os";
5253
+ import { dirname, join, resolve as resolve2, sep } from "path";
5254
+ import { randomUUID } from "crypto";
5255
+ var MIN_DIMENSION = 16;
5256
+ var MAX_DIMENSION = 4096;
5257
+ var RENDER_ANALYZE_PROMPT = 'You are reviewing a browser-rendered graphic (composed from HTML/CSS by a designer) for fidelity. Report: whether the composition fills the full canvas or leaves unintended gaps at any edge, any clipped or overflowing text, whether custom webfonts appear to have loaded (distinctive letterforms vs generic fallback serif/sans), any misalignment or uneven spacing, any unintended scrollbars or default-styling artifacts, and \u2014 if the background is transparent \u2014 any fringing or stray opaque pixels at the edges. Then briefly describe the overall composition and how polished it looks. Be concise and practical. Respond only with your analysis as Markdown (starting with the title "Render Review") and absolutely no other text. Do not use emojis - use unicode if you need symbols.';
5258
+ var definition7 = {
5259
+ name: "renderImage",
5260
+ description: "Render a self-contained HTML document in a real browser and capture it as a hosted PNG at exact pixel dimensions, with a fidelity review included. Deterministic \u2014 exact hex colors, real loaded webfonts, precise geometry \u2014 unlike generateImages, which is an image model. Use for token-exact graphics: Open Graph share cards, wordmarks, flat/geometric icon tiles, badges, any composition where letterforms and spacing carry the design. Compose with HTML/CSS (link webfonts from CDNs \u2014 the renderer waits for them to load); inline existing SVG markup when needed, but never hand-write new SVG path data.",
5261
+ inputSchema: {
5262
+ type: "object",
5263
+ properties: {
5264
+ html: {
5265
+ type: "string",
5266
+ description: "A complete, self-contained HTML document sized to fill the viewport (style html/body to the full dimensions with margin 0). External webfonts and images from CDNs are fine \u2014 loading is awaited before capture."
5267
+ },
5268
+ width: {
5269
+ type: "number",
5270
+ description: "Viewport width in CSS pixels (e.g. 1200 for an OG card, 512 for an icon tile). Range: 16-4096."
5271
+ },
5272
+ height: {
5273
+ type: "number",
5274
+ description: "Viewport height in CSS pixels. Range: 16-4096."
5275
+ },
5276
+ scale: {
5277
+ type: "number",
5278
+ description: "Device scale factor, 1-3. Output pixels = css \xD7 scale. Use 2 for crisp icon masters (e.g. a 512\xD7512 document captured at 1024\xD71024)."
5279
+ },
5280
+ transparentBackground: {
5281
+ type: "boolean",
5282
+ description: "Capture with true alpha: leave the document background transparent (no background on html/body) and the PNG keeps it. No background-removal model involved."
5283
+ },
5284
+ savePath: {
5285
+ type: "string",
5286
+ description: "Optional project-relative path to also save the PNG into the app (e.g. 'dist/interfaces/web/public/og-image.png' so the deployed site self-hosts it)."
5287
+ }
5288
+ },
5289
+ required: ["html", "width", "height"]
5290
+ }
5291
+ };
5292
+ async function execute7(input, onLog, context) {
5293
+ const html = typeof input.html === "string" ? input.html : "";
5294
+ const width = Math.round(Number(input.width));
5295
+ const height = Math.round(Number(input.height));
5296
+ if (!html || !Number.isFinite(width) || !Number.isFinite(height) || width < MIN_DIMENSION || width > MAX_DIMENSION || height < MIN_DIMENSION || height > MAX_DIMENSION) {
5297
+ return `Error: renderImage requires an html string plus width/height between ${MIN_DIMENSION} and ${MAX_DIMENSION}.`;
5298
+ }
5299
+ const release = await acquireBrowserLock();
5300
+ let rendered;
5301
+ try {
5302
+ onLog?.("Rendering document in the sandbox browser...");
5303
+ rendered = await renderHtmlViaSidecar({
5304
+ html,
5305
+ width,
5306
+ height,
5307
+ transparent: input.transparentBackground === true,
5308
+ scale: typeof input.scale === "number" ? input.scale : void 0
5309
+ });
5310
+ } catch (err) {
5311
+ return `Error: render failed: ${err?.message ?? err}`;
5312
+ } finally {
5313
+ release();
5314
+ }
5315
+ let bytes;
5316
+ try {
5317
+ const res = await fetch(rendered.url);
5318
+ if (!res.ok) {
5319
+ throw new Error(`fetch returned ${res.status}`);
5320
+ }
5321
+ bytes = Buffer.from(await res.arrayBuffer());
5322
+ } catch (err) {
5323
+ return `Error: rendered but could not download the capture (${err?.message ?? err}). Temporary URL: ${rendered.url}`;
5324
+ }
5325
+ let savedPath;
5326
+ if (typeof input.savePath === "string" && input.savePath) {
5327
+ const absolute = resolve2(PROJECT_ROOT, input.savePath);
5328
+ if (!absolute.startsWith(PROJECT_ROOT + sep)) {
5329
+ return "Error: savePath must resolve inside the project.";
5330
+ }
5331
+ await mkdir(dirname(absolute), { recursive: true });
5332
+ await writeFile(absolute, bytes);
5333
+ savedPath = input.savePath;
5334
+ }
5335
+ onLog?.("Hosting the capture...");
5336
+ const tmpPath = join(tmpdir(), `render-${randomUUID()}.png`);
5337
+ let url = rendered.url;
5338
+ let temporary = true;
5339
+ try {
5340
+ await writeFile(tmpPath, bytes);
5341
+ const upload = await runMindstudioCliResult(["upload", tmpPath], {
5342
+ timeout: 6e4,
5343
+ onLog,
5344
+ caller: "designExpert"
5345
+ });
5346
+ const match = upload.ok ? upload.value.match(/https:\/\/\S+/g) : null;
5347
+ if (match?.length) {
5348
+ url = match[match.length - 1];
5349
+ temporary = false;
5350
+ }
5351
+ } finally {
5352
+ await unlink(tmpPath).catch(() => {
5353
+ });
5354
+ }
5355
+ const analysis = await analyzeImage({
5356
+ prompt: RENDER_ANALYZE_PROMPT,
5357
+ image: url,
5358
+ onLog,
5359
+ model: resolveModel("imageAnalysis", context?.models, context?.model)
5360
+ }).then((r) => r.analysis).catch((err) => `Could not review this image: ${err.message}`);
5361
+ return JSON.stringify({
5362
+ images: [
5363
+ {
5364
+ url,
5365
+ ...temporary ? {
5366
+ note: "Durable hosting failed \u2014 this URL is dev-session scratch storage and may expire. Do not use it for app metadata; retry if a durable URL is needed."
5367
+ } : {},
5368
+ ...savedPath ? { savedPath } : {},
5369
+ analysis,
5370
+ width: rendered.width,
5371
+ height: rendered.height
5372
+ }
5373
+ ]
5374
+ });
5375
+ }
5376
+
5140
5377
  // src/subagents/designExpert/tools/polishCopy.ts
5141
5378
  var polishCopy_exports = {};
5142
5379
  __export(polishCopy_exports, {
5143
- definition: () => definition7,
5144
- execute: () => execute7
5380
+ definition: () => definition8,
5381
+ execute: () => execute8
5145
5382
  });
5146
5383
 
5147
5384
  // src/subagents/copyEditor/tools.ts
@@ -5197,7 +5434,7 @@ var copyEditorTool = {
5197
5434
  };
5198
5435
 
5199
5436
  // src/subagents/designExpert/tools/polishCopy.ts
5200
- var definition7 = {
5437
+ var definition8 = {
5201
5438
  name: "polishCopy",
5202
5439
  description: "Hand off any user-facing copy you've written \u2014 headlines, captions, labels, body text \u2014 and get back a sharper version: better built for its audience and free of the fingerprints that make writing read as AI. It elevates how the copy communicates without inventing facts or claims you didn't give it. Give it the text plus what it's for (where it appears, the audience).",
5203
5440
  inputSchema: {
@@ -5211,7 +5448,7 @@ var definition7 = {
5211
5448
  required: ["task"]
5212
5449
  }
5213
5450
  };
5214
- async function execute7(input, _onLog, context) {
5451
+ async function execute8(input, _onLog, context) {
5215
5452
  return copyEditorTool.execute(input, context);
5216
5453
  }
5217
5454
 
@@ -5227,6 +5464,7 @@ var tools = {
5227
5464
  screenshot: { definition: screenshotDefinition, execute: executeScreenshot },
5228
5465
  generateImages: generateImages_exports,
5229
5466
  editImages: editImages_exports,
5467
+ renderImage: renderImage_exports,
5230
5468
  polishCopy: polishCopy_exports
5231
5469
  };
5232
5470
  var DESIGN_EXPERT_TOOLS = [
@@ -5646,13 +5884,13 @@ var PITCH_DECK_SHELL = readAsset(
5646
5884
  "subagents/productVision",
5647
5885
  "pitch-deck-shell.html"
5648
5886
  );
5649
- function resolve2(filePath) {
5887
+ function resolve3(filePath) {
5650
5888
  return path10.join(ROADMAP_DIR, filePath);
5651
5889
  }
5652
5890
  async function executeVisionTool(name, input, context) {
5653
5891
  switch (name) {
5654
5892
  case "writeFile": {
5655
- const filePath = resolve2(input.path);
5893
+ const filePath = resolve3(input.path);
5656
5894
  try {
5657
5895
  fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5658
5896
  let oldContent = null;
@@ -5670,7 +5908,7 @@ ${unifiedDiff(filePath, oldContent ?? "", input.content)}`;
5670
5908
  }
5671
5909
  }
5672
5910
  case "deleteFile": {
5673
- const filePath = resolve2(input.path);
5911
+ const filePath = resolve3(input.path);
5674
5912
  try {
5675
5913
  if (!fs19.existsSync(filePath)) {
5676
5914
  return `Error: ${filePath} does not exist`;
@@ -5687,7 +5925,7 @@ ${unifiedDiff(filePath, oldContent, "")}`;
5687
5925
  if (!context) {
5688
5926
  return "Error: writePitchDeck requires execution context for design expert delegation";
5689
5927
  }
5690
- const filePath = resolve2("pitch.html");
5928
+ const filePath = resolve3("pitch.html");
5691
5929
  try {
5692
5930
  fs19.mkdirSync(ROADMAP_DIR, { recursive: true });
5693
5931
  const exists = fs19.existsSync(filePath);
@@ -6677,14 +6915,9 @@ import path11 from "path";
6677
6915
  var log10 = createLogger("session");
6678
6916
  var SESSION_FILE = ".remy-session.json";
6679
6917
  var ARCHIVE_DIR = ".logs/sessions";
6680
- var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6681
- var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6682
- var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
6683
6918
  var ARCHIVE_NAME_RE = /^(cleared|rotated)-.*\.json$/;
6684
6919
  var archiveSortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
6685
6920
  var ARCHIVE_COUNT_RE = /\.c(\d+)\.json$/;
6686
- var HISTORY_DEFAULT_LIMIT = 500;
6687
- var HISTORY_MAX_LIMIT = 2e3;
6688
6921
  var archiveCountCache = /* @__PURE__ */ new Map();
6689
6922
  var archiveMsgCache = /* @__PURE__ */ new Map();
6690
6923
  var ARCHIVE_MSG_CACHE_MAX = 3;
@@ -6714,22 +6947,11 @@ function loadSession(state) {
6714
6947
  }
6715
6948
  return false;
6716
6949
  }
6717
- function capOversizedResults(msg) {
6718
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
6719
- for (const block of msg.content) {
6720
- if (block.type === "tool" && typeof block.result === "string") {
6721
- block.result = capToolResult(block.result);
6722
- }
6723
- }
6724
- } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
6725
- msg.content = capToolResult(msg.content);
6726
- }
6727
- }
6728
6950
  function sanitizeMessages(messages) {
6729
6951
  const result = [];
6730
6952
  for (let i = 0; i < messages.length; i++) {
6731
6953
  const msg = messages[i];
6732
- capOversizedResults(msg);
6954
+ capMessageForHistory(msg);
6733
6955
  result.push(msg);
6734
6956
  if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
6735
6957
  continue;
@@ -6841,6 +7063,9 @@ function parseArchive(name) {
6841
7063
  const raw = fs21.readFileSync(path11.join(ARCHIVE_DIR, name), "utf-8");
6842
7064
  const data = JSON.parse(raw);
6843
7065
  const messages = Array.isArray(data?.messages) ? data.messages : [];
7066
+ for (const msg of messages) {
7067
+ capMessageForHistory(msg);
7068
+ }
6844
7069
  archiveCountCache.set(name, messages.length);
6845
7070
  archiveMsgCache.set(name, messages);
6846
7071
  while (archiveMsgCache.size > ARCHIVE_MSG_CACHE_MAX) {
@@ -6951,6 +7176,29 @@ function getHistoryPage(state, opts) {
6951
7176
  messages.push(state.messages[i]);
6952
7177
  }
6953
7178
  }
7179
+ if (messages.length > 1) {
7180
+ let bytes = 0;
7181
+ let cut = 0;
7182
+ for (let i = messages.length - 1; i >= 0; i--) {
7183
+ bytes += Buffer.byteLength(JSON.stringify(messages[i]), "utf-8") + 1;
7184
+ if (bytes > HISTORY_PAGE_MAX_BYTES && i < messages.length - 1) {
7185
+ cut = i + 1;
7186
+ break;
7187
+ }
7188
+ }
7189
+ if (cut > 0) {
7190
+ while (cut < messages.length - 1 && messages[cut]?.role === "user" && messages[cut]?.toolCallId) {
7191
+ cut++;
7192
+ }
7193
+ messages.splice(0, cut);
7194
+ startIndex += cut;
7195
+ log10.info("History page trimmed to byte budget", {
7196
+ dropped: cut,
7197
+ kept: messages.length,
7198
+ startIndex
7199
+ });
7200
+ }
7201
+ }
6954
7202
  return { messages, startIndex, endIndex, totalMessageCount: total };
6955
7203
  }
6956
7204
  function rotate(state) {
@@ -7298,14 +7546,14 @@ function buildCorpus() {
7298
7546
  });
7299
7547
  }
7300
7548
  }
7301
- const sep = "\n\n---\n\n";
7549
+ const sep2 = "\n\n---\n\n";
7302
7550
  const sections = [];
7303
7551
  let usedChars = 0;
7304
7552
  for (const { path: p, content } of files) {
7305
7553
  const section = `## File: ${p}
7306
7554
 
7307
7555
  ${content}`;
7308
- const added = section.length + (sections.length > 0 ? sep.length : 0);
7556
+ const added = section.length + (sections.length > 0 ? sep2.length : 0);
7309
7557
  if (sections.length > 0 && usedChars + added > BRAND_CORPUS_CHAR_LIMIT) {
7310
7558
  sections.push(
7311
7559
  `(brand corpus truncated: included ${sections.length} of ${files.length} files, ~${(usedChars / 1024).toFixed(0)}KB; brand-relevant files were prioritized.)`
@@ -7315,7 +7563,7 @@ ${content}`;
7315
7563
  sections.push(section);
7316
7564
  usedChars += added;
7317
7565
  }
7318
- return sections.join(sep);
7566
+ return sections.join(sep2);
7319
7567
  }
7320
7568
  function headSlice(content) {
7321
7569
  if (content.length <= HEAD_SLICE_CHARS) {
@@ -7845,6 +8093,8 @@ async function runTurn(params) {
7845
8093
  let lastCallInputTokens = 0;
7846
8094
  let lastCallCacheCreation = 0;
7847
8095
  let lastCallCacheRead = 0;
8096
+ let abnormalStopRecoveries = 0;
8097
+ const MAX_ABNORMAL_STOP_RECOVERIES = 2;
7848
8098
  const statusWatcher = isFirstMessage ? { stop() {
7849
8099
  }, pause() {
7850
8100
  }, resume() {
@@ -8188,6 +8438,18 @@ async function runTurn(params) {
8188
8438
  });
8189
8439
  }
8190
8440
  const toolCalls = getToolCalls(contentBlocks);
8441
+ if (toolCalls.length === 0 && (stopReason === "repetition" || stopReason === "max_tokens") && abnormalStopRecoveries < MAX_ABNORMAL_STOP_RECOVERIES && !signal?.aborted) {
8442
+ abnormalStopRecoveries++;
8443
+ log14.warn("Abnormal stop \u2014 nudging model to continue", {
8444
+ requestId,
8445
+ stopReason,
8446
+ attempt: abnormalStopRecoveries
8447
+ });
8448
+ const nudge = "Your previous response was cut off \u2014 it degenerated into repeated text or hit the output limit, and the repeated portion was removed. Reassess where you are in the task and continue from where you left off. Prefer a tool call over restating what you were about to do.";
8449
+ state.messages.push({ role: "user", content: nudge, hidden: true });
8450
+ onEvent({ type: "user_message", text: nudge, hidden: true });
8451
+ continue;
8452
+ }
8191
8453
  if (stopReason !== "tool_use" || toolCalls.length === 0) {
8192
8454
  statusWatcher.stop();
8193
8455
  saveSession(state);
@@ -8354,7 +8616,7 @@ async function runTurn(params) {
8354
8616
  block.completedAt = Date.now();
8355
8617
  const msgs = subAgentMessages.get(r.id);
8356
8618
  if (msgs) {
8357
- block.subAgentMessages = msgs;
8619
+ block.subAgentMessages = capSubAgentTranscript(msgs);
8358
8620
  }
8359
8621
  }
8360
8622
  }
@@ -8398,8 +8660,8 @@ async function runTurn(params) {
8398
8660
 
8399
8661
  // src/headless/attachments.ts
8400
8662
  import { mkdirSync, existsSync } from "fs";
8401
- import { writeFile } from "fs/promises";
8402
- import { basename as basename2, join, extname as extname2 } from "path";
8663
+ import { writeFile as writeFile2 } from "fs/promises";
8664
+ import { basename as basename2, join as join2, extname as extname2 } from "path";
8403
8665
  var log15 = createLogger("headless:attachments");
8404
8666
  var UPLOADS_DIR = "src/.user-uploads";
8405
8667
  function filenameFromUrl(url) {
@@ -8412,7 +8674,7 @@ function filenameFromUrl(url) {
8412
8674
  }
8413
8675
  }
8414
8676
  function resolveUniqueFilename(name, claimed) {
8415
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join(UPLOADS_DIR, candidate));
8677
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join2(UPLOADS_DIR, candidate));
8416
8678
  if (isFree(name)) {
8417
8679
  return name;
8418
8680
  }
@@ -8447,7 +8709,7 @@ async function persistAttachments(attachments) {
8447
8709
  const results = await Promise.allSettled(
8448
8710
  nonVoice.map(async (att, i) => {
8449
8711
  const name = names[i];
8450
- const localPath = join(UPLOADS_DIR, name);
8712
+ const localPath = join2(UPLOADS_DIR, name);
8451
8713
  const res = await fetch(att.url, {
8452
8714
  signal: AbortSignal.timeout(3e4)
8453
8715
  });
@@ -8455,7 +8717,7 @@ async function persistAttachments(attachments) {
8455
8717
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
8456
8718
  }
8457
8719
  const buffer = Buffer.from(await res.arrayBuffer());
8458
- await writeFile(localPath, buffer);
8720
+ await writeFile2(localPath, buffer);
8459
8721
  log15.info("Attachment saved", {
8460
8722
  filename: name,
8461
8723
  path: localPath,
@@ -8469,7 +8731,7 @@ async function persistAttachments(attachments) {
8469
8731
  });
8470
8732
  if (textRes.ok) {
8471
8733
  extractedTextPath = `${localPath}.txt`;
8472
- await writeFile(extractedTextPath, await textRes.text(), "utf-8");
8734
+ await writeFile2(extractedTextPath, await textRes.text(), "utf-8");
8473
8735
  log15.info("Extracted text saved", { path: extractedTextPath });
8474
8736
  }
8475
8737
  } catch {
@@ -9056,17 +9318,17 @@ var HeadlessSession = class {
9056
9318
  return Promise.resolve(early);
9057
9319
  }
9058
9320
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
9059
- return new Promise((resolve3) => {
9321
+ return new Promise((resolve4) => {
9060
9322
  const timeout = shouldTimeout ? setTimeout(() => {
9061
9323
  this.pendingTools.delete(id);
9062
- resolve3(
9324
+ resolve4(
9063
9325
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
9064
9326
  );
9065
9327
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
9066
9328
  this.pendingTools.set(id, {
9067
9329
  resolve: (result) => {
9068
9330
  clearTimeout(timeout);
9069
- resolve3(result);
9331
+ resolve4(result);
9070
9332
  },
9071
9333
  timeout
9072
9334
  });