@mindstudio-ai/remy 0.1.185 → 0.1.187

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
@@ -2902,6 +2902,82 @@ function acquireBrowserLock() {
2902
2902
  return wait.then(() => release);
2903
2903
  }
2904
2904
 
2905
+ // src/toolRegistry.ts
2906
+ var log5 = createLogger("tool-registry");
2907
+ var USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
2908
+ var ToolRegistry = class {
2909
+ entries = /* @__PURE__ */ new Map();
2910
+ onEvent;
2911
+ register(entry) {
2912
+ this.entries.set(entry.id, entry);
2913
+ }
2914
+ unregister(id) {
2915
+ this.entries.delete(id);
2916
+ }
2917
+ get(id) {
2918
+ return this.entries.get(id);
2919
+ }
2920
+ /**
2921
+ * Stop a running tool.
2922
+ *
2923
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
2924
+ * - hard: abort and settle with a generic error
2925
+ *
2926
+ * Returns true if the tool was found and stopped.
2927
+ */
2928
+ stop(id, mode) {
2929
+ const entry = this.entries.get(id);
2930
+ if (!entry) {
2931
+ return false;
2932
+ }
2933
+ log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
2934
+ entry.abortController.abort(mode);
2935
+ if (mode === "graceful") {
2936
+ const partial = entry.getPartialResult?.() ?? "";
2937
+ const result = partial ? `[INTERRUPTED]
2938
+
2939
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
2940
+ entry.settle(result, false);
2941
+ } else {
2942
+ entry.settle(USER_CANCELLED_RESULT, true);
2943
+ }
2944
+ this.onEvent?.({
2945
+ type: "tool_stopped",
2946
+ id: entry.id,
2947
+ name: entry.name,
2948
+ mode,
2949
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
2950
+ });
2951
+ this.entries.delete(id);
2952
+ return true;
2953
+ }
2954
+ /**
2955
+ * Restart a running tool with the same or patched input.
2956
+ * The original controllable promise stays pending and settles
2957
+ * when the new execution finishes.
2958
+ *
2959
+ * Returns true if the tool was found and restarted.
2960
+ */
2961
+ restart(id, patchedInput) {
2962
+ const entry = this.entries.get(id);
2963
+ if (!entry) {
2964
+ return false;
2965
+ }
2966
+ log5.info("Tool restarted", { toolCallId: id, name: entry.name });
2967
+ entry.abortController.abort("restart");
2968
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
2969
+ this.onEvent?.({
2970
+ type: "tool_restarted",
2971
+ id: entry.id,
2972
+ name: entry.name,
2973
+ input: newInput,
2974
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
2975
+ });
2976
+ entry.rerun(newInput);
2977
+ return true;
2978
+ }
2979
+ };
2980
+
2905
2981
  // src/statusWatcher.ts
2906
2982
  function startStatusWatcher(config) {
2907
2983
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
@@ -3147,7 +3223,7 @@ ${content}` : attachmentHeader;
3147
3223
  }
3148
3224
 
3149
3225
  // src/subagents/runner.ts
3150
- var log5 = createLogger("sub-agent");
3226
+ var log6 = createLogger("sub-agent");
3151
3227
  async function runSubAgent(config) {
3152
3228
  const {
3153
3229
  system,
@@ -3174,7 +3250,7 @@ async function runSubAgent(config) {
3174
3250
  const signal = background ? bgAbort.signal : parentSignal;
3175
3251
  const agentName = subAgentId || "sub-agent";
3176
3252
  const runStart = Date.now();
3177
- log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3253
+ log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3178
3254
  const emit = (e) => {
3179
3255
  onEvent({ ...e, parentToolId });
3180
3256
  };
@@ -3209,7 +3285,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3209
3285
  messages: thisInvocation()
3210
3286
  };
3211
3287
  }
3212
- return { text: "Error: cancelled", messages: thisInvocation() };
3288
+ return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3213
3289
  }
3214
3290
  let lastToolResult = "";
3215
3291
  while (true) {
@@ -3391,7 +3467,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3391
3467
  ...hasArtifacts ? { artifacts } : {}
3392
3468
  };
3393
3469
  }
3394
- log5.info("Tools executing", {
3470
+ log6.info("Tools executing", {
3395
3471
  requestId,
3396
3472
  parentToolId,
3397
3473
  count: toolCalls.length,
@@ -3401,7 +3477,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3401
3477
  const results = await Promise.all(
3402
3478
  toolCalls.map(async (tc) => {
3403
3479
  if (signal?.aborted) {
3404
- return { id: tc.id, result: "Error: cancelled", isError: true };
3480
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
3405
3481
  }
3406
3482
  let settle;
3407
3483
  const resultPromise = new Promise((res) => {
@@ -3468,7 +3544,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3468
3544
  run2(tc.input);
3469
3545
  const r = await resultPromise;
3470
3546
  toolRegistry?.unregister(tc.id);
3471
- log5.info("Tool completed", {
3547
+ log6.info("Tool completed", {
3472
3548
  requestId,
3473
3549
  parentToolId,
3474
3550
  toolCallId: tc.id,
@@ -3519,7 +3595,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3519
3595
  const wrapRun = async () => {
3520
3596
  try {
3521
3597
  const result = await run();
3522
- log5.info("Sub-agent complete", {
3598
+ log6.info("Sub-agent complete", {
3523
3599
  requestId,
3524
3600
  parentToolId,
3525
3601
  agentName,
@@ -3528,7 +3604,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3528
3604
  });
3529
3605
  return result;
3530
3606
  } catch (err) {
3531
- log5.warn("Sub-agent error", {
3607
+ log6.warn("Sub-agent error", {
3532
3608
  requestId,
3533
3609
  parentToolId,
3534
3610
  agentName,
@@ -3540,7 +3616,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3540
3616
  if (!background) {
3541
3617
  return wrapRun();
3542
3618
  }
3543
- log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3619
+ log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3544
3620
  toolRegistry?.register({
3545
3621
  id: parentToolId,
3546
3622
  name: agentName,
@@ -3814,7 +3890,7 @@ function resolveModel(surfaceId, models, fallback) {
3814
3890
  }
3815
3891
 
3816
3892
  // src/subagents/browserAutomation/index.ts
3817
- var log6 = createLogger("browser-automation");
3893
+ var log7 = createLogger("browser-automation");
3818
3894
  async function runBrowserAutomation(task, context) {
3819
3895
  const release = await acquireBrowserLock();
3820
3896
  try {
@@ -3906,7 +3982,7 @@ async function runBrowserAutomation(task, context) {
3906
3982
  }
3907
3983
  }
3908
3984
  } catch {
3909
- log6.debug("Failed to parse batch analysis result", {
3985
+ log7.debug("Failed to parse batch analysis result", {
3910
3986
  batchResult
3911
3987
  });
3912
3988
  }
@@ -5573,7 +5649,7 @@ function executeTool(name, input, context) {
5573
5649
  }
5574
5650
 
5575
5651
  // src/compaction/trigger.ts
5576
- var log7 = createLogger("compaction:trigger");
5652
+ var log8 = createLogger("compaction:trigger");
5577
5653
  var pendingSummaries = [];
5578
5654
  var inflightCompaction = null;
5579
5655
  function getPendingSummaries() {
@@ -5600,11 +5676,11 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5600
5676
  ).then((summaries) => {
5601
5677
  pendingSummaries.push(...summaries);
5602
5678
  listener?.({ type: "complete", requestId });
5603
- log7.info("Compaction complete");
5679
+ log8.info("Compaction complete");
5604
5680
  }).catch((err) => {
5605
5681
  const message = err.message || "Compaction failed";
5606
5682
  listener?.({ type: "complete", error: message, requestId });
5607
- log7.error("Compaction failed", { error: message });
5683
+ log8.error("Compaction failed", { error: message });
5608
5684
  throw err;
5609
5685
  }).finally(() => {
5610
5686
  inflightCompaction = null;
@@ -5616,7 +5692,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5616
5692
  import fs20 from "fs";
5617
5693
  import path10 from "path";
5618
5694
  import { createHash } from "crypto";
5619
- var log8 = createLogger("brandExtraction");
5695
+ var log9 = createLogger("brandExtraction");
5620
5696
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
5621
5697
  var BRAND_FILE = ".remy-brand.json";
5622
5698
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -5624,17 +5700,17 @@ async function runExtraction(apiConfig, model) {
5624
5700
  const inputHash = computeInputHash();
5625
5701
  const cached2 = readCache();
5626
5702
  if (cached2 && cached2.inputHash === inputHash) {
5627
- log8.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
5703
+ log9.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
5628
5704
  return null;
5629
5705
  }
5630
- log8.info("Extracting brand", { inputHash });
5706
+ log9.info("Extracting brand", { inputHash });
5631
5707
  const brand = await extractBrand(apiConfig, model);
5632
5708
  if (!brand) {
5633
- log8.warn("Brand extraction failed \u2014 leaving cache untouched");
5709
+ log9.warn("Brand extraction failed \u2014 leaving cache untouched");
5634
5710
  return null;
5635
5711
  }
5636
5712
  persistBrand(brand, inputHash);
5637
- log8.info("Brand persisted", { inputHash });
5713
+ log9.info("Brand persisted", { inputHash });
5638
5714
  return brand;
5639
5715
  }
5640
5716
  function computeInputHash() {
@@ -5700,7 +5776,7 @@ function parseFrontmatter3(filePath) {
5700
5776
  async function extractBrand(apiConfig, model) {
5701
5777
  const corpus = buildCorpus();
5702
5778
  if (!corpus.trim()) {
5703
- log8.debug("No spec corpus \u2014 emitting empty brand");
5779
+ log9.debug("No spec corpus \u2014 emitting empty brand");
5704
5780
  return { version: 1 };
5705
5781
  }
5706
5782
  let responseText = "";
@@ -5731,17 +5807,17 @@ async function extractBrand(apiConfig, model) {
5731
5807
  toolNames: []
5732
5808
  });
5733
5809
  } else if (event.type === "error") {
5734
- log8.error("Brand extraction stream error", { error: event.error });
5810
+ log9.error("Brand extraction stream error", { error: event.error });
5735
5811
  return null;
5736
5812
  }
5737
5813
  }
5738
5814
  } catch (err) {
5739
- log8.error("Brand extraction threw", { error: err?.message });
5815
+ log9.error("Brand extraction threw", { error: err?.message });
5740
5816
  return null;
5741
5817
  }
5742
5818
  const parsed = parseJsonResponse(responseText);
5743
5819
  if (!parsed) {
5744
- log8.warn("Brand extraction returned unparseable JSON", {
5820
+ log9.warn("Brand extraction returned unparseable JSON", {
5745
5821
  preview: responseText.slice(0, 200)
5746
5822
  });
5747
5823
  return null;
@@ -5883,7 +5959,7 @@ function readCache() {
5883
5959
  }
5884
5960
 
5885
5961
  // src/brandExtraction/trigger.ts
5886
- var log9 = createLogger("brandExtraction:trigger");
5962
+ var log10 = createLogger("brandExtraction:trigger");
5887
5963
  var inflight = false;
5888
5964
  var dirty = false;
5889
5965
  function triggerBrandExtraction(apiConfig, model) {
@@ -5893,7 +5969,7 @@ function triggerBrandExtraction(apiConfig, model) {
5893
5969
  }
5894
5970
  inflight = true;
5895
5971
  void runExtraction(apiConfig, model).catch((err) => {
5896
- log9.error("Brand extraction failed", { error: err?.message });
5972
+ log10.error("Brand extraction failed", { error: err?.message });
5897
5973
  }).finally(() => {
5898
5974
  inflight = false;
5899
5975
  if (dirty) {
@@ -5906,7 +5982,7 @@ function triggerBrandExtraction(apiConfig, model) {
5906
5982
  // src/session.ts
5907
5983
  import fs21 from "fs";
5908
5984
  import path11 from "path";
5909
- var log10 = createLogger("session");
5985
+ var log11 = createLogger("session");
5910
5986
  var SESSION_FILE = ".remy-session.json";
5911
5987
  var ARCHIVE_DIR = ".logs/sessions";
5912
5988
  function loadSession(state) {
@@ -5918,7 +5994,7 @@ function loadSession(state) {
5918
5994
  }
5919
5995
  if (Array.isArray(data.messages) && data.messages.length > 0) {
5920
5996
  state.messages = sanitizeMessages(data.messages);
5921
- log10.info("Session loaded", {
5997
+ log11.info("Session loaded", {
5922
5998
  messageCount: state.messages.length,
5923
5999
  ...state.models && { models: state.models }
5924
6000
  });
@@ -5971,9 +6047,9 @@ function saveSession(state) {
5971
6047
  payload.models = state.models;
5972
6048
  }
5973
6049
  fs21.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
5974
- log10.info("Session saved", { messageCount: state.messages.length });
6050
+ log11.info("Session saved", { messageCount: state.messages.length });
5975
6051
  } catch (err) {
5976
- log10.warn("Session save failed", { error: err.message });
6052
+ log11.warn("Session save failed", { error: err.message });
5977
6053
  }
5978
6054
  }
5979
6055
  function clearSession(state) {
@@ -5984,10 +6060,10 @@ function clearSession(state) {
5984
6060
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5985
6061
  const dest = path11.join(ARCHIVE_DIR, `cleared-${ts}.json`);
5986
6062
  fs21.renameSync(SESSION_FILE, dest);
5987
- log10.info("Session archived on clear", { dest });
6063
+ log11.info("Session archived on clear", { dest });
5988
6064
  }
5989
6065
  } catch (err) {
5990
- log10.warn("Session archive on clear failed, deleting instead", {
6066
+ log11.warn("Session archive on clear failed, deleting instead", {
5991
6067
  error: err.message
5992
6068
  });
5993
6069
  try {
@@ -6191,7 +6267,7 @@ function friendlyError(raw) {
6191
6267
  }
6192
6268
 
6193
6269
  // src/agent.ts
6194
- var log11 = createLogger("agent");
6270
+ var log12 = createLogger("agent");
6195
6271
  var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
6196
6272
  function getTextContent(blocks) {
6197
6273
  return blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
@@ -6240,7 +6316,7 @@ async function runTurn(params) {
6240
6316
  } = params;
6241
6317
  const tools2 = getToolDefinitions(onboardingState);
6242
6318
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
6243
- log11.info("Turn started", {
6319
+ log12.info("Turn started", {
6244
6320
  requestId,
6245
6321
  model,
6246
6322
  toolCount: tools2.length,
@@ -6493,7 +6569,7 @@ async function runTurn(params) {
6493
6569
  const tool = getToolByName(event.name);
6494
6570
  const wasStreamed = acc?.started ?? false;
6495
6571
  const isInputStreaming = !!tool?.streaming?.partialInput;
6496
- log11.info("Tool received", {
6572
+ log12.info("Tool received", {
6497
6573
  requestId,
6498
6574
  toolCallId: event.id,
6499
6575
  name: event.name
@@ -6607,7 +6683,7 @@ async function runTurn(params) {
6607
6683
  });
6608
6684
  return;
6609
6685
  }
6610
- log11.info("Tools executing", {
6686
+ log12.info("Tools executing", {
6611
6687
  requestId,
6612
6688
  count: toolCalls.length,
6613
6689
  tools: toolCalls.map((tc) => tc.name)
@@ -6627,7 +6703,7 @@ async function runTurn(params) {
6627
6703
  const results = await Promise.all(
6628
6704
  toolCalls.map(async (tc) => {
6629
6705
  if (signal?.aborted) {
6630
- return { id: tc.id, result: "Error: cancelled", isError: true };
6706
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
6631
6707
  }
6632
6708
  const toolStart = Date.now();
6633
6709
  let settle;
@@ -6646,7 +6722,7 @@ async function runTurn(params) {
6646
6722
  };
6647
6723
  const cascadeAbort = () => {
6648
6724
  toolAbort.abort();
6649
- safeSettle("Error: cancelled", true);
6725
+ safeSettle(USER_CANCELLED_RESULT, true);
6650
6726
  };
6651
6727
  signal?.addEventListener("abort", cascadeAbort, { once: true });
6652
6728
  const run = async (input) => {
@@ -6654,7 +6730,7 @@ async function runTurn(params) {
6654
6730
  let result;
6655
6731
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
6656
6732
  saveSession(state);
6657
- log11.info("Waiting for external tool result", {
6733
+ log12.info("Waiting for external tool result", {
6658
6734
  requestId,
6659
6735
  toolCallId: tc.id,
6660
6736
  name: tc.name
@@ -6722,7 +6798,7 @@ async function runTurn(params) {
6722
6798
  if (!tc.input.background) {
6723
6799
  toolRegistry?.unregister(tc.id);
6724
6800
  }
6725
- log11.info("Tool completed", {
6801
+ log12.info("Tool completed", {
6726
6802
  requestId,
6727
6803
  toolCallId: tc.id,
6728
6804
  name: tc.name,
@@ -6782,81 +6858,6 @@ async function runTurn(params) {
6782
6858
  }
6783
6859
  }
6784
6860
 
6785
- // src/toolRegistry.ts
6786
- var log12 = createLogger("tool-registry");
6787
- var ToolRegistry = class {
6788
- entries = /* @__PURE__ */ new Map();
6789
- onEvent;
6790
- register(entry) {
6791
- this.entries.set(entry.id, entry);
6792
- }
6793
- unregister(id) {
6794
- this.entries.delete(id);
6795
- }
6796
- get(id) {
6797
- return this.entries.get(id);
6798
- }
6799
- /**
6800
- * Stop a running tool.
6801
- *
6802
- * - graceful: abort and settle with [INTERRUPTED] + partial result
6803
- * - hard: abort and settle with a generic error
6804
- *
6805
- * Returns true if the tool was found and stopped.
6806
- */
6807
- stop(id, mode) {
6808
- const entry = this.entries.get(id);
6809
- if (!entry) {
6810
- return false;
6811
- }
6812
- log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
6813
- entry.abortController.abort(mode);
6814
- if (mode === "graceful") {
6815
- const partial = entry.getPartialResult?.() ?? "";
6816
- const result = partial ? `[INTERRUPTED]
6817
-
6818
- ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6819
- entry.settle(result, false);
6820
- } else {
6821
- entry.settle("Error: tool was cancelled", true);
6822
- }
6823
- this.onEvent?.({
6824
- type: "tool_stopped",
6825
- id: entry.id,
6826
- name: entry.name,
6827
- mode,
6828
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
6829
- });
6830
- this.entries.delete(id);
6831
- return true;
6832
- }
6833
- /**
6834
- * Restart a running tool with the same or patched input.
6835
- * The original controllable promise stays pending and settles
6836
- * when the new execution finishes.
6837
- *
6838
- * Returns true if the tool was found and restarted.
6839
- */
6840
- restart(id, patchedInput) {
6841
- const entry = this.entries.get(id);
6842
- if (!entry) {
6843
- return false;
6844
- }
6845
- log12.info("Tool restarted", { toolCallId: id, name: entry.name });
6846
- entry.abortController.abort("restart");
6847
- const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
6848
- this.onEvent?.({
6849
- type: "tool_restarted",
6850
- id: entry.id,
6851
- name: entry.name,
6852
- input: newInput,
6853
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
6854
- });
6855
- entry.rerun(newInput);
6856
- return true;
6857
- }
6858
- };
6859
-
6860
6861
  // src/headless/attachments.ts
6861
6862
  import { mkdirSync, existsSync } from "fs";
6862
6863
  import { writeFile } from "fs/promises";
@@ -7805,7 +7806,7 @@ var HeadlessSession = class {
7805
7806
  }
7806
7807
  for (const [id, pending] of this.pendingTools) {
7807
7808
  clearTimeout(pending.timeout);
7808
- pending.resolve("Error: cancelled");
7809
+ pending.resolve(USER_CANCELLED_RESULT);
7809
7810
  this.pendingTools.delete(id);
7810
7811
  }
7811
7812
  return this.queue.drain();
package/dist/index.js CHANGED
@@ -3320,6 +3320,89 @@ var init_browserLock = __esm({
3320
3320
  }
3321
3321
  });
3322
3322
 
3323
+ // src/toolRegistry.ts
3324
+ var log5, USER_CANCELLED_RESULT, ToolRegistry;
3325
+ var init_toolRegistry = __esm({
3326
+ "src/toolRegistry.ts"() {
3327
+ "use strict";
3328
+ init_logger();
3329
+ log5 = createLogger("tool-registry");
3330
+ USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
3331
+ ToolRegistry = class {
3332
+ entries = /* @__PURE__ */ new Map();
3333
+ onEvent;
3334
+ register(entry) {
3335
+ this.entries.set(entry.id, entry);
3336
+ }
3337
+ unregister(id) {
3338
+ this.entries.delete(id);
3339
+ }
3340
+ get(id) {
3341
+ return this.entries.get(id);
3342
+ }
3343
+ /**
3344
+ * Stop a running tool.
3345
+ *
3346
+ * - graceful: abort and settle with [INTERRUPTED] + partial result
3347
+ * - hard: abort and settle with a generic error
3348
+ *
3349
+ * Returns true if the tool was found and stopped.
3350
+ */
3351
+ stop(id, mode) {
3352
+ const entry = this.entries.get(id);
3353
+ if (!entry) {
3354
+ return false;
3355
+ }
3356
+ log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
3357
+ entry.abortController.abort(mode);
3358
+ if (mode === "graceful") {
3359
+ const partial = entry.getPartialResult?.() ?? "";
3360
+ const result = partial ? `[INTERRUPTED]
3361
+
3362
+ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3363
+ entry.settle(result, false);
3364
+ } else {
3365
+ entry.settle(USER_CANCELLED_RESULT, true);
3366
+ }
3367
+ this.onEvent?.({
3368
+ type: "tool_stopped",
3369
+ id: entry.id,
3370
+ name: entry.name,
3371
+ mode,
3372
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
3373
+ });
3374
+ this.entries.delete(id);
3375
+ return true;
3376
+ }
3377
+ /**
3378
+ * Restart a running tool with the same or patched input.
3379
+ * The original controllable promise stays pending and settles
3380
+ * when the new execution finishes.
3381
+ *
3382
+ * Returns true if the tool was found and restarted.
3383
+ */
3384
+ restart(id, patchedInput) {
3385
+ const entry = this.entries.get(id);
3386
+ if (!entry) {
3387
+ return false;
3388
+ }
3389
+ log5.info("Tool restarted", { toolCallId: id, name: entry.name });
3390
+ entry.abortController.abort("restart");
3391
+ const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
3392
+ this.onEvent?.({
3393
+ type: "tool_restarted",
3394
+ id: entry.id,
3395
+ name: entry.name,
3396
+ input: newInput,
3397
+ ...entry.parentToolId && { parentToolId: entry.parentToolId }
3398
+ });
3399
+ entry.rerun(newInput);
3400
+ return true;
3401
+ }
3402
+ };
3403
+ }
3404
+ });
3405
+
3323
3406
  // src/statusWatcher.ts
3324
3407
  function startStatusWatcher(config) {
3325
3408
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
@@ -3607,7 +3690,7 @@ async function runSubAgent(config) {
3607
3690
  const signal = background ? bgAbort.signal : parentSignal;
3608
3691
  const agentName = subAgentId || "sub-agent";
3609
3692
  const runStart = Date.now();
3610
- log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3693
+ log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3611
3694
  const emit = (e) => {
3612
3695
  onEvent({ ...e, parentToolId });
3613
3696
  };
@@ -3642,7 +3725,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3642
3725
  messages: thisInvocation()
3643
3726
  };
3644
3727
  }
3645
- return { text: "Error: cancelled", messages: thisInvocation() };
3728
+ return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3646
3729
  }
3647
3730
  let lastToolResult = "";
3648
3731
  while (true) {
@@ -3824,7 +3907,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3824
3907
  ...hasArtifacts ? { artifacts } : {}
3825
3908
  };
3826
3909
  }
3827
- log5.info("Tools executing", {
3910
+ log6.info("Tools executing", {
3828
3911
  requestId,
3829
3912
  parentToolId,
3830
3913
  count: toolCalls.length,
@@ -3834,7 +3917,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3834
3917
  const results = await Promise.all(
3835
3918
  toolCalls.map(async (tc) => {
3836
3919
  if (signal?.aborted) {
3837
- return { id: tc.id, result: "Error: cancelled", isError: true };
3920
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
3838
3921
  }
3839
3922
  let settle;
3840
3923
  const resultPromise = new Promise((res) => {
@@ -3901,7 +3984,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3901
3984
  run2(tc.input);
3902
3985
  const r = await resultPromise;
3903
3986
  toolRegistry?.unregister(tc.id);
3904
- log5.info("Tool completed", {
3987
+ log6.info("Tool completed", {
3905
3988
  requestId,
3906
3989
  parentToolId,
3907
3990
  toolCallId: tc.id,
@@ -3952,7 +4035,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3952
4035
  const wrapRun = async () => {
3953
4036
  try {
3954
4037
  const result = await run();
3955
- log5.info("Sub-agent complete", {
4038
+ log6.info("Sub-agent complete", {
3956
4039
  requestId,
3957
4040
  parentToolId,
3958
4041
  agentName,
@@ -3961,7 +4044,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3961
4044
  });
3962
4045
  return result;
3963
4046
  } catch (err) {
3964
- log5.warn("Sub-agent error", {
4047
+ log6.warn("Sub-agent error", {
3965
4048
  requestId,
3966
4049
  parentToolId,
3967
4050
  agentName,
@@ -3973,7 +4056,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3973
4056
  if (!background) {
3974
4057
  return wrapRun();
3975
4058
  }
3976
- log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
4059
+ log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3977
4060
  toolRegistry?.register({
3978
4061
  id: parentToolId,
3979
4062
  name: agentName,
@@ -4000,16 +4083,17 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4000
4083
  });
4001
4084
  return { text: ack, messages: [], backgrounded: true };
4002
4085
  }
4003
- var log5;
4086
+ var log6;
4004
4087
  var init_runner = __esm({
4005
4088
  "src/subagents/runner.ts"() {
4006
4089
  "use strict";
4007
4090
  init_api();
4008
4091
  init_logger();
4009
4092
  init_usageLedger();
4093
+ init_toolRegistry();
4010
4094
  init_statusWatcher();
4011
4095
  init_cleanMessages();
4012
- log5 = createLogger("sub-agent");
4096
+ log6 = createLogger("sub-agent");
4013
4097
  }
4014
4098
  });
4015
4099
 
@@ -4269,7 +4353,7 @@ async function runBrowserAutomation(task, context) {
4269
4353
  }
4270
4354
  }
4271
4355
  } catch {
4272
- log6.debug("Failed to parse batch analysis result", {
4356
+ log7.debug("Failed to parse batch analysis result", {
4273
4357
  batchResult
4274
4358
  });
4275
4359
  }
@@ -4293,7 +4377,7 @@ async function runBrowserAutomation(task, context) {
4293
4377
  release();
4294
4378
  }
4295
4379
  }
4296
- var log6, browserAutomationTool;
4380
+ var log7, browserAutomationTool;
4297
4381
  var init_browserAutomation = __esm({
4298
4382
  "src/subagents/browserAutomation/index.ts"() {
4299
4383
  "use strict";
@@ -4306,7 +4390,7 @@ var init_browserAutomation = __esm({
4306
4390
  init_runMindstudioCli();
4307
4391
  init_surfaces();
4308
4392
  init_logger();
4309
- log6 = createLogger("browser-automation");
4393
+ log7 = createLogger("browser-automation");
4310
4394
  browserAutomationTool = {
4311
4395
  clearable: true,
4312
4396
  definition: {
@@ -6237,7 +6321,7 @@ function loadSession(state) {
6237
6321
  }
6238
6322
  if (Array.isArray(data.messages) && data.messages.length > 0) {
6239
6323
  state.messages = sanitizeMessages(data.messages);
6240
- log7.info("Session loaded", {
6324
+ log8.info("Session loaded", {
6241
6325
  messageCount: state.messages.length,
6242
6326
  ...state.models && { models: state.models }
6243
6327
  });
@@ -6290,9 +6374,9 @@ function saveSession(state) {
6290
6374
  payload.models = state.models;
6291
6375
  }
6292
6376
  fs19.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
6293
- log7.info("Session saved", { messageCount: state.messages.length });
6377
+ log8.info("Session saved", { messageCount: state.messages.length });
6294
6378
  } catch (err) {
6295
- log7.warn("Session save failed", { error: err.message });
6379
+ log8.warn("Session save failed", { error: err.message });
6296
6380
  }
6297
6381
  }
6298
6382
  function clearSession(state) {
@@ -6303,10 +6387,10 @@ function clearSession(state) {
6303
6387
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6304
6388
  const dest = path9.join(ARCHIVE_DIR, `cleared-${ts}.json`);
6305
6389
  fs19.renameSync(SESSION_FILE, dest);
6306
- log7.info("Session archived on clear", { dest });
6390
+ log8.info("Session archived on clear", { dest });
6307
6391
  }
6308
6392
  } catch (err) {
6309
- log7.warn("Session archive on clear failed, deleting instead", {
6393
+ log8.warn("Session archive on clear failed, deleting instead", {
6310
6394
  error: err.message
6311
6395
  });
6312
6396
  try {
@@ -6315,12 +6399,12 @@ function clearSession(state) {
6315
6399
  }
6316
6400
  }
6317
6401
  }
6318
- var log7, SESSION_FILE, ARCHIVE_DIR;
6402
+ var log8, SESSION_FILE, ARCHIVE_DIR;
6319
6403
  var init_session = __esm({
6320
6404
  "src/session.ts"() {
6321
6405
  "use strict";
6322
6406
  init_logger();
6323
- log7 = createLogger("session");
6407
+ log8 = createLogger("session");
6324
6408
  SESSION_FILE = ".remy-session.json";
6325
6409
  ARCHIVE_DIR = ".logs/sessions";
6326
6410
  }
@@ -6539,17 +6623,17 @@ async function runExtraction(apiConfig, model) {
6539
6623
  const inputHash = computeInputHash();
6540
6624
  const cached2 = readCache();
6541
6625
  if (cached2 && cached2.inputHash === inputHash) {
6542
- log8.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6626
+ log9.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6543
6627
  return null;
6544
6628
  }
6545
- log8.info("Extracting brand", { inputHash });
6629
+ log9.info("Extracting brand", { inputHash });
6546
6630
  const brand = await extractBrand(apiConfig, model);
6547
6631
  if (!brand) {
6548
- log8.warn("Brand extraction failed \u2014 leaving cache untouched");
6632
+ log9.warn("Brand extraction failed \u2014 leaving cache untouched");
6549
6633
  return null;
6550
6634
  }
6551
6635
  persistBrand(brand, inputHash);
6552
- log8.info("Brand persisted", { inputHash });
6636
+ log9.info("Brand persisted", { inputHash });
6553
6637
  return brand;
6554
6638
  }
6555
6639
  function computeInputHash() {
@@ -6615,7 +6699,7 @@ function parseFrontmatter3(filePath) {
6615
6699
  async function extractBrand(apiConfig, model) {
6616
6700
  const corpus = buildCorpus();
6617
6701
  if (!corpus.trim()) {
6618
- log8.debug("No spec corpus \u2014 emitting empty brand");
6702
+ log9.debug("No spec corpus \u2014 emitting empty brand");
6619
6703
  return { version: 1 };
6620
6704
  }
6621
6705
  let responseText = "";
@@ -6646,17 +6730,17 @@ async function extractBrand(apiConfig, model) {
6646
6730
  toolNames: []
6647
6731
  });
6648
6732
  } else if (event.type === "error") {
6649
- log8.error("Brand extraction stream error", { error: event.error });
6733
+ log9.error("Brand extraction stream error", { error: event.error });
6650
6734
  return null;
6651
6735
  }
6652
6736
  }
6653
6737
  } catch (err) {
6654
- log8.error("Brand extraction threw", { error: err?.message });
6738
+ log9.error("Brand extraction threw", { error: err?.message });
6655
6739
  return null;
6656
6740
  }
6657
6741
  const parsed = parseJsonResponse(responseText);
6658
6742
  if (!parsed) {
6659
- log8.warn("Brand extraction returned unparseable JSON", {
6743
+ log9.warn("Brand extraction returned unparseable JSON", {
6660
6744
  preview: responseText.slice(0, 200)
6661
6745
  });
6662
6746
  return null;
@@ -6796,7 +6880,7 @@ function readCache() {
6796
6880
  return null;
6797
6881
  }
6798
6882
  }
6799
- var log8, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE;
6883
+ var log9, EXTRACT_PROMPT, BRAND_FILE, CACHE_FILE;
6800
6884
  var init_brandExtraction = __esm({
6801
6885
  "src/brandExtraction/index.ts"() {
6802
6886
  "use strict";
@@ -6804,7 +6888,7 @@ var init_brandExtraction = __esm({
6804
6888
  init_assets();
6805
6889
  init_logger();
6806
6890
  init_usageLedger();
6807
- log8 = createLogger("brandExtraction");
6891
+ log9 = createLogger("brandExtraction");
6808
6892
  EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6809
6893
  BRAND_FILE = ".remy-brand.json";
6810
6894
  CACHE_FILE = ".remy-brand.cache.json";
@@ -6819,7 +6903,7 @@ function triggerBrandExtraction(apiConfig, model) {
6819
6903
  }
6820
6904
  inflight = true;
6821
6905
  void runExtraction(apiConfig, model).catch((err) => {
6822
- log9.error("Brand extraction failed", { error: err?.message });
6906
+ log10.error("Brand extraction failed", { error: err?.message });
6823
6907
  }).finally(() => {
6824
6908
  inflight = false;
6825
6909
  if (dirty) {
@@ -6828,13 +6912,13 @@ function triggerBrandExtraction(apiConfig, model) {
6828
6912
  }
6829
6913
  });
6830
6914
  }
6831
- var log9, inflight, dirty;
6915
+ var log10, inflight, dirty;
6832
6916
  var init_trigger2 = __esm({
6833
6917
  "src/brandExtraction/trigger.ts"() {
6834
6918
  "use strict";
6835
6919
  init_brandExtraction();
6836
6920
  init_logger();
6837
- log9 = createLogger("brandExtraction:trigger");
6921
+ log10 = createLogger("brandExtraction:trigger");
6838
6922
  inflight = false;
6839
6923
  dirty = false;
6840
6924
  }
@@ -6872,7 +6956,7 @@ async function runTurn(params) {
6872
6956
  } = params;
6873
6957
  const tools2 = getToolDefinitions(onboardingState);
6874
6958
  const excludeToolsFromClearing = tools2.filter((t) => !CLEARABLE_TOOLS.has(t.name)).map((t) => t.name);
6875
- log10.info("Turn started", {
6959
+ log11.info("Turn started", {
6876
6960
  requestId,
6877
6961
  model,
6878
6962
  toolCount: tools2.length,
@@ -7125,7 +7209,7 @@ async function runTurn(params) {
7125
7209
  const tool = getToolByName(event.name);
7126
7210
  const wasStreamed = acc?.started ?? false;
7127
7211
  const isInputStreaming = !!tool?.streaming?.partialInput;
7128
- log10.info("Tool received", {
7212
+ log11.info("Tool received", {
7129
7213
  requestId,
7130
7214
  toolCallId: event.id,
7131
7215
  name: event.name
@@ -7239,7 +7323,7 @@ async function runTurn(params) {
7239
7323
  });
7240
7324
  return;
7241
7325
  }
7242
- log10.info("Tools executing", {
7326
+ log11.info("Tools executing", {
7243
7327
  requestId,
7244
7328
  count: toolCalls.length,
7245
7329
  tools: toolCalls.map((tc) => tc.name)
@@ -7259,7 +7343,7 @@ async function runTurn(params) {
7259
7343
  const results = await Promise.all(
7260
7344
  toolCalls.map(async (tc) => {
7261
7345
  if (signal?.aborted) {
7262
- return { id: tc.id, result: "Error: cancelled", isError: true };
7346
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
7263
7347
  }
7264
7348
  const toolStart = Date.now();
7265
7349
  let settle;
@@ -7278,7 +7362,7 @@ async function runTurn(params) {
7278
7362
  };
7279
7363
  const cascadeAbort = () => {
7280
7364
  toolAbort.abort();
7281
- safeSettle("Error: cancelled", true);
7365
+ safeSettle(USER_CANCELLED_RESULT, true);
7282
7366
  };
7283
7367
  signal?.addEventListener("abort", cascadeAbort, { once: true });
7284
7368
  const run = async (input) => {
@@ -7286,7 +7370,7 @@ async function runTurn(params) {
7286
7370
  let result;
7287
7371
  if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
7288
7372
  saveSession(state);
7289
- log10.info("Waiting for external tool result", {
7373
+ log11.info("Waiting for external tool result", {
7290
7374
  requestId,
7291
7375
  toolCallId: tc.id,
7292
7376
  name: tc.name
@@ -7354,7 +7438,7 @@ async function runTurn(params) {
7354
7438
  if (!tc.input.background) {
7355
7439
  toolRegistry?.unregister(tc.id);
7356
7440
  }
7357
- log10.info("Tool completed", {
7441
+ log11.info("Tool completed", {
7358
7442
  requestId,
7359
7443
  toolCallId: tc.id,
7360
7444
  name: tc.name,
@@ -7413,7 +7497,7 @@ async function runTurn(params) {
7413
7497
  }
7414
7498
  }
7415
7499
  }
7416
- var log10, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
7500
+ var log11, BRAND_TRIGGERING_TOOLS, EXTERNAL_TOOLS, USER_BLOCKING_EXTERNAL_TOOLS;
7417
7501
  var init_agent = __esm({
7418
7502
  "src/agent.ts"() {
7419
7503
  "use strict";
@@ -7430,7 +7514,8 @@ var init_agent = __esm({
7430
7514
  init_sentinel();
7431
7515
  init_trigger2();
7432
7516
  init_surfaces();
7433
- log10 = createLogger("agent");
7517
+ init_toolRegistry();
7518
+ log11 = createLogger("agent");
7434
7519
  BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set(["writeSpec", "editSpec"]);
7435
7520
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
7436
7521
  "promptUser",
@@ -7458,10 +7543,10 @@ import os from "os";
7458
7543
  function loadConfigFile() {
7459
7544
  try {
7460
7545
  const raw = fs21.readFileSync(CONFIG_PATH, "utf-8");
7461
- log11.debug("Loaded config file", { path: CONFIG_PATH });
7546
+ log12.debug("Loaded config file", { path: CONFIG_PATH });
7462
7547
  return JSON.parse(raw);
7463
7548
  } catch (err) {
7464
- log11.debug("No config file found", {
7549
+ log12.debug("No config file found", {
7465
7550
  path: CONFIG_PATH,
7466
7551
  error: err.message
7467
7552
  });
@@ -7476,13 +7561,13 @@ function resolveConfig(flags2) {
7476
7561
  const baseUrl2 = flags2?.baseUrl || process.env.MINDSTUDIO_BASE_URL || env?.apiBaseUrl || DEFAULT_BASE_URL;
7477
7562
  const appId = process.env.MINDSTUDIO_APP_ID || void 0;
7478
7563
  if (!apiKey) {
7479
- log11.error("No API key found");
7564
+ log12.error("No API key found");
7480
7565
  throw new Error(
7481
7566
  "No API key found. Set MINDSTUDIO_API_KEY or configure ~/.mindstudio-local-tunnel/config.json."
7482
7567
  );
7483
7568
  }
7484
7569
  const keySource = flags2?.apiKey ? "cli flag" : process.env.MINDSTUDIO_API_KEY ? "env var" : "config file";
7485
- log11.info("Config resolved", {
7570
+ log12.info("Config resolved", {
7486
7571
  baseUrl: baseUrl2,
7487
7572
  keySource,
7488
7573
  environment: activeEnv,
@@ -7490,12 +7575,12 @@ function resolveConfig(flags2) {
7490
7575
  });
7491
7576
  return { apiKey, baseUrl: baseUrl2, appId };
7492
7577
  }
7493
- var log11, CONFIG_PATH, DEFAULT_BASE_URL;
7578
+ var log12, CONFIG_PATH, DEFAULT_BASE_URL;
7494
7579
  var init_config = __esm({
7495
7580
  "src/config.ts"() {
7496
7581
  "use strict";
7497
7582
  init_logger();
7498
- log11 = createLogger("config");
7583
+ log12 = createLogger("config");
7499
7584
  CONFIG_PATH = path11.join(
7500
7585
  os.homedir(),
7501
7586
  ".mindstudio-local-tunnel",
@@ -7505,88 +7590,6 @@ var init_config = __esm({
7505
7590
  }
7506
7591
  });
7507
7592
 
7508
- // src/toolRegistry.ts
7509
- var log12, ToolRegistry;
7510
- var init_toolRegistry = __esm({
7511
- "src/toolRegistry.ts"() {
7512
- "use strict";
7513
- init_logger();
7514
- log12 = createLogger("tool-registry");
7515
- ToolRegistry = class {
7516
- entries = /* @__PURE__ */ new Map();
7517
- onEvent;
7518
- register(entry) {
7519
- this.entries.set(entry.id, entry);
7520
- }
7521
- unregister(id) {
7522
- this.entries.delete(id);
7523
- }
7524
- get(id) {
7525
- return this.entries.get(id);
7526
- }
7527
- /**
7528
- * Stop a running tool.
7529
- *
7530
- * - graceful: abort and settle with [INTERRUPTED] + partial result
7531
- * - hard: abort and settle with a generic error
7532
- *
7533
- * Returns true if the tool was found and stopped.
7534
- */
7535
- stop(id, mode) {
7536
- const entry = this.entries.get(id);
7537
- if (!entry) {
7538
- return false;
7539
- }
7540
- log12.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
7541
- entry.abortController.abort(mode);
7542
- if (mode === "graceful") {
7543
- const partial = entry.getPartialResult?.() ?? "";
7544
- const result = partial ? `[INTERRUPTED]
7545
-
7546
- ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
7547
- entry.settle(result, false);
7548
- } else {
7549
- entry.settle("Error: tool was cancelled", true);
7550
- }
7551
- this.onEvent?.({
7552
- type: "tool_stopped",
7553
- id: entry.id,
7554
- name: entry.name,
7555
- mode,
7556
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
7557
- });
7558
- this.entries.delete(id);
7559
- return true;
7560
- }
7561
- /**
7562
- * Restart a running tool with the same or patched input.
7563
- * The original controllable promise stays pending and settles
7564
- * when the new execution finishes.
7565
- *
7566
- * Returns true if the tool was found and restarted.
7567
- */
7568
- restart(id, patchedInput) {
7569
- const entry = this.entries.get(id);
7570
- if (!entry) {
7571
- return false;
7572
- }
7573
- log12.info("Tool restarted", { toolCallId: id, name: entry.name });
7574
- entry.abortController.abort("restart");
7575
- const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
7576
- this.onEvent?.({
7577
- type: "tool_restarted",
7578
- id: entry.id,
7579
- name: entry.name,
7580
- input: newInput,
7581
- ...entry.parentToolId && { parentToolId: entry.parentToolId }
7582
- });
7583
- entry.rerun(newInput);
7584
- return true;
7585
- }
7586
- };
7587
- }
7588
- });
7589
-
7590
7593
  // src/headless/attachments.ts
7591
7594
  import { mkdirSync, existsSync } from "fs";
7592
7595
  import { writeFile } from "fs/promises";
@@ -8594,7 +8597,7 @@ var init_headless = __esm({
8594
8597
  }
8595
8598
  for (const [id, pending] of this.pendingTools) {
8596
8599
  clearTimeout(pending.timeout);
8597
- pending.resolve("Error: cancelled");
8600
+ pending.resolve(USER_CANCELLED_RESULT);
8598
8601
  this.pendingTools.delete(id);
8599
8602
  }
8600
8603
  return this.queue.drain();
@@ -344,7 +344,19 @@ Accepts any HTTP method. The method receives `{ method, headers, query, body }`
344
344
 
345
345
  ## Email
346
346
 
347
- Inbound email triggers. An app can register one inbound address that routes all inbound emails to one method.
347
+ Inbound email triggers. Each app has one email-handler method; the platform routes all inbound mail destined for the app — across any of its address tiers — to that method.
348
+
349
+ ### Address tiers
350
+
351
+ Three tiers, all delivered to the same handler method. The new tiers are catchall (no localpart registration); the legacy tier is specific-localpart and frozen for new apps.
352
+
353
+ | Tier | Address | How it's set up |
354
+ |---|---|---|
355
+ | Platform subdomain (default) | `*@<custom_subdomain>.madewithremy.com` | Automatic the moment the app has a `custom_subdomain` set. Every address on that subdomain delivers to the handler. |
356
+ | Custom domain | `*@<their-domain>` | The user adds a domain in the dashboard's email-domains settings and points one MX record at `mx.msagent.ai`. Not something the agent provisions. |
357
+ | Legacy `mindstudio-hooks.com` | `<name>@mindstudio-hooks.com` | Existing apps only — frozen for new apps. Don't recommend it; treat as read-only history. |
358
+
359
+ Because the new tiers are catchall, `to` carries an arbitrary localpart. Methods that need to branch on it should read `input.to` (e.g. `if (input.to.startsWith('support@')) ...`).
348
360
 
349
361
  ### Config (`interface.json`)
350
362
 
@@ -357,27 +369,27 @@ Inbound email triggers. An app can register one inbound address that routes all
357
369
  }
358
370
  ```
359
371
 
360
- `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com` wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before the method runs.
361
-
362
- Address pattern: `{custom-name}@mindstudio-hooks.com`.
372
+ `approvedSenders` is optional. When set, only senders matching an exact address or `*@domain.com` wildcard reach the method; everything else is rejected by the platform with `400 invalid_sender` before the method runs (silently — the sender isn't bounced). Matching is case-insensitive. The same list applies uniformly across all three address tiers.
363
373
 
364
374
  ### Input shape
365
375
 
366
376
  ```ts
367
377
  {
368
- to: string; // resolved from the SMTP envelope
378
+ to: string; // full recipient address; localpart is arbitrary on catchall tiers
369
379
  from: string; // bare address, extracted from "Name <a@b>" form
370
380
  subject: string; // 'No Subject' if missing
371
- message: string; // plain text body; 'No Body' if neither text nor html was sent
381
+ message: string; // plain text body, falls back to HTML if text is missing; 'No Body' if neither was sent
372
382
  html: string; // HTML body, or '' when text-only
373
383
  attachments: string[]; // CDN URLs — already uploaded by the platform
374
384
  }
375
385
  ```
376
386
 
377
- ### Attachments
387
+ ### Attachments and size limits
378
388
 
379
389
  `attachments[]` is an array of CDN URLs — the platform has already received and uploaded the files. Fetch them server-side via the URL when you need the bytes; pass them through as URLs to UI or downstream services.
380
390
 
391
+ Max inbound message size is 25 MB total (including all attachments). Oversized messages are rejected by the platform before the method runs.
392
+
381
393
  ### Auth
382
394
 
383
395
  Methods invoked through this interface run with `auth.roles: ['system']` (see the system-roles section above). They have no user session and can't impersonate. Use `auth.requireRole('system')` to gate methods that should only be reachable via email.
@@ -78,6 +78,6 @@ You have access to the `mindstudio` CLI, which exposes every SDK action as a com
78
78
  ### Production App Management
79
79
  You have access to `mindstudio-prod`, a CLI for managing the user's production MindStudio app. Use it via your bash tool. All output is JSON. Run `mindstudio-prod --help` or `mindstudio-prod <command> --help` to discover usage and available options.
80
80
 
81
- Available commands: `requests` (logs, error rates, latency), `releases` (deploy status, history), `domains` (custom subdomains and fully custom domains), `users` (list, set roles), `db` (query production sql db), `methods` (list, invoke), `secrets` (list, get, set, delete).
81
+ Available commands: `requests` (server-side request logs, error rates, latency), `crashes` (frontend browser errors — grouped issues + drill-down to individual events), `analytics` (traffic, top pages/referrers/geo, AI-referral attribution, live counters), `releases` (deploy status, history), `domains` (custom subdomains and fully custom domains), `users` (list, set roles), `db` (query production sql db), `data` (live db operations like lift-from-dev), `methods` (list, invoke), `secrets` (list, get, set, delete).
82
82
 
83
- Use when the user asks about production behavior (errors, logs, metrics), wants to manage their live app (domains, users, roles), needs to seed or query production data, or wants to check release status.
83
+ Use when the user asks about production behavior (server errors via `requests`, browser crashes via `crashes`, traffic/engagement via `analytics`), wants to manage their live app (domains, users, roles), needs to seed or query production data, or wants to check release status.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.185",
3
+ "version": "0.1.187",
4
4
  "description": "MindStudio coding agent",
5
5
  "repository": {
6
6
  "type": "git",