@mindstudio-ai/remy 0.1.147 → 0.1.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -830,7 +830,7 @@ var init_writePlan = __esm({
830
830
  clearable: false,
831
831
  definition: {
832
832
  name: "writePlan",
833
- description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is saved to .remy-plan.md and the user can review, discuss, and approve or reject it. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
833
+ description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is displayed standalone in the UI with approve/reject buttons, so write only the plan itself \u2014 no conversational text, no 'what do you think?', no next-steps narration. Say those things in your chat message instead. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
834
834
  inputSchema: {
835
835
  type: "object",
836
836
  properties: {
@@ -912,13 +912,13 @@ var init_setProjectOnboardingState = __esm({
912
912
  clearable: false,
913
913
  definition: {
914
914
  name: "setProjectOnboardingState",
915
- description: "Advance the project onboarding state. Call at natural transition points: after writing the first draft of the spec (initialSpecReview), before starting the first code generation (initialCodegen), after the first build succeeds (onboardingFinished). Forward-only progression.",
915
+ description: "Advance the project onboarding state. Only call this when an automated action explicitly instructs you to \u2014 calling it at the wrong time skips stages the user hasn't experienced. Forward-only: building \u2192 buildComplete \u2192 onboardingFinished. `onboardingFinished` is set by the frontend after the user dismisses the reveal; do not call it yourself.",
916
916
  inputSchema: {
917
917
  type: "object",
918
918
  properties: {
919
919
  state: {
920
920
  type: "string",
921
- enum: ["initialSpecReview", "initialCodegen", "onboardingFinished"],
921
+ enum: ["building", "buildComplete", "onboardingFinished"],
922
922
  description: "The onboarding state to advance to."
923
923
  }
924
924
  },
@@ -1793,11 +1793,11 @@ Old tool results are periodically cleared from the conversation to save context
1793
1793
  </conversation_summaries>
1794
1794
 
1795
1795
  <project_onboarding>
1796
- New projects progress through four onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
1796
+ New projects progress through three onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
1797
1797
 
1798
- - **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code.
1799
- - **initialSpecAuthoring**: Writing and refining the first spec. The user can see it in the editor as it streams in and can give feedback to iterate on it. This phase covers both the initial draft and any back-and-forth refinement before code generation.
1800
- - **initialCodegen**: First code generation from the spec. The agent is generating methods, tables, interfaces, manifest updates, and scenarios. This can take a while and involves heavy tool use. The user sees a full-screen build progress view.
1798
+ - **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code. Intake ends with a plan proposal via writePlan.
1799
+ - **building**: The user approved the initial plan. The agent is writing the spec and building the app. This can take a while and involves heavy tool use (spec authoring, design expert consultation, code generation, verification, polishing).
1800
+ - **buildComplete**: The build is done. The frontend is showing the user a reveal experience (pitch deck, app preview). The agent does not need to do anything in this state; the frontend advances to onboardingFinished when the user is ready.
1801
1801
  - **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
1802
1802
  </project_onboarding>
1803
1803
 
@@ -1872,7 +1872,7 @@ var init_compactConversation = __esm({
1872
1872
  clearable: false,
1873
1873
  definition: {
1874
1874
  name: "compactConversation",
1875
- description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful. Use this when you have just finished a large block of mechanical work (building, refactoring, debugging) and are about to shift back into conversational mode with the user. Runs in the background. Do not use after small changes like fixing a bug or editing copy.",
1875
+ description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful. Runs in the background.",
1876
1876
  inputSchema: {
1877
1877
  type: "object",
1878
1878
  properties: {}
@@ -2788,7 +2788,7 @@ var init_runMethod = __esm({
2788
2788
  clearable: true,
2789
2789
  definition: {
2790
2790
  name: "runMethod",
2791
- description: "Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Return synchronously - no need to sleep before checking results.",
2791
+ description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
2792
2792
  inputSchema: {
2793
2793
  type: "object",
2794
2794
  properties: {
@@ -2800,14 +2800,14 @@ var init_runMethod = __esm({
2800
2800
  type: "object",
2801
2801
  description: "The input payload to pass to the method. Omit for methods that take no input."
2802
2802
  },
2803
+ userId: {
2804
+ type: "string",
2805
+ description: 'Optional. Run the method as a specific user. Pass "testUser" to auto-auth as the default test user (the sandbox handles user creation/lookup \u2014 no scenario setup needed). Or pass a real user ID from scenario-seeded data for a specific user. Overrides session-level impersonation for this call only.'
2806
+ },
2803
2807
  roles: {
2804
2808
  type: "array",
2805
2809
  items: { type: "string" },
2806
- description: 'Optional. Role names for this request (e.g. ["admin"]). Can be used without userId to test role-gated logic. Overrides session-level impersonation for this call only.'
2807
- },
2808
- userId: {
2809
- type: "string",
2810
- description: "Optional. User ID for this request \u2014 use a managed user's ID to simulate their identity. Overrides session-level impersonation for this call only."
2810
+ description: 'Optional. Role names for this request (e.g. ["admin"]). Combine with `userId` to test a specific role, or use alone to test role-gated logic without a full identity. Overrides session-level impersonation for this call only.'
2811
2811
  }
2812
2812
  },
2813
2813
  required: ["method"]
@@ -2937,10 +2937,10 @@ var init_screenshot = __esm({
2937
2937
  "use strict";
2938
2938
  init_sidecar();
2939
2939
  init_analyzeImage();
2940
- SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).
2940
+ SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
2941
+ TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
2941
2942
 
2942
2943
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2943
- TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
2944
2944
  }
2945
2945
  });
2946
2946
 
@@ -2964,31 +2964,30 @@ async function checkBrowserConnected() {
2964
2964
  if (!status.connected) {
2965
2965
  return {
2966
2966
  connected: false,
2967
- error: "The browser preview is not connected. The user needs to open the preview."
2967
+ reason: BROWSER_UNAVAILABLE_MESSAGE
2968
2968
  };
2969
2969
  }
2970
2970
  return { connected: true };
2971
- } catch (err) {
2971
+ } catch {
2972
2972
  return {
2973
2973
  connected: false,
2974
- error: err?.message || "Could not check browser status. The dev environment may not be running."
2974
+ reason: BROWSER_UNAVAILABLE_MESSAGE
2975
2975
  };
2976
2976
  }
2977
2977
  }
2978
- var lockQueue;
2978
+ var lockQueue, BROWSER_UNAVAILABLE_MESSAGE;
2979
2979
  var init_browserLock = __esm({
2980
2980
  "src/tools/_helpers/browserLock.ts"() {
2981
2981
  "use strict";
2982
2982
  init_sidecar();
2983
2983
  lockQueue = Promise.resolve();
2984
+ BROWSER_UNAVAILABLE_MESSAGE = "Browser preview unavailable \u2014 the user has closed their browser and we are continuing to work in the background. This is not a code failure and not something to diagnose. Do not tell the user to click or open anything. Skip the visual check and verify your work through other means: runMethod for backend behavior, queryDatabase for data checks, .logs/devServer.ndjson for build errors, .logs/browser.ndjson for runtime errors, lspDiagnostics for type/syntax, or read the code directly.";
2984
2985
  }
2985
2986
  });
2986
2987
 
2987
2988
  // src/statusWatcher.ts
2988
2989
  function startStatusWatcher(config) {
2989
- const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2990
- let lastLabel = "";
2991
- let lastContext = "";
2990
+ const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
2992
2991
  let inflight = false;
2993
2992
  let stopped = false;
2994
2993
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
@@ -2999,10 +2998,9 @@ function startStatusWatcher(config) {
2999
2998
  inflight = true;
3000
2999
  try {
3001
3000
  const context = getContext();
3002
- if (!context || context === lastContext) {
3001
+ if (!context) {
3003
3002
  return;
3004
3003
  }
3005
- lastContext = context;
3006
3004
  const res = await fetch(url, {
3007
3005
  method: "POST",
3008
3006
  headers: {
@@ -3012,15 +3010,11 @@ function startStatusWatcher(config) {
3012
3010
  body: JSON.stringify({ context }),
3013
3011
  signal
3014
3012
  });
3015
- if (!res.ok) {
3013
+ if (!res.ok || stopped) {
3016
3014
  return;
3017
3015
  }
3018
3016
  const data = await res.json();
3019
- if (!data.label || data.label === lastLabel) {
3020
- return;
3021
- }
3022
- lastLabel = data.label;
3023
- if (stopped) {
3017
+ if (!data.label) {
3024
3018
  return;
3025
3019
  }
3026
3020
  onStatus(data.label);
@@ -3045,6 +3039,48 @@ var init_statusWatcher = __esm({
3045
3039
  }
3046
3040
  });
3047
3041
 
3042
+ // src/automatedActions/sentinel.ts
3043
+ function sentinel(name) {
3044
+ return `@@automated::${name}@@`;
3045
+ }
3046
+ function automatedMessage(name, body) {
3047
+ return body ? `${sentinel(name)}
3048
+ ${body}` : sentinel(name);
3049
+ }
3050
+ function hasSentinel(text, name) {
3051
+ return text.startsWith(sentinel(name));
3052
+ }
3053
+ function isAutomatedMessage(text) {
3054
+ return text.startsWith("@@automated::");
3055
+ }
3056
+ function parseSentinel(text) {
3057
+ const match = text.match(/^@@automated::(\w+)@@(.*)/s);
3058
+ if (!match) {
3059
+ return null;
3060
+ }
3061
+ return { name: match[1], remainder: match[2] };
3062
+ }
3063
+ function stripSentinelLine(text) {
3064
+ return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
3065
+ }
3066
+ function buildBackgroundResultsMessage(results) {
3067
+ const xml = results.map(
3068
+ (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
3069
+ ${r.result}
3070
+ </tool_result>`
3071
+ ).join("\n\n");
3072
+ const body = `This is an automated message containing the result of a tool call that has been working in the background. This is not a direct message from the user.
3073
+ <background_results>
3074
+ ${xml}
3075
+ </background_results>`;
3076
+ return automatedMessage("background_results", body);
3077
+ }
3078
+ var init_sentinel = __esm({
3079
+ "src/automatedActions/sentinel.ts"() {
3080
+ "use strict";
3081
+ }
3082
+ });
3083
+
3048
3084
  // src/subagents/common/cleanMessages.ts
3049
3085
  function findLastSummaryCheckpoint(messages, name) {
3050
3086
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -3138,11 +3174,8 @@ ${summaryBlock.text}
3138
3174
  }
3139
3175
  return true;
3140
3176
  }).map((msg) => {
3141
- if (msg.role === "user" && typeof msg.content === "string" && msg.content.startsWith("@@automated::")) {
3142
- return {
3143
- ...msg,
3144
- content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
3145
- };
3177
+ if (msg.role === "user" && typeof msg.content === "string" && isAutomatedMessage(msg.content)) {
3178
+ return { ...msg, content: stripSentinelLine(msg.content) };
3146
3179
  }
3147
3180
  if (!Array.isArray(msg.content)) {
3148
3181
  return msg;
@@ -3173,6 +3206,7 @@ ${summaryBlock.text}
3173
3206
  var init_cleanMessages = __esm({
3174
3207
  "src/subagents/common/cleanMessages.ts"() {
3175
3208
  "use strict";
3209
+ init_sentinel();
3176
3210
  }
3177
3211
  });
3178
3212
 
@@ -3204,7 +3238,7 @@ async function runSubAgent(config) {
3204
3238
  const agentName = subAgentId || "sub-agent";
3205
3239
  const runStart = Date.now();
3206
3240
  log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3207
- const emit2 = (e) => {
3241
+ const emit = (e) => {
3208
3242
  onEvent({ ...e, parentToolId });
3209
3243
  };
3210
3244
  const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
@@ -3269,7 +3303,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3269
3303
  }
3270
3304
  return parts.join("\n");
3271
3305
  },
3272
- onStatus: (label) => emit2({ type: "status", message: label }),
3306
+ onStatus: (label) => emit({ type: "status", message: label }),
3273
3307
  signal
3274
3308
  });
3275
3309
  try {
@@ -3286,7 +3320,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3286
3320
  signal
3287
3321
  },
3288
3322
  {
3289
- onRetry: (attempt) => emit2({
3323
+ onRetry: (attempt) => emit({
3290
3324
  type: "status",
3291
3325
  message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
3292
3326
  })
@@ -3307,14 +3341,14 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3307
3341
  startedAt: event.ts
3308
3342
  });
3309
3343
  }
3310
- emit2({ type: "text", text: event.text });
3344
+ emit({ type: "text", text: event.text });
3311
3345
  break;
3312
3346
  }
3313
3347
  case "thinking":
3314
3348
  if (!thinkingStartedAt) {
3315
3349
  thinkingStartedAt = event.ts;
3316
3350
  }
3317
- emit2({ type: "thinking", text: event.text });
3351
+ emit({ type: "thinking", text: event.text });
3318
3352
  break;
3319
3353
  case "thinking_complete":
3320
3354
  contentBlocks.push({
@@ -3334,7 +3368,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3334
3368
  input: event.input,
3335
3369
  startedAt: Date.now()
3336
3370
  });
3337
- emit2({
3371
+ emit({
3338
3372
  type: "tool_start",
3339
3373
  id: event.id,
3340
3374
  name: event.name,
@@ -3411,7 +3445,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3411
3445
  if (externalTools.has(tc.name) && resolveExternalTool) {
3412
3446
  result = await resolveExternalTool(tc.id, tc.name, input);
3413
3447
  } else {
3414
- const onLog = (line) => emit2({
3448
+ const onLog = (line) => emit({
3415
3449
  type: "tool_input_delta",
3416
3450
  id: tc.id,
3417
3451
  name: tc.name,
@@ -3462,7 +3496,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3462
3496
  durationMs: Date.now() - toolStart,
3463
3497
  isError: r.isError
3464
3498
  });
3465
- emit2({
3499
+ emit({
3466
3500
  type: "tool_done",
3467
3501
  id: tc.id,
3468
3502
  name: tc.name,
@@ -3767,7 +3801,7 @@ var init_browserAutomation = __esm({
3767
3801
  try {
3768
3802
  const browserStatus = await checkBrowserConnected();
3769
3803
  if (!browserStatus.connected) {
3770
- return `Error: ${browserStatus.error}`;
3804
+ return browserStatus.reason ?? "Browser preview unavailable.";
3771
3805
  }
3772
3806
  try {
3773
3807
  await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
@@ -3963,7 +3997,7 @@ var init_screenshot2 = __esm({
3963
3997
  try {
3964
3998
  const browserStatus = await checkBrowserConnected();
3965
3999
  if (!browserStatus.connected) {
3966
- return `Error: ${browserStatus.error}`;
4000
+ return browserStatus.reason ?? "Browser preview unavailable.";
3967
4001
  }
3968
4002
  return await captureAndAnalyzeScreenshot({
3969
4003
  prompt: input.prompt,
@@ -4317,7 +4351,7 @@ async function execute5(input, onLog, context) {
4317
4351
  try {
4318
4352
  const browserStatus = await checkBrowserConnected();
4319
4353
  if (!browserStatus.connected) {
4320
- return `Error: ${browserStatus.error}`;
4354
+ return browserStatus.reason ?? "Browser preview unavailable.";
4321
4355
  }
4322
4356
  return await captureAndAnalyzeScreenshot({
4323
4357
  prompt: input.prompt,
@@ -4826,7 +4860,7 @@ Each interface type invokes the same backend methods. Methods don't know which i
4826
4860
  TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
4827
4861
 
4828
4862
  - Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
4829
- - Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email/SMS), cookie sessions, and role enforcement. Backend methods use auth.requireRole() for access control.
4863
+ - Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email-code, sms-code) and cookie sessions. API key auth for programmatic access. No OAuth, no social login (no Apple, Google, Facebook, or GitHub sign-in). Backend methods use auth.requireRole() for access control.
4830
4864
  - Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
4831
4865
  - Git-native deployment. Push to default branch to deploy.
4832
4866
 
@@ -5324,7 +5358,7 @@ Use <current_deck> as your starting point and replace or update the content as n
5324
5358
  - The deck must be a single HTML file \u2014 it will be rendered in an iframe.
5325
5359
  - Must look beautiful on desktop and mobile.
5326
5360
  - Animation between slides must be seamless, no flicker or flashing. For reveal animations: hide elements with CSS \`opacity: 0\` only (no transform in CSS). Let GSAP handle transforms via inline styles and never use \`clearProps\`. Use the existing scaffold, do not write your own transition logic or slide mechanics.
5327
- - Be bold and impactful.
5361
+ - Be bold and impactful. Use images from the spec or generate new images when needed.
5328
5362
  - Code must be clean, bug free, and easy to parse. Use GSAP for animations. Pay close attention to layout and alignment to make sure everything is perfect.
5329
5363
  - Keep the progress bar and edge chevrons from the shell \u2014 they are part of the navigation UX.
5330
5364
 
@@ -6108,20 +6142,23 @@ async function runTurn(params) {
6108
6142
  parts.push(`Tool: ${toolName}`);
6109
6143
  }
6110
6144
  if (lastCompletedInput) {
6111
- parts.push(`Tool input: ${lastCompletedInput.slice(-300)}`);
6145
+ parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
6112
6146
  }
6113
6147
  if (lastCompletedResult) {
6114
- parts.push(`Tool result: ${lastCompletedResult.slice(-200)}`);
6148
+ parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
6115
6149
  }
6116
- const text = subAgentText || getTextContent(contentBlocks).slice(-500);
6150
+ const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
6117
6151
  if (text) {
6118
6152
  parts.push(`Assistant text: ${text}`);
6119
6153
  }
6120
6154
  if (onboardingState && onboardingState !== "onboardingFinished") {
6121
6155
  parts.push(`Build phase: ${onboardingState}`);
6122
6156
  }
6123
- if (userMessage) {
6124
- parts.push(`User request: ${userMessage.slice(-100)}`);
6157
+ const automated = parseSentinel(userMessage);
6158
+ if (automated) {
6159
+ parts.push(`Automated action: ${automated.name}`);
6160
+ } else if (userMessage) {
6161
+ parts.push(`User request: ${userMessage.slice(-500)}`);
6125
6162
  }
6126
6163
  return parts.join("\n");
6127
6164
  },
@@ -6518,6 +6555,7 @@ var init_agent = __esm({
6518
6555
  init_errors();
6519
6556
  init_cleanMessages();
6520
6557
  init_tools6();
6558
+ init_sentinel();
6521
6559
  log8 = createLogger("agent");
6522
6560
  EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
6523
6561
  "promptUser",
@@ -6667,18 +6705,267 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6667
6705
  }
6668
6706
  });
6669
6707
 
6708
+ // src/headless/attachments.ts
6709
+ import { mkdirSync, existsSync } from "fs";
6710
+ import { writeFile } from "fs/promises";
6711
+ import { basename, join, extname } from "path";
6712
+ function filenameFromUrl(url) {
6713
+ try {
6714
+ const pathname = new URL(url).pathname;
6715
+ const name = basename(pathname);
6716
+ return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
6717
+ } catch {
6718
+ return `upload-${Date.now()}`;
6719
+ }
6720
+ }
6721
+ function resolveUniqueFilename(name) {
6722
+ if (!existsSync(join(UPLOADS_DIR, name))) {
6723
+ return name;
6724
+ }
6725
+ const ext = extname(name);
6726
+ const base = name.slice(0, name.length - ext.length);
6727
+ let counter = 1;
6728
+ while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
6729
+ counter++;
6730
+ }
6731
+ return `${base}-${counter}${ext}`;
6732
+ }
6733
+ function isImageAttachment(att) {
6734
+ const name = att.filename || filenameFromUrl(att.url);
6735
+ return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
6736
+ }
6737
+ async function persistAttachments(attachments) {
6738
+ const nonVoice = attachments.filter((a) => !a.isVoice);
6739
+ if (nonVoice.length === 0) {
6740
+ return { documents: [], images: [] };
6741
+ }
6742
+ mkdirSync(UPLOADS_DIR, { recursive: true });
6743
+ const results = await Promise.allSettled(
6744
+ nonVoice.map(async (att) => {
6745
+ const name = resolveUniqueFilename(
6746
+ att.filename || filenameFromUrl(att.url)
6747
+ );
6748
+ const localPath = join(UPLOADS_DIR, name);
6749
+ const res = await fetch(att.url, {
6750
+ signal: AbortSignal.timeout(3e4)
6751
+ });
6752
+ if (!res.ok) {
6753
+ throw new Error(`HTTP ${res.status} downloading ${att.url}`);
6754
+ }
6755
+ const buffer = Buffer.from(await res.arrayBuffer());
6756
+ await writeFile(localPath, buffer);
6757
+ log11.info("Attachment saved", {
6758
+ filename: name,
6759
+ path: localPath,
6760
+ bytes: buffer.length
6761
+ });
6762
+ let extractedTextPath;
6763
+ if (att.extractedTextUrl) {
6764
+ try {
6765
+ const textRes = await fetch(att.extractedTextUrl, {
6766
+ signal: AbortSignal.timeout(3e4)
6767
+ });
6768
+ if (textRes.ok) {
6769
+ extractedTextPath = `${localPath}.txt`;
6770
+ await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6771
+ log11.info("Extracted text saved", { path: extractedTextPath });
6772
+ }
6773
+ } catch {
6774
+ }
6775
+ }
6776
+ return {
6777
+ filename: name,
6778
+ localPath,
6779
+ remoteUrl: att.url,
6780
+ extractedTextPath
6781
+ };
6782
+ })
6783
+ );
6784
+ const settled = results.map((r, i) => ({
6785
+ result: r.status === "fulfilled" ? r.value : null,
6786
+ isImage: isImageAttachment(nonVoice[i])
6787
+ }));
6788
+ return {
6789
+ documents: settled.filter((s) => !s.isImage).map((s) => s.result),
6790
+ images: settled.filter((s) => s.isImage).map((s) => s.result)
6791
+ };
6792
+ }
6793
+ function buildUploadHeader(results) {
6794
+ const succeeded = results.filter(Boolean);
6795
+ if (succeeded.length === 0) {
6796
+ return "";
6797
+ }
6798
+ if (succeeded.length === 1) {
6799
+ const r = succeeded[0];
6800
+ const parts = [`[Uploaded file: ${r.localPath} (CDN: ${r.remoteUrl})`];
6801
+ if (r.extractedTextPath) {
6802
+ parts.push(`extracted text: ${r.extractedTextPath}`);
6803
+ }
6804
+ return parts.join(" \u2014 ") + "]";
6805
+ }
6806
+ const lines = succeeded.map((r) => {
6807
+ const parts = [`- ${r.localPath} (CDN: ${r.remoteUrl})`];
6808
+ if (r.extractedTextPath) {
6809
+ parts.push(` extracted text: ${r.extractedTextPath}`);
6810
+ }
6811
+ return parts.join("\n");
6812
+ });
6813
+ return `[Uploaded files]
6814
+ ${lines.join("\n")}`;
6815
+ }
6816
+ var log11, UPLOADS_DIR, IMAGE_EXTENSIONS;
6817
+ var init_attachments = __esm({
6818
+ "src/headless/attachments.ts"() {
6819
+ "use strict";
6820
+ init_logger();
6821
+ log11 = createLogger("headless:attachments");
6822
+ UPLOADS_DIR = "src/.user-uploads";
6823
+ IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
6824
+ ".png",
6825
+ ".jpg",
6826
+ ".jpeg",
6827
+ ".gif",
6828
+ ".webp",
6829
+ ".svg",
6830
+ ".bmp",
6831
+ ".ico",
6832
+ ".tiff",
6833
+ ".tif",
6834
+ ".avif",
6835
+ ".heic",
6836
+ ".heif"
6837
+ ]);
6838
+ }
6839
+ });
6840
+
6841
+ // src/headless/planFile.ts
6842
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
6843
+ function applyPlanFileSideEffect(rawText) {
6844
+ if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
6845
+ try {
6846
+ const plan = readFileSync(PLAN_FILE3, "utf-8");
6847
+ writeFileSync(
6848
+ PLAN_FILE3,
6849
+ plan.replace(/^status:\s*pending/m, "status: approved"),
6850
+ "utf-8"
6851
+ );
6852
+ } catch {
6853
+ }
6854
+ } else if (hasSentinel(rawText, "rejectPlan")) {
6855
+ try {
6856
+ unlinkSync(PLAN_FILE3);
6857
+ } catch {
6858
+ }
6859
+ }
6860
+ }
6861
+ var PLAN_FILE3;
6862
+ var init_planFile = __esm({
6863
+ "src/headless/planFile.ts"() {
6864
+ "use strict";
6865
+ init_sentinel();
6866
+ PLAN_FILE3 = ".remy-plan.md";
6867
+ }
6868
+ });
6869
+
6870
+ // src/headless/stats.ts
6871
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
6872
+ function createSessionStats() {
6873
+ return {
6874
+ messageCount: 0,
6875
+ turns: 0,
6876
+ totalInputTokens: 0,
6877
+ totalOutputTokens: 0,
6878
+ totalCacheCreationTokens: 0,
6879
+ totalCacheReadTokens: 0,
6880
+ lastContextSize: 0,
6881
+ compactionInProgress: false,
6882
+ updatedAt: 0
6883
+ };
6884
+ }
6885
+ function loadQueue() {
6886
+ try {
6887
+ const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
6888
+ if (Array.isArray(stats.queue)) {
6889
+ return stats.queue;
6890
+ }
6891
+ } catch {
6892
+ }
6893
+ return [];
6894
+ }
6895
+ function writeStats(stats, queue) {
6896
+ try {
6897
+ writeFileSync2(
6898
+ STATS_FILE,
6899
+ JSON.stringify({
6900
+ ...stats,
6901
+ queue
6902
+ })
6903
+ );
6904
+ } catch {
6905
+ }
6906
+ }
6907
+ var STATS_FILE;
6908
+ var init_stats = __esm({
6909
+ "src/headless/stats.ts"() {
6910
+ "use strict";
6911
+ STATS_FILE = ".remy-stats.json";
6912
+ }
6913
+ });
6914
+
6915
+ // src/headless/messageQueue.ts
6916
+ var MessageQueue;
6917
+ var init_messageQueue = __esm({
6918
+ "src/headless/messageQueue.ts"() {
6919
+ "use strict";
6920
+ MessageQueue = class {
6921
+ items = [];
6922
+ onChange;
6923
+ constructor(initial = [], onChange) {
6924
+ this.items = [...initial];
6925
+ this.onChange = onChange;
6926
+ }
6927
+ push(item) {
6928
+ this.items.push(item);
6929
+ this.onChange?.();
6930
+ }
6931
+ shift() {
6932
+ const item = this.items.shift();
6933
+ if (item) {
6934
+ this.onChange?.();
6935
+ }
6936
+ return item;
6937
+ }
6938
+ /** Remove and return all queued items. */
6939
+ drain() {
6940
+ if (this.items.length === 0) {
6941
+ return [];
6942
+ }
6943
+ const all = this.items.splice(0);
6944
+ this.onChange?.();
6945
+ return all;
6946
+ }
6947
+ /** Copy of current queue contents (for surfacing on events). */
6948
+ snapshot() {
6949
+ return [...this.items];
6950
+ }
6951
+ get length() {
6952
+ return this.items.length;
6953
+ }
6954
+ };
6955
+ }
6956
+ });
6957
+
6670
6958
  // src/automatedActions/resolve.ts
6671
6959
  function resolveAction(text) {
6672
- const match = text.match(/^@@automated::(\w+)@@(.*)/s);
6673
- if (!match) {
6960
+ const parsed = parseSentinel(text);
6961
+ if (!parsed) {
6674
6962
  return null;
6675
6963
  }
6676
- const triggerName = match[1];
6964
+ const { name: triggerName, remainder } = parsed;
6677
6965
  if (NON_ACTION_SENTINELS.has(triggerName)) {
6678
6966
  return null;
6679
6967
  }
6680
6968
  let params = {};
6681
- const remainder = match[2];
6682
6969
  if (remainder) {
6683
6970
  try {
6684
6971
  params = JSON.parse(remainder.split("\n")[0]);
@@ -6700,8 +6987,7 @@ function resolveAction(text) {
6700
6987
  body = body.replaceAll(`{{${key}}}`, str);
6701
6988
  }
6702
6989
  return {
6703
- message: `@@automated::${triggerName}@@
6704
- ${body}`,
6990
+ message: automatedMessage(triggerName, body),
6705
6991
  next
6706
6992
  };
6707
6993
  }
@@ -6710,744 +6996,758 @@ var init_resolve = __esm({
6710
6996
  "src/automatedActions/resolve.ts"() {
6711
6997
  "use strict";
6712
6998
  init_assets();
6999
+ init_sentinel();
6713
7000
  NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
6714
7001
  }
6715
7002
  });
6716
7003
 
6717
- // src/headless.ts
7004
+ // src/headless/index.ts
6718
7005
  var headless_exports = {};
6719
7006
  __export(headless_exports, {
6720
- startHeadless: () => startHeadless
7007
+ HeadlessSession: () => HeadlessSession
6721
7008
  });
6722
7009
  import { createInterface } from "readline";
6723
- import {
6724
- writeFileSync,
6725
- readFileSync,
6726
- unlinkSync,
6727
- mkdirSync,
6728
- existsSync
6729
- } from "fs";
6730
- import { writeFile } from "fs/promises";
6731
- import { basename, join, extname } from "path";
6732
- function emit(event, data, requestId) {
6733
- const payload = { event, ...data };
6734
- if (requestId) {
6735
- payload.requestId = requestId;
6736
- }
6737
- process.stdout.write(JSON.stringify(payload) + "\n");
6738
- }
6739
- function handleClear(state) {
6740
- clearSession(state);
6741
- return {};
6742
- }
6743
- function handleCancel(currentAbort, pendingTools) {
6744
- if (currentAbort) {
6745
- currentAbort.abort();
6746
- }
6747
- for (const [id, pending] of pendingTools) {
6748
- clearTimeout(pending.timeout);
6749
- pending.resolve("Error: cancelled");
6750
- pendingTools.delete(id);
6751
- }
6752
- return {};
6753
- }
6754
- function dispatchSimple(requestId, eventName, handler) {
6755
- try {
6756
- const data = handler();
6757
- if (eventName) {
6758
- emit(eventName, data, requestId);
6759
- }
6760
- emit("completed", { success: true }, requestId);
6761
- } catch (err) {
6762
- emit("completed", { success: false, error: err.message }, requestId);
6763
- }
6764
- }
6765
- async function startHeadless(opts = {}) {
6766
- const stderrWrite = (...args2) => {
6767
- process.stderr.write(args2.map(String).join(" ") + "\n");
6768
- };
6769
- console.log = stderrWrite;
6770
- console.warn = stderrWrite;
6771
- console.info = stderrWrite;
6772
- if (opts.lspUrl) {
6773
- setLspBaseUrl(opts.lspUrl);
6774
- }
6775
- const config = resolveConfig({
6776
- apiKey: opts.apiKey,
6777
- baseUrl: opts.baseUrl
6778
- });
6779
- const state = createAgentState();
6780
- const resumed = loadSession(state);
6781
- if (resumed) {
6782
- emit("session_restored", { messageCount: state.messages.length });
6783
- }
6784
- let running = false;
6785
- let currentAbort = null;
6786
- let currentRequestId;
6787
- let completedEmitted = false;
6788
- let turnStart = 0;
6789
- let pendingNextAction;
6790
- const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6791
- const pendingTools = /* @__PURE__ */ new Map();
6792
- const earlyResults = /* @__PURE__ */ new Map();
6793
- const toolRegistry = new ToolRegistry();
6794
- const sessionStats = {
6795
- messageCount: 0,
6796
- turns: 0,
6797
- totalInputTokens: 0,
6798
- totalOutputTokens: 0,
6799
- totalCacheCreationTokens: 0,
6800
- totalCacheReadTokens: 0,
6801
- lastContextSize: 0,
6802
- compactionInProgress: false,
6803
- updatedAt: 0
6804
- };
6805
- const backgroundQueue = [];
6806
- function flushBackgroundQueue() {
6807
- if (backgroundQueue.length === 0) {
6808
- return;
6809
- }
6810
- const results = backgroundQueue.splice(0);
6811
- const xmlParts = results.map(
6812
- (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
6813
- ${r.result}
6814
- </tool_result>`
6815
- ).join("\n\n");
6816
- const message = `@@automated::background_results@@
6817
- This is an automated message containing the result of a tool call that has been working in the background. This is not a direct message from the user.
6818
- <background_results>
6819
- ${xmlParts}
6820
- </background_results>`;
6821
- const bgRequestId = `bg-${Date.now()}`;
6822
- handleMessage({ action: "message", text: message }, bgRequestId);
6823
- }
6824
- const pendingBlockUpdates = [];
6825
- function applyPendingBlockUpdates() {
6826
- if (pendingBlockUpdates.length === 0) {
6827
- return;
6828
- }
6829
- const updates = pendingBlockUpdates.splice(0);
6830
- for (const update of updates) {
6831
- for (const msg of state.messages) {
6832
- if (!Array.isArray(msg.content)) {
6833
- continue;
7010
+ var log12, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
7011
+ var init_headless = __esm({
7012
+ "src/headless/index.ts"() {
7013
+ "use strict";
7014
+ init_logger();
7015
+ init_config();
7016
+ init_prompt();
7017
+ init_trigger();
7018
+ init_compaction();
7019
+ init_lsp();
7020
+ init_agent();
7021
+ init_session();
7022
+ init_toolRegistry();
7023
+ init_attachments();
7024
+ init_planFile();
7025
+ init_stats();
7026
+ init_messageQueue();
7027
+ init_resolve();
7028
+ init_sentinel();
7029
+ log12 = createLogger("headless");
7030
+ EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
7031
+ USER_FACING_TOOLS = /* @__PURE__ */ new Set([
7032
+ "promptUser",
7033
+ "confirmDestructiveAction",
7034
+ "presentPublishPlan"
7035
+ ]);
7036
+ HeadlessSession = class {
7037
+ // Configuration
7038
+ opts;
7039
+ config;
7040
+ // Conversation state
7041
+ state = createAgentState();
7042
+ sessionStats = createSessionStats();
7043
+ // Turn lifecycle
7044
+ running = false;
7045
+ currentAbort = null;
7046
+ /** RequestId of the in-flight message command — injected into streamed events. */
7047
+ currentRequestId;
7048
+ /** Guard: track whether terminal `completed` was already sent so we emit exactly one. */
7049
+ completedEmitted = false;
7050
+ turnStart = 0;
7051
+ /**
7052
+ * Onboarding state of the currently-running turn. Captured at runSingleTurn
7053
+ * start so onBackgroundComplete can enqueue background results with the
7054
+ * right state (the triggering turn's state, not a stale one).
7055
+ */
7056
+ currentOnboardingState;
7057
+ /**
7058
+ * Unified message queue. Holds pending work to deliver after the current
7059
+ * turn completes: chained automated actions, background sub-agent results,
7060
+ * and user messages sent while a turn is running. Strict FIFO. Persisted
7061
+ * to .remy-stats.json so queued work survives process restarts.
7062
+ */
7063
+ queue;
7064
+ // External tool bridge
7065
+ pendingTools = /* @__PURE__ */ new Map();
7066
+ earlyResults = /* @__PURE__ */ new Map();
7067
+ // Tool block updates from background completions (separate from the message queue)
7068
+ pendingBlockUpdates = [];
7069
+ // Tool lifecycle management — shared across all nesting depths
7070
+ toolRegistry = new ToolRegistry();
7071
+ // IO
7072
+ readline = null;
7073
+ constructor(opts = {}) {
7074
+ this.opts = opts;
7075
+ }
7076
+ //////////////////////////////////////////////////////////////////////////////
7077
+ // Lifecycle
7078
+ //////////////////////////////////////////////////////////////////////////////
7079
+ async start() {
7080
+ const stderrWrite = (...args2) => {
7081
+ process.stderr.write(args2.map(String).join(" ") + "\n");
7082
+ };
7083
+ console.log = stderrWrite;
7084
+ console.warn = stderrWrite;
7085
+ console.info = stderrWrite;
7086
+ if (this.opts.lspUrl) {
7087
+ setLspBaseUrl(this.opts.lspUrl);
7088
+ }
7089
+ this.config = resolveConfig({
7090
+ apiKey: this.opts.apiKey,
7091
+ baseUrl: this.opts.baseUrl
7092
+ });
7093
+ const resumed = loadSession(this.state);
7094
+ this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
7095
+ if (resumed) {
7096
+ this.emit("session_restored", {
7097
+ messageCount: this.state.messages.length,
7098
+ ...this.queueFields()
7099
+ });
7100
+ }
7101
+ this.toolRegistry.onEvent = this.onEvent;
7102
+ this.readline = createInterface({ input: process.stdin });
7103
+ this.readline.on("line", this.handleStdinLine);
7104
+ this.readline.on("close", () => {
7105
+ this.emit("stopping");
7106
+ this.emit("stopped");
7107
+ process.exit(0);
7108
+ });
7109
+ process.on("SIGTERM", this.shutdown);
7110
+ process.on("SIGINT", this.shutdown);
7111
+ this.emit("ready", this.queueFields());
7112
+ }
7113
+ shutdown = () => {
7114
+ this.emit("stopping");
7115
+ this.emit("stopped");
7116
+ process.exit(0);
7117
+ };
7118
+ //////////////////////////////////////////////////////////////////////////////
7119
+ // Wire protocol
7120
+ //////////////////////////////////////////////////////////////////////////////
7121
+ emit(event, data, requestId) {
7122
+ const payload = { event, ...data };
7123
+ if (requestId) {
7124
+ payload.requestId = requestId;
7125
+ }
7126
+ process.stdout.write(JSON.stringify(payload) + "\n");
7127
+ }
7128
+ /**
7129
+ * Emit a `completed` event and mark completedEmitted. Includes
7130
+ * `queuedMessages` if the queue has items (sandbox uses this to know the
7131
+ * agent is still busy with pipeline work).
7132
+ */
7133
+ emitCompleted(rid, data) {
7134
+ this.emit("completed", { ...data, ...this.queueFields() }, rid);
7135
+ this.completedEmitted = true;
7136
+ }
7137
+ /** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
7138
+ queueFields() {
7139
+ return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
7140
+ }
7141
+ /** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
7142
+ dispatchSimple(requestId, eventName, handler) {
7143
+ try {
7144
+ const data = handler();
7145
+ if (eventName) {
7146
+ this.emit(eventName, data, requestId);
7147
+ }
7148
+ this.emit("completed", { success: true }, requestId);
7149
+ } catch (err) {
7150
+ this.emit("completed", { success: false, error: err.message }, requestId);
7151
+ }
7152
+ }
7153
+ //////////////////////////////////////////////////////////////////////////////
7154
+ // Stats + queue persistence
7155
+ //////////////////////////////////////////////////////////////////////////////
7156
+ /** Persist sessionStats + queue snapshot to .remy-stats.json. */
7157
+ persistStats() {
7158
+ this.sessionStats.updatedAt = Date.now();
7159
+ writeStats(this.sessionStats, this.queue.snapshot());
7160
+ }
7161
+ //////////////////////////////////////////////////////////////////////////////
7162
+ // Background completions (tool-block mutation; message delivery via queue)
7163
+ //////////////////////////////////////////////////////////////////////////////
7164
+ /** Apply queued tool block updates to state.messages. Safe to call any time. */
7165
+ applyPendingBlockUpdates() {
7166
+ if (this.pendingBlockUpdates.length === 0) {
7167
+ return;
6834
7168
  }
6835
- for (const block of msg.content) {
6836
- if (block.type === "tool" && block.id === update.toolCallId) {
6837
- block.backgroundResult = update.result;
6838
- block.completedAt = Date.now();
6839
- if (update.subAgentMessages) {
6840
- block.subAgentMessages = update.subAgentMessages;
7169
+ const updates = this.pendingBlockUpdates.splice(0);
7170
+ for (const update of updates) {
7171
+ for (const msg of this.state.messages) {
7172
+ if (!Array.isArray(msg.content)) {
7173
+ continue;
7174
+ }
7175
+ for (const block of msg.content) {
7176
+ if (block.type === "tool" && block.id === update.toolCallId) {
7177
+ block.backgroundResult = update.result;
7178
+ block.completedAt = Date.now();
7179
+ if (update.subAgentMessages) {
7180
+ block.subAgentMessages = update.subAgentMessages;
7181
+ }
7182
+ }
6841
7183
  }
6842
7184
  }
6843
7185
  }
7186
+ saveSession(this.state);
6844
7187
  }
6845
- }
6846
- }
6847
- function applyPendingSummaries() {
6848
- const summaries = getPendingSummaries();
6849
- if (summaries.length === 0) {
6850
- return;
6851
- }
6852
- const idx = findSafeInsertionPoint(state.messages);
6853
- state.messages.splice(idx, 0, ...summaries);
6854
- saveSession(state);
6855
- }
6856
- function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
6857
- pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6858
- log11.info("Background complete", {
6859
- toolCallId,
6860
- name,
6861
- requestId: currentRequestId
6862
- });
6863
- onEvent({
6864
- type: "tool_background_complete",
6865
- id: toolCallId,
6866
- name,
6867
- result
6868
- });
6869
- backgroundQueue.push({
6870
- toolCallId,
6871
- name,
6872
- result,
6873
- completedAt: Date.now()
6874
- });
6875
- if (!running) {
6876
- applyPendingBlockUpdates();
6877
- flushBackgroundQueue();
6878
- }
6879
- }
6880
- const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6881
- "promptUser",
6882
- "confirmDestructiveAction",
6883
- "presentPublishPlan"
6884
- ]);
6885
- function resolveExternalTool(id, name, _input) {
6886
- const early = earlyResults.get(id);
6887
- if (early !== void 0) {
6888
- earlyResults.delete(id);
6889
- return Promise.resolve(early);
6890
- }
6891
- const shouldTimeout = !USER_FACING_TOOLS.has(name);
6892
- return new Promise((resolve2) => {
6893
- const timeout = shouldTimeout ? setTimeout(() => {
6894
- pendingTools.delete(id);
6895
- resolve2(
6896
- "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
6897
- );
6898
- }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
6899
- pendingTools.set(id, {
6900
- resolve: (result) => {
6901
- clearTimeout(timeout);
6902
- resolve2(result);
6903
- },
6904
- timeout
6905
- });
6906
- });
6907
- }
6908
- function onEvent(e) {
6909
- const rid = currentRequestId;
6910
- switch (e.type) {
6911
- case "turn_started":
6912
- emit("turn_started", {}, rid);
6913
- return;
6914
- // Terminal events — translate to `completed`
6915
- case "turn_done":
6916
- completedEmitted = true;
6917
- if (e.stats) {
6918
- sessionStats.turns++;
6919
- sessionStats.totalInputTokens += e.stats.inputTokens;
6920
- sessionStats.totalOutputTokens += e.stats.outputTokens;
6921
- sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
6922
- sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
6923
- sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
6924
- }
6925
- sessionStats.messageCount = state.messages.length;
6926
- sessionStats.updatedAt = Date.now();
6927
- try {
6928
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
6929
- } catch {
7188
+ /** Drain pending compaction summaries and insert at a safe point. */
7189
+ applyPendingSummaries() {
7190
+ const summaries = getPendingSummaries();
7191
+ if (summaries.length === 0) {
7192
+ return;
6930
7193
  }
6931
- emit(
6932
- "completed",
6933
- { success: true, durationMs: Date.now() - turnStart },
6934
- rid
6935
- );
6936
- setTimeout(() => {
6937
- applyPendingSummaries();
6938
- applyPendingBlockUpdates();
6939
- flushBackgroundQueue();
6940
- if (pendingNextAction) {
6941
- const next = pendingNextAction;
6942
- pendingNextAction = void 0;
6943
- handleMessage(
6944
- { action: "message", text: `@@automated::${next}@@` },
6945
- `chain-${Date.now()}`
7194
+ const idx = findSafeInsertionPoint(this.state.messages);
7195
+ this.state.messages.splice(idx, 0, ...summaries);
7196
+ saveSession(this.state);
7197
+ }
7198
+ onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
7199
+ this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
7200
+ log12.info("Background complete", {
7201
+ toolCallId,
7202
+ name,
7203
+ requestId: this.currentRequestId
7204
+ });
7205
+ this.onEvent({
7206
+ type: "tool_background_complete",
7207
+ id: toolCallId,
7208
+ name,
7209
+ result
7210
+ });
7211
+ this.queue.push({
7212
+ command: {
7213
+ action: "message",
7214
+ text: buildBackgroundResultsMessage([{ toolCallId, name, result }]),
7215
+ ...this.currentOnboardingState && {
7216
+ onboardingState: this.currentOnboardingState
7217
+ }
7218
+ },
7219
+ source: "background",
7220
+ enqueuedAt: Date.now()
7221
+ });
7222
+ if (!this.running) {
7223
+ this.applyPendingBlockUpdates();
7224
+ this.kickDrain();
7225
+ }
7226
+ };
7227
+ //////////////////////////////////////////////////////////////////////////////
7228
+ // External tool bridge
7229
+ //////////////////////////////////////////////////////////////////////////////
7230
+ resolveExternalTool = (id, name, _input) => {
7231
+ const early = this.earlyResults.get(id);
7232
+ if (early !== void 0) {
7233
+ this.earlyResults.delete(id);
7234
+ return Promise.resolve(early);
7235
+ }
7236
+ const shouldTimeout = !USER_FACING_TOOLS.has(name);
7237
+ return new Promise((resolve2) => {
7238
+ const timeout = shouldTimeout ? setTimeout(() => {
7239
+ this.pendingTools.delete(id);
7240
+ resolve2(
7241
+ "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
7242
+ );
7243
+ }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
7244
+ this.pendingTools.set(id, {
7245
+ resolve: (result) => {
7246
+ clearTimeout(timeout);
7247
+ resolve2(result);
7248
+ },
7249
+ timeout
7250
+ });
7251
+ });
7252
+ };
7253
+ //////////////////////////////////////////////////////////////////////////////
7254
+ // AgentEvent → wire protocol translation
7255
+ //////////////////////////////////////////////////////////////////////////////
7256
+ onEvent = (e) => {
7257
+ const rid = this.currentRequestId;
7258
+ switch (e.type) {
7259
+ case "turn_started":
7260
+ this.emit("turn_started", {}, rid);
7261
+ return;
7262
+ case "user_message":
7263
+ this.emit("user_message", { text: e.text }, rid);
7264
+ return;
7265
+ // Terminal events — translate to `completed`.
7266
+ // Post-turn queue drain happens in handleMessage AFTER runTurn returns,
7267
+ // so that `running` is held across the drain and no user message can
7268
+ // slip in mid-pipeline.
7269
+ case "turn_done":
7270
+ if (e.stats) {
7271
+ this.sessionStats.turns++;
7272
+ this.sessionStats.totalInputTokens += e.stats.inputTokens;
7273
+ this.sessionStats.totalOutputTokens += e.stats.outputTokens;
7274
+ this.sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
7275
+ this.sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
7276
+ this.sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
7277
+ }
7278
+ this.sessionStats.messageCount = this.state.messages.length;
7279
+ this.persistStats();
7280
+ this.emitCompleted(rid, {
7281
+ success: true,
7282
+ durationMs: Date.now() - this.turnStart
7283
+ });
7284
+ return;
7285
+ case "turn_cancelled": {
7286
+ this.emit(
7287
+ "completed",
7288
+ { success: false, error: "cancelled", ...this.queueFields() },
7289
+ rid
6946
7290
  );
7291
+ this.completedEmitted = true;
7292
+ return;
6947
7293
  }
6948
- }, 0);
6949
- return;
6950
- case "turn_cancelled":
6951
- completedEmitted = true;
6952
- pendingNextAction = void 0;
6953
- emit("completed", { success: false, error: "cancelled" }, rid);
6954
- return;
6955
- // Streaming events — forward with requestId
6956
- case "text":
6957
- emit(
6958
- "text",
6959
- {
6960
- text: e.text,
6961
- ...e.parentToolId && { parentToolId: e.parentToolId }
6962
- },
6963
- rid
6964
- );
6965
- return;
6966
- case "thinking":
6967
- emit(
6968
- "thinking",
6969
- {
6970
- text: e.text,
6971
- ...e.parentToolId && { parentToolId: e.parentToolId }
6972
- },
6973
- rid
6974
- );
6975
- return;
6976
- case "tool_input_delta":
6977
- emit(
6978
- "tool_input_delta",
6979
- {
6980
- id: e.id,
6981
- name: e.name,
6982
- result: e.result,
6983
- ...e.parentToolId && { parentToolId: e.parentToolId }
6984
- },
6985
- rid
6986
- );
6987
- return;
6988
- case "tool_start":
6989
- emit(
6990
- "tool_start",
6991
- {
6992
- id: e.id,
6993
- name: e.name,
6994
- input: e.input,
6995
- ...e.partial && { partial: true },
6996
- ...e.background && { background: true },
6997
- ...e.parentToolId && { parentToolId: e.parentToolId }
6998
- },
6999
- rid
7000
- );
7001
- return;
7002
- case "tool_done":
7003
- emit(
7004
- "tool_done",
7005
- {
7006
- id: e.id,
7007
- name: e.name,
7008
- result: e.result,
7009
- isError: e.isError,
7010
- ...e.parentToolId && { parentToolId: e.parentToolId }
7011
- },
7012
- rid
7013
- );
7014
- return;
7015
- case "tool_background_complete":
7016
- emit(
7017
- "tool_background_complete",
7018
- {
7019
- id: e.id,
7020
- name: e.name,
7021
- result: e.result,
7022
- ...e.parentToolId && { parentToolId: e.parentToolId }
7023
- },
7024
- rid
7025
- );
7026
- return;
7027
- case "tool_stopped":
7028
- emit(
7029
- "tool_stopped",
7030
- {
7031
- id: e.id,
7032
- name: e.name,
7033
- mode: e.mode,
7034
- ...e.parentToolId && { parentToolId: e.parentToolId }
7035
- },
7036
- rid
7037
- );
7038
- return;
7039
- case "tool_restarted":
7040
- emit(
7041
- "tool_restarted",
7042
- {
7043
- id: e.id,
7044
- name: e.name,
7045
- input: e.input,
7046
- ...e.parentToolId && { parentToolId: e.parentToolId }
7047
- },
7048
- rid
7049
- );
7050
- return;
7051
- case "status":
7052
- emit(
7053
- "status",
7054
- {
7055
- message: e.message,
7056
- ...e.parentToolId && { parentToolId: e.parentToolId }
7057
- },
7058
- rid
7059
- );
7060
- return;
7061
- case "error":
7062
- emit("error", { error: e.error }, rid);
7063
- return;
7064
- }
7065
- }
7066
- toolRegistry.onEvent = onEvent;
7067
- const UPLOADS_DIR = "src/.user-uploads";
7068
- function filenameFromUrl(url) {
7069
- try {
7070
- const pathname = new URL(url).pathname;
7071
- const name = basename(pathname);
7072
- return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
7073
- } catch {
7074
- return `upload-${Date.now()}`;
7075
- }
7076
- }
7077
- function resolveUniqueFilename(name) {
7078
- if (!existsSync(join(UPLOADS_DIR, name))) {
7079
- return name;
7080
- }
7081
- const ext = extname(name);
7082
- const base = name.slice(0, name.length - ext.length);
7083
- let counter = 1;
7084
- while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
7085
- counter++;
7086
- }
7087
- return `${base}-${counter}${ext}`;
7088
- }
7089
- const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
7090
- ".png",
7091
- ".jpg",
7092
- ".jpeg",
7093
- ".gif",
7094
- ".webp",
7095
- ".svg",
7096
- ".bmp",
7097
- ".ico",
7098
- ".tiff",
7099
- ".tif",
7100
- ".avif",
7101
- ".heic",
7102
- ".heif"
7103
- ]);
7104
- function isImageAttachment(att) {
7105
- const name = att.filename || filenameFromUrl(att.url);
7106
- return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
7107
- }
7108
- async function persistAttachments(attachments) {
7109
- const nonVoice = attachments.filter((a) => !a.isVoice);
7110
- if (nonVoice.length === 0) {
7111
- return { documents: [], images: [] };
7112
- }
7113
- mkdirSync(UPLOADS_DIR, { recursive: true });
7114
- const results = await Promise.allSettled(
7115
- nonVoice.map(async (att) => {
7116
- const name = resolveUniqueFilename(
7117
- att.filename || filenameFromUrl(att.url)
7118
- );
7119
- const localPath = join(UPLOADS_DIR, name);
7120
- const res = await fetch(att.url, {
7121
- signal: AbortSignal.timeout(3e4)
7122
- });
7123
- if (!res.ok) {
7124
- throw new Error(`HTTP ${res.status} downloading ${att.url}`);
7125
- }
7126
- const buffer = Buffer.from(await res.arrayBuffer());
7127
- await writeFile(localPath, buffer);
7128
- log11.info("Attachment saved", {
7129
- filename: name,
7130
- path: localPath,
7131
- bytes: buffer.length
7132
- });
7133
- let extractedTextPath;
7134
- if (att.extractedTextUrl) {
7294
+ // Streaming events — forward with requestId
7295
+ case "text":
7296
+ this.emit(
7297
+ "text",
7298
+ {
7299
+ text: e.text,
7300
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7301
+ },
7302
+ rid
7303
+ );
7304
+ return;
7305
+ case "thinking":
7306
+ this.emit(
7307
+ "thinking",
7308
+ {
7309
+ text: e.text,
7310
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7311
+ },
7312
+ rid
7313
+ );
7314
+ return;
7315
+ case "tool_input_delta":
7316
+ this.emit(
7317
+ "tool_input_delta",
7318
+ {
7319
+ id: e.id,
7320
+ name: e.name,
7321
+ result: e.result,
7322
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7323
+ },
7324
+ rid
7325
+ );
7326
+ return;
7327
+ case "tool_start":
7328
+ this.emit(
7329
+ "tool_start",
7330
+ {
7331
+ id: e.id,
7332
+ name: e.name,
7333
+ input: e.input,
7334
+ ...e.partial && { partial: true },
7335
+ ...e.background && { background: true },
7336
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7337
+ },
7338
+ rid
7339
+ );
7340
+ return;
7341
+ case "tool_done":
7342
+ this.emit(
7343
+ "tool_done",
7344
+ {
7345
+ id: e.id,
7346
+ name: e.name,
7347
+ result: e.result,
7348
+ isError: e.isError,
7349
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7350
+ },
7351
+ rid
7352
+ );
7353
+ return;
7354
+ case "tool_background_complete":
7355
+ this.emit(
7356
+ "tool_background_complete",
7357
+ {
7358
+ id: e.id,
7359
+ name: e.name,
7360
+ result: e.result,
7361
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7362
+ },
7363
+ rid
7364
+ );
7365
+ return;
7366
+ case "tool_stopped":
7367
+ this.emit(
7368
+ "tool_stopped",
7369
+ {
7370
+ id: e.id,
7371
+ name: e.name,
7372
+ mode: e.mode,
7373
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7374
+ },
7375
+ rid
7376
+ );
7377
+ return;
7378
+ case "tool_restarted":
7379
+ this.emit(
7380
+ "tool_restarted",
7381
+ {
7382
+ id: e.id,
7383
+ name: e.name,
7384
+ input: e.input,
7385
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7386
+ },
7387
+ rid
7388
+ );
7389
+ return;
7390
+ case "status":
7391
+ this.emit(
7392
+ "status",
7393
+ {
7394
+ message: e.message,
7395
+ ...e.parentToolId && { parentToolId: e.parentToolId }
7396
+ },
7397
+ rid
7398
+ );
7399
+ return;
7400
+ case "error":
7401
+ this.emit("error", { error: e.error }, rid);
7402
+ return;
7403
+ }
7404
+ };
7405
+ //////////////////////////////////////////////////////////////////////////////
7406
+ // Message command handler (long-running / streaming)
7407
+ //////////////////////////////////////////////////////////////////////////////
7408
+ /**
7409
+ * Run one turn (without acquiring the `running` lock). Called by
7410
+ * handleMessage for the initial turn, then repeatedly for each queued
7411
+ * message — `running` stays held across the queue drain so no user
7412
+ * message can slip in mid-pipeline.
7413
+ */
7414
+ async runSingleTurn(parsed, requestId) {
7415
+ this.currentRequestId = requestId;
7416
+ this.currentAbort = new AbortController();
7417
+ this.completedEmitted = false;
7418
+ this.turnStart = Date.now();
7419
+ const attachments = parsed.attachments;
7420
+ if (attachments?.length) {
7421
+ log12.info("Message has attachments", {
7422
+ count: attachments.length,
7423
+ urls: attachments.map((a) => a.url)
7424
+ });
7425
+ }
7426
+ let userMessage = parsed.text ?? "";
7427
+ if (attachments?.some((a) => !a.isVoice)) {
7135
7428
  try {
7136
- const textRes = await fetch(att.extractedTextUrl, {
7137
- signal: AbortSignal.timeout(3e4)
7138
- });
7139
- if (textRes.ok) {
7140
- extractedTextPath = `${localPath}.txt`;
7141
- await writeFile(extractedTextPath, await textRes.text(), "utf-8");
7142
- log11.info("Extracted text saved", { path: extractedTextPath });
7429
+ const { documents, images } = await persistAttachments(attachments);
7430
+ const all = [...documents, ...images];
7431
+ const header = buildUploadHeader(all);
7432
+ if (header) {
7433
+ userMessage = userMessage ? `${header}
7434
+
7435
+ ${userMessage}` : header;
7143
7436
  }
7144
- } catch {
7437
+ } catch (err) {
7438
+ log12.warn("Attachment persistence failed", { error: err.message });
7145
7439
  }
7146
7440
  }
7147
- return { filename: name, localPath, extractedTextPath };
7148
- })
7149
- );
7150
- const settled = results.map((r, i) => ({
7151
- result: r.status === "fulfilled" ? r.value : null,
7152
- isImage: isImageAttachment(nonVoice[i])
7153
- }));
7154
- return {
7155
- documents: settled.filter((s) => !s.isImage).map((s) => s.result),
7156
- images: settled.filter((s) => s.isImage).map((s) => s.result)
7157
- };
7158
- }
7159
- function buildUploadHeader(results) {
7160
- const succeeded = results.filter(Boolean);
7161
- if (succeeded.length === 0) {
7162
- return "";
7163
- }
7164
- if (succeeded.length === 1) {
7165
- const r = succeeded[0];
7166
- const parts = [`[Uploaded file: ${r.localPath}`];
7167
- if (r.extractedTextPath) {
7168
- parts.push(`extracted text: ${r.extractedTextPath}`);
7169
- }
7170
- return parts.join(" \u2014 ") + "]";
7171
- }
7172
- const lines = succeeded.map((r) => {
7173
- if (r.extractedTextPath) {
7174
- return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
7175
- }
7176
- return `- ${r.localPath}`;
7177
- });
7178
- return `[Uploaded files]
7179
- ${lines.join("\n")}`;
7180
- }
7181
- async function handleMessage(parsed, requestId) {
7182
- if (running) {
7183
- emit(
7184
- "error",
7185
- { error: "Agent is already processing a message" },
7186
- requestId
7187
- );
7188
- emit(
7189
- "completed",
7190
- { success: false, error: "Agent is already processing a message" },
7191
- requestId
7192
- );
7193
- return;
7194
- }
7195
- running = true;
7196
- currentRequestId = requestId;
7197
- currentAbort = new AbortController();
7198
- completedEmitted = false;
7199
- turnStart = Date.now();
7200
- const attachments = parsed.attachments;
7201
- if (attachments?.length) {
7202
- log11.info("Message has attachments", {
7203
- count: attachments.length,
7204
- urls: attachments.map((a) => a.url)
7205
- });
7206
- }
7207
- let userMessage = parsed.text ?? "";
7208
- if (attachments?.some((a) => !a.isVoice)) {
7209
- try {
7210
- const { documents, images } = await persistAttachments(attachments);
7211
- const all = [...documents, ...images];
7212
- const header = buildUploadHeader(all);
7213
- if (header) {
7214
- userMessage = userMessage ? `${header}
7215
-
7216
- ${userMessage}` : header;
7441
+ let resolved = null;
7442
+ try {
7443
+ resolved = resolveAction(userMessage);
7444
+ } catch (err) {
7445
+ this.emitCompleted(requestId, {
7446
+ success: false,
7447
+ error: err.message || "Failed to resolve action"
7448
+ });
7449
+ return;
7217
7450
  }
7218
- } catch (err) {
7219
- log11.warn("Attachment persistence failed", { error: err.message });
7220
- }
7221
- }
7222
- let resolved = null;
7223
- try {
7224
- resolved = resolveAction(userMessage);
7225
- } catch (err) {
7226
- emit(
7227
- "completed",
7228
- { success: false, error: err.message || "Failed to resolve action" },
7229
- requestId
7230
- );
7231
- return;
7232
- }
7233
- pendingNextAction = void 0;
7234
- if (resolved !== null) {
7235
- userMessage = resolved.message;
7236
- pendingNextAction = resolved.next;
7237
- }
7238
- const isHidden = resolved !== null || !!parsed.hidden;
7239
- const rawText = parsed.text ?? "";
7240
- if (rawText.startsWith("@@automated::approvePlan@@")) {
7241
- try {
7242
- const plan = readFileSync(".remy-plan.md", "utf-8");
7243
- writeFileSync(
7244
- ".remy-plan.md",
7245
- plan.replace(/^status:\s*pending/m, "status: approved"),
7246
- "utf-8"
7451
+ if (resolved !== null) {
7452
+ userMessage = resolved.message;
7453
+ }
7454
+ const isHidden = resolved !== null || !!parsed.hidden;
7455
+ applyPlanFileSideEffect(parsed.text ?? "");
7456
+ const onboardingState = parsed.onboardingState ?? "onboardingFinished";
7457
+ this.currentOnboardingState = onboardingState;
7458
+ const system = buildSystemPrompt(
7459
+ onboardingState,
7460
+ parsed.viewContext
7247
7461
  );
7248
- } catch {
7462
+ if (resolved?.next) {
7463
+ this.queue.push({
7464
+ command: {
7465
+ action: "message",
7466
+ text: sentinel(resolved.next),
7467
+ onboardingState
7468
+ },
7469
+ source: "chain",
7470
+ enqueuedAt: Date.now()
7471
+ });
7472
+ }
7473
+ try {
7474
+ await runTurn({
7475
+ state: this.state,
7476
+ userMessage,
7477
+ attachments,
7478
+ apiConfig: this.config,
7479
+ system,
7480
+ model: this.opts.model,
7481
+ onboardingState,
7482
+ requestId,
7483
+ signal: this.currentAbort.signal,
7484
+ onEvent: this.onEvent,
7485
+ resolveExternalTool: this.resolveExternalTool,
7486
+ hidden: isHidden,
7487
+ toolRegistry: this.toolRegistry,
7488
+ onBackgroundComplete: this.onBackgroundComplete
7489
+ });
7490
+ if (!this.completedEmitted) {
7491
+ this.emitCompleted(requestId, {
7492
+ success: false,
7493
+ error: "Turn ended unexpectedly"
7494
+ });
7495
+ }
7496
+ log12.info("Turn complete", {
7497
+ requestId,
7498
+ durationMs: Date.now() - this.turnStart
7499
+ });
7500
+ } catch (err) {
7501
+ if (!this.completedEmitted) {
7502
+ this.emit("error", { error: err.message }, requestId);
7503
+ this.emitCompleted(requestId, {
7504
+ success: false,
7505
+ error: err.message
7506
+ });
7507
+ }
7508
+ log12.warn("Command failed", {
7509
+ action: "message",
7510
+ requestId,
7511
+ error: err.message
7512
+ });
7513
+ this.queue.drain();
7514
+ }
7515
+ this.applyPendingSummaries();
7516
+ this.applyPendingBlockUpdates();
7249
7517
  }
7250
- } else if (rawText.startsWith("@@automated::rejectPlan@@")) {
7251
- try {
7252
- unlinkSync(".remy-plan.md");
7253
- } catch {
7518
+ async handleMessage(parsed, requestId) {
7519
+ if (this.running) {
7520
+ const command = { ...parsed };
7521
+ if (requestId && command.requestId === void 0) {
7522
+ command.requestId = requestId;
7523
+ }
7524
+ this.queue.push({
7525
+ command,
7526
+ source: "user",
7527
+ enqueuedAt: Date.now()
7528
+ });
7529
+ this.emit(
7530
+ "queued",
7531
+ { position: this.queue.length, ...this.queueFields() },
7532
+ requestId
7533
+ );
7534
+ return;
7535
+ }
7536
+ this.running = true;
7537
+ try {
7538
+ await this.runSingleTurn(parsed, requestId);
7539
+ await this.drainQueueLoop();
7540
+ } finally {
7541
+ this.currentAbort = null;
7542
+ this.currentRequestId = void 0;
7543
+ this.running = false;
7544
+ }
7254
7545
  }
7255
- }
7256
- const onboardingState = parsed.onboardingState ?? "onboardingFinished";
7257
- const system = buildSystemPrompt(
7258
- onboardingState,
7259
- parsed.viewContext
7260
- );
7261
- try {
7262
- await runTurn({
7263
- state,
7264
- userMessage,
7265
- attachments,
7266
- apiConfig: config,
7267
- system,
7268
- model: opts.model,
7269
- onboardingState,
7270
- requestId,
7271
- signal: currentAbort.signal,
7272
- onEvent,
7273
- resolveExternalTool,
7274
- hidden: isHidden,
7275
- toolRegistry,
7276
- onBackgroundComplete
7277
- });
7278
- if (!completedEmitted) {
7279
- emit(
7280
- "completed",
7281
- { success: false, error: "Turn ended unexpectedly" },
7282
- requestId
7283
- );
7546
+ /**
7547
+ * Drain the queue in strict FIFO order. Caller must hold `running = true`.
7548
+ * User messages arriving during the drain will be enqueued behind current items.
7549
+ */
7550
+ async drainQueueLoop() {
7551
+ while (true) {
7552
+ const next = this.queue.shift();
7553
+ if (!next) {
7554
+ break;
7555
+ }
7556
+ const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
7557
+ await this.runSingleTurn(next.command, nextRid);
7558
+ }
7284
7559
  }
7285
- log11.info("Turn complete", {
7286
- requestId,
7287
- durationMs: Date.now() - turnStart
7288
- });
7289
- } catch (err) {
7290
- if (!completedEmitted) {
7291
- emit("error", { error: err.message }, requestId);
7292
- emit("completed", { success: false, error: err.message }, requestId);
7560
+ /**
7561
+ * Resume draining the queue when the agent is idle. Acquires the lock,
7562
+ * drains, releases. Used by the `resume` stdin action (sandbox-initiated)
7563
+ * and by kickDrain (background-completion-initiated).
7564
+ */
7565
+ async resumeQueue() {
7566
+ if (this.running || this.queue.length === 0) {
7567
+ return;
7568
+ }
7569
+ this.running = true;
7570
+ try {
7571
+ await this.drainQueueLoop();
7572
+ } finally {
7573
+ this.currentAbort = null;
7574
+ this.currentRequestId = void 0;
7575
+ this.running = false;
7576
+ }
7293
7577
  }
7294
- log11.warn("Command failed", {
7295
- action: "message",
7296
- requestId,
7297
- error: err.message
7298
- });
7299
- }
7300
- currentAbort = null;
7301
- currentRequestId = void 0;
7302
- running = false;
7303
- }
7304
- const rl = createInterface({ input: process.stdin });
7305
- rl.on("line", async (line) => {
7306
- let parsed;
7307
- try {
7308
- parsed = JSON.parse(line);
7309
- } catch {
7310
- emit("error", { error: "Invalid JSON on stdin" });
7311
- return;
7312
- }
7313
- const { action, requestId } = parsed;
7314
- log11.info("Command received", { action, requestId });
7315
- if (action === "tool_result" && parsed.id) {
7316
- const id = parsed.id;
7317
- const result = parsed.result ?? "";
7318
- const pending = pendingTools.get(id);
7319
- if (pending) {
7320
- pendingTools.delete(id);
7321
- pending.resolve(result);
7322
- } else if (!running) {
7323
- log11.info("Late tool_result while idle, dismissing", { id });
7324
- emit("completed", { success: true }, requestId);
7325
- } else {
7326
- earlyResults.set(id, result);
7578
+ /**
7579
+ * Kick off drainage of the queue when the agent is idle. Used by
7580
+ * onBackgroundComplete (when !running) to deliver results without
7581
+ * racing any currently-synchronous path.
7582
+ */
7583
+ kickDrain() {
7584
+ if (this.running || this.queue.length === 0) {
7585
+ return;
7586
+ }
7587
+ setTimeout(() => this.resumeQueue(), 0);
7327
7588
  }
7328
- return;
7329
- }
7330
- if (action === "get_history") {
7331
- applyPendingBlockUpdates();
7332
- dispatchSimple(requestId, "history", () => ({
7333
- messages: state.messages,
7334
- running,
7335
- ...running && currentRequestId ? { currentRequestId } : {}
7336
- }));
7337
- return;
7338
- }
7339
- if (action === "clear") {
7340
- dispatchSimple(requestId, "session_cleared", () => handleClear(state));
7341
- return;
7342
- }
7343
- if (action === "cancel") {
7344
- handleCancel(currentAbort, pendingTools);
7345
- emit("completed", { success: true }, requestId);
7346
- return;
7347
- }
7348
- if (action === "stop_tool") {
7349
- const id = parsed.id;
7350
- const mode = parsed.mode ?? "hard";
7351
- const found = toolRegistry.stop(id, mode);
7352
- if (found) {
7353
- emit("completed", { success: true }, requestId);
7354
- } else {
7355
- emit(
7356
- "completed",
7357
- { success: false, error: "Tool not found" },
7358
- requestId
7359
- );
7589
+ //////////////////////////////////////////////////////////////////////////////
7590
+ // Simple command handlers
7591
+ //////////////////////////////////////////////////////////////////////////////
7592
+ handleClear() {
7593
+ clearSession(this.state);
7594
+ return {};
7360
7595
  }
7361
- return;
7362
- }
7363
- if (action === "restart_tool") {
7364
- const id = parsed.id;
7365
- const patchedInput = parsed.input;
7366
- const found = toolRegistry.restart(id, patchedInput);
7367
- if (found) {
7368
- emit("completed", { success: true }, requestId);
7369
- } else {
7370
- emit(
7371
- "completed",
7372
- { success: false, error: "Tool not found" },
7373
- requestId
7374
- );
7596
+ /** Cancel the running turn and drain the queue. Returns the drained items. */
7597
+ handleCancel() {
7598
+ if (this.currentAbort) {
7599
+ this.currentAbort.abort();
7600
+ }
7601
+ for (const [id, pending] of this.pendingTools) {
7602
+ clearTimeout(pending.timeout);
7603
+ pending.resolve("Error: cancelled");
7604
+ this.pendingTools.delete(id);
7605
+ }
7606
+ return this.queue.drain();
7375
7607
  }
7376
- return;
7377
- }
7378
- if (action === "compact") {
7379
- triggerCompaction(state, config, {
7380
- onStart: () => {
7381
- sessionStats.compactionInProgress = true;
7382
- sessionStats.updatedAt = Date.now();
7383
- try {
7384
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
7385
- } catch {
7608
+ //////////////////////////////////////////////////////////////////////////////
7609
+ // Stdin router
7610
+ //////////////////////////////////////////////////////////////////////////////
7611
+ handleStdinLine = async (line) => {
7612
+ let parsed;
7613
+ try {
7614
+ parsed = JSON.parse(line);
7615
+ } catch {
7616
+ this.emit("error", { error: "Invalid JSON on stdin" });
7617
+ return;
7618
+ }
7619
+ const { action, requestId } = parsed;
7620
+ log12.info("Command received", { action, requestId });
7621
+ if (action === "tool_result" && parsed.id) {
7622
+ const id = parsed.id;
7623
+ const result = parsed.result ?? "";
7624
+ const pending = this.pendingTools.get(id);
7625
+ if (pending) {
7626
+ this.pendingTools.delete(id);
7627
+ pending.resolve(result);
7628
+ } else if (!this.running) {
7629
+ log12.info("Late tool_result while idle, dismissing", { id });
7630
+ this.emit("completed", { success: true }, requestId);
7631
+ } else {
7632
+ this.earlyResults.set(id, result);
7386
7633
  }
7387
- },
7388
- onSummariesReady: () => {
7389
- if (!running) {
7390
- applyPendingSummaries();
7634
+ return;
7635
+ }
7636
+ if (action === "get_history") {
7637
+ this.applyPendingBlockUpdates();
7638
+ this.dispatchSimple(requestId, "history", () => ({
7639
+ messages: this.state.messages,
7640
+ running: this.running,
7641
+ ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
7642
+ ...this.queueFields()
7643
+ }));
7644
+ return;
7645
+ }
7646
+ if (action === "clear") {
7647
+ this.dispatchSimple(
7648
+ requestId,
7649
+ "session_cleared",
7650
+ () => this.handleClear()
7651
+ );
7652
+ return;
7653
+ }
7654
+ if (action === "cancel") {
7655
+ const cancelled = this.handleCancel();
7656
+ this.emit(
7657
+ "completed",
7658
+ {
7659
+ success: true,
7660
+ ...cancelled.length > 0 && { cancelledMessages: cancelled }
7661
+ },
7662
+ requestId
7663
+ );
7664
+ return;
7665
+ }
7666
+ if (action === "stop_tool") {
7667
+ const id = parsed.id;
7668
+ const mode = parsed.mode ?? "hard";
7669
+ const found = this.toolRegistry.stop(id, mode);
7670
+ if (found) {
7671
+ this.emit("completed", { success: true }, requestId);
7672
+ } else {
7673
+ this.emit(
7674
+ "completed",
7675
+ { success: false, error: "Tool not found" },
7676
+ requestId
7677
+ );
7391
7678
  }
7392
- emit("compaction_complete", {}, requestId);
7393
- emit("completed", { success: true }, requestId);
7394
- },
7395
- onError: (error) => {
7396
- emit("compaction_complete", { error }, requestId);
7397
- emit("completed", { success: false, error }, requestId);
7398
- },
7399
- onFinally: () => {
7400
- sessionStats.compactionInProgress = false;
7401
- sessionStats.messageCount = state.messages.length;
7402
- sessionStats.updatedAt = Date.now();
7403
- try {
7404
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
7405
- } catch {
7679
+ return;
7680
+ }
7681
+ if (action === "restart_tool") {
7682
+ const id = parsed.id;
7683
+ const patchedInput = parsed.input;
7684
+ const found = this.toolRegistry.restart(id, patchedInput);
7685
+ if (found) {
7686
+ this.emit("completed", { success: true }, requestId);
7687
+ } else {
7688
+ this.emit(
7689
+ "completed",
7690
+ { success: false, error: "Tool not found" },
7691
+ requestId
7692
+ );
7406
7693
  }
7694
+ return;
7407
7695
  }
7408
- });
7409
- return;
7410
- }
7411
- if (action === "message") {
7412
- await handleMessage(parsed, requestId);
7413
- return;
7414
- }
7415
- emit("error", { error: `Unknown action: ${action}` }, requestId);
7416
- emit(
7417
- "completed",
7418
- { success: false, error: `Unknown action: ${action}` },
7419
- requestId
7420
- );
7421
- });
7422
- rl.on("close", () => {
7423
- emit("stopping");
7424
- emit("stopped");
7425
- process.exit(0);
7426
- });
7427
- function shutdown() {
7428
- emit("stopping");
7429
- emit("stopped");
7430
- process.exit(0);
7431
- }
7432
- process.on("SIGTERM", shutdown);
7433
- process.on("SIGINT", shutdown);
7434
- emit("ready");
7435
- }
7436
- var log11;
7437
- var init_headless = __esm({
7438
- "src/headless.ts"() {
7439
- "use strict";
7440
- init_logger();
7441
- init_config();
7442
- init_prompt();
7443
- init_trigger();
7444
- init_compaction();
7445
- init_lsp();
7446
- init_agent();
7447
- init_session();
7448
- init_toolRegistry();
7449
- init_resolve();
7450
- log11 = createLogger("headless");
7696
+ if (action === "compact") {
7697
+ triggerCompaction(this.state, this.config, {
7698
+ onStart: () => {
7699
+ this.sessionStats.compactionInProgress = true;
7700
+ this.persistStats();
7701
+ },
7702
+ onSummariesReady: () => {
7703
+ if (!this.running) {
7704
+ this.applyPendingSummaries();
7705
+ }
7706
+ this.emit("compaction_complete", {}, requestId);
7707
+ this.emit("completed", { success: true }, requestId);
7708
+ },
7709
+ onError: (error) => {
7710
+ this.emit("compaction_complete", { error }, requestId);
7711
+ this.emit("completed", { success: false, error }, requestId);
7712
+ },
7713
+ onFinally: () => {
7714
+ this.sessionStats.compactionInProgress = false;
7715
+ this.sessionStats.lastContextSize = 0;
7716
+ this.sessionStats.messageCount = this.state.messages.length;
7717
+ this.persistStats();
7718
+ }
7719
+ });
7720
+ return;
7721
+ }
7722
+ if (action === "message") {
7723
+ await this.handleMessage(parsed, requestId);
7724
+ return;
7725
+ }
7726
+ if (action === "resume") {
7727
+ if (this.running) {
7728
+ this.emit(
7729
+ "completed",
7730
+ { success: false, error: "already running" },
7731
+ requestId
7732
+ );
7733
+ return;
7734
+ }
7735
+ if (this.queue.length === 0) {
7736
+ this.emit("completed", { success: true }, requestId);
7737
+ return;
7738
+ }
7739
+ this.emit("completed", { success: true }, requestId);
7740
+ await this.resumeQueue();
7741
+ return;
7742
+ }
7743
+ this.emit("error", { error: `Unknown action: ${action}` }, requestId);
7744
+ this.emit(
7745
+ "completed",
7746
+ { success: false, error: `Unknown action: ${action}` },
7747
+ requestId
7748
+ );
7749
+ };
7750
+ };
7451
7751
  }
7452
7752
  });
7453
7753
 
@@ -7795,13 +8095,13 @@ function printDebugInfo(config) {
7795
8095
  var logLevel = flags.logLevel || void 0;
7796
8096
  if (headless) {
7797
8097
  initLoggerHeadless(logLevel);
7798
- const { startHeadless: startHeadless2 } = await Promise.resolve().then(() => (init_headless(), headless_exports));
7799
- startHeadless2({
8098
+ const { HeadlessSession: HeadlessSession2 } = await Promise.resolve().then(() => (init_headless(), headless_exports));
8099
+ new HeadlessSession2({
7800
8100
  apiKey: flags.apiKey,
7801
8101
  baseUrl: flags.baseUrl,
7802
8102
  model: flags.model,
7803
8103
  lspUrl: flags.lspUrl
7804
- }).catch((err) => {
8104
+ }).start().catch((err) => {
7805
8105
  console.error(err.message);
7806
8106
  process.exit(1);
7807
8107
  });