@mindstudio-ai/remy 0.1.322 → 0.1.324

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
  }
@@ -2334,11 +2362,23 @@ var bashTool = {
2334
2362
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
2335
2363
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2336
2364
  return new Promise((resolve4) => {
2337
- const child = spawn2("sh", ["-c", input.command], {
2365
+ const child = spawn2("bash", ["-c", input.command], {
2338
2366
  // Pinned rather than inherited. `undefined` here means "wherever the
2339
2367
  // process happens to be", which is the project root only by luck.
2340
2368
  cwd: input.cwd ? path7.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
2341
- env: { ...process.env, FORCE_COLOR: "1" }
2369
+ // Output is rendered in a terminal view in the IDE, so ask tools for
2370
+ // color. The devbox image sets none of these — it declares only
2371
+ // WORKSPACE_DIR, LANG, LC_ALL, PIP_BREAK_SYSTEM_PACKAGES,
2372
+ // NPM_CONFIG_PREFIX and PATH — and a container gets no TERM unless
2373
+ // something sets it, so without TERM anything driving terminfo assumes
2374
+ // a dumb terminal. Same trio the sandbox's pty handler sets, for the
2375
+ // same reason; FORCE_COLOR alone only reaches the Node/chalk ecosystem.
2376
+ env: {
2377
+ ...process.env,
2378
+ TERM: "xterm-256color",
2379
+ CLICOLOR: "1",
2380
+ FORCE_COLOR: "1"
2381
+ }
2342
2382
  });
2343
2383
  let output = "";
2344
2384
  child.stdout.on("data", (chunk) => {
@@ -3374,6 +3414,10 @@ function acquireBrowserLock() {
3374
3414
  // src/toolRegistry.ts
3375
3415
  var log6 = createLogger("tool-registry");
3376
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
+ }
3377
3421
  var ToolRegistry = class {
3378
3422
  entries = /* @__PURE__ */ new Map();
3379
3423
  onEvent;
@@ -3448,6 +3492,20 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
3448
3492
  };
3449
3493
 
3450
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
+ }
3451
3509
  function liftRecording(result) {
3452
3510
  if (!result.includes('"recording"')) {
3453
3511
  return { result };
@@ -3503,6 +3561,18 @@ function capToolResult(result, maxBytes = MAX_TOOL_RESULT_BYTES) {
3503
3561
  }
3504
3562
  var MAX_SUBAGENT_RESULT_BYTES = 32 * 1024;
3505
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
+ }
3506
3576
  function capSubAgentTranscript(messages) {
3507
3577
  for (const msg of messages) {
3508
3578
  capMessageForHistory(msg, MAX_SUBAGENT_RESULT_BYTES);
@@ -3547,7 +3617,7 @@ function capMessageForHistory(msg, maxBytes = MAX_TOOL_RESULT_BYTES) {
3547
3617
  );
3548
3618
  }
3549
3619
  if (Array.isArray(block.subAgentMessages)) {
3550
- block.subAgentMessages = capSubAgentTranscript(block.subAgentMessages);
3620
+ attachSubAgentTranscript(block, block.subAgentMessages);
3551
3621
  }
3552
3622
  }
3553
3623
  } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
@@ -3892,7 +3962,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3892
3962
  messages: thisInvocation()
3893
3963
  };
3894
3964
  }
3895
- return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3965
+ return { text: cancelledToolResult(signal), messages: thisInvocation() };
3896
3966
  }
3897
3967
  let lastToolResult = "";
3898
3968
  let watchedBlocks = [];
@@ -4128,7 +4198,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4128
4198
  if (signal?.aborted) {
4129
4199
  return {
4130
4200
  id: tc.id,
4131
- result: USER_CANCELLED_RESULT,
4201
+ result: cancelledToolResult(signal),
4132
4202
  isError: true
4133
4203
  };
4134
4204
  }
@@ -4245,7 +4315,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4245
4315
  }
4246
4316
  const innerMsgs = subAgentMessages.get(r.id);
4247
4317
  if (innerMsgs) {
4248
- block.subAgentMessages = capSubAgentTranscript(innerMsgs);
4318
+ attachSubAgentTranscript(block, innerMsgs);
4249
4319
  }
4250
4320
  if (captureArtifacts?.includes(block.name) && !r.isError) {
4251
4321
  try {
@@ -4787,13 +4857,12 @@ async function runBrowserAutomation(task, context, opts) {
4787
4857
  },
4788
4858
  toolRegistry: context.toolRegistry
4789
4859
  });
4790
- context.subAgentMessages?.set(context.toolCallId, result.messages);
4791
- const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4792
- const recorded = result.messages.some(
4793
- (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
4794
- (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
4795
- )
4860
+ context.subAgentMessages?.set(
4861
+ context.toolCallId,
4862
+ dropToolResultMessages(result.messages)
4796
4863
  );
4864
+ const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
4865
+ const recorded = collectRecordings(result.messages).length > 0;
4797
4866
  return {
4798
4867
  text: result.text,
4799
4868
  recorded,
@@ -4948,6 +5017,79 @@ var screenshotTool = {
4948
5017
  execute: (input, context) => executeScreenshot(input, context?.onLog, context)
4949
5018
  };
4950
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
+
4951
5093
  // src/subagents/research/tools.ts
4952
5094
  var searchGoogleDefinition = {
4953
5095
  name: "searchGoogle",
@@ -5007,22 +5149,6 @@ async function executeSearchGoogle(input, onLog, caller) {
5007
5149
  }
5008
5150
  );
5009
5151
  }
5010
- async function executeScrapeWebUrl(input, onLog, caller) {
5011
- return runMindstudioCli(
5012
- [
5013
- "scrape-url",
5014
- "--url",
5015
- String(input.url),
5016
- "--page-options",
5017
- JSON.stringify({ onlyMainContent: true })
5018
- ],
5019
- {
5020
- maxBuffer: SCRAPE_MAX_BUFFER,
5021
- onLog,
5022
- caller
5023
- }
5024
- );
5025
- }
5026
5152
 
5027
5153
  // src/subagents/research/index.ts
5028
5154
  var BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
@@ -5048,7 +5174,11 @@ async function runResearch(task, context) {
5048
5174
  return executeSearchGoogle(toolInput, childCtx.onLog, "research");
5049
5175
  }
5050
5176
  if (name === "scrapeWebUrl") {
5051
- return executeScrapeWebUrl(toolInput, childCtx.onLog, "research");
5177
+ return fetchWebPage(String(toolInput.url), {
5178
+ screenshot: false,
5179
+ caller: "research",
5180
+ onLog: childCtx.onLog
5181
+ });
5052
5182
  }
5053
5183
  return executeTool(name, toolInput, childCtx);
5054
5184
  },
@@ -5096,7 +5226,7 @@ __export(scrapeWebUrl_exports, {
5096
5226
  });
5097
5227
  var definition = {
5098
5228
  name: "scrapeWebUrl",
5099
- 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.",
5100
5230
  inputSchema: {
5101
5231
  type: "object",
5102
5232
  properties: {
@@ -5109,17 +5239,11 @@ var definition = {
5109
5239
  }
5110
5240
  };
5111
5241
  async function execute(input, onLog) {
5112
- const pageOptions = { onlyMainContent: true };
5113
- return runMindstudioCli(
5114
- [
5115
- "scrape-url",
5116
- "--url",
5117
- input.url,
5118
- "--page-options",
5119
- JSON.stringify(pageOptions)
5120
- ],
5121
- { onLog, caller: "designExpert", maxBuffer: SCRAPE_MAX_BUFFER }
5122
- );
5242
+ return fetchWebPage(String(input.url), {
5243
+ screenshot: true,
5244
+ caller: "designExpert",
5245
+ onLog
5246
+ });
5123
5247
  }
5124
5248
 
5125
5249
  // src/subagents/designExpert/tools/analyzeDesign.ts
@@ -7300,50 +7424,6 @@ var reviewExistingProjectTool = {
7300
7424
  }
7301
7425
  };
7302
7426
 
7303
- // src/tools/common/scrapeWebUrl.ts
7304
- var scrapeWebUrlTool = {
7305
- definition: {
7306
- name: "scrapeWebUrl",
7307
- 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",
7308
- inputSchema: {
7309
- type: "object",
7310
- properties: {
7311
- url: {
7312
- type: "string",
7313
- description: "The URL to fetch."
7314
- },
7315
- screenshot: {
7316
- type: "boolean",
7317
- 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."
7318
- }
7319
- },
7320
- required: ["url"]
7321
- }
7322
- },
7323
- async execute(input, context) {
7324
- const url = input.url;
7325
- const screenshot = input.screenshot;
7326
- const pageOptions = { onlyMainContent: true };
7327
- if (screenshot) {
7328
- pageOptions.screenshot = true;
7329
- }
7330
- return runMindstudioCli(
7331
- [
7332
- "scrape-url",
7333
- "--url",
7334
- url,
7335
- "--page-options",
7336
- JSON.stringify(pageOptions)
7337
- ],
7338
- {
7339
- onLog: context?.onLog,
7340
- maxBuffer: SCRAPE_MAX_BUFFER,
7341
- caller: "parent"
7342
- }
7343
- );
7344
- }
7345
- };
7346
-
7347
7427
  // src/tools/index.ts
7348
7428
  function deriveContext(parent, toolCallId, onLog) {
7349
7429
  return { ...parent, toolCallId, onLog };
@@ -8829,17 +8909,11 @@ function resolveAction(text) {
8829
8909
  if (!parsed) {
8830
8910
  return null;
8831
8911
  }
8832
- const { name: triggerName, remainder } = parsed;
8912
+ const { name: triggerName } = parsed;
8833
8913
  if (NON_ACTION_SENTINELS.has(triggerName)) {
8834
8914
  return null;
8835
8915
  }
8836
- let params = {};
8837
- if (remainder) {
8838
- try {
8839
- params = JSON.parse(remainder.split("\n")[0]);
8840
- } catch {
8841
- }
8842
- }
8916
+ const params = sentinelParams(text);
8843
8917
  let body = readAsset("automatedActions", `${triggerName}.md`);
8844
8918
  let next;
8845
8919
  const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
@@ -8854,8 +8928,16 @@ function resolveAction(text) {
8854
8928
  const str = typeof value === "string" ? value : JSON.stringify(value);
8855
8929
  body = body.replaceAll(`{{${key}}}`, str);
8856
8930
  }
8931
+ const resumed = params.resumed === true;
8932
+ if (resumed) {
8933
+ body = `${readAsset("automatedActions", "_resumed.md")}
8934
+
8935
+ ${body}`;
8936
+ }
8857
8937
  return {
8858
- message: automatedMessage(triggerName, body),
8938
+ message: resumed ? setSentinelParams(automatedMessage(triggerName, body), {
8939
+ resumed: true
8940
+ }) : automatedMessage(triggerName, body),
8859
8941
  next
8860
8942
  };
8861
8943
  }
@@ -9238,7 +9320,10 @@ async function runTurn(params) {
9238
9320
  const automated = parseSentinel(entry.text);
9239
9321
  if (automated) {
9240
9322
  if (!NON_ACTION_SENTINELS.has(automated.name)) {
9241
- 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
+ );
9242
9327
  hasUserSignal = true;
9243
9328
  }
9244
9329
  } else if (entry.text) {
@@ -9627,7 +9712,11 @@ async function runTurn(params) {
9627
9712
  const results = await Promise.all(
9628
9713
  toolCalls.map(async (tc) => {
9629
9714
  if (signal?.aborted) {
9630
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
9715
+ return {
9716
+ id: tc.id,
9717
+ result: cancelledToolResult(signal),
9718
+ isError: true
9719
+ };
9631
9720
  }
9632
9721
  const toolStart = Date.now();
9633
9722
  let settle;
@@ -9646,7 +9735,7 @@ async function runTurn(params) {
9646
9735
  };
9647
9736
  const cascadeAbort = () => {
9648
9737
  toolAbort.abort();
9649
- safeSettle(USER_CANCELLED_RESULT, true);
9738
+ safeSettle(cancelledToolResult(signal), true);
9650
9739
  };
9651
9740
  signal?.addEventListener("abort", cascadeAbort, { once: true });
9652
9741
  const run = async (input) => {
@@ -9755,7 +9844,7 @@ async function runTurn(params) {
9755
9844
  block.completedAt = Date.now();
9756
9845
  const msgs = subAgentMessages.get(r.id);
9757
9846
  if (msgs) {
9758
- block.subAgentMessages = capSubAgentTranscript(msgs);
9847
+ attachSubAgentTranscript(block, msgs);
9759
9848
  }
9760
9849
  }
9761
9850
  }
@@ -10007,6 +10096,15 @@ function holdRestoredUserItems(items) {
10007
10096
  (item) => item.source === "user" ? { ...item, held: true } : item
10008
10097
  );
10009
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
+ }
10010
10108
  var MessageQueue = class {
10011
10109
  items = [];
10012
10110
  onChange;
@@ -10078,16 +10176,27 @@ var MessageQueue = class {
10078
10176
  /**
10079
10177
  * Mark matching items `held` — waiting on the user rather than on the agent.
10080
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.
10081
10185
  */
10082
- holdWhere(predicate) {
10186
+ holdWhere(predicate, heldBy) {
10083
10187
  const held = [];
10084
10188
  let changed = false;
10085
10189
  for (const item of this.items) {
10086
10190
  if (!predicate(item)) {
10087
10191
  continue;
10088
10192
  }
10089
- changed = changed || !item.held;
10090
- item.held = true;
10193
+ if (!item.held) {
10194
+ changed = true;
10195
+ item.held = true;
10196
+ if (heldBy) {
10197
+ item.heldBy = heldBy;
10198
+ }
10199
+ }
10091
10200
  held.push(item);
10092
10201
  }
10093
10202
  if (changed) {
@@ -10135,6 +10244,7 @@ var MessageQueue = class {
10135
10244
  const [item] = this.items.splice(idx, 1);
10136
10245
  item.delivery = "asap";
10137
10246
  delete item.held;
10247
+ delete item.heldBy;
10138
10248
  this.items.unshift(item);
10139
10249
  this.onChange?.();
10140
10250
  return item;
@@ -10158,6 +10268,7 @@ var MessageQueue = class {
10158
10268
  continue;
10159
10269
  }
10160
10270
  delete item.held;
10271
+ delete item.heldBy;
10161
10272
  released.push(item);
10162
10273
  }
10163
10274
  const back = this.items.filter(defer);
@@ -10287,10 +10398,13 @@ var HeadlessSession = class {
10287
10398
  });
10288
10399
  await initOrgContext(this.config);
10289
10400
  const resumed = loadSession(this.state);
10290
- this.queue = new MessageQueue(holdRestoredUserItems(loadQueue()), () => {
10291
- this.persistStats();
10292
- this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
10293
- });
10401
+ this.queue = new MessageQueue(
10402
+ releaseShutdownHolds(holdRestoredUserItems(loadQueue())),
10403
+ () => {
10404
+ this.persistStats();
10405
+ this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
10406
+ }
10407
+ );
10294
10408
  this.passivePen = loadPassiveResults();
10295
10409
  this.persistStats();
10296
10410
  if (resumed) {
@@ -10412,6 +10526,11 @@ var HeadlessSession = class {
10412
10526
  this.emit("ready");
10413
10527
  }
10414
10528
  shutdown = () => {
10529
+ try {
10530
+ this.handleCancel("shutdown");
10531
+ } catch (err) {
10532
+ log17.warn("Shutdown cancel failed", { error: err?.message });
10533
+ }
10415
10534
  this.emit("stopping");
10416
10535
  this.emit("stopped");
10417
10536
  process.exit(0);
@@ -11349,6 +11468,19 @@ var HeadlessSession = class {
11349
11468
  * pipeline where it stopped. Holding only the remainder would resume one step
11350
11469
  * PAST the interruption, polishing and finalizing half-built code.
11351
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
+ *
11352
11484
  * A compaction is cancelled here too, unconditionally. It gates every queued
11353
11485
  * message and outlives the turn that started it, so leaving it running means
11354
11486
  * Stop can't reach idle. The cost is the summary work in flight; the forced
@@ -11359,14 +11491,16 @@ var HeadlessSession = class {
11359
11491
  * `{cancelled, absorbed:true}` terminal. Only items still sitting in the
11360
11492
  * queue survive.
11361
11493
  */
11362
- handleCancel() {
11494
+ handleCancel(reason) {
11363
11495
  if (this.currentAbort) {
11364
- this.currentAbort.abort();
11496
+ this.currentAbort.abort(reason);
11365
11497
  }
11366
11498
  const cancelledCompaction = cancelInflightCompaction();
11367
11499
  for (const [id, pending2] of this.pendingTools) {
11368
11500
  clearTimeout(pending2.timeout);
11369
- pending2.resolve(USER_CANCELLED_RESULT);
11501
+ pending2.resolve(
11502
+ reason === "shutdown" ? ENV_INTERRUPTED_RESULT : USER_CANCELLED_RESULT
11503
+ );
11370
11504
  this.pendingTools.delete(id);
11371
11505
  }
11372
11506
  const flushed = this.queue.removeWhere(
@@ -11377,7 +11511,13 @@ var HeadlessSession = class {
11377
11511
  this.queue.unshift({
11378
11512
  command: {
11379
11513
  action: "message",
11380
- 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 }),
11381
11521
  onboardingState: step.onboardingState,
11382
11522
  // Fresh id: the original command's terminal has already gone out as
11383
11523
  // cancelled, and one command gets exactly one `completed`.
@@ -11385,13 +11525,18 @@ var HeadlessSession = class {
11385
11525
  },
11386
11526
  source: "chain",
11387
11527
  enqueuedAt: Date.now(),
11388
- held: true
11528
+ held: true,
11529
+ ...reason === "shutdown" && { heldBy: reason }
11389
11530
  });
11390
11531
  this.currentChainStep = null;
11391
11532
  }
11392
- const held = this.queue.holdWhere(
11393
- (item) => item.source === "user" || item.source === "chain"
11394
- );
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
+ ];
11395
11540
  return {
11396
11541
  flushed,
11397
11542
  held,
@@ -11403,9 +11548,10 @@ var HeadlessSession = class {
11403
11548
  * Remove pending queued messages: all user messages (no id), or a single item
11404
11549
  * by id. Does not affect the in-flight turn (use `cancel` for that).
11405
11550
  *
11406
- * Held chain items — a paused pipeline — are removable only by explicit id.
11407
- * The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
11408
- * 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
11409
11555
  * chain item is never removable: that's live pipeline work. (The step
11410
11556
  * actually running isn't in the queue at all — drainQueueLoop takes it out
11411
11557
  * before running it.)
@@ -11511,7 +11657,9 @@ var HeadlessSession = class {
11511
11657
  return;
11512
11658
  }
11513
11659
  if (action === "cancel") {
11514
- const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
11660
+ const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel(
11661
+ parsed.reason === "shutdown" ? "shutdown" : void 0
11662
+ );
11515
11663
  this.emit(
11516
11664
  "completed",
11517
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") {
@@ -3519,11 +3573,23 @@ var init_bash = __esm({
3519
3573
  const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES2;
3520
3574
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
3521
3575
  return new Promise((resolve4) => {
3522
- const child = spawn2("sh", ["-c", input.command], {
3576
+ const child = spawn2("bash", ["-c", input.command], {
3523
3577
  // Pinned rather than inherited. `undefined` here means "wherever the
3524
3578
  // process happens to be", which is the project root only by luck.
3525
3579
  cwd: input.cwd ? path7.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
3526
- env: { ...process.env, FORCE_COLOR: "1" }
3580
+ // Output is rendered in a terminal view in the IDE, so ask tools for
3581
+ // color. The devbox image sets none of these — it declares only
3582
+ // WORKSPACE_DIR, LANG, LC_ALL, PIP_BREAK_SYSTEM_PACKAGES,
3583
+ // NPM_CONFIG_PREFIX and PATH — and a container gets no TERM unless
3584
+ // something sets it, so without TERM anything driving terminfo assumes
3585
+ // a dumb terminal. Same trio the sandbox's pty handler sets, for the
3586
+ // same reason; FORCE_COLOR alone only reaches the Node/chalk ecosystem.
3587
+ env: {
3588
+ ...process.env,
3589
+ TERM: "xterm-256color",
3590
+ CLICOLOR: "1",
3591
+ FORCE_COLOR: "1"
3592
+ }
3527
3593
  });
3528
3594
  let output = "";
3529
3595
  child.stdout.on("data", (chunk) => {
@@ -4654,13 +4720,17 @@ var init_browserLock = __esm({
4654
4720
  });
4655
4721
 
4656
4722
  // src/toolRegistry.ts
4657
- 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;
4658
4727
  var init_toolRegistry = __esm({
4659
4728
  "src/toolRegistry.ts"() {
4660
4729
  "use strict";
4661
4730
  init_logger();
4662
4731
  log7 = createLogger("tool-registry");
4663
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.";
4664
4734
  ToolRegistry = class {
4665
4735
  entries = /* @__PURE__ */ new Map();
4666
4736
  onEvent;
@@ -4916,7 +4986,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
4916
4986
  messages: thisInvocation()
4917
4987
  };
4918
4988
  }
4919
- return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
4989
+ return { text: cancelledToolResult(signal), messages: thisInvocation() };
4920
4990
  }
4921
4991
  let lastToolResult = "";
4922
4992
  let watchedBlocks = [];
@@ -5152,7 +5222,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5152
5222
  if (signal?.aborted) {
5153
5223
  return {
5154
5224
  id: tc.id,
5155
- result: USER_CANCELLED_RESULT,
5225
+ result: cancelledToolResult(signal),
5156
5226
  isError: true
5157
5227
  };
5158
5228
  }
@@ -5269,7 +5339,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
5269
5339
  }
5270
5340
  const innerMsgs = subAgentMessages.get(r.id);
5271
5341
  if (innerMsgs) {
5272
- block.subAgentMessages = capSubAgentTranscript(innerMsgs);
5342
+ attachSubAgentTranscript(block, innerMsgs);
5273
5343
  }
5274
5344
  if (captureArtifacts?.includes(block.name) && !r.isError) {
5275
5345
  try {
@@ -5855,13 +5925,12 @@ async function runBrowserAutomation(task, context, opts) {
5855
5925
  },
5856
5926
  toolRegistry: context.toolRegistry
5857
5927
  });
5858
- context.subAgentMessages?.set(context.toolCallId, result.messages);
5859
- const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5860
- const recorded = result.messages.some(
5861
- (m) => m.role === "assistant" && Array.isArray(m.content) && m.content.some(
5862
- (b) => b.type === "tool" && b.name === "browserCommand" && !!b.recording
5863
- )
5928
+ context.subAgentMessages?.set(
5929
+ context.toolCallId,
5930
+ dropToolResultMessages(result.messages)
5864
5931
  );
5932
+ const preferred = opts?.capture === "viewport" ? lastCapture.viewport ?? lastCapture.fullPage : lastCapture.fullPage ?? lastCapture.viewport;
5933
+ const recorded = collectRecordings(result.messages).length > 0;
5865
5934
  return {
5866
5935
  text: result.text,
5867
5936
  recorded,
@@ -5877,6 +5946,8 @@ var init_browserAutomation = __esm({
5877
5946
  "use strict";
5878
5947
  init_tools10();
5879
5948
  init_runner();
5949
+ init_historyLimits();
5950
+ init_recording();
5880
5951
  init_tools2();
5881
5952
  init_tools();
5882
5953
  init_readSpec();
@@ -6046,6 +6117,88 @@ var init_screenshot2 = __esm({
6046
6117
  }
6047
6118
  });
6048
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
+
6049
6202
  // src/subagents/research/tools.ts
6050
6203
  async function executeSearchGoogle(input, onLog, caller) {
6051
6204
  const fetchTopN = Math.max(0, Math.round(Number(input.fetchTopN) || 0));
@@ -6067,22 +6220,6 @@ async function executeSearchGoogle(input, onLog, caller) {
6067
6220
  }
6068
6221
  );
6069
6222
  }
6070
- async function executeScrapeWebUrl(input, onLog, caller) {
6071
- return runMindstudioCli(
6072
- [
6073
- "scrape-url",
6074
- "--url",
6075
- String(input.url),
6076
- "--page-options",
6077
- JSON.stringify({ onlyMainContent: true })
6078
- ],
6079
- {
6080
- maxBuffer: SCRAPE_MAX_BUFFER,
6081
- onLog,
6082
- caller
6083
- }
6084
- );
6085
- }
6086
6223
  var searchGoogleDefinition, scrapeWebUrlDefinition, RESEARCH_TOOLS;
6087
6224
  var init_tools3 = __esm({
6088
6225
  "src/subagents/research/tools.ts"() {
@@ -6155,7 +6292,11 @@ async function runResearch(task, context) {
6155
6292
  return executeSearchGoogle(toolInput, childCtx.onLog, "research");
6156
6293
  }
6157
6294
  if (name === "scrapeWebUrl") {
6158
- return executeScrapeWebUrl(toolInput, childCtx.onLog, "research");
6295
+ return fetchWebPage(String(toolInput.url), {
6296
+ screenshot: false,
6297
+ caller: "research",
6298
+ onLog: childCtx.onLog
6299
+ });
6159
6300
  }
6160
6301
  return executeTool(name, toolInput, childCtx);
6161
6302
  },
@@ -6180,6 +6321,7 @@ var init_research = __esm({
6180
6321
  init_runner();
6181
6322
  init_context();
6182
6323
  init_tools10();
6324
+ init_scrapeWebUrl();
6183
6325
  init_tools3();
6184
6326
  init_surfaces();
6185
6327
  BASE_PROMPT2 = readAsset("subagents/research", "prompt.md");
@@ -6215,27 +6357,20 @@ __export(scrapeWebUrl_exports, {
6215
6357
  execute: () => execute
6216
6358
  });
6217
6359
  async function execute(input, onLog) {
6218
- const pageOptions = { onlyMainContent: true };
6219
- return runMindstudioCli(
6220
- [
6221
- "scrape-url",
6222
- "--url",
6223
- input.url,
6224
- "--page-options",
6225
- JSON.stringify(pageOptions)
6226
- ],
6227
- { onLog, caller: "designExpert", maxBuffer: SCRAPE_MAX_BUFFER }
6228
- );
6360
+ return fetchWebPage(String(input.url), {
6361
+ screenshot: true,
6362
+ caller: "designExpert",
6363
+ onLog
6364
+ });
6229
6365
  }
6230
6366
  var definition;
6231
- var init_scrapeWebUrl = __esm({
6367
+ var init_scrapeWebUrl2 = __esm({
6232
6368
  "src/subagents/designExpert/tools/scrapeWebUrl.ts"() {
6233
6369
  "use strict";
6234
- init_runMindstudioCli();
6235
- init_runCli();
6370
+ init_scrapeWebUrl();
6236
6371
  definition = {
6237
6372
  name: "scrapeWebUrl",
6238
- 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.",
6239
6374
  inputSchema: {
6240
6375
  type: "object",
6241
6376
  properties: {
@@ -7380,7 +7515,7 @@ var init_tools5 = __esm({
7380
7515
  init_tools10();
7381
7516
  init_tools();
7382
7517
  init_research();
7383
- init_scrapeWebUrl();
7518
+ init_scrapeWebUrl2();
7384
7519
  init_analyzeDesign();
7385
7520
  init_analyzeImage2();
7386
7521
  init_generateImages();
@@ -8817,58 +8952,6 @@ var init_reviewExistingProject = __esm({
8817
8952
  }
8818
8953
  });
8819
8954
 
8820
- // src/tools/common/scrapeWebUrl.ts
8821
- var scrapeWebUrlTool;
8822
- var init_scrapeWebUrl2 = __esm({
8823
- "src/tools/common/scrapeWebUrl.ts"() {
8824
- "use strict";
8825
- init_runMindstudioCli();
8826
- init_runCli();
8827
- scrapeWebUrlTool = {
8828
- definition: {
8829
- name: "scrapeWebUrl",
8830
- 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",
8831
- inputSchema: {
8832
- type: "object",
8833
- properties: {
8834
- url: {
8835
- type: "string",
8836
- description: "The URL to fetch."
8837
- },
8838
- screenshot: {
8839
- type: "boolean",
8840
- 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."
8841
- }
8842
- },
8843
- required: ["url"]
8844
- }
8845
- },
8846
- async execute(input, context) {
8847
- const url = input.url;
8848
- const screenshot = input.screenshot;
8849
- const pageOptions = { onlyMainContent: true };
8850
- if (screenshot) {
8851
- pageOptions.screenshot = true;
8852
- }
8853
- return runMindstudioCli(
8854
- [
8855
- "scrape-url",
8856
- "--url",
8857
- url,
8858
- "--page-options",
8859
- JSON.stringify(pageOptions)
8860
- ],
8861
- {
8862
- onLog: context?.onLog,
8863
- maxBuffer: SCRAPE_MAX_BUFFER,
8864
- caller: "parent"
8865
- }
8866
- );
8867
- }
8868
- };
8869
- }
8870
- });
8871
-
8872
8955
  // src/tools/index.ts
8873
8956
  function deriveContext(parent, toolCallId, onLog) {
8874
8957
  return { ...parent, toolCallId, onLog };
@@ -8927,7 +9010,7 @@ var init_tools10 = __esm({
8927
9010
  init_specSync();
8928
9011
  init_research();
8929
9012
  init_reviewExistingProject();
8930
- init_scrapeWebUrl2();
9013
+ init_scrapeWebUrl();
8931
9014
  init_writeBuildOverview();
8932
9015
  ALL_TOOLS = [
8933
9016
  // Common
@@ -9167,17 +9250,11 @@ function resolveAction(text) {
9167
9250
  if (!parsed) {
9168
9251
  return null;
9169
9252
  }
9170
- const { name: triggerName, remainder } = parsed;
9253
+ const { name: triggerName } = parsed;
9171
9254
  if (NON_ACTION_SENTINELS.has(triggerName)) {
9172
9255
  return null;
9173
9256
  }
9174
- let params = {};
9175
- if (remainder) {
9176
- try {
9177
- params = JSON.parse(remainder.split("\n")[0]);
9178
- } catch {
9179
- }
9180
- }
9257
+ const params = sentinelParams(text);
9181
9258
  let body = readAsset("automatedActions", `${triggerName}.md`);
9182
9259
  let next;
9183
9260
  const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
@@ -9192,8 +9269,16 @@ function resolveAction(text) {
9192
9269
  const str = typeof value === "string" ? value : JSON.stringify(value);
9193
9270
  body = body.replaceAll(`{{${key}}}`, str);
9194
9271
  }
9272
+ const resumed = params.resumed === true;
9273
+ if (resumed) {
9274
+ body = `${readAsset("automatedActions", "_resumed.md")}
9275
+
9276
+ ${body}`;
9277
+ }
9195
9278
  return {
9196
- message: automatedMessage(triggerName, body),
9279
+ message: resumed ? setSentinelParams(automatedMessage(triggerName, body), {
9280
+ resumed: true
9281
+ }) : automatedMessage(triggerName, body),
9197
9282
  next
9198
9283
  };
9199
9284
  }
@@ -9938,7 +10023,10 @@ async function runTurn(params) {
9938
10023
  const automated = parseSentinel(entry.text);
9939
10024
  if (automated) {
9940
10025
  if (!NON_ACTION_SENTINELS.has(automated.name)) {
9941
- 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
+ );
9942
10030
  hasUserSignal = true;
9943
10031
  }
9944
10032
  } else if (entry.text) {
@@ -10327,7 +10415,11 @@ async function runTurn(params) {
10327
10415
  const results = await Promise.all(
10328
10416
  toolCalls.map(async (tc) => {
10329
10417
  if (signal?.aborted) {
10330
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
10418
+ return {
10419
+ id: tc.id,
10420
+ result: cancelledToolResult(signal),
10421
+ isError: true
10422
+ };
10331
10423
  }
10332
10424
  const toolStart = Date.now();
10333
10425
  let settle;
@@ -10346,7 +10438,7 @@ async function runTurn(params) {
10346
10438
  };
10347
10439
  const cascadeAbort = () => {
10348
10440
  toolAbort.abort();
10349
- safeSettle(USER_CANCELLED_RESULT, true);
10441
+ safeSettle(cancelledToolResult(signal), true);
10350
10442
  };
10351
10443
  signal?.addEventListener("abort", cascadeAbort, { once: true });
10352
10444
  const run = async (input) => {
@@ -10455,7 +10547,7 @@ async function runTurn(params) {
10455
10547
  block.completedAt = Date.now();
10456
10548
  const msgs = subAgentMessages.get(r.id);
10457
10549
  if (msgs) {
10458
- block.subAgentMessages = capSubAgentTranscript(msgs);
10550
+ attachSubAgentTranscript(block, msgs);
10459
10551
  }
10460
10552
  }
10461
10553
  }
@@ -11019,6 +11111,15 @@ function holdRestoredUserItems(items) {
11019
11111
  (item) => item.source === "user" ? { ...item, held: true } : item
11020
11112
  );
11021
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
+ }
11022
11123
  var MessageQueue;
11023
11124
  var init_messageQueue = __esm({
11024
11125
  "src/headless/messageQueue.ts"() {
@@ -11094,16 +11195,27 @@ var init_messageQueue = __esm({
11094
11195
  /**
11095
11196
  * Mark matching items `held` — waiting on the user rather than on the agent.
11096
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.
11097
11204
  */
11098
- holdWhere(predicate) {
11205
+ holdWhere(predicate, heldBy) {
11099
11206
  const held = [];
11100
11207
  let changed = false;
11101
11208
  for (const item of this.items) {
11102
11209
  if (!predicate(item)) {
11103
11210
  continue;
11104
11211
  }
11105
- changed = changed || !item.held;
11106
- item.held = true;
11212
+ if (!item.held) {
11213
+ changed = true;
11214
+ item.held = true;
11215
+ if (heldBy) {
11216
+ item.heldBy = heldBy;
11217
+ }
11218
+ }
11107
11219
  held.push(item);
11108
11220
  }
11109
11221
  if (changed) {
@@ -11151,6 +11263,7 @@ var init_messageQueue = __esm({
11151
11263
  const [item] = this.items.splice(idx, 1);
11152
11264
  item.delivery = "asap";
11153
11265
  delete item.held;
11266
+ delete item.heldBy;
11154
11267
  this.items.unshift(item);
11155
11268
  this.onChange?.();
11156
11269
  return item;
@@ -11174,6 +11287,7 @@ var init_messageQueue = __esm({
11174
11287
  continue;
11175
11288
  }
11176
11289
  delete item.held;
11290
+ delete item.heldBy;
11177
11291
  released.push(item);
11178
11292
  }
11179
11293
  const back = this.items.filter(defer);
@@ -11331,10 +11445,13 @@ var init_headless = __esm({
11331
11445
  });
11332
11446
  await initOrgContext(this.config);
11333
11447
  const resumed = loadSession(this.state);
11334
- this.queue = new MessageQueue(holdRestoredUserItems(loadQueue()), () => {
11335
- this.persistStats();
11336
- this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
11337
- });
11448
+ this.queue = new MessageQueue(
11449
+ releaseShutdownHolds(holdRestoredUserItems(loadQueue())),
11450
+ () => {
11451
+ this.persistStats();
11452
+ this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
11453
+ }
11454
+ );
11338
11455
  this.passivePen = loadPassiveResults();
11339
11456
  this.persistStats();
11340
11457
  if (resumed) {
@@ -11456,6 +11573,11 @@ var init_headless = __esm({
11456
11573
  this.emit("ready");
11457
11574
  }
11458
11575
  shutdown = () => {
11576
+ try {
11577
+ this.handleCancel("shutdown");
11578
+ } catch (err) {
11579
+ log17.warn("Shutdown cancel failed", { error: err?.message });
11580
+ }
11459
11581
  this.emit("stopping");
11460
11582
  this.emit("stopped");
11461
11583
  process.exit(0);
@@ -12393,6 +12515,19 @@ var init_headless = __esm({
12393
12515
  * pipeline where it stopped. Holding only the remainder would resume one step
12394
12516
  * PAST the interruption, polishing and finalizing half-built code.
12395
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
+ *
12396
12531
  * A compaction is cancelled here too, unconditionally. It gates every queued
12397
12532
  * message and outlives the turn that started it, so leaving it running means
12398
12533
  * Stop can't reach idle. The cost is the summary work in flight; the forced
@@ -12403,14 +12538,16 @@ var init_headless = __esm({
12403
12538
  * `{cancelled, absorbed:true}` terminal. Only items still sitting in the
12404
12539
  * queue survive.
12405
12540
  */
12406
- handleCancel() {
12541
+ handleCancel(reason) {
12407
12542
  if (this.currentAbort) {
12408
- this.currentAbort.abort();
12543
+ this.currentAbort.abort(reason);
12409
12544
  }
12410
12545
  const cancelledCompaction = cancelInflightCompaction();
12411
12546
  for (const [id, pending2] of this.pendingTools) {
12412
12547
  clearTimeout(pending2.timeout);
12413
- pending2.resolve(USER_CANCELLED_RESULT);
12548
+ pending2.resolve(
12549
+ reason === "shutdown" ? ENV_INTERRUPTED_RESULT : USER_CANCELLED_RESULT
12550
+ );
12414
12551
  this.pendingTools.delete(id);
12415
12552
  }
12416
12553
  const flushed = this.queue.removeWhere(
@@ -12421,7 +12558,13 @@ var init_headless = __esm({
12421
12558
  this.queue.unshift({
12422
12559
  command: {
12423
12560
  action: "message",
12424
- 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 }),
12425
12568
  onboardingState: step.onboardingState,
12426
12569
  // Fresh id: the original command's terminal has already gone out as
12427
12570
  // cancelled, and one command gets exactly one `completed`.
@@ -12429,13 +12572,18 @@ var init_headless = __esm({
12429
12572
  },
12430
12573
  source: "chain",
12431
12574
  enqueuedAt: Date.now(),
12432
- held: true
12575
+ held: true,
12576
+ ...reason === "shutdown" && { heldBy: reason }
12433
12577
  });
12434
12578
  this.currentChainStep = null;
12435
12579
  }
12436
- const held = this.queue.holdWhere(
12437
- (item) => item.source === "user" || item.source === "chain"
12438
- );
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
+ ];
12439
12587
  return {
12440
12588
  flushed,
12441
12589
  held,
@@ -12447,9 +12595,10 @@ var init_headless = __esm({
12447
12595
  * Remove pending queued messages: all user messages (no id), or a single item
12448
12596
  * by id. Does not affect the in-flight turn (use `cancel` for that).
12449
12597
  *
12450
- * Held chain items — a paused pipeline — are removable only by explicit id.
12451
- * The id-less form is the queue card's "Clear" and the pre-destroy quiesce,
12452
- * 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
12453
12602
  * chain item is never removable: that's live pipeline work. (The step
12454
12603
  * actually running isn't in the queue at all — drainQueueLoop takes it out
12455
12604
  * before running it.)
@@ -12555,7 +12704,9 @@ var init_headless = __esm({
12555
12704
  return;
12556
12705
  }
12557
12706
  if (action === "cancel") {
12558
- const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel();
12707
+ const { flushed, held, pausedPipeline, cancelledCompaction } = this.handleCancel(
12708
+ parsed.reason === "shutdown" ? "shutdown" : void 0
12709
+ );
12559
12710
  this.emit(
12560
12711
  "completed",
12561
12712
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindstudio-ai/remy",
3
- "version": "0.1.322",
3
+ "version": "0.1.324",
4
4
  "description": "Remy coding agent",
5
5
  "repository": {
6
6
  "type": "git",