@mindstudio-ai/remy 0.1.323 → 0.1.325

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,3 @@
1
+ This step was already started once and was interrupted before it finished. Work may have continued in the conversation since — including work that covers part or all of it.
2
+
3
+ Take stock of the current state of the project before you act, and pick up from there rather than starting over. If everything below has already been done, say so briefly and end the turn.
@@ -270,6 +270,19 @@ declare class HeadlessSession {
270
270
  * pipeline where it stopped. Holding only the remainder would resume one step
271
271
  * PAST the interruption, polishing and finalizing half-built code.
272
272
  *
273
+ * That re-queued step is marked `resumed`, which is what stops it being a
274
+ * verbatim replay. The message the user sends to un-pause a build is usually
275
+ * the rest of that build ("try again, I fixed the adapter") and Remy does the
276
+ * work inside that turn — so re-delivering the original "build everything
277
+ * now" afterwards had it rebuild an app it had just finished.
278
+ *
279
+ * `reason: 'shutdown'` is the environment taking the agent down rather than a
280
+ * person pressing Stop. It pauses identically — nothing may start a turn
281
+ * while the workspace is being tarred — but tags the chain steps it holds so
282
+ * the next boot releases them and the pipeline carries on by itself. Called
283
+ * from the stdin `cancel` (the sandbox's pre-destroy quiesce) and from our own
284
+ * SIGTERM handler, whichever gets there first.
285
+ *
273
286
  * A compaction is cancelled here too, unconditionally. It gates every queued
274
287
  * message and outlives the turn that started it, so leaving it running means
275
288
  * Stop can't reach idle. The cost is the summary work in flight; the forced
@@ -285,9 +298,10 @@ declare class HeadlessSession {
285
298
  * Remove pending queued messages: all user messages (no id), or a single item
286
299
  * by id. Does not affect the in-flight turn (use `cancel` for that).
287
300
  *
288
- * Held chain items — a paused pipeline — are removable only by explicit id.
289
- * The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
290
- * neither of which should get to decide the pipeline's fate. A DELIVERABLE
301
+ * Held chain items — a paused pipeline — are removable only by explicit id:
302
+ * the queue card's Discard sends one call per step, and its per-step X sends
303
+ * one. The id-less form is the card's "Clear", which is about the user's own
304
+ * messages and shouldn't get to decide the pipeline's fate. A DELIVERABLE
291
305
  * chain item is never removable: that's live pipeline work. (The step
292
306
  * actually running isn't in the queue at all — drainQueueLoop takes it out
293
307
  * before running it.)
package/dist/headless.js CHANGED
@@ -981,6 +981,34 @@ function parseSentinel(text) {
981
981
  }
982
982
  return { name: match[1], remainder: match[2] };
983
983
  }
984
+ function sentinelParams(text) {
985
+ const parsed = parseSentinel(text);
986
+ if (!parsed) {
987
+ return {};
988
+ }
989
+ const firstLine = parsed.remainder.split("\n")[0].trim();
990
+ if (!firstLine) {
991
+ return {};
992
+ }
993
+ try {
994
+ const value = JSON.parse(firstLine);
995
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
996
+ } catch {
997
+ return {};
998
+ }
999
+ }
1000
+ function setSentinelParams(text, patch) {
1001
+ const parsed = parseSentinel(text);
1002
+ if (!parsed) {
1003
+ return text;
1004
+ }
1005
+ const newlineAt = parsed.remainder.indexOf("\n");
1006
+ const body = newlineAt === -1 ? "" : parsed.remainder.slice(newlineAt + 1);
1007
+ const params = { ...sentinelParams(text), ...patch };
1008
+ const line = `${sentinel(parsed.name)}${JSON.stringify(params)}`;
1009
+ return body ? `${line}
1010
+ ${body}` : line;
1011
+ }
984
1012
  function stripSentinelLine(text) {
985
1013
  return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
986
1014
  }
@@ -3386,6 +3414,10 @@ function acquireBrowserLock() {
3386
3414
  // src/toolRegistry.ts
3387
3415
  var log6 = createLogger("tool-registry");
3388
3416
  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.";
3417
+ var ENV_INTERRUPTED_RESULT = "[INTERRUPTED] The environment shut down while this tool was running. No user action was involved \u2014 pick up from here.";
3418
+ function cancelledToolResult(signal) {
3419
+ return signal?.reason === "shutdown" ? ENV_INTERRUPTED_RESULT : USER_CANCELLED_RESULT;
3420
+ }
3389
3421
  var ToolRegistry = class {
3390
3422
  entries = /* @__PURE__ */ new Map();
3391
3423
  onEvent;
@@ -3460,6 +3492,20 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3460
3492
  };
3461
3493
 
3462
3494
  // src/recording.ts
3495
+ function collectRecordings(messages) {
3496
+ const recordings = [];
3497
+ for (const msg of messages) {
3498
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
3499
+ continue;
3500
+ }
3501
+ for (const block of msg.content) {
3502
+ if (block.type === "tool" && block.recording) {
3503
+ recordings.push(block.recording);
3504
+ }
3505
+ }
3506
+ }
3507
+ return recordings;
3508
+ }
3463
3509
  function liftRecording(result) {
3464
3510
  if (!result.includes('"recording"')) {
3465
3511
  return { result };
@@ -3515,6 +3561,18 @@ function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
3515
3561
  }
3516
3562
  var MAX_SUBAGENT_RESULT_BYTES = 32 * 1024;
3517
3563
  var MAX_SUBAGENT_TRANSCRIPT_BYTES = 512 * 1024;
3564
+ function attachSubAgentTranscript(block, messages) {
3565
+ if (!block.recordings) {
3566
+ const recordings = collectRecordings(messages);
3567
+ if (recordings.length > 0) {
3568
+ block.recordings = recordings;
3569
+ }
3570
+ }
3571
+ block.subAgentMessages = capSubAgentTranscript(messages);
3572
+ }
3573
+ function dropToolResultMessages(messages) {
3574
+ return messages.filter((m) => !(m.role === "user" && m.toolCallId));
3575
+ }
3518
3576
  function capSubAgentTranscript(messages) {
3519
3577
  for (const msg of messages) {
3520
3578
  capMessageForHistory(msg, MAX_SUBAGENT_RESULT_BYTES);
@@ -3559,7 +3617,7 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
3559
3617
  );
3560
3618
  }
3561
3619
  if (Array.isArray(block.subAgentMessages)) {
3562
- block.subAgentMessages = capSubAgentTranscript(block.subAgentMessages);
3620
+ attachSubAgentTranscript(block, block.subAgentMessages);
3563
3621
  }
3564
3622
  }
3565
3623
  } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
@@ -3904,7 +3962,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3904
3962
  messages: thisInvocation()
3905
3963
  };
3906
3964
  }
3907
- return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3965
+ return { text: cancelledToolResult(signal), messages: thisInvocation() };
3908
3966
  }
3909
3967
  let lastToolResult = "";
3910
3968
  let watchedBlocks = [];
@@ -4140,7 +4198,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4140
4198
  if (signal?.aborted) {
4141
4199
  return {
4142
4200
  id: tc.id,
4143
- result: USER_CANCELLED_RESULT,
4201
+ result: cancelledToolResult(signal),
4144
4202
  isError: true
4145
4203
  };
4146
4204
  }
@@ -4257,7 +4315,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4257
4315
  }
4258
4316
  const innerMsgs = subAgentMessages.get(r.id);
4259
4317
  if (innerMsgs) {
4260
- block.subAgentMessages = capSubAgentTranscript(innerMsgs);
4318
+ attachSubAgentTranscript(block, innerMsgs);
4261
4319
  }
4262
4320
  if (captureArtifacts?.includes(block.name) && !r.isError) {
4263
4321
  try {
@@ -4799,13 +4857,12 @@ async function runBrowserAutomation(task, context, opts) {
4799
4857
  },
4800
4858
  toolRegistry: context.toolRegistry
4801
4859
  });
4802
- context.subAgentMessages?.set(context.toolCallId, result.messages);
4803
- const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4804
- const recorded = result.messages.some(
4805
- (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
4806
- (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
4807
- )
4860
+ context.subAgentMessages?.set(
4861
+ context.toolCallId,
4862
+ dropToolResultMessages(result.messages)
4808
4863
  );
4864
+ const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4865
+ const recorded = collectRecordings(result.messages).length > 0;
4809
4866
  return {
4810
4867
  text: result.text,
4811
4868
  recorded,
@@ -4960,6 +5017,79 @@ var screenshotTool = {
4960
5017
  execute: (input, context) => executeScreenshot(input, context?.onLog, context)
4961
5018
  };
4962
5019
 
5020
+ // src/tools/common/scrapeWebUrl.ts
5021
+ var CONTENT_BUDGET_BYTES = MAX_TOOL_RESULT_BYTES - 4 * 1024;
5022
+ var TRUNCATED_SUFFIX = "\n\n[output truncated]";
5023
+ async function fetchWebPage(url, opts) {
5024
+ const raw = await runMindstudioCli(
5025
+ [
5026
+ "scrape-url",
5027
+ "--url",
5028
+ url,
5029
+ "--page-options",
5030
+ JSON.stringify({ onlyMainContent: true, screenshot: opts.screenshot })
5031
+ ],
5032
+ {
5033
+ onLog: opts.onLog,
5034
+ maxBuffer: SCRAPE_MAX_BUFFER,
5035
+ caller: opts.caller
5036
+ }
5037
+ );
5038
+ return shapeFetchResult(url, raw);
5039
+ }
5040
+ function shapeFetchResult(url, raw) {
5041
+ const body = raw.endsWith(TRUNCATED_SUFFIX) ? raw.slice(0, -TRUNCATED_SUFFIX.length) : raw;
5042
+ let envelope;
5043
+ try {
5044
+ envelope = JSON.parse(body);
5045
+ } catch {
5046
+ return raw;
5047
+ }
5048
+ if (!envelope || typeof envelope !== "object" || typeof envelope.content !== "string") {
5049
+ return raw;
5050
+ }
5051
+ const shot = envelope.screenshot;
5052
+ const screenshot = typeof shot === "string" ? shot : Array.isArray(shot) && typeof shot[0] === "string" ? shot[0] : void 0;
5053
+ return JSON.stringify({
5054
+ url,
5055
+ ...screenshot ? { screenshot } : {},
5056
+ content: capContent(envelope.content)
5057
+ });
5058
+ }
5059
+ function capContent(content) {
5060
+ const total = Buffer.byteLength(content, "utf-8");
5061
+ if (total <= CONTENT_BUDGET_BYTES) {
5062
+ return content;
5063
+ }
5064
+ const head = Buffer.from(content, "utf-8").subarray(0, CONTENT_BUDGET_BYTES).toString("utf-8");
5065
+ return head + `
5066
+
5067
+ [page content truncated at ${(CONTENT_BUDGET_BYTES / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 narrow what you need or fetch a sub-page]`;
5068
+ }
5069
+ var scrapeWebUrlTool = {
5070
+ definition: {
5071
+ name: "scrapeWebUrl",
5072
+ description: "Fetch a web page. Returns its main content as markdown plus `screenshot`, a full-page capture the person sees in the editor. Read the markdown; analyze the screenshot with analyzeImage only when the visual design matters. Use this when you need to fetch or analyze content from a website.",
5073
+ inputSchema: {
5074
+ type: "object",
5075
+ properties: {
5076
+ url: {
5077
+ type: "string",
5078
+ description: "The URL to fetch."
5079
+ }
5080
+ },
5081
+ required: ["url"]
5082
+ }
5083
+ },
5084
+ async execute(input, context) {
5085
+ return fetchWebPage(input.url, {
5086
+ screenshot: true,
5087
+ caller: "parent",
5088
+ onLog: context?.onLog
5089
+ });
5090
+ }
5091
+ };
5092
+
4963
5093
  // src/subagents/research/tools.ts
4964
5094
  var searchGoogleDefinition = {
4965
5095
  name: "searchGoogle",
@@ -5019,22 +5149,6 @@ async function executeSearchGoogle(input, onLog, caller) {
5019
5149
  }
5020
5150
  );
5021
5151
  }
5022
- async function executeScrapeWebUrl(input, onLog, caller) {
5023
- return runMindstudioCli(
5024
- [
5025
- "scrape-url",
5026
- "--url",
5027
- String(input.url),
5028
- "--page-options",
5029
- JSON.stringify({ onlyMainContent: true })
5030
- ],
5031
- {
5032
- maxBuffer: SCRAPE_MAX_BUFFER,
5033
- onLog,
5034
- caller
5035
- }
5036
- );
5037
- }
5038
5152
 
5039
5153
  // src/subagents/research/index.ts
5040
5154
  var BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
@@ -5060,7 +5174,11 @@ async function runResearch(task, context) {
5060
5174
  return executeSearchGoogle(toolInput, childCtx.onLog, "research");
5061
5175
  }
5062
5176
  if (name === "scrapeWebUrl") {
5063
- return executeScrapeWebUrl(toolInput, childCtx.onLog, "research");
5177
+ return fetchWebPage(String(toolInput.url), {
5178
+ screenshot: false,
5179
+ caller: "research",
5180
+ onLog: childCtx.onLog
5181
+ });
5064
5182
  }
5065
5183
  return executeTool(name, toolInput, childCtx);
5066
5184
  },
@@ -5108,7 +5226,7 @@ __export(scrapeWebUrl_exports, {
5108
5226
  });
5109
5227
  var definition = {
5110
5228
  name: "scrapeWebUrl",
5111
- description: "Fetch the content of a web page as markdown. Use for reading a specific URL \u2014 a site the user referenced, a brand to match, a page the researcher cited that you want in full.",
5229
+ description: "Fetch a web page as markdown, plus `screenshot`, a full-page capture you can analyze. Use for reading a specific URL \u2014 a site the user referenced, a brand to match, a page the researcher cited that you want in full.",
5112
5230
  inputSchema: {
5113
5231
  type: "object",
5114
5232
  properties: {
@@ -5121,17 +5239,11 @@ var definition = {
5121
5239
  }
5122
5240
  };
5123
5241
  async function execute(input, onLog) {
5124
- const pageOptions = { onlyMainContent: true };
5125
- return runMindstudioCli(
5126
- [
5127
- "scrape-url",
5128
- "--url",
5129
- input.url,
5130
- "--page-options",
5131
- JSON.stringify(pageOptions)
5132
- ],
5133
- { onLog, caller: "designExpert", maxBuffer: SCRAPE_MAX_BUFFER }
5134
- );
5242
+ return fetchWebPage(String(input.url), {
5243
+ screenshot: true,
5244
+ caller: "designExpert",
5245
+ onLog
5246
+ });
5135
5247
  }
5136
5248
 
5137
5249
  // src/subagents/designExpert/tools/analyzeDesign.ts
@@ -7312,50 +7424,6 @@ var reviewExistingProjectTool = {
7312
7424
  }
7313
7425
  };
7314
7426
 
7315
- // src/tools/common/scrapeWebUrl.ts
7316
- var scrapeWebUrlTool = {
7317
- definition: {
7318
- name: "scrapeWebUrl",
7319
- description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
7320
- inputSchema: {
7321
- type: "object",
7322
- properties: {
7323
- url: {
7324
- type: "string",
7325
- description: "The URL to fetch."
7326
- },
7327
- screenshot: {
7328
- type: "boolean",
7329
- description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
7330
- }
7331
- },
7332
- required: ["url"]
7333
- }
7334
- },
7335
- async execute(input, context) {
7336
- const url = input.url;
7337
- const screenshot = input.screenshot;
7338
- const pageOptions = { onlyMainContent: true };
7339
- if (screenshot) {
7340
- pageOptions.screenshot = true;
7341
- }
7342
- return runMindstudioCli(
7343
- [
7344
- "scrape-url",
7345
- "--url",
7346
- url,
7347
- "--page-options",
7348
- JSON.stringify(pageOptions)
7349
- ],
7350
- {
7351
- onLog: context?.onLog,
7352
- maxBuffer: SCRAPE_MAX_BUFFER,
7353
- caller: "parent"
7354
- }
7355
- );
7356
- }
7357
- };
7358
-
7359
7427
  // src/tools/index.ts
7360
7428
  function deriveContext(parent, toolCallId, onLog) {
7361
7429
  return { ...parent, toolCallId, onLog };
@@ -8841,17 +8909,11 @@ function resolveAction(text) {
8841
8909
  if (!parsed) {
8842
8910
  return null;
8843
8911
  }
8844
- const { name: triggerName, remainder } = parsed;
8912
+ const { name: triggerName } = parsed;
8845
8913
  if (NON_ACTION_SENTINELS.has(triggerName)) {
8846
8914
  return null;
8847
8915
  }
8848
- let params = {};
8849
- if (remainder) {
8850
- try {
8851
- params = JSON.parse(remainder.split("\n")[0]);
8852
- } catch {
8853
- }
8854
- }
8916
+ const params = sentinelParams(text);
8855
8917
  let body = readAsset("automatedActions", `${triggerName}.md`);
8856
8918
  let next;
8857
8919
  const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
@@ -8866,8 +8928,16 @@ function resolveAction(text) {
8866
8928
  const str = typeof value === "string" ? value : JSON.stringify(value);
8867
8929
  body = body.replaceAll(`{{${key}}}`, str);
8868
8930
  }
8931
+ const resumed = params.resumed === true;
8932
+ if (resumed) {
8933
+ body = `${readAsset("automatedActions", "_resumed.md")}
8934
+
8935
+ ${body}`;
8936
+ }
8869
8937
  return {
8870
- message: automatedMessage(triggerName, body),
8938
+ message: resumed ? setSentinelParams(automatedMessage(triggerName, body), {
8939
+ resumed: true
8940
+ }) : automatedMessage(triggerName, body),
8871
8941
  next
8872
8942
  };
8873
8943
  }
@@ -9250,7 +9320,10 @@ async function runTurn(params) {
9250
9320
  const automated = parseSentinel(entry.text);
9251
9321
  if (automated) {
9252
9322
  if (!NON_ACTION_SENTINELS.has(automated.name)) {
9253
- parts.push(`Automated action: ${automated.name}`);
9323
+ const resumed = sentinelParams(entry.text).resumed === true;
9324
+ parts.push(
9325
+ `Automated action: ${automated.name}${resumed ? " (resuming interrupted work)" : ""}`
9326
+ );
9254
9327
  hasUserSignal = true;
9255
9328
  }
9256
9329
  } else if (entry.text) {
@@ -9639,7 +9712,11 @@ async function runTurn(params) {
9639
9712
  const results = await Promise.all(
9640
9713
  toolCalls.map(async (tc) => {
9641
9714
  if (signal?.aborted) {
9642
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
9715
+ return {
9716
+ id: tc.id,
9717
+ result: cancelledToolResult(signal),
9718
+ isError: true
9719
+ };
9643
9720
  }
9644
9721
  const toolStart = Date.now();
9645
9722
  let settle;
@@ -9658,7 +9735,7 @@ async function runTurn(params) {
9658
9735
  };
9659
9736
  const cascadeAbort = () => {
9660
9737
  toolAbort.abort();
9661
- safeSettle(USER_CANCELLED_RESULT, true);
9738
+ safeSettle(cancelledToolResult(signal), true);
9662
9739
  };
9663
9740
  signal?.addEventListener("abort", cascadeAbort, { once: true });
9664
9741
  const run = async (input) => {
@@ -9767,7 +9844,7 @@ async function runTurn(params) {
9767
9844
  block.completedAt = Date.now();
9768
9845
  const msgs = subAgentMessages.get(r.id);
9769
9846
  if (msgs) {
9770
- block.subAgentMessages = capSubAgentTranscript(msgs);
9847
+ attachSubAgentTranscript(block, msgs);
9771
9848
  }
9772
9849
  }
9773
9850
  }
@@ -10019,6 +10096,15 @@ function holdRestoredUserItems(items) {
10019
10096
  (item) => item.source === "user" ? { ...item, held: true } : item
10020
10097
  );
10021
10098
  }
10099
+ function releaseShutdownHolds(items) {
10100
+ return items.map((item) => {
10101
+ if (item.heldBy !== "shutdown" || item.source === "user") {
10102
+ return item;
10103
+ }
10104
+ const { held: _held, heldBy: _heldBy, ...released } = item;
10105
+ return released;
10106
+ });
10107
+ }
10022
10108
  var MessageQueue = class {
10023
10109
  items = [];
10024
10110
  onChange;
@@ -10090,16 +10176,27 @@ var MessageQueue = class {
10090
10176
  /**
10091
10177
  * Mark matching items `held` — waiting on the user rather than on the agent.
10092
10178
  * Fires onChange only if something changed. Returns the held items.
10179
+ *
10180
+ * `heldBy` is stamped only on items this call actually transitions. An item
10181
+ * that was ALREADY held keeps whatever hold it had: otherwise a shutdown
10182
+ * arriving after a user's Stop — a box reaped while they were away from the
10183
+ * tab — would retag their deliberate pause as environmental and the next
10184
+ * boot would auto-run the very build they stopped.
10093
10185
  */
10094
- holdWhere(predicate) {
10186
+ holdWhere(predicate, heldBy) {
10095
10187
  const held = [];
10096
10188
  let changed = false;
10097
10189
  for (const item of this.items) {
10098
10190
  if (!predicate(item)) {
10099
10191
  continue;
10100
10192
  }
10101
- changed = changed || !item.held;
10102
- item.held = true;
10193
+ if (!item.held) {
10194
+ changed = true;
10195
+ item.held = true;
10196
+ if (heldBy) {
10197
+ item.heldBy = heldBy;
10198
+ }
10199
+ }
10103
10200
  held.push(item);
10104
10201
  }
10105
10202
  if (changed) {
@@ -10147,6 +10244,7 @@ var MessageQueue = class {
10147
10244
  const [item] = this.items.splice(idx, 1);
10148
10245
  item.delivery = "asap";
10149
10246
  delete item.held;
10247
+ delete item.heldBy;
10150
10248
  this.items.unshift(item);
10151
10249
  this.onChange?.();
10152
10250
  return item;
@@ -10170,6 +10268,7 @@ var MessageQueue = class {
10170
10268
  continue;
10171
10269
  }
10172
10270
  delete item.held;
10271
+ delete item.heldBy;
10173
10272
  released.push(item);
10174
10273
  }
10175
10274
  const back = this.items.filter(defer);
@@ -10299,10 +10398,13 @@ var HeadlessSession = class {
10299
10398
  });
10300
10399
  await initOrgContext(this.config);
10301
10400
  const resumed = loadSession(this.state);
10302
- this.queue = new MessageQueue(holdRestoredUserItems(loadQueue()), () => {
10303
- this.persistStats();
10304
- this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
10305
- });
10401
+ this.queue = new MessageQueue(
10402
+ releaseShutdownHolds(holdRestoredUserItems(loadQueue())),
10403
+ () => {
10404
+ this.persistStats();
10405
+ this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
10406
+ }
10407
+ );
10306
10408
  this.passivePen = loadPassiveResults();
10307
10409
  this.persistStats();
10308
10410
  if (resumed) {
@@ -10424,6 +10526,11 @@ var HeadlessSession = class {
10424
10526
  this.emit("ready");
10425
10527
  }
10426
10528
  shutdown = () => {
10529
+ try {
10530
+ this.handleCancel("shutdown");
10531
+ } catch (err) {
10532
+ log17.warn("Shutdown cancel failed", { error: err?.message });
10533
+ }
10427
10534
  this.emit("stopping");
10428
10535
  this.emit("stopped");
10429
10536
  process.exit(0);
@@ -11361,6 +11468,19 @@ var HeadlessSession = class {
11361
11468
  * pipeline where it stopped. Holding only the remainder would resume one step
11362
11469
  * PAST the interruption, polishing and finalizing half-built code.
11363
11470
  *
11471
+ * That re-queued step is marked `resumed`, which is what stops it being a
11472
+ * verbatim replay. The message the user sends to un-pause a build is usually
11473
+ * the rest of that build ("try again, I fixed the adapter") and Remy does the
11474
+ * work inside that turn — so re-delivering the original "build everything
11475
+ * now" afterwards had it rebuild an app it had just finished.
11476
+ *
11477
+ * `reason: 'shutdown'` is the environment taking the agent down rather than a
11478
+ * person pressing Stop. It pauses identically — nothing may start a turn
11479
+ * while the workspace is being tarred — but tags the chain steps it holds so
11480
+ * the next boot releases them and the pipeline carries on by itself. Called
11481
+ * from the stdin `cancel` (the sandbox's pre-destroy quiesce) and from our own
11482
+ * SIGTERM handler, whichever gets there first.
11483
+ *
11364
11484
  * A compaction is cancelled here too, unconditionally. It gates every queued
11365
11485
  * message and outlives the turn that started it, so leaving it running means
11366
11486
  * Stop can't reach idle. The cost is the summary work in flight; the forced
@@ -11371,14 +11491,16 @@ var HeadlessSession = class {
11371
11491
  * `{cancelled, absorbed:true}` terminal. Only items still sitting in the
11372
11492
  * queue survive.
11373
11493
  */
11374
- handleCancel() {
11494
+ handleCancel(reason) {
11375
11495
  if (this.currentAbort) {
11376
- this.currentAbort.abort();
11496
+ this.currentAbort.abort(reason);
11377
11497
  }
11378
11498
  const cancelledCompaction = cancelInflightCompaction();
11379
11499
  for (const [id, pending2] of this.pendingTools) {
11380
11500
  clearTimeout(pending2.timeout);
11381
- pending2.resolve(USER_CANCELLED_RESULT);
11501
+ pending2.resolve(
11502
+ reason === "shutdown" ? ENV_INTERRUPTED_RESULT : USER_CANCELLED_RESULT
11503
+ );
11382
11504
  this.pendingTools.delete(id);
11383
11505
  }
11384
11506
  const flushed = this.queue.removeWhere(
@@ -11389,7 +11511,13 @@ var HeadlessSession = class {
11389
11511
  this.queue.unshift({
11390
11512
  command: {
11391
11513
  action: "message",
11392
- text: step.text,
11514
+ // Marked so it comes back as a resumption rather than a replay: the
11515
+ // step already ran once, and whatever the user does between here and
11516
+ // its re-delivery may well be the rest of it. resolveAction turns
11517
+ // this param into a preamble, and both the chat row and the queue
11518
+ // card label the step off it. Merged, so a step interrupted twice
11519
+ // keeps its own params and re-marking stays idempotent.
11520
+ text: setSentinelParams(step.text, { resumed: true }),
11393
11521
  onboardingState: step.onboardingState,
11394
11522
  // Fresh id: the original command's terminal has already gone out as
11395
11523
  // cancelled, and one command gets exactly one `completed`.
@@ -11397,13 +11525,18 @@ var HeadlessSession = class {
11397
11525
  },
11398
11526
  source: "chain",
11399
11527
  enqueuedAt: Date.now(),
11400
- held: true
11528
+ held: true,
11529
+ ...reason === "shutdown" && { heldBy: reason }
11401
11530
  });
11402
11531
  this.currentChainStep = null;
11403
11532
  }
11404
- const held = this.queue.holdWhere(
11405
- (item) => item.source === "user" || item.source === "chain"
11406
- );
11533
+ const held = [
11534
+ ...this.queue.holdWhere(
11535
+ (item) => item.source === "chain",
11536
+ reason === "shutdown" ? reason : void 0
11537
+ ),
11538
+ ...this.queue.holdWhere((item) => item.source === "user")
11539
+ ];
11407
11540
  return {
11408
11541
  flushed,
11409
11542
  held,
@@ -11415,9 +11548,10 @@ var HeadlessSession = class {
11415
11548
  * Remove pending queued messages: all user messages (no id), or a single item
11416
11549
  * by id. Does not affect the in-flight turn (use `cancel` for that).
11417
11550
  *
11418
- * Held chain items — a paused pipeline — are removable only by explicit id.
11419
- * The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
11420
- * neither of which should get to decide the pipeline's fate. A DELIVERABLE
11551
+ * Held chain items — a paused pipeline — are removable only by explicit id:
11552
+ * the queue card's Discard sends one call per step, and its per-step X sends
11553
+ * one. The id-less form is the card's "Clear", which is about the user's own
11554
+ * messages and shouldn't get to decide the pipeline's fate. A DELIVERABLE
11421
11555
  * chain item is never removable: that's live pipeline work. (The step
11422
11556
  * actually running isn't in the queue at all — drainQueueLoop takes it out
11423
11557
  * before running it.)
@@ -11523,7 +11657,9 @@ var HeadlessSession = class {
11523
11657
  return;
11524
11658
  }
11525
11659
  if (action === "cancel") {
11526
- const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
11660
+ const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel(
11661
+ parsed.reason === "shutdown" ? "shutdown" : void 0
11662
+ );
11527
11663
  this.emit(
11528
11664
  "completed",
11529
11665
  {
package/dist/index.js CHANGED
@@ -1481,6 +1481,34 @@ function parseSentinel(text) {
1481
1481
  }
1482
1482
  return { name: match[1], remainder: match[2] };
1483
1483
  }
1484
+ function sentinelParams(text) {
1485
+ const parsed = parseSentinel(text);
1486
+ if (!parsed) {
1487
+ return {};
1488
+ }
1489
+ const firstLine = parsed.remainder.split("\n")[0].trim();
1490
+ if (!firstLine) {
1491
+ return {};
1492
+ }
1493
+ try {
1494
+ const value = JSON.parse(firstLine);
1495
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1496
+ } catch {
1497
+ return {};
1498
+ }
1499
+ }
1500
+ function setSentinelParams(text, patch) {
1501
+ const parsed = parseSentinel(text);
1502
+ if (!parsed) {
1503
+ return text;
1504
+ }
1505
+ const newlineAt = parsed.remainder.indexOf("\n");
1506
+ const body = newlineAt === -1 ? "" : parsed.remainder.slice(newlineAt + 1);
1507
+ const params = { ...sentinelParams(text), ...patch };
1508
+ const line = `${sentinel(parsed.name)}${JSON.stringify(params)}`;
1509
+ return body ? `${line}
1510
+ ${body}` : line;
1511
+ }
1484
1512
  function stripSentinelLine(text) {
1485
1513
  return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
1486
1514
  }
@@ -2343,6 +2371,20 @@ var init_cleanMessages = __esm({
2343
2371
  });
2344
2372
 
2345
2373
  // src/recording.ts
2374
+ function collectRecordings(messages) {
2375
+ const recordings = [];
2376
+ for (const msg of messages) {
2377
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
2378
+ continue;
2379
+ }
2380
+ for (const block of msg.content) {
2381
+ if (block.type === "tool" && block.recording) {
2382
+ recordings.push(block.recording);
2383
+ }
2384
+ }
2385
+ }
2386
+ return recordings;
2387
+ }
2346
2388
  function liftRecording(result) {
2347
2389
  if (!result.includes('"recording"')) {
2348
2390
  return { result };
@@ -2400,6 +2442,18 @@ function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
2400
2442
 
2401
2443
  (tool result truncated at ${(maxBytes / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 too large to keep in context. Narrow the call (select fewer fields, paginate, or query a subset) instead of fetching everything.)`;
2402
2444
  }
2445
+ function attachSubAgentTranscript(block, messages) {
2446
+ if (!block.recordings) {
2447
+ const recordings = collectRecordings(messages);
2448
+ if (recordings.length > 0) {
2449
+ block.recordings = recordings;
2450
+ }
2451
+ }
2452
+ block.subAgentMessages = capSubAgentTranscript(messages);
2453
+ }
2454
+ function dropToolResultMessages(messages) {
2455
+ return messages.filter((m) => !(m.role === "user" && m.toolCallId));
2456
+ }
2403
2457
  function capSubAgentTranscript(messages) {
2404
2458
  for (const msg of messages) {
2405
2459
  capMessageForHistory(msg, MAX_SUBAGENT_RESULT_BYTES);
@@ -2444,7 +2498,7 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
2444
2498
  );
2445
2499
  }
2446
2500
  if (Array.isArray(block.subAgentMessages)) {
2447
- block.subAgentMessages = capSubAgentTranscript(block.subAgentMessages);
2501
+ attachSubAgentTranscript(block, block.subAgentMessages);
2448
2502
  }
2449
2503
  }
2450
2504
  } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
@@ -4666,13 +4720,17 @@ var init_browserLock = __esm({
4666
4720
  });
4667
4721
 
4668
4722
  // src/toolRegistry.ts
4669
- var log7, USER_CANCELLED_RESULT, ToolRegistry;
4723
+ function cancelledToolResult(signal) {
4724
+ return signal?.reason === "shutdown" ? ENV_INTERRUPTED_RESULT : USER_CANCELLED_RESULT;
4725
+ }
4726
+ var log7, USER_CANCELLED_RESULT, ENV_INTERRUPTED_RESULT, ToolRegistry;
4670
4727
  var init_toolRegistry = __esm({
4671
4728
  "src/toolRegistry.ts"() {
4672
4729
  "use strict";
4673
4730
  init_logger();
4674
4731
  log7 = createLogger("tool-registry");
4675
4732
  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.";
4733
+ ENV_INTERRUPTED_RESULT = "[INTERRUPTED] The environment shut down while this tool was running. No user action was involved \u2014 pick up from here.";
4676
4734
  ToolRegistry = class {
4677
4735
  entries = /* @__PURE__ */ new Map();
4678
4736
  onEvent;
@@ -4928,7 +4986,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4928
4986
  messages: thisInvocation()
4929
4987
  };
4930
4988
  }
4931
- return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
4989
+ return { text: cancelledToolResult(signal), messages: thisInvocation() };
4932
4990
  }
4933
4991
  let lastToolResult = "";
4934
4992
  let watchedBlocks = [];
@@ -5164,7 +5222,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5164
5222
  if (signal?.aborted) {
5165
5223
  return {
5166
5224
  id: tc.id,
5167
- result: USER_CANCELLED_RESULT,
5225
+ result: cancelledToolResult(signal),
5168
5226
  isError: true
5169
5227
  };
5170
5228
  }
@@ -5281,7 +5339,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5281
5339
  }
5282
5340
  const innerMsgs = subAgentMessages.get(r.id);
5283
5341
  if (innerMsgs) {
5284
- block.subAgentMessages = capSubAgentTranscript(innerMsgs);
5342
+ attachSubAgentTranscript(block, innerMsgs);
5285
5343
  }
5286
5344
  if (captureArtifacts?.includes(block.name) && !r.isError) {
5287
5345
  try {
@@ -5867,13 +5925,12 @@ async function runBrowserAutomation(task, context, opts) {
5867
5925
  },
5868
5926
  toolRegistry: context.toolRegistry
5869
5927
  });
5870
- context.subAgentMessages?.set(context.toolCallId, result.messages);
5871
- const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5872
- const recorded = result.messages.some(
5873
- (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
5874
- (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
5875
- )
5928
+ context.subAgentMessages?.set(
5929
+ context.toolCallId,
5930
+ dropToolResultMessages(result.messages)
5876
5931
  );
5932
+ const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5933
+ const recorded = collectRecordings(result.messages).length > 0;
5877
5934
  return {
5878
5935
  text: result.text,
5879
5936
  recorded,
@@ -5889,6 +5946,8 @@ var init_browserAutomation = __esm({
5889
5946
  "use strict";
5890
5947
  init_tools10();
5891
5948
  init_runner();
5949
+ init_historyLimits();
5950
+ init_recording();
5892
5951
  init_tools2();
5893
5952
  init_tools();
5894
5953
  init_readSpec();
@@ -6058,6 +6117,88 @@ var init_screenshot2 = __esm({
6058
6117
  }
6059
6118
  });
6060
6119
 
6120
+ // src/tools/common/scrapeWebUrl.ts
6121
+ async function fetchWebPage(url, opts) {
6122
+ const raw = await runMindstudioCli(
6123
+ [
6124
+ "scrape-url",
6125
+ "--url",
6126
+ url,
6127
+ "--page-options",
6128
+ JSON.stringify({ onlyMainContent: true, screenshot: opts.screenshot })
6129
+ ],
6130
+ {
6131
+ onLog: opts.onLog,
6132
+ maxBuffer: SCRAPE_MAX_BUFFER,
6133
+ caller: opts.caller
6134
+ }
6135
+ );
6136
+ return shapeFetchResult(url, raw);
6137
+ }
6138
+ function shapeFetchResult(url, raw) {
6139
+ const body = raw.endsWith(TRUNCATED_SUFFIX) ? raw.slice(0, -TRUNCATED_SUFFIX.length) : raw;
6140
+ let envelope;
6141
+ try {
6142
+ envelope = JSON.parse(body);
6143
+ } catch {
6144
+ return raw;
6145
+ }
6146
+ if (!envelope || typeof envelope !== "object" || typeof envelope.content !== "string") {
6147
+ return raw;
6148
+ }
6149
+ const shot = envelope.screenshot;
6150
+ const screenshot = typeof shot === "string" ? shot : Array.isArray(shot) && typeof shot[0] === "string" ? shot[0] : void 0;
6151
+ return JSON.stringify({
6152
+ url,
6153
+ ...screenshot ? { screenshot } : {},
6154
+ content: capContent(envelope.content)
6155
+ });
6156
+ }
6157
+ function capContent(content) {
6158
+ const total = Buffer.byteLength(content, "utf-8");
6159
+ if (total <= CONTENT_BUDGET_BYTES) {
6160
+ return content;
6161
+ }
6162
+ const head = Buffer.from(content, "utf-8").subarray(0, CONTENT_BUDGET_BYTES).toString("utf-8");
6163
+ return head + `
6164
+
6165
+ [page content truncated at ${(CONTENT_BUDGET_BYTES / 1024).toFixed(0)}KB of ${(total / 1024).toFixed(0)}KB \u2014 narrow what you need or fetch a sub-page]`;
6166
+ }
6167
+ var CONTENT_BUDGET_BYTES, TRUNCATED_SUFFIX, scrapeWebUrlTool;
6168
+ var init_scrapeWebUrl = __esm({
6169
+ "src/tools/common/scrapeWebUrl.ts"() {
6170
+ "use strict";
6171
+ init_runMindstudioCli();
6172
+ init_runCli();
6173
+ init_historyLimits();
6174
+ CONTENT_BUDGET_BYTES = MAX_TOOL_RESULT_BYTES - 4 * 1024;
6175
+ TRUNCATED_SUFFIX = "\n\n[output truncated]";
6176
+ scrapeWebUrlTool = {
6177
+ definition: {
6178
+ name: "scrapeWebUrl",
6179
+ description: "Fetch a web page. Returns its main content as markdown plus `screenshot`, a full-page capture the person sees in the editor. Read the markdown; analyze the screenshot with analyzeImage only when the visual design matters. Use this when you need to fetch or analyze content from a website.",
6180
+ inputSchema: {
6181
+ type: "object",
6182
+ properties: {
6183
+ url: {
6184
+ type: "string",
6185
+ description: "The URL to fetch."
6186
+ }
6187
+ },
6188
+ required: ["url"]
6189
+ }
6190
+ },
6191
+ async execute(input, context) {
6192
+ return fetchWebPage(input.url, {
6193
+ screenshot: true,
6194
+ caller: "parent",
6195
+ onLog: context?.onLog
6196
+ });
6197
+ }
6198
+ };
6199
+ }
6200
+ });
6201
+
6061
6202
  // src/subagents/research/tools.ts
6062
6203
  async function executeSearchGoogle(input, onLog, caller) {
6063
6204
  const fetchTopN = Math.max(0, Math.round(Number(input.fetchTopN) || 0));
@@ -6079,22 +6220,6 @@ async function executeSearchGoogle(input, onLog, caller) {
6079
6220
  }
6080
6221
  );
6081
6222
  }
6082
- async function executeScrapeWebUrl(input, onLog, caller) {
6083
- return runMindstudioCli(
6084
- [
6085
- "scrape-url",
6086
- "--url",
6087
- String(input.url),
6088
- "--page-options",
6089
- JSON.stringify({ onlyMainContent: true })
6090
- ],
6091
- {
6092
- maxBuffer: SCRAPE_MAX_BUFFER,
6093
- onLog,
6094
- caller
6095
- }
6096
- );
6097
- }
6098
6223
  var searchGoogleDefinition, scrapeWebUrlDefinition, RESEARCH_TOOLS;
6099
6224
  var init_tools3 = __esm({
6100
6225
  "src/subagents/research/tools.ts"() {
@@ -6167,7 +6292,11 @@ async function runResearch(task, context) {
6167
6292
  return executeSearchGoogle(toolInput, childCtx.onLog, "research");
6168
6293
  }
6169
6294
  if (name === "scrapeWebUrl") {
6170
- return executeScrapeWebUrl(toolInput, childCtx.onLog, "research");
6295
+ return fetchWebPage(String(toolInput.url), {
6296
+ screenshot: false,
6297
+ caller: "research",
6298
+ onLog: childCtx.onLog
6299
+ });
6171
6300
  }
6172
6301
  return executeTool(name, toolInput, childCtx);
6173
6302
  },
@@ -6192,6 +6321,7 @@ var init_research = __esm({
6192
6321
  init_runner();
6193
6322
  init_context();
6194
6323
  init_tools10();
6324
+ init_scrapeWebUrl();
6195
6325
  init_tools3();
6196
6326
  init_surfaces();
6197
6327
  BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
@@ -6227,27 +6357,20 @@ __export(scrapeWebUrl_exports, {
6227
6357
  execute: () => execute
6228
6358
  });
6229
6359
  async function execute(input, onLog) {
6230
- const pageOptions = { onlyMainContent: true };
6231
- return runMindstudioCli(
6232
- [
6233
- "scrape-url",
6234
- "--url",
6235
- input.url,
6236
- "--page-options",
6237
- JSON.stringify(pageOptions)
6238
- ],
6239
- { onLog, caller: "designExpert", maxBuffer: SCRAPE_MAX_BUFFER }
6240
- );
6360
+ return fetchWebPage(String(input.url), {
6361
+ screenshot: true,
6362
+ caller: "designExpert",
6363
+ onLog
6364
+ });
6241
6365
  }
6242
6366
  var definition;
6243
- var init_scrapeWebUrl = __esm({
6367
+ var init_scrapeWebUrl2 = __esm({
6244
6368
  "src/subagents/designExpert/tools/scrapeWebUrl.ts"() {
6245
6369
  "use strict";
6246
- init_runMindstudioCli();
6247
- init_runCli();
6370
+ init_scrapeWebUrl();
6248
6371
  definition = {
6249
6372
  name: "scrapeWebUrl",
6250
- description: "Fetch the content of a web page as markdown. Use for reading a specific URL \u2014 a site the user referenced, a brand to match, a page the researcher cited that you want in full.",
6373
+ description: "Fetch a web page as markdown, plus `screenshot`, a full-page capture you can analyze. Use for reading a specific URL \u2014 a site the user referenced, a brand to match, a page the researcher cited that you want in full.",
6251
6374
  inputSchema: {
6252
6375
  type: "object",
6253
6376
  properties: {
@@ -7392,7 +7515,7 @@ var init_tools5 = __esm({
7392
7515
  init_tools10();
7393
7516
  init_tools();
7394
7517
  init_research();
7395
- init_scrapeWebUrl();
7518
+ init_scrapeWebUrl2();
7396
7519
  init_analyzeDesign();
7397
7520
  init_analyzeImage2();
7398
7521
  init_generateImages();
@@ -8829,58 +8952,6 @@ var init_reviewExistingProject = __esm({
8829
8952
  }
8830
8953
  });
8831
8954
 
8832
- // src/tools/common/scrapeWebUrl.ts
8833
- var scrapeWebUrlTool;
8834
- var init_scrapeWebUrl2 = __esm({
8835
- "src/tools/common/scrapeWebUrl.ts"() {
8836
- "use strict";
8837
- init_runMindstudioCli();
8838
- init_runCli();
8839
- scrapeWebUrlTool = {
8840
- definition: {
8841
- name: "scrapeWebUrl",
8842
- description: "Scrape the content of a web page. Returns the HTML of the page as markdown text. Optionally capture a screenshot if you need see the visual design. Use this when you need to fetch or analyze content from a website",
8843
- inputSchema: {
8844
- type: "object",
8845
- properties: {
8846
- url: {
8847
- type: "string",
8848
- description: "The URL to fetch."
8849
- },
8850
- screenshot: {
8851
- type: "boolean",
8852
- description: "Capture a screenshot of the page in addition to the text content. Adds latency; only use when you need to see the visual design."
8853
- }
8854
- },
8855
- required: ["url"]
8856
- }
8857
- },
8858
- async execute(input, context) {
8859
- const url = input.url;
8860
- const screenshot = input.screenshot;
8861
- const pageOptions = { onlyMainContent: true };
8862
- if (screenshot) {
8863
- pageOptions.screenshot = true;
8864
- }
8865
- return runMindstudioCli(
8866
- [
8867
- "scrape-url",
8868
- "--url",
8869
- url,
8870
- "--page-options",
8871
- JSON.stringify(pageOptions)
8872
- ],
8873
- {
8874
- onLog: context?.onLog,
8875
- maxBuffer: SCRAPE_MAX_BUFFER,
8876
- caller: "parent"
8877
- }
8878
- );
8879
- }
8880
- };
8881
- }
8882
- });
8883
-
8884
8955
  // src/tools/index.ts
8885
8956
  function deriveContext(parent, toolCallId, onLog) {
8886
8957
  return { ...parent, toolCallId, onLog };
@@ -8939,7 +9010,7 @@ var init_tools10 = __esm({
8939
9010
  init_specSync();
8940
9011
  init_research();
8941
9012
  init_reviewExistingProject();
8942
- init_scrapeWebUrl2();
9013
+ init_scrapeWebUrl();
8943
9014
  init_writeBuildOverview();
8944
9015
  ALL_TOOLS = [
8945
9016
  // Common
@@ -9179,17 +9250,11 @@ function resolveAction(text) {
9179
9250
  if (!parsed) {
9180
9251
  return null;
9181
9252
  }
9182
- const { name: triggerName, remainder } = parsed;
9253
+ const { name: triggerName } = parsed;
9183
9254
  if (NON_ACTION_SENTINELS.has(triggerName)) {
9184
9255
  return null;
9185
9256
  }
9186
- let params = {};
9187
- if (remainder) {
9188
- try {
9189
- params = JSON.parse(remainder.split("\n")[0]);
9190
- } catch {
9191
- }
9192
- }
9257
+ const params = sentinelParams(text);
9193
9258
  let body = readAsset("automatedActions", `${triggerName}.md`);
9194
9259
  let next;
9195
9260
  const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
@@ -9204,8 +9269,16 @@ function resolveAction(text) {
9204
9269
  const str = typeof value === "string" ? value : JSON.stringify(value);
9205
9270
  body = body.replaceAll(`{{${key}}}`, str);
9206
9271
  }
9272
+ const resumed = params.resumed === true;
9273
+ if (resumed) {
9274
+ body = `${readAsset("automatedActions", "_resumed.md")}
9275
+
9276
+ ${body}`;
9277
+ }
9207
9278
  return {
9208
- message: automatedMessage(triggerName, body),
9279
+ message: resumed ? setSentinelParams(automatedMessage(triggerName, body), {
9280
+ resumed: true
9281
+ }) : automatedMessage(triggerName, body),
9209
9282
  next
9210
9283
  };
9211
9284
  }
@@ -9950,7 +10023,10 @@ async function runTurn(params) {
9950
10023
  const automated = parseSentinel(entry.text);
9951
10024
  if (automated) {
9952
10025
  if (!NON_ACTION_SENTINELS.has(automated.name)) {
9953
- parts.push(`Automated action: ${automated.name}`);
10026
+ const resumed = sentinelParams(entry.text).resumed === true;
10027
+ parts.push(
10028
+ `Automated action: ${automated.name}${resumed ? " (resuming interrupted work)" : ""}`
10029
+ );
9954
10030
  hasUserSignal = true;
9955
10031
  }
9956
10032
  } else if (entry.text) {
@@ -10339,7 +10415,11 @@ async function runTurn(params) {
10339
10415
  const results = await Promise.all(
10340
10416
  toolCalls.map(async (tc) => {
10341
10417
  if (signal?.aborted) {
10342
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
10418
+ return {
10419
+ id: tc.id,
10420
+ result: cancelledToolResult(signal),
10421
+ isError: true
10422
+ };
10343
10423
  }
10344
10424
  const toolStart = Date.now();
10345
10425
  let settle;
@@ -10358,7 +10438,7 @@ async function runTurn(params) {
10358
10438
  };
10359
10439
  const cascadeAbort = () => {
10360
10440
  toolAbort.abort();
10361
- safeSettle(USER_CANCELLED_RESULT, true);
10441
+ safeSettle(cancelledToolResult(signal), true);
10362
10442
  };
10363
10443
  signal?.addEventListener("abort", cascadeAbort, { once: true });
10364
10444
  const run = async (input) => {
@@ -10467,7 +10547,7 @@ async function runTurn(params) {
10467
10547
  block.completedAt = Date.now();
10468
10548
  const msgs = subAgentMessages.get(r.id);
10469
10549
  if (msgs) {
10470
- block.subAgentMessages = capSubAgentTranscript(msgs);
10550
+ attachSubAgentTranscript(block, msgs);
10471
10551
  }
10472
10552
  }
10473
10553
  }
@@ -11031,6 +11111,15 @@ function holdRestoredUserItems(items) {
11031
11111
  (item) => item.source === "user" ? { ...item, held: true } : item
11032
11112
  );
11033
11113
  }
11114
+ function releaseShutdownHolds(items) {
11115
+ return items.map((item) => {
11116
+ if (item.heldBy !== "shutdown" || item.source === "user") {
11117
+ return item;
11118
+ }
11119
+ const { held: _held, heldBy: _heldBy, ...released } = item;
11120
+ return released;
11121
+ });
11122
+ }
11034
11123
  var MessageQueue;
11035
11124
  var init_messageQueue = __esm({
11036
11125
  "src/headless/messageQueue.ts"() {
@@ -11106,16 +11195,27 @@ var init_messageQueue = __esm({
11106
11195
  /**
11107
11196
  * Mark matching items `held` — waiting on the user rather than on the agent.
11108
11197
  * Fires onChange only if something changed. Returns the held items.
11198
+ *
11199
+ * `heldBy` is stamped only on items this call actually transitions. An item
11200
+ * that was ALREADY held keeps whatever hold it had: otherwise a shutdown
11201
+ * arriving after a user's Stop — a box reaped while they were away from the
11202
+ * tab — would retag their deliberate pause as environmental and the next
11203
+ * boot would auto-run the very build they stopped.
11109
11204
  */
11110
- holdWhere(predicate) {
11205
+ holdWhere(predicate, heldBy) {
11111
11206
  const held = [];
11112
11207
  let changed = false;
11113
11208
  for (const item of this.items) {
11114
11209
  if (!predicate(item)) {
11115
11210
  continue;
11116
11211
  }
11117
- changed = changed || !item.held;
11118
- item.held = true;
11212
+ if (!item.held) {
11213
+ changed = true;
11214
+ item.held = true;
11215
+ if (heldBy) {
11216
+ item.heldBy = heldBy;
11217
+ }
11218
+ }
11119
11219
  held.push(item);
11120
11220
  }
11121
11221
  if (changed) {
@@ -11163,6 +11263,7 @@ var init_messageQueue = __esm({
11163
11263
  const [item] = this.items.splice(idx, 1);
11164
11264
  item.delivery = "asap";
11165
11265
  delete item.held;
11266
+ delete item.heldBy;
11166
11267
  this.items.unshift(item);
11167
11268
  this.onChange?.();
11168
11269
  return item;
@@ -11186,6 +11287,7 @@ var init_messageQueue = __esm({
11186
11287
  continue;
11187
11288
  }
11188
11289
  delete item.held;
11290
+ delete item.heldBy;
11189
11291
  released.push(item);
11190
11292
  }
11191
11293
  const back = this.items.filter(defer);
@@ -11343,10 +11445,13 @@ var init_headless = __esm({
11343
11445
  });
11344
11446
  await initOrgContext(this.config);
11345
11447
  const resumed = loadSession(this.state);
11346
- this.queue = new MessageQueue(holdRestoredUserItems(loadQueue()), () => {
11347
- this.persistStats();
11348
- this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
11349
- });
11448
+ this.queue = new MessageQueue(
11449
+ releaseShutdownHolds(holdRestoredUserItems(loadQueue())),
11450
+ () => {
11451
+ this.persistStats();
11452
+ this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
11453
+ }
11454
+ );
11350
11455
  this.passivePen = loadPassiveResults();
11351
11456
  this.persistStats();
11352
11457
  if (resumed) {
@@ -11468,6 +11573,11 @@ var init_headless = __esm({
11468
11573
  this.emit("ready");
11469
11574
  }
11470
11575
  shutdown = () => {
11576
+ try {
11577
+ this.handleCancel("shutdown");
11578
+ } catch (err) {
11579
+ log17.warn("Shutdown cancel failed", { error: err?.message });
11580
+ }
11471
11581
  this.emit("stopping");
11472
11582
  this.emit("stopped");
11473
11583
  process.exit(0);
@@ -12405,6 +12515,19 @@ var init_headless = __esm({
12405
12515
  * pipeline where it stopped. Holding only the remainder would resume one step
12406
12516
  * PAST the interruption, polishing and finalizing half-built code.
12407
12517
  *
12518
+ * That re-queued step is marked `resumed`, which is what stops it being a
12519
+ * verbatim replay. The message the user sends to un-pause a build is usually
12520
+ * the rest of that build ("try again, I fixed the adapter") and Remy does the
12521
+ * work inside that turn — so re-delivering the original "build everything
12522
+ * now" afterwards had it rebuild an app it had just finished.
12523
+ *
12524
+ * `reason: 'shutdown'` is the environment taking the agent down rather than a
12525
+ * person pressing Stop. It pauses identically — nothing may start a turn
12526
+ * while the workspace is being tarred — but tags the chain steps it holds so
12527
+ * the next boot releases them and the pipeline carries on by itself. Called
12528
+ * from the stdin `cancel` (the sandbox's pre-destroy quiesce) and from our own
12529
+ * SIGTERM handler, whichever gets there first.
12530
+ *
12408
12531
  * A compaction is cancelled here too, unconditionally. It gates every queued
12409
12532
  * message and outlives the turn that started it, so leaving it running means
12410
12533
  * Stop can't reach idle. The cost is the summary work in flight; the forced
@@ -12415,14 +12538,16 @@ var init_headless = __esm({
12415
12538
  * `{cancelled, absorbed:true}` terminal. Only items still sitting in the
12416
12539
  * queue survive.
12417
12540
  */
12418
- handleCancel() {
12541
+ handleCancel(reason) {
12419
12542
  if (this.currentAbort) {
12420
- this.currentAbort.abort();
12543
+ this.currentAbort.abort(reason);
12421
12544
  }
12422
12545
  const cancelledCompaction = cancelInflightCompaction();
12423
12546
  for (const [id, pending2] of this.pendingTools) {
12424
12547
  clearTimeout(pending2.timeout);
12425
- pending2.resolve(USER_CANCELLED_RESULT);
12548
+ pending2.resolve(
12549
+ reason === "shutdown" ? ENV_INTERRUPTED_RESULT : USER_CANCELLED_RESULT
12550
+ );
12426
12551
  this.pendingTools.delete(id);
12427
12552
  }
12428
12553
  const flushed = this.queue.removeWhere(
@@ -12433,7 +12558,13 @@ var init_headless = __esm({
12433
12558
  this.queue.unshift({
12434
12559
  command: {
12435
12560
  action: "message",
12436
- text: step.text,
12561
+ // Marked so it comes back as a resumption rather than a replay: the
12562
+ // step already ran once, and whatever the user does between here and
12563
+ // its re-delivery may well be the rest of it. resolveAction turns
12564
+ // this param into a preamble, and both the chat row and the queue
12565
+ // card label the step off it. Merged, so a step interrupted twice
12566
+ // keeps its own params and re-marking stays idempotent.
12567
+ text: setSentinelParams(step.text, { resumed: true }),
12437
12568
  onboardingState: step.onboardingState,
12438
12569
  // Fresh id: the original command's terminal has already gone out as
12439
12570
  // cancelled, and one command gets exactly one `completed`.
@@ -12441,13 +12572,18 @@ var init_headless = __esm({
12441
12572
  },
12442
12573
  source: "chain",
12443
12574
  enqueuedAt: Date.now(),
12444
- held: true
12575
+ held: true,
12576
+ ...reason === "shutdown" && { heldBy: reason }
12445
12577
  });
12446
12578
  this.currentChainStep = null;
12447
12579
  }
12448
- const held = this.queue.holdWhere(
12449
- (item) => item.source === "user" || item.source === "chain"
12450
- );
12580
+ const held = [
12581
+ ...this.queue.holdWhere(
12582
+ (item) => item.source === "chain",
12583
+ reason === "shutdown" ? reason : void 0
12584
+ ),
12585
+ ...this.queue.holdWhere((item) => item.source === "user")
12586
+ ];
12451
12587
  return {
12452
12588
  flushed,
12453
12589
  held,
@@ -12459,9 +12595,10 @@ var init_headless = __esm({
12459
12595
  * Remove pending queued messages: all user messages (no id), or a single item
12460
12596
  * by id. Does not affect the in-flight turn (use `cancel` for that).
12461
12597
  *
12462
- * Held chain items — a paused pipeline — are removable only by explicit id.
12463
- * The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
12464
- * neither of which should get to decide the pipeline's fate. A DELIVERABLE
12598
+ * Held chain items — a paused pipeline — are removable only by explicit id:
12599
+ * the queue card's Discard sends one call per step, and its per-step X sends
12600
+ * one. The id-less form is the card's "Clear", which is about the user's own
12601
+ * messages and shouldn't get to decide the pipeline's fate. A DELIVERABLE
12465
12602
  * chain item is never removable: that's live pipeline work. (The step
12466
12603
  * actually running isn't in the queue at all — drainQueueLoop takes it out
12467
12604
  * before running it.)
@@ -12567,7 +12704,9 @@ var init_headless = __esm({
12567
12704
  return;
12568
12705
  }
12569
12706
  if (action === "cancel") {
12570
- const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
12707
+ const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel(
12708
+ parsed.reason === "shutdown" ? "shutdown" : void 0
12709
+ );
12571
12710
  this.emit(
12572
12711
  "completed",
12573
12712
  {
@@ -132,6 +132,7 @@ remy-admin secrets set ARCHIVE_S3_KEY --prod <value> # the user sets thes
132
132
  remy-admin secrets set ARCHIVE_S3_SECRET --prod <value>
133
133
  remy-admin datasources connect --source archive --bucket acme-docs --region us-east-1 --prefix contracts/ --access-key-secret ARCHIVE_S3_KEY --secret-key-secret ARCHIVE_S3_SECRET --budget-per-sync 5
134
134
  remy-admin datasources sync --source archive --wait # first sync: plans the whole bucket, stops for approval if over the budget
135
+ remy-admin datasources sync --source archive --limit 20000 --concurrency 64 # a slice, run hard: how a big backfill is measured before it is approved
135
136
  remy-admin datasources connector --source archive # what it follows, last sync, object counts
136
137
  ```
137
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.323",
3
+ "version": "0.1.325",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",