@mindstudio-ai/remy 0.1.316 → 0.1.318

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
@@ -525,6 +525,8 @@ var TEXT_MODELS = {
525
525
  "grok-4.6": { forceCompactAt: 4e5 },
526
526
  // 500K window
527
527
  "glm-5.2": { forceCompactAt: 85e4 },
528
+ "glm-5.3": { forceCompactAt: 85e4 },
529
+ "glm-5.3-flash": { forceCompactAt: 85e4 },
528
530
  "muse-spark-1.1": { forceCompactAt: 85e4 },
529
531
  "muse-spark-1.2": { forceCompactAt: 85e4 },
530
532
  "muse-spark-1.3": { forceCompactAt: 85e4 },
@@ -1590,7 +1592,7 @@ var promptUserTool = {
1590
1592
  type: {
1591
1593
  type: "string",
1592
1594
  enum: ["select", "checklist", "text", "file"],
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, returns CDN URL(s) that can be referenced directly or curled onto disk.'
1595
+ 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.'
1594
1596
  },
1595
1597
  helpText: {
1596
1598
  type: "string",
@@ -1625,7 +1627,7 @@ var promptUserTool = {
1625
1627
  },
1626
1628
  multiple: {
1627
1629
  type: "boolean",
1628
- description: "For file type: allow multiple uploads (returns array of URLs). Defaults to false."
1630
+ description: "For file type: allow multiple uploads (the answer is an array of local paths). Defaults to false."
1629
1631
  },
1630
1632
  format: {
1631
1633
  type: "string",
@@ -9601,10 +9603,10 @@ function isImageAttachment(att) {
9601
9603
  const name = att.filename || filenameFromUrl(att.url);
9602
9604
  return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
9603
9605
  }
9604
- async function persistAttachments(attachments) {
9606
+ async function persistAttachmentList(attachments) {
9605
9607
  const nonVoice = attachments.filter((a) => !a.isVoice);
9606
9608
  if (nonVoice.length === 0) {
9607
- return { documents: [], images: [] };
9609
+ return [];
9608
9610
  }
9609
9611
  mkdirSync(UPLOADS_DIR, { recursive: true });
9610
9612
  const claimed = /* @__PURE__ */ new Set();
@@ -9655,8 +9657,13 @@ async function persistAttachments(attachments) {
9655
9657
  };
9656
9658
  })
9657
9659
  );
9658
- const settled = results.map((r, i) => ({
9659
- result: r.status === "fulfilled" ? r.value : null,
9660
+ return results.map((r) => r.status === "fulfilled" ? r.value : null);
9661
+ }
9662
+ async function persistAttachments(attachments) {
9663
+ const nonVoice = attachments.filter((a) => !a.isVoice);
9664
+ const results = await persistAttachmentList(nonVoice);
9665
+ const settled = results.map((result, i) => ({
9666
+ result,
9660
9667
  isImage: isImageAttachment(nonVoice[i])
9661
9668
  }));
9662
9669
  return {
@@ -10345,7 +10352,7 @@ var HeadlessSession = class {
10345
10352
  //////////////////////////////////////////////////////////////////////////////
10346
10353
  // External tool bridge
10347
10354
  //////////////////////////////////////////////////////////////////////////////
10348
- resolveExternalTool = (id, name, _input) => {
10355
+ awaitExternalToolResult = (id, name) => {
10349
10356
  const early = this.earlyResults.get(id);
10350
10357
  if (early !== void 0) {
10351
10358
  this.earlyResults.delete(id);
@@ -10369,6 +10376,13 @@ var HeadlessSession = class {
10369
10376
  });
10370
10377
  });
10371
10378
  };
10379
+ // promptUser answers carry private-upload descriptors for `file` questions;
10380
+ // persist them to disk and hand the model local paths (see
10381
+ // persistPromptUserUploads). Every other external tool passes through.
10382
+ resolveExternalTool = (id, name, input) => {
10383
+ const result = this.awaitExternalToolResult(id, name);
10384
+ return name === "promptUser" ? result.then((raw) => this.persistPromptUserUploads(input, raw)) : result;
10385
+ };
10372
10386
  //////////////////////////////////////////////////////////////////////////////
10373
10387
  // AgentEvent → wire protocol translation
10374
10388
  //////////////////////////////////////////////////////////////////////////////
@@ -10574,6 +10588,87 @@ var HeadlessSession = class {
10574
10588
  return void 0;
10575
10589
  }
10576
10590
  }
10591
+ /**
10592
+ * promptUser `file` answers arrive as private-upload descriptors
10593
+ * ({url, key, filename, extractedTextUrl}) — the same shape as a chat
10594
+ * attachment. Download them to src/.user-uploads/ and rewrite each answer to
10595
+ * the local path(s) BEFORE the result settles, so the model, the tool block,
10596
+ * the saved session and the frontend transcript all see paths and never a
10597
+ * signed url that dies in an hour. Non-file answers, dismissals and anything
10598
+ * unparseable pass through untouched.
10599
+ *
10600
+ * Runs while the turn is blocked on this tool, so it can't overlap the
10601
+ * turn's own persistEntryAttachments; the ASAP/queue persist paths only run
10602
+ * after the tool batch settles. One prompt is open at a time, so two results
10603
+ * can't race either.
10604
+ */
10605
+ async persistPromptUserUploads(input, raw) {
10606
+ let answers;
10607
+ try {
10608
+ answers = JSON.parse(raw);
10609
+ } catch {
10610
+ return raw;
10611
+ }
10612
+ if (!answers || typeof answers !== "object" || Array.isArray(answers) || answers._dismissed) {
10613
+ return raw;
10614
+ }
10615
+ const isDescriptor = (d) => !!d && typeof d === "object" && typeof d.url === "string";
10616
+ const questions = Array.isArray(input?.questions) ? input.questions : [];
10617
+ const batch = [];
10618
+ const slots = [];
10619
+ for (const q of questions) {
10620
+ if (q?.type !== "file" || typeof q.id !== "string") {
10621
+ continue;
10622
+ }
10623
+ const val = answers[q.id];
10624
+ const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
10625
+ if (items.length === 0) {
10626
+ continue;
10627
+ }
10628
+ slots.push({
10629
+ id: q.id,
10630
+ isArray: Array.isArray(val),
10631
+ count: items.length
10632
+ });
10633
+ for (const d of items) {
10634
+ batch.push({
10635
+ url: d.url,
10636
+ key: d.key,
10637
+ filename: d.filename,
10638
+ extractedTextUrl: d.extractedTextUrl ?? void 0
10639
+ });
10640
+ }
10641
+ }
10642
+ if (batch.length === 0) {
10643
+ return raw;
10644
+ }
10645
+ let results;
10646
+ try {
10647
+ results = await persistAttachmentList(batch);
10648
+ } catch (err) {
10649
+ log17.warn("promptUser upload persistence failed", {
10650
+ error: err.message
10651
+ });
10652
+ results = batch.map(() => null);
10653
+ }
10654
+ let cursor = 0;
10655
+ for (const slot of slots) {
10656
+ const paths = results.slice(cursor, cursor + slot.count).map((r, i) => {
10657
+ if (r) {
10658
+ return r.localPath;
10659
+ }
10660
+ const att = batch[cursor + i];
10661
+ log17.warn("promptUser upload not persisted; falling back to url", {
10662
+ filename: att.filename
10663
+ });
10664
+ return att.url;
10665
+ });
10666
+ cursor += slot.count;
10667
+ answers[slot.id] = slot.isArray ? paths : paths[0];
10668
+ }
10669
+ log17.info("promptUser uploads persisted", { count: batch.length });
10670
+ return JSON.stringify(answers);
10671
+ }
10577
10672
  /**
10578
10673
  * Stable id for a queued chain step, so the frontend can address one (to
10579
10674
  * 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",
@@ -2142,6 +2142,8 @@ var init_surfaces = __esm({
2142
2142
  "grok-4.6": { forceCompactAt: 4e5 },
2143
2143
  // 500K window
2144
2144
  "glm-5.2": { forceCompactAt: 85e4 },
2145
+ "glm-5.3": { forceCompactAt: 85e4 },
2146
+ "glm-5.3-flash": { forceCompactAt: 85e4 },
2145
2147
  "muse-spark-1.1": { forceCompactAt: 85e4 },
2146
2148
  "muse-spark-1.2": { forceCompactAt: 85e4 },
2147
2149
  "muse-spark-1.3": { forceCompactAt: 85e4 },
@@ -10552,10 +10554,10 @@ function isImageAttachment(att) {
10552
10554
  const name = att.filename || filenameFromUrl(att.url);
10553
10555
  return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
10554
10556
  }
10555
- async function persistAttachments(attachments) {
10557
+ async function persistAttachmentList(attachments) {
10556
10558
  const nonVoice = attachments.filter((a) => !a.isVoice);
10557
10559
  if (nonVoice.length === 0) {
10558
- return { documents: [], images: [] };
10560
+ return [];
10559
10561
  }
10560
10562
  mkdirSync(UPLOADS_DIR, { recursive: true });
10561
10563
  const claimed = /* @__PURE__ */ new Set();
@@ -10606,8 +10608,13 @@ async function persistAttachments(attachments) {
10606
10608
  };
10607
10609
  })
10608
10610
  );
10609
- const settled = results.map((r, i) => ({
10610
- result: r.status === "fulfilled" ? r.value : null,
10611
+ return results.map((r) => r.status === "fulfilled" ? r.value : null);
10612
+ }
10613
+ async function persistAttachments(attachments) {
10614
+ const nonVoice = attachments.filter((a) => !a.isVoice);
10615
+ const results = await persistAttachmentList(nonVoice);
10616
+ const settled = results.map((result, i) => ({
10617
+ result,
10611
10618
  isImage: isImageAttachment(nonVoice[i])
10612
10619
  }));
10613
10620
  return {
@@ -11353,7 +11360,7 @@ var init_headless = __esm({
11353
11360
  //////////////////////////////////////////////////////////////////////////////
11354
11361
  // External tool bridge
11355
11362
  //////////////////////////////////////////////////////////////////////////////
11356
- resolveExternalTool = (id, name, _input) => {
11363
+ awaitExternalToolResult = (id, name) => {
11357
11364
  const early = this.earlyResults.get(id);
11358
11365
  if (early !== void 0) {
11359
11366
  this.earlyResults.delete(id);
@@ -11377,6 +11384,13 @@ var init_headless = __esm({
11377
11384
  });
11378
11385
  });
11379
11386
  };
11387
+ // promptUser answers carry private-upload descriptors for `file` questions;
11388
+ // persist them to disk and hand the model local paths (see
11389
+ // persistPromptUserUploads). Every other external tool passes through.
11390
+ resolveExternalTool = (id, name, input) => {
11391
+ const result = this.awaitExternalToolResult(id, name);
11392
+ return name === "promptUser" ? result.then((raw) => this.persistPromptUserUploads(input, raw)) : result;
11393
+ };
11380
11394
  //////////////////////////////////////////////////////////////////////////////
11381
11395
  // AgentEvent → wire protocol translation
11382
11396
  //////////////////////////////////////////////////////////////////////////////
@@ -11582,6 +11596,87 @@ var init_headless = __esm({
11582
11596
  return void 0;
11583
11597
  }
11584
11598
  }
11599
+ /**
11600
+ * promptUser `file` answers arrive as private-upload descriptors
11601
+ * ({url, key, filename, extractedTextUrl}) — the same shape as a chat
11602
+ * attachment. Download them to src/.user-uploads/ and rewrite each answer to
11603
+ * the local path(s) BEFORE the result settles, so the model, the tool block,
11604
+ * the saved session and the frontend transcript all see paths and never a
11605
+ * signed url that dies in an hour. Non-file answers, dismissals and anything
11606
+ * unparseable pass through untouched.
11607
+ *
11608
+ * Runs while the turn is blocked on this tool, so it can't overlap the
11609
+ * turn's own persistEntryAttachments; the ASAP/queue persist paths only run
11610
+ * after the tool batch settles. One prompt is open at a time, so two results
11611
+ * can't race either.
11612
+ */
11613
+ async persistPromptUserUploads(input, raw) {
11614
+ let answers;
11615
+ try {
11616
+ answers = JSON.parse(raw);
11617
+ } catch {
11618
+ return raw;
11619
+ }
11620
+ if (!answers || typeof answers !== "object" || Array.isArray(answers) || answers._dismissed) {
11621
+ return raw;
11622
+ }
11623
+ const isDescriptor = (d) => !!d && typeof d === "object" && typeof d.url === "string";
11624
+ const questions = Array.isArray(input?.questions) ? input.questions : [];
11625
+ const batch = [];
11626
+ const slots = [];
11627
+ for (const q of questions) {
11628
+ if (q?.type !== "file" || typeof q.id !== "string") {
11629
+ continue;
11630
+ }
11631
+ const val = answers[q.id];
11632
+ const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
11633
+ if (items.length === 0) {
11634
+ continue;
11635
+ }
11636
+ slots.push({
11637
+ id: q.id,
11638
+ isArray: Array.isArray(val),
11639
+ count: items.length
11640
+ });
11641
+ for (const d of items) {
11642
+ batch.push({
11643
+ url: d.url,
11644
+ key: d.key,
11645
+ filename: d.filename,
11646
+ extractedTextUrl: d.extractedTextUrl ?? void 0
11647
+ });
11648
+ }
11649
+ }
11650
+ if (batch.length === 0) {
11651
+ return raw;
11652
+ }
11653
+ let results;
11654
+ try {
11655
+ results = await persistAttachmentList(batch);
11656
+ } catch (err) {
11657
+ log17.warn("promptUser upload persistence failed", {
11658
+ error: err.message
11659
+ });
11660
+ results = batch.map(() => null);
11661
+ }
11662
+ let cursor = 0;
11663
+ for (const slot of slots) {
11664
+ const paths = results.slice(cursor, cursor + slot.count).map((r, i) => {
11665
+ if (r) {
11666
+ return r.localPath;
11667
+ }
11668
+ const att = batch[cursor + i];
11669
+ log17.warn("promptUser upload not persisted; falling back to url", {
11670
+ filename: att.filename
11671
+ });
11672
+ return att.url;
11673
+ });
11674
+ cursor += slot.count;
11675
+ answers[slot.id] = slot.isArray ? paths : paths[0];
11676
+ }
11677
+ log17.info("promptUser uploads persisted", { count: batch.length });
11678
+ return JSON.stringify(answers);
11679
+ }
11585
11680
  /**
11586
11681
  * Stable id for a queued chain step, so the frontend can address one (to
11587
11682
  * 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.316",
3
+ "version": "0.1.318",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",