@mindstudio-ai/remy 0.1.319 → 0.1.321

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ trigger: reviewExistingProject
3
+ ---
4
+
5
+ This is an automated message triggered by the user choosing "Bring an existing project" on the Remy start screen and uploading what they have. They can't see this message. The uploads are attached and already saved under `src/.user-uploads/` (paths are in the attachment header; a folder upload arrives as one zip). Repository URL, blank if none: {{repoUrl}}
6
+
7
+ Let the user know you're going to review their project, and then hand the upload paths (and the URL, if any) to `reviewExistingProject` first, before saying anything substantive, and do not open the archive yourself. Then run intake as usual on top of what comes back: tell the user in a few lines what you found and what you think they were building, and put the review's questions to them as a form. The prior attempt is evidence of intent, not a spec. Oftentimes, the user is uploading something that never worked or was half-finished (hence why they're here!). What actually gets built in this session is decided with the user, not inherited from the upload. Treat the results of the review as a *communication shortcut* to more quickly align with the user, not as a guide for "importing" their project!
package/dist/headless.js CHANGED
@@ -441,6 +441,13 @@ var MODEL_SURFACES = {
441
441
  modelType: "text",
442
442
  userPickable: true
443
443
  },
444
+ reviewExistingProject: {
445
+ default: "claude-5-sonnet",
446
+ label: "Existing Project Review",
447
+ description: "Reviews a project you bring from another tool and reports what is worth carrying forward.",
448
+ modelType: "text",
449
+ userPickable: true
450
+ },
444
451
  copyEditor: {
445
452
  default: "claude-5-sonnet",
446
453
  label: "Copy Agent",
@@ -1420,6 +1427,9 @@ async function listRecursive(dir) {
1420
1427
  for (const entry of entries) {
1421
1428
  const fullPath = path5.join(dir, entry.name);
1422
1429
  if (entry.isDirectory()) {
1430
+ if (entry.name.startsWith(".")) {
1431
+ continue;
1432
+ }
1423
1433
  results.push(`${fullPath}/`);
1424
1434
  results.push(...await listRecursive(fullPath));
1425
1435
  } else {
@@ -1593,7 +1603,7 @@ var promptUserTool = {
1593
1603
  type: {
1594
1604
  type: "string",
1595
1605
  enum: ["select", "checklist", "text", "file"],
1596
- description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: file/image upload. The answer is the local path (string) under src/.user-uploads/ \u2014 the file is already downloaded to disk. Documents may have an extracted-text sidecar at <path>.txt. Reference the path directly; pass image paths straight to analyzeImage / screenshot tools.'
1606
+ description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: the user hands over files, a folder, or a paste (set `multiple: true` to take several). Each answer arrives in one of two shapes. A local path (string) under src/.user-uploads/: the file is already on disk, documents may have an extracted-text sidecar at <path>.txt, and a folder arrives as src/.user-uploads/<folder>.zip for you to unpack with bash. Or, when the user brought a dataset (a folder over about 100 MB or 500 files, or any single file over 100 MB), a store landing object { kind: "store", store, prefix, files, bytes, failed } \u2014 the files were streamed into the app\'s private file store `uploads` under that prefix and are NOT on disk; read them in place with `remy-admin datasources inspect --store <store> --prefix <prefix>` and `remy-admin files get`, never download the whole set. Pass image paths straight to analyzeImage / screenshot tools.'
1597
1607
  },
1598
1608
  helpText: {
1599
1609
  type: "string",
@@ -1628,7 +1638,7 @@ var promptUserTool = {
1628
1638
  },
1629
1639
  multiple: {
1630
1640
  type: "boolean",
1631
- description: "For file type: allow multiple uploads (the answer is an array of local paths). Defaults to false."
1641
+ description: 'For file type: allow several files or folders (the answer is an array of local paths and/or store landings). Defaults to false. Set it whenever you ask for "a few examples" or "a sample of your data".'
1632
1642
  },
1633
1643
  format: {
1634
1644
  type: "string",
@@ -2619,7 +2629,7 @@ async function readAndSort(dirPath) {
2619
2629
  async function collapsePath(basePath, name) {
2620
2630
  let display = name;
2621
2631
  let current = path8.join(basePath, name);
2622
- for (; ; ) {
2632
+ while (true) {
2623
2633
  let children;
2624
2634
  try {
2625
2635
  children = await readAndSort(current);
@@ -2646,8 +2656,8 @@ function formatSize(bytes) {
2646
2656
  }
2647
2657
  async function formatFile(dirPath, name, indent) {
2648
2658
  try {
2649
- const stat4 = await fs15.stat(path8.join(dirPath, name));
2650
- return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat4.size)}`;
2659
+ const stat6 = await fs15.stat(path8.join(dirPath, name));
2660
+ return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat6.size)}`;
2651
2661
  } catch {
2652
2662
  return `${indent}${name}`;
2653
2663
  }
@@ -3437,6 +3447,48 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3437
3447
  }
3438
3448
  };
3439
3449
 
3450
+ // src/recording.ts
3451
+ function liftRecording(result) {
3452
+ if (!result.includes('"recording"')) {
3453
+ return { result };
3454
+ }
3455
+ let parsed;
3456
+ try {
3457
+ parsed = JSON.parse(result);
3458
+ } catch {
3459
+ return { result };
3460
+ }
3461
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3462
+ return { result };
3463
+ }
3464
+ const obj = parsed;
3465
+ const r = obj.recording;
3466
+ if (!r || typeof r !== "object") {
3467
+ return { result };
3468
+ }
3469
+ const ref = r;
3470
+ if (typeof ref.path !== "string" || typeof ref.sessionId !== "string" || typeof ref.runId !== "string" || typeof ref.seq !== "number") {
3471
+ return { result };
3472
+ }
3473
+ const recording = {
3474
+ path: ref.path,
3475
+ sessionId: ref.sessionId,
3476
+ runId: ref.runId,
3477
+ seq: ref.seq,
3478
+ containsSnapshot: ref.containsSnapshot === true,
3479
+ startTs: typeof ref.startTs === "number" ? ref.startTs : 0,
3480
+ endTs: typeof ref.endTs === "number" ? ref.endTs : 0,
3481
+ width: typeof ref.width === "number" ? ref.width : 0,
3482
+ height: typeof ref.height === "number" ? ref.height : 0
3483
+ };
3484
+ const rest = { ...obj };
3485
+ delete rest.recording;
3486
+ return {
3487
+ result: JSON.stringify({ ...rest, recorded: true }),
3488
+ recording
3489
+ };
3490
+ }
3491
+
3440
3492
  // src/historyLimits.ts
3441
3493
  var MAX_TOOL_RESULT_BYTES = 256 * 1024;
3442
3494
  function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
@@ -3479,6 +3531,13 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
3479
3531
  continue;
3480
3532
  }
3481
3533
  if (typeof block.result === "string") {
3534
+ if (block.name === "browserCommand" && !block.recording) {
3535
+ const lifted = liftRecording(block.result);
3536
+ if (lifted.recording) {
3537
+ block.result = lifted.result;
3538
+ block.recording = lifted.recording;
3539
+ }
3540
+ }
3482
3541
  block.result = capToolResult(block.result, maxBytes);
3483
3542
  }
3484
3543
  if (typeof block.backgroundResult === "string") {
@@ -4075,25 +4134,36 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4075
4134
  }
4076
4135
  let settle;
4077
4136
  const resultPromise = new Promise((res) => {
4078
- settle = (result, isError) => res({ id: tc.id, result, isError });
4137
+ settle = (result, isError, recording) => res({
4138
+ id: tc.id,
4139
+ result,
4140
+ isError,
4141
+ ...recording ? { recording } : {}
4142
+ });
4079
4143
  });
4080
4144
  let toolAbort = new AbortController();
4081
4145
  const cascadeAbort = () => toolAbort.abort();
4082
4146
  signal?.addEventListener("abort", cascadeAbort, { once: true });
4083
4147
  let settled = false;
4084
- const safeSettle = (result, isError) => {
4148
+ const safeSettle = (result, isError, recording) => {
4085
4149
  if (settled) {
4086
4150
  return;
4087
4151
  }
4088
4152
  settled = true;
4089
4153
  signal?.removeEventListener("abort", cascadeAbort);
4090
- settle(result, isError);
4154
+ settle(result, isError, recording);
4091
4155
  };
4092
4156
  const run2 = async (input) => {
4093
4157
  try {
4094
4158
  let result;
4159
+ let recording;
4095
4160
  if (externalTools.has(tc.name) && resolveExternalTool) {
4096
4161
  result = await resolveExternalTool(tc.id, tc.name, input);
4162
+ if (tc.name === "browserCommand") {
4163
+ const lifted = liftRecording(result);
4164
+ result = lifted.result;
4165
+ recording = lifted.recording;
4166
+ }
4097
4167
  } else {
4098
4168
  const onLog = (line) => emit({
4099
4169
  type: "tool_input_delta",
@@ -4109,7 +4179,11 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4109
4179
  subAgentMessages
4110
4180
  );
4111
4181
  }
4112
- safeSettle(capToolResult(result), result.startsWith("Error"));
4182
+ safeSettle(
4183
+ capToolResult(result),
4184
+ result.startsWith("Error"),
4185
+ recording
4186
+ );
4113
4187
  } catch (err) {
4114
4188
  safeSettle(`Error: ${err.message}`, true);
4115
4189
  }
@@ -4151,7 +4225,8 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4151
4225
  id: tc.id,
4152
4226
  name: tc.name,
4153
4227
  result: r.result,
4154
- isError: r.isError
4228
+ isError: r.isError,
4229
+ ...r.recording ? { recording: r.recording } : {}
4155
4230
  });
4156
4231
  return r;
4157
4232
  })
@@ -4165,6 +4240,9 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4165
4240
  block.result = r.result;
4166
4241
  block.isError = r.isError;
4167
4242
  block.completedAt = Date.now();
4243
+ if (r.recording) {
4244
+ block.recording = r.recording;
4245
+ }
4168
4246
  const innerMsgs = subAgentMessages.get(r.id);
4169
4247
  if (innerMsgs) {
4170
4248
  block.subAgentMessages = capSubAgentTranscript(innerMsgs);
@@ -4295,7 +4373,7 @@ var BROWSER_TOOLS = [
4295
4373
  },
4296
4374
  {
4297
4375
  name: "browserCommand",
4298
- description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) also return a `recording` object \u2014 one chunk of a continuous per-session rrweb recording that the viewer stitches into a single replay (not a standalone per-call clip). Timeout: 120s.",
4376
+ description: "Interact with the app's live preview by sending browser commands. Commands execute sequentially with an animated cursor. Always start with a snapshot to see the current state and get ref identifiers. The result includes a snapshot field with the final page state after all steps complete. On error, the failing step has an error field and execution stops. Batches that contain an interactive step (click, type, select) are recorded for the user to replay and return `recorded: true`. Timeout: 120s.",
4299
4377
  inputSchema: {
4300
4378
  type: "object",
4301
4379
  properties: {
@@ -4413,7 +4491,7 @@ function walkMdFiles(dir, skip) {
4413
4491
  for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
4414
4492
  const full = path9.join(dir, entry.name);
4415
4493
  if (entry.isDirectory()) {
4416
- if (!skip?.has(entry.name)) {
4494
+ if (!skip?.has(entry.name) && !entry.name.startsWith(".")) {
4417
4495
  files.push(...walkMdFiles(full, skip));
4418
4496
  }
4419
4497
  } else if (entry.name.endsWith(".md")) {
@@ -4711,8 +4789,14 @@ async function runBrowserAutomation(task, context, opts) {
4711
4789
  });
4712
4790
  context.subAgentMessages?.set(context.toolCallId, result.messages);
4713
4791
  const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4792
+ const recorded = result.messages.some(
4793
+ (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
4794
+ (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
4795
+ )
4796
+ );
4714
4797
  return {
4715
4798
  text: result.text,
4799
+ recorded,
4716
4800
  ...preferred?.url ? { screenshot: preferred } : {}
4717
4801
  };
4718
4802
  } finally {
@@ -4739,12 +4823,18 @@ var browserAutomationTool = {
4739
4823
  return "Error: browser automation requires execution context (only available in headless mode)";
4740
4824
  }
4741
4825
  const result = await runBrowserAutomation(input.task, context);
4826
+ let text = result.text;
4742
4827
  if (result.screenshot) {
4743
- return `${result.text}
4828
+ text += `
4744
4829
 
4745
4830
  ![Final state](${result.screenshot.url})`;
4746
4831
  }
4747
- return result.text;
4832
+ if (result.recorded) {
4833
+ text += `
4834
+
4835
+ Replay of this run: ![Browser test replay](replay:${context.toolCallId})`;
4836
+ }
4837
+ return text;
4748
4838
  }
4749
4839
  };
4750
4840
 
@@ -7078,6 +7168,138 @@ After reconciling the spec, refresh the Build Overview: author the complete upda
7078
7168
  }
7079
7169
  };
7080
7170
 
7171
+ // src/subagents/reviewExistingProject/tools.ts
7172
+ var REVIEW_TOOLS = [
7173
+ ...COMMON_READ_TOOLS,
7174
+ bashTool.definition
7175
+ ];
7176
+
7177
+ // src/subagents/reviewExistingProject/validatePaths.ts
7178
+ import { stat as stat4 } from "fs/promises";
7179
+ import { join as join5 } from "path";
7180
+ var UPLOAD_REF_RE = /`(src\/\.user-uploads\/[^`\n]+?)\/?`/g;
7181
+ async function validateUploadPaths(text) {
7182
+ const refs = /* @__PURE__ */ new Set();
7183
+ for (const m of text.matchAll(UPLOAD_REF_RE)) {
7184
+ refs.add(m[1]);
7185
+ }
7186
+ if (refs.size === 0) {
7187
+ return null;
7188
+ }
7189
+ const missing = [];
7190
+ for (const ref of refs) {
7191
+ const exists = await stat4(join5(PROJECT_ROOT, ref)).then(
7192
+ () => true,
7193
+ () => false
7194
+ );
7195
+ if (!exists) {
7196
+ missing.push(ref);
7197
+ }
7198
+ }
7199
+ if (missing.length === 0) {
7200
+ return null;
7201
+ }
7202
+ return [
7203
+ `Your review cites paths under src/.user-uploads/ that do not exist on disk:`,
7204
+ ...missing.map((p) => `- ${p}`),
7205
+ ``,
7206
+ `Check the real location with listDir \u2014 the trimmed tree may have landed under a different name, or the original archive may already have been deleted \u2014 and correct or remove each path. Then send your complete review again from the top \u2014 it fully replaces your previous response, so include everything, not just the fixes.`
7207
+ ].join("\n");
7208
+ }
7209
+
7210
+ // src/subagents/reviewExistingProject/index.ts
7211
+ var BASE_PROMPT7 = readAsset("subagents/reviewExistingProject", "prompt.md");
7212
+ function buildTask(input) {
7213
+ const parts = [];
7214
+ const paths = (input.paths ?? []).filter(
7215
+ (p) => typeof p === "string" && p.trim()
7216
+ );
7217
+ if (paths.length > 0) {
7218
+ parts.push(
7219
+ `Uploads (local paths):
7220
+ ${paths.map((p) => `- ${p.trim()}`).join("\n")}`
7221
+ );
7222
+ }
7223
+ if (input.repoUrl?.trim()) {
7224
+ parts.push(`Repository URL: ${input.repoUrl.trim()}`);
7225
+ }
7226
+ if (input.context?.trim()) {
7227
+ parts.push(`Context from the conversation: ${input.context.trim()}`);
7228
+ }
7229
+ return parts.join("\n\n");
7230
+ }
7231
+ async function runReviewExistingProject(input, context) {
7232
+ const task = buildTask(input);
7233
+ if (!task) {
7234
+ return "Error: reviewExistingProject needs at least one upload path or a repository URL.";
7235
+ }
7236
+ const specIndex = loadSpecIndex();
7237
+ const parts = [BASE_PROMPT7, loadPlatformBrief()];
7238
+ parts.push("<!-- cache_breakpoint -->");
7239
+ if (specIndex) {
7240
+ parts.push(specIndex);
7241
+ }
7242
+ const system = parts.join("\n\n");
7243
+ const result = await runSubAgent({
7244
+ system,
7245
+ task,
7246
+ tools: REVIEW_TOOLS,
7247
+ externalTools: /* @__PURE__ */ new Set(),
7248
+ executeTool: (name, toolInput, toolCallId, onLog, sams) => {
7249
+ const childCtx = toolCallId ? {
7250
+ ...deriveContext(context, toolCallId, onLog),
7251
+ subAgentMessages: sams
7252
+ } : context;
7253
+ return executeTool(name, toolInput, childCtx);
7254
+ },
7255
+ apiConfig: context.apiConfig,
7256
+ model: resolveModel("reviewExistingProject", context.models, context.model),
7257
+ subAgentId: "reviewExistingProject",
7258
+ signal: context.signal,
7259
+ parentToolId: context.toolCallId,
7260
+ requestId: context.requestId,
7261
+ onEvent: context.onEvent,
7262
+ resolveExternalTool: context.resolveExternalTool,
7263
+ toolRegistry: context.toolRegistry,
7264
+ // Every quoted src/.user-uploads/ path in the review must exist on disk
7265
+ // (see validatePaths.ts) — the paths are the part the caller acts on.
7266
+ validateResult: (text) => validateUploadPaths(text)
7267
+ });
7268
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
7269
+ return result.text;
7270
+ }
7271
+ var reviewExistingProjectTool = {
7272
+ definition: {
7273
+ name: "reviewExistingProject",
7274
+ description: "Your reviewer for anything a user brings from a prior/extant project or attempt at a project: a zip of a codebase from another coding agent, an export from a vibe coding platform, a folder of specs and docs, a public git URL. It unpacks the upload, strips the noise, lands the trimmed tree in src/.user-uploads/, and returns a short review: what the project was trying to be, the materials worth carrying forward with their paths, what to ignore, state signals, and the questions only the user can answer. Treat the review as evidence of what the user wants, never as a spec. Do not open the archive yourself.",
7275
+ inputSchema: {
7276
+ type: "object",
7277
+ properties: {
7278
+ paths: {
7279
+ type: "array",
7280
+ items: { type: "string" },
7281
+ description: "Local paths under src/.user-uploads/ to the uploaded archive(s) and/or loose files, exactly as given in the attachment header."
7282
+ },
7283
+ context: {
7284
+ type: "string",
7285
+ description: "One or two lines about the user and what they said they want. The reviewer cannot see the conversation."
7286
+ },
7287
+ repoUrl: {
7288
+ type: "string",
7289
+ description: "A public git repository URL to shallow-clone and review, instead of or alongside the uploads."
7290
+ }
7291
+ },
7292
+ required: []
7293
+ }
7294
+ },
7295
+ async execute(input, context) {
7296
+ if (!context) {
7297
+ return "Error: reviewExistingProject requires execution context";
7298
+ }
7299
+ return runReviewExistingProject(input, context);
7300
+ }
7301
+ };
7302
+
7081
7303
  // src/tools/common/scrapeWebUrl.ts
7082
7304
  var scrapeWebUrlTool = {
7083
7305
  definition: {
@@ -7172,7 +7394,8 @@ var ALL_TOOLS = [
7172
7394
  // new tool goes at the end to leave every existing session's prefix intact.
7173
7395
  loadSkillTool,
7174
7396
  testJewelTool,
7175
- researchTool
7397
+ researchTool,
7398
+ reviewExistingProjectTool
7176
7399
  ];
7177
7400
  var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7178
7401
  "visualDesignExpert",
@@ -7182,7 +7405,8 @@ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
7182
7405
  "specSync",
7183
7406
  "runAutomatedBrowserTest",
7184
7407
  "askMindStudioSdk",
7185
- "research"
7408
+ "research",
7409
+ "reviewExistingProject"
7186
7410
  ]);
7187
7411
  function getToolDefinitions() {
7188
7412
  return ALL_TOOLS.map((t) => t.definition);
@@ -8172,7 +8396,9 @@ function walkMdFiles2(dir) {
8172
8396
  for (const entry of entries) {
8173
8397
  const full = path12.join(dir, entry.name);
8174
8398
  if (entry.isDirectory()) {
8175
- results.push(...walkMdFiles2(full));
8399
+ if (!entry.name.startsWith(".")) {
8400
+ results.push(...walkMdFiles2(full));
8401
+ }
8176
8402
  } else if (entry.name.endsWith(".md")) {
8177
8403
  results.push(full);
8178
8404
  }
@@ -9572,11 +9798,15 @@ async function runTurn(params) {
9572
9798
  }
9573
9799
 
9574
9800
  // src/headless/attachments.ts
9575
- import { mkdirSync, existsSync } from "fs";
9576
- import { writeFile as writeFile3 } from "fs/promises";
9577
- import { basename as basename2, join as join5, extname as extname3 } from "path";
9801
+ import { mkdirSync, existsSync, createWriteStream } from "fs";
9802
+ import { writeFile as writeFile3, stat as stat5 } from "fs/promises";
9803
+ import { Readable } from "stream";
9804
+ import { pipeline } from "stream/promises";
9805
+ import { basename as basename2, join as join6, extname as extname3 } from "path";
9578
9806
  var log16 = createLogger("headless:attachments");
9579
9807
  var UPLOADS_DIR = "src/.user-uploads";
9808
+ var IMAGE_DOWNLOAD_TIMEOUT_MS = 3e4;
9809
+ var DOCUMENT_DOWNLOAD_TIMEOUT_MS = 3e5;
9580
9810
  function filenameFromUrl(url) {
9581
9811
  try {
9582
9812
  const pathname = new URL(url).pathname;
@@ -9587,7 +9817,7 @@ function filenameFromUrl(url) {
9587
9817
  }
9588
9818
  }
9589
9819
  function resolveUniqueFilename(name, claimed) {
9590
- const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join5(UPLOADS_DIR, candidate));
9820
+ const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join6(UPLOADS_DIR, candidate));
9591
9821
  if (isFree(name)) {
9592
9822
  return name;
9593
9823
  }
@@ -9622,19 +9852,23 @@ async function persistAttachmentList(attachments) {
9622
9852
  const results = await Promise.allSettled(
9623
9853
  nonVoice.map(async (att, i) => {
9624
9854
  const name = names[i];
9625
- const localPath = join5(UPLOADS_DIR, name);
9855
+ const localPath = join6(UPLOADS_DIR, name);
9856
+ const timeoutMs = isImageAttachment(att) ? IMAGE_DOWNLOAD_TIMEOUT_MS : DOCUMENT_DOWNLOAD_TIMEOUT_MS;
9626
9857
  const res = await fetch(att.url, {
9627
- signal: AbortSignal.timeout(3e4)
9858
+ signal: AbortSignal.timeout(timeoutMs)
9628
9859
  });
9629
- if (!res.ok) {
9860
+ if (!res.ok || !res.body) {
9630
9861
  throw new Error(`HTTP ${res.status} downloading ${att.url}`);
9631
9862
  }
9632
- const buffer = Buffer.from(await res.arrayBuffer());
9633
- await writeFile3(localPath, buffer);
9863
+ await pipeline(
9864
+ Readable.fromWeb(res.body),
9865
+ createWriteStream(localPath)
9866
+ );
9867
+ const { size } = await stat5(localPath);
9634
9868
  log16.info("Attachment saved", {
9635
9869
  filename: name,
9636
9870
  path: localPath,
9637
- bytes: buffer.length
9871
+ bytes: size
9638
9872
  });
9639
9873
  let extractedTextPath;
9640
9874
  if (att.extractedTextUrl) {
@@ -10505,6 +10739,7 @@ var HeadlessSession = class {
10505
10739
  name: e.name,
10506
10740
  result: e.result,
10507
10741
  isError: e.isError,
10742
+ ...e.recording && { recording: e.recording },
10508
10743
  ...e.parentToolId && { parentToolId: e.parentToolId }
10509
10744
  },
10510
10745
  rid
@@ -10617,12 +10852,15 @@ var HeadlessSession = class {
10617
10852
  const questions = Array.isArray(input?.questions) ? input.questions : [];
10618
10853
  const batch = [];
10619
10854
  const slots = [];
10855
+ let landings = 0;
10620
10856
  for (const q of questions) {
10621
10857
  if (q?.type !== "file" || typeof q.id !== "string") {
10622
10858
  continue;
10623
10859
  }
10624
10860
  const val = answers[q.id];
10625
- const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
10861
+ const all = Array.isArray(val) ? val : [val];
10862
+ landings += all.filter((v) => v && v.kind === "store").length;
10863
+ const items = all.filter(isDescriptor);
10626
10864
  if (items.length === 0) {
10627
10865
  continue;
10628
10866
  }
@@ -10641,6 +10879,9 @@ var HeadlessSession = class {
10641
10879
  }
10642
10880
  }
10643
10881
  if (batch.length === 0) {
10882
+ if (landings > 0) {
10883
+ log17.info("promptUser store landings passed through", { landings });
10884
+ }
10644
10885
  return raw;
10645
10886
  }
10646
10887
  let results;
@@ -10984,7 +11225,7 @@ var HeadlessSession = class {
10984
11225
  * there would strand the chain steps and background results behind it.
10985
11226
  */
10986
11227
  async drainQueueLoop() {
10987
- for (; ; ) {
11228
+ while (true) {
10988
11229
  const at = this.queue.firstDeliverableIndex();
10989
11230
  if (at === -1) {
10990
11231
  return;