@mindstudio-ai/remy 0.1.315 → 0.1.317

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.
@@ -137,6 +137,7 @@ declare class HeadlessSession {
137
137
  */
138
138
  private runForcedCompactionIfNeeded;
139
139
  private onBackgroundComplete;
140
+ private awaitExternalToolResult;
140
141
  private resolveExternalTool;
141
142
  private onEvent;
142
143
  /**
@@ -149,6 +150,21 @@ declare class HeadlessSession {
149
150
  * filename de-dup set is per-call, so parallel calls race on names.
150
151
  */
151
152
  private persistEntryAttachments;
153
+ /**
154
+ * promptUser `file` answers arrive as private-upload descriptors
155
+ * ({url, key, filename, extractedTextUrl}) — the same shape as a chat
156
+ * attachment. Download them to src/.user-uploads/ and rewrite each answer to
157
+ * the local path(s) BEFORE the result settles, so the model, the tool block,
158
+ * the saved session and the frontend transcript all see paths and never a
159
+ * signed url that dies in an hour. Non-file answers, dismissals and anything
160
+ * unparseable pass through untouched.
161
+ *
162
+ * Runs while the turn is blocked on this tool, so it can't overlap the
163
+ * turn's own persistEntryAttachments; the ASAP/queue persist paths only run
164
+ * after the tool batch settles. One prompt is open at a time, so two results
165
+ * can't race either.
166
+ */
167
+ private persistPromptUserUploads;
152
168
  /**
153
169
  * Stable id for a queued chain step, so the frontend can address one (to
154
170
  * discard a paused pipeline) rather than waiting for drainQueueLoop to
package/dist/headless.js CHANGED
@@ -532,6 +532,7 @@ var TEXT_MODELS = {
532
532
  // 262K window
533
533
  "kimi-k3": { forceCompactAt: 85e4 },
534
534
  "deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
535
+ "deepseek-v4-pro": { forceCompactAt: 85e4 },
535
536
  "qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
536
537
  // 262K window
537
538
  "qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
@@ -1589,7 +1590,7 @@ var promptUserTool = {
1589
1590
  type: {
1590
1591
  type: "string",
1591
1592
  enum: ["select", "checklist", "text", "file"],
1592
- 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, returns CDN URL(s) that can be referenced directly or curled onto disk.'
1593
+ 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.'
1593
1594
  },
1594
1595
  helpText: {
1595
1596
  type: "string",
@@ -1624,7 +1625,7 @@ var promptUserTool = {
1624
1625
  },
1625
1626
  multiple: {
1626
1627
  type: "boolean",
1627
- description: "For file type: allow multiple uploads (returns array of URLs). Defaults to false."
1628
+ description: "For file type: allow multiple uploads (the answer is an array of local paths). Defaults to false."
1628
1629
  },
1629
1630
  format: {
1630
1631
  type: "string",
@@ -9600,10 +9601,10 @@ function isImageAttachment(att) {
9600
9601
  const name = att.filename || filenameFromUrl(att.url);
9601
9602
  return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
9602
9603
  }
9603
- async function persistAttachments(attachments) {
9604
+ async function persistAttachmentList(attachments) {
9604
9605
  const nonVoice = attachments.filter((a) => !a.isVoice);
9605
9606
  if (nonVoice.length === 0) {
9606
- return { documents: [], images: [] };
9607
+ return [];
9607
9608
  }
9608
9609
  mkdirSync(UPLOADS_DIR, { recursive: true });
9609
9610
  const claimed = /* @__PURE__ */ new Set();
@@ -9654,8 +9655,13 @@ async function persistAttachments(attachments) {
9654
9655
  };
9655
9656
  })
9656
9657
  );
9657
- const settled = results.map((r, i) => ({
9658
- result: r.status === "fulfilled" ? r.value : null,
9658
+ return results.map((r) => r.status === "fulfilled" ? r.value : null);
9659
+ }
9660
+ async function persistAttachments(attachments) {
9661
+ const nonVoice = attachments.filter((a) => !a.isVoice);
9662
+ const results = await persistAttachmentList(nonVoice);
9663
+ const settled = results.map((result, i) => ({
9664
+ result,
9659
9665
  isImage: isImageAttachment(nonVoice[i])
9660
9666
  }));
9661
9667
  return {
@@ -10344,7 +10350,7 @@ var HeadlessSession = class {
10344
10350
  //////////////////////////////////////////////////////////////////////////////
10345
10351
  // External tool bridge
10346
10352
  //////////////////////////////////////////////////////////////////////////////
10347
- resolveExternalTool = (id, name, _input) => {
10353
+ awaitExternalToolResult = (id, name) => {
10348
10354
  const early = this.earlyResults.get(id);
10349
10355
  if (early !== void 0) {
10350
10356
  this.earlyResults.delete(id);
@@ -10368,6 +10374,13 @@ var HeadlessSession = class {
10368
10374
  });
10369
10375
  });
10370
10376
  };
10377
+ // promptUser answers carry private-upload descriptors for `file` questions;
10378
+ // persist them to disk and hand the model local paths (see
10379
+ // persistPromptUserUploads). Every other external tool passes through.
10380
+ resolveExternalTool = (id, name, input) => {
10381
+ const result = this.awaitExternalToolResult(id, name);
10382
+ return name === "promptUser" ? result.then((raw) => this.persistPromptUserUploads(input, raw)) : result;
10383
+ };
10371
10384
  //////////////////////////////////////////////////////////////////////////////
10372
10385
  // AgentEvent → wire protocol translation
10373
10386
  //////////////////////////////////////////////////////////////////////////////
@@ -10573,6 +10586,87 @@ var HeadlessSession = class {
10573
10586
  return void 0;
10574
10587
  }
10575
10588
  }
10589
+ /**
10590
+ * promptUser `file` answers arrive as private-upload descriptors
10591
+ * ({url, key, filename, extractedTextUrl}) — the same shape as a chat
10592
+ * attachment. Download them to src/.user-uploads/ and rewrite each answer to
10593
+ * the local path(s) BEFORE the result settles, so the model, the tool block,
10594
+ * the saved session and the frontend transcript all see paths and never a
10595
+ * signed url that dies in an hour. Non-file answers, dismissals and anything
10596
+ * unparseable pass through untouched.
10597
+ *
10598
+ * Runs while the turn is blocked on this tool, so it can't overlap the
10599
+ * turn's own persistEntryAttachments; the ASAP/queue persist paths only run
10600
+ * after the tool batch settles. One prompt is open at a time, so two results
10601
+ * can't race either.
10602
+ */
10603
+ async persistPromptUserUploads(input, raw) {
10604
+ let answers;
10605
+ try {
10606
+ answers = JSON.parse(raw);
10607
+ } catch {
10608
+ return raw;
10609
+ }
10610
+ if (!answers || typeof answers !== "object" || Array.isArray(answers) || answers._dismissed) {
10611
+ return raw;
10612
+ }
10613
+ const isDescriptor = (d) => !!d && typeof d === "object" && typeof d.url === "string";
10614
+ const questions = Array.isArray(input?.questions) ? input.questions : [];
10615
+ const batch = [];
10616
+ const slots = [];
10617
+ for (const q of questions) {
10618
+ if (q?.type !== "file" || typeof q.id !== "string") {
10619
+ continue;
10620
+ }
10621
+ const val = answers[q.id];
10622
+ const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
10623
+ if (items.length === 0) {
10624
+ continue;
10625
+ }
10626
+ slots.push({
10627
+ id: q.id,
10628
+ isArray: Array.isArray(val),
10629
+ count: items.length
10630
+ });
10631
+ for (const d of items) {
10632
+ batch.push({
10633
+ url: d.url,
10634
+ key: d.key,
10635
+ filename: d.filename,
10636
+ extractedTextUrl: d.extractedTextUrl ?? void 0
10637
+ });
10638
+ }
10639
+ }
10640
+ if (batch.length === 0) {
10641
+ return raw;
10642
+ }
10643
+ let results;
10644
+ try {
10645
+ results = await persistAttachmentList(batch);
10646
+ } catch (err) {
10647
+ log17.warn("promptUser upload persistence failed", {
10648
+ error: err.message
10649
+ });
10650
+ results = batch.map(() => null);
10651
+ }
10652
+ let cursor = 0;
10653
+ for (const slot of slots) {
10654
+ const paths = results.slice(cursor, cursor + slot.count).map((r, i) => {
10655
+ if (r) {
10656
+ return r.localPath;
10657
+ }
10658
+ const att = batch[cursor + i];
10659
+ log17.warn("promptUser upload not persisted; falling back to url", {
10660
+ filename: att.filename
10661
+ });
10662
+ return att.url;
10663
+ });
10664
+ cursor += slot.count;
10665
+ answers[slot.id] = slot.isArray ? paths : paths[0];
10666
+ }
10667
+ log17.info("promptUser uploads persisted", { count: batch.length });
10668
+ return JSON.stringify(answers);
10669
+ }
10576
10670
  /**
10577
10671
  * Stable id for a queued chain step, so the frontend can address one (to
10578
10672
  * discard a paused pipeline) rather than waiting for drainQueueLoop to
package/dist/index.js CHANGED
@@ -1064,7 +1064,7 @@ var init_promptUser = __esm({
1064
1064
  type: {
1065
1065
  type: "string",
1066
1066
  enum: ["select", "checklist", "text", "file"],
1067
- 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, returns CDN URL(s) that can be referenced directly or curled onto disk.'
1067
+ 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.'
1068
1068
  },
1069
1069
  helpText: {
1070
1070
  type: "string",
@@ -1099,7 +1099,7 @@ var init_promptUser = __esm({
1099
1099
  },
1100
1100
  multiple: {
1101
1101
  type: "boolean",
1102
- description: "For file type: allow multiple uploads (returns array of URLs). Defaults to false."
1102
+ description: "For file type: allow multiple uploads (the answer is an array of local paths). Defaults to false."
1103
1103
  },
1104
1104
  format: {
1105
1105
  type: "string",
@@ -2149,6 +2149,7 @@ var init_surfaces = __esm({
2149
2149
  // 262K window
2150
2150
  "kimi-k3": { forceCompactAt: 85e4 },
2151
2151
  "deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
2152
+ "deepseek-v4-pro": { forceCompactAt: 85e4 },
2152
2153
  "qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
2153
2154
  // 262K window
2154
2155
  "qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
@@ -10551,10 +10552,10 @@ function isImageAttachment(att) {
10551
10552
  const name = att.filename || filenameFromUrl(att.url);
10552
10553
  return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
10553
10554
  }
10554
- async function persistAttachments(attachments) {
10555
+ async function persistAttachmentList(attachments) {
10555
10556
  const nonVoice = attachments.filter((a) => !a.isVoice);
10556
10557
  if (nonVoice.length === 0) {
10557
- return { documents: [], images: [] };
10558
+ return [];
10558
10559
  }
10559
10560
  mkdirSync(UPLOADS_DIR, { recursive: true });
10560
10561
  const claimed = /* @__PURE__ */ new Set();
@@ -10605,8 +10606,13 @@ async function persistAttachments(attachments) {
10605
10606
  };
10606
10607
  })
10607
10608
  );
10608
- const settled = results.map((r, i) => ({
10609
- result: r.status === "fulfilled" ? r.value : null,
10609
+ return results.map((r) => r.status === "fulfilled" ? r.value : null);
10610
+ }
10611
+ async function persistAttachments(attachments) {
10612
+ const nonVoice = attachments.filter((a) => !a.isVoice);
10613
+ const results = await persistAttachmentList(nonVoice);
10614
+ const settled = results.map((result, i) => ({
10615
+ result,
10610
10616
  isImage: isImageAttachment(nonVoice[i])
10611
10617
  }));
10612
10618
  return {
@@ -11352,7 +11358,7 @@ var init_headless = __esm({
11352
11358
  //////////////////////////////////////////////////////////////////////////////
11353
11359
  // External tool bridge
11354
11360
  //////////////////////////////////////////////////////////////////////////////
11355
- resolveExternalTool = (id, name, _input) => {
11361
+ awaitExternalToolResult = (id, name) => {
11356
11362
  const early = this.earlyResults.get(id);
11357
11363
  if (early !== void 0) {
11358
11364
  this.earlyResults.delete(id);
@@ -11376,6 +11382,13 @@ var init_headless = __esm({
11376
11382
  });
11377
11383
  });
11378
11384
  };
11385
+ // promptUser answers carry private-upload descriptors for `file` questions;
11386
+ // persist them to disk and hand the model local paths (see
11387
+ // persistPromptUserUploads). Every other external tool passes through.
11388
+ resolveExternalTool = (id, name, input) => {
11389
+ const result = this.awaitExternalToolResult(id, name);
11390
+ return name === "promptUser" ? result.then((raw) => this.persistPromptUserUploads(input, raw)) : result;
11391
+ };
11379
11392
  //////////////////////////////////////////////////////////////////////////////
11380
11393
  // AgentEvent → wire protocol translation
11381
11394
  //////////////////////////////////////////////////////////////////////////////
@@ -11581,6 +11594,87 @@ var init_headless = __esm({
11581
11594
  return void 0;
11582
11595
  }
11583
11596
  }
11597
+ /**
11598
+ * promptUser `file` answers arrive as private-upload descriptors
11599
+ * ({url, key, filename, extractedTextUrl}) — the same shape as a chat
11600
+ * attachment. Download them to src/.user-uploads/ and rewrite each answer to
11601
+ * the local path(s) BEFORE the result settles, so the model, the tool block,
11602
+ * the saved session and the frontend transcript all see paths and never a
11603
+ * signed url that dies in an hour. Non-file answers, dismissals and anything
11604
+ * unparseable pass through untouched.
11605
+ *
11606
+ * Runs while the turn is blocked on this tool, so it can't overlap the
11607
+ * turn's own persistEntryAttachments; the ASAP/queue persist paths only run
11608
+ * after the tool batch settles. One prompt is open at a time, so two results
11609
+ * can't race either.
11610
+ */
11611
+ async persistPromptUserUploads(input, raw) {
11612
+ let answers;
11613
+ try {
11614
+ answers = JSON.parse(raw);
11615
+ } catch {
11616
+ return raw;
11617
+ }
11618
+ if (!answers || typeof answers !== "object" || Array.isArray(answers) || answers._dismissed) {
11619
+ return raw;
11620
+ }
11621
+ const isDescriptor = (d) => !!d && typeof d === "object" && typeof d.url === "string";
11622
+ const questions = Array.isArray(input?.questions) ? input.questions : [];
11623
+ const batch = [];
11624
+ const slots = [];
11625
+ for (const q of questions) {
11626
+ if (q?.type !== "file" || typeof q.id !== "string") {
11627
+ continue;
11628
+ }
11629
+ const val = answers[q.id];
11630
+ const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
11631
+ if (items.length === 0) {
11632
+ continue;
11633
+ }
11634
+ slots.push({
11635
+ id: q.id,
11636
+ isArray: Array.isArray(val),
11637
+ count: items.length
11638
+ });
11639
+ for (const d of items) {
11640
+ batch.push({
11641
+ url: d.url,
11642
+ key: d.key,
11643
+ filename: d.filename,
11644
+ extractedTextUrl: d.extractedTextUrl ?? void 0
11645
+ });
11646
+ }
11647
+ }
11648
+ if (batch.length === 0) {
11649
+ return raw;
11650
+ }
11651
+ let results;
11652
+ try {
11653
+ results = await persistAttachmentList(batch);
11654
+ } catch (err) {
11655
+ log17.warn("promptUser upload persistence failed", {
11656
+ error: err.message
11657
+ });
11658
+ results = batch.map(() => null);
11659
+ }
11660
+ let cursor = 0;
11661
+ for (const slot of slots) {
11662
+ const paths = results.slice(cursor, cursor + slot.count).map((r, i) => {
11663
+ if (r) {
11664
+ return r.localPath;
11665
+ }
11666
+ const att = batch[cursor + i];
11667
+ log17.warn("promptUser upload not persisted; falling back to url", {
11668
+ filename: att.filename
11669
+ });
11670
+ return att.url;
11671
+ });
11672
+ cursor += slot.count;
11673
+ answers[slot.id] = slot.isArray ? paths : paths[0];
11674
+ }
11675
+ log17.info("promptUser uploads persisted", { count: batch.length });
11676
+ return JSON.stringify(answers);
11677
+ }
11584
11678
  /**
11585
11679
  * Stable id for a queued chain step, so the frontend can address one (to
11586
11680
  * discard a paused pipeline) rather than waiting for drainQueueLoop to
@@ -216,6 +216,8 @@ const rows = await db.sql<{ questionId: string; n: number }>(
216
216
 
217
217
  `SELECT`/`WITH` only — writes throw; use Table methods for writes. Positional `?` bind params. Lazy and batchable via `db.batch()`. Raw rows come back close to how SQLite stores them and may not exactly match the typed API's representations.
218
218
 
219
+ Every read, typed or raw, returns at most 100,000 rows or 64MB of JSON; past either limit the query fails with `result_too_large`. Paginate, select fewer columns, or use an aggregate instead.
220
+
219
221
  ### Updating Records
220
222
 
221
223
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.315",
3
+ "version": "0.1.317",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",