@mindstudio-ai/remy 0.1.148 → 0.1.150

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/headless.js CHANGED
@@ -4,17 +4,8 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
- // src/headless.ts
7
+ // src/headless/index.ts
8
8
  import { createInterface } from "readline";
9
- import {
10
- writeFileSync,
11
- readFileSync,
12
- unlinkSync,
13
- mkdirSync,
14
- existsSync
15
- } from "fs";
16
- import { writeFile } from "fs/promises";
17
- import { basename, join, extname } from "path";
18
9
 
19
10
  // src/logger.ts
20
11
  import fs from "fs";
@@ -377,11 +368,11 @@ Old tool results are periodically cleared from the conversation to save context
377
368
  </conversation_summaries>
378
369
 
379
370
  <project_onboarding>
380
- 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:
371
+ 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:
381
372
 
382
- - **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.
383
- - **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.
384
- - **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.
373
+ - **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.
374
+ - **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).
375
+ - **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.
385
376
  - **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.
386
377
  </project_onboarding>
387
378
 
@@ -1298,7 +1289,7 @@ var writePlanTool = {
1298
1289
  clearable: false,
1299
1290
  definition: {
1300
1291
  name: "writePlan",
1301
- 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.",
1292
+ 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.",
1302
1293
  inputSchema: {
1303
1294
  type: "object",
1304
1295
  properties: {
@@ -1368,13 +1359,13 @@ var setProjectOnboardingStateTool = {
1368
1359
  clearable: false,
1369
1360
  definition: {
1370
1361
  name: "setProjectOnboardingState",
1371
- 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.",
1362
+ 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.",
1372
1363
  inputSchema: {
1373
1364
  type: "object",
1374
1365
  properties: {
1375
1366
  state: {
1376
1367
  type: "string",
1377
- enum: ["initialSpecReview", "initialCodegen", "onboardingFinished"],
1368
+ enum: ["building", "buildComplete", "onboardingFinished"],
1378
1369
  description: "The onboarding state to advance to."
1379
1370
  }
1380
1371
  },
@@ -2541,7 +2532,7 @@ var runMethodTool = {
2541
2532
  clearable: true,
2542
2533
  definition: {
2543
2534
  name: "runMethod",
2544
- 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.",
2535
+ 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.',
2545
2536
  inputSchema: {
2546
2537
  type: "object",
2547
2538
  properties: {
@@ -2553,14 +2544,14 @@ var runMethodTool = {
2553
2544
  type: "object",
2554
2545
  description: "The input payload to pass to the method. Omit for methods that take no input."
2555
2546
  },
2547
+ userId: {
2548
+ type: "string",
2549
+ 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.'
2550
+ },
2556
2551
  roles: {
2557
2552
  type: "array",
2558
2553
  items: { type: "string" },
2559
- 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.'
2560
- },
2561
- userId: {
2562
- type: "string",
2563
- 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."
2554
+ 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.'
2564
2555
  }
2565
2556
  },
2566
2557
  required: ["method"]
@@ -2608,10 +2599,10 @@ async function analyzeImage(params) {
2608
2599
  }
2609
2600
 
2610
2601
  // src/tools/_helpers/screenshot.ts
2611
- var 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).
2602
+ var 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).`;
2603
+ var 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.
2612
2604
 
2613
2605
  Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
2614
- var 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.`;
2615
2606
  function buildScreenshotAnalysisPrompt(opts) {
2616
2607
  let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
2617
2608
  if (opts?.styleMap) {
@@ -2695,23 +2686,22 @@ async function checkBrowserConnected() {
2695
2686
  if (!status.connected) {
2696
2687
  return {
2697
2688
  connected: false,
2698
- error: "The browser preview is not connected. The user needs to open the preview."
2689
+ reason: BROWSER_UNAVAILABLE_MESSAGE
2699
2690
  };
2700
2691
  }
2701
2692
  return { connected: true };
2702
- } catch (err) {
2693
+ } catch {
2703
2694
  return {
2704
2695
  connected: false,
2705
- error: err?.message || "Could not check browser status. The dev environment may not be running."
2696
+ reason: BROWSER_UNAVAILABLE_MESSAGE
2706
2697
  };
2707
2698
  }
2708
2699
  }
2700
+ var 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.";
2709
2701
 
2710
2702
  // src/statusWatcher.ts
2711
2703
  function startStatusWatcher(config) {
2712
- const { apiConfig, getContext, onStatus, interval = 3e3, signal } = config;
2713
- let lastLabel = "";
2714
- let lastContext = "";
2704
+ const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
2715
2705
  let inflight = false;
2716
2706
  let stopped = false;
2717
2707
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
@@ -2722,10 +2712,9 @@ function startStatusWatcher(config) {
2722
2712
  inflight = true;
2723
2713
  try {
2724
2714
  const context = getContext();
2725
- if (!context || context === lastContext) {
2715
+ if (!context) {
2726
2716
  return;
2727
2717
  }
2728
- lastContext = context;
2729
2718
  const res = await fetch(url, {
2730
2719
  method: "POST",
2731
2720
  headers: {
@@ -2735,15 +2724,11 @@ function startStatusWatcher(config) {
2735
2724
  body: JSON.stringify({ context }),
2736
2725
  signal
2737
2726
  });
2738
- if (!res.ok) {
2727
+ if (!res.ok || stopped) {
2739
2728
  return;
2740
2729
  }
2741
2730
  const data = await res.json();
2742
- if (!data.label || data.label === lastLabel) {
2743
- return;
2744
- }
2745
- lastLabel = data.label;
2746
- if (stopped) {
2731
+ if (!data.label) {
2747
2732
  return;
2748
2733
  }
2749
2734
  onStatus(data.label);
@@ -2763,6 +2748,54 @@ function startStatusWatcher(config) {
2763
2748
  };
2764
2749
  }
2765
2750
 
2751
+ // src/automatedActions/sentinel.ts
2752
+ function sentinel(name) {
2753
+ return `@@automated::${name}@@`;
2754
+ }
2755
+ function automatedMessage(name, body) {
2756
+ return body ? `${sentinel(name)}
2757
+ ${body}` : sentinel(name);
2758
+ }
2759
+ function hasSentinel(text, name) {
2760
+ return text.startsWith(sentinel(name));
2761
+ }
2762
+ function isAutomatedMessage(text) {
2763
+ return text.startsWith("@@automated::");
2764
+ }
2765
+ function parseSentinel(text) {
2766
+ const match = text.match(/^@@automated::(\w+)@@(.*)/s);
2767
+ if (!match) {
2768
+ return null;
2769
+ }
2770
+ return { name: match[1], remainder: match[2] };
2771
+ }
2772
+ function stripSentinelLine(text) {
2773
+ return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
2774
+ }
2775
+ function buildBackgroundResultsMessage(results) {
2776
+ const xml = results.map(
2777
+ (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
2778
+ ${r.result}
2779
+ </tool_result>`
2780
+ ).join("\n\n");
2781
+ const plural = results.length > 1 ? "s" : "";
2782
+ const body = `This is an automated message containing the result${plural} of ${results.length > 1 ? "tool calls" : "a tool call"} that ${results.length > 1 ? "have" : "has"} been working in the background. This is not a direct message from the user.
2783
+ <background_results>
2784
+ ${xml}
2785
+ </background_results>`;
2786
+ return automatedMessage("background_results", body);
2787
+ }
2788
+ function mergeBackgroundResultsMessages(messages) {
2789
+ const results = [];
2790
+ const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
2791
+ for (const msg of messages) {
2792
+ for (const m of msg.matchAll(toolRe)) {
2793
+ results.push({ toolCallId: m[1], name: m[2], result: m[3] });
2794
+ }
2795
+ }
2796
+ return buildBackgroundResultsMessage(results);
2797
+ }
2798
+
2766
2799
  // src/subagents/common/cleanMessages.ts
2767
2800
  function findLastSummaryCheckpoint(messages, name) {
2768
2801
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -2856,11 +2889,8 @@ ${summaryBlock.text}
2856
2889
  }
2857
2890
  return true;
2858
2891
  }).map((msg) => {
2859
- if (msg.role === "user" && typeof msg.content === "string" && msg.content.startsWith("@@automated::")) {
2860
- return {
2861
- ...msg,
2862
- content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
2863
- };
2892
+ if (msg.role === "user" && typeof msg.content === "string" && isAutomatedMessage(msg.content)) {
2893
+ return { ...msg, content: stripSentinelLine(msg.content) };
2864
2894
  }
2865
2895
  if (!Array.isArray(msg.content)) {
2866
2896
  return msg;
@@ -2918,7 +2948,7 @@ async function runSubAgent(config) {
2918
2948
  const agentName = subAgentId || "sub-agent";
2919
2949
  const runStart = Date.now();
2920
2950
  log5.info("Sub-agent started", { requestId, parentToolId, agentName });
2921
- const emit2 = (e) => {
2951
+ const emit = (e) => {
2922
2952
  onEvent({ ...e, parentToolId });
2923
2953
  };
2924
2954
  const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
@@ -2983,7 +3013,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2983
3013
  }
2984
3014
  return parts.join("\n");
2985
3015
  },
2986
- onStatus: (label) => emit2({ type: "status", message: label }),
3016
+ onStatus: (label) => emit({ type: "status", message: label }),
2987
3017
  signal
2988
3018
  });
2989
3019
  try {
@@ -3000,7 +3030,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3000
3030
  signal
3001
3031
  },
3002
3032
  {
3003
- onRetry: (attempt) => emit2({
3033
+ onRetry: (attempt) => emit({
3004
3034
  type: "status",
3005
3035
  message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
3006
3036
  })
@@ -3021,14 +3051,14 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3021
3051
  startedAt: event.ts
3022
3052
  });
3023
3053
  }
3024
- emit2({ type: "text", text: event.text });
3054
+ emit({ type: "text", text: event.text });
3025
3055
  break;
3026
3056
  }
3027
3057
  case "thinking":
3028
3058
  if (!thinkingStartedAt) {
3029
3059
  thinkingStartedAt = event.ts;
3030
3060
  }
3031
- emit2({ type: "thinking", text: event.text });
3061
+ emit({ type: "thinking", text: event.text });
3032
3062
  break;
3033
3063
  case "thinking_complete":
3034
3064
  contentBlocks.push({
@@ -3048,7 +3078,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3048
3078
  input: event.input,
3049
3079
  startedAt: Date.now()
3050
3080
  });
3051
- emit2({
3081
+ emit({
3052
3082
  type: "tool_start",
3053
3083
  id: event.id,
3054
3084
  name: event.name,
@@ -3125,7 +3155,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3125
3155
  if (externalTools.has(tc.name) && resolveExternalTool) {
3126
3156
  result = await resolveExternalTool(tc.id, tc.name, input);
3127
3157
  } else {
3128
- const onLog = (line) => emit2({
3158
+ const onLog = (line) => emit({
3129
3159
  type: "tool_input_delta",
3130
3160
  id: tc.id,
3131
3161
  name: tc.name,
@@ -3176,7 +3206,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3176
3206
  durationMs: Date.now() - toolStart,
3177
3207
  isError: r.isError
3178
3208
  });
3179
- emit2({
3209
+ emit({
3180
3210
  type: "tool_done",
3181
3211
  id: tc.id,
3182
3212
  name: tc.name,
@@ -3445,7 +3475,7 @@ var browserAutomationTool = {
3445
3475
  try {
3446
3476
  const browserStatus = await checkBrowserConnected();
3447
3477
  if (!browserStatus.connected) {
3448
- return `Error: ${browserStatus.error}`;
3478
+ return browserStatus.reason ?? "Browser preview unavailable.";
3449
3479
  }
3450
3480
  try {
3451
3481
  await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
@@ -3631,7 +3661,7 @@ var screenshotTool = {
3631
3661
  try {
3632
3662
  const browserStatus = await checkBrowserConnected();
3633
3663
  if (!browserStatus.connected) {
3634
- return `Error: ${browserStatus.error}`;
3664
+ return browserStatus.reason ?? "Browser preview unavailable.";
3635
3665
  }
3636
3666
  return await captureAndAnalyzeScreenshot({
3637
3667
  prompt: input.prompt,
@@ -3969,7 +3999,7 @@ async function execute5(input, onLog, context) {
3969
3999
  try {
3970
4000
  const browserStatus = await checkBrowserConnected();
3971
4001
  if (!browserStatus.connected) {
3972
- return `Error: ${browserStatus.error}`;
4002
+ return browserStatus.reason ?? "Browser preview unavailable.";
3973
4003
  }
3974
4004
  return await captureAndAnalyzeScreenshot({
3975
4005
  prompt: input.prompt,
@@ -4402,7 +4432,7 @@ Each interface type invokes the same backend methods. Methods don't know which i
4402
4432
  TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
4403
4433
 
4404
4434
  - Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
4405
- - 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.
4435
+ - 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.
4406
4436
  - Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
4407
4437
  - Git-native deployment. Push to default branch to deploy.
4408
4438
 
@@ -4834,7 +4864,7 @@ Use <current_deck> as your starting point and replace or update the content as n
4834
4864
  - The deck must be a single HTML file \u2014 it will be rendered in an iframe.
4835
4865
  - Must look beautiful on desktop and mobile.
4836
4866
  - 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.
4837
- - Be bold and impactful.
4867
+ - Be bold and impactful. Use images from the spec or generate new images when needed.
4838
4868
  - 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.
4839
4869
  - Keep the progress bar and edge chevrons from the shell \u2014 they are part of the navigation UX.
4840
4870
 
@@ -5532,20 +5562,23 @@ async function runTurn(params) {
5532
5562
  parts.push(`Tool: ${toolName}`);
5533
5563
  }
5534
5564
  if (lastCompletedInput) {
5535
- parts.push(`Tool input: ${lastCompletedInput.slice(-300)}`);
5565
+ parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
5536
5566
  }
5537
5567
  if (lastCompletedResult) {
5538
- parts.push(`Tool result: ${lastCompletedResult.slice(-200)}`);
5568
+ parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
5539
5569
  }
5540
- const text = subAgentText || getTextContent(contentBlocks).slice(-500);
5570
+ const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
5541
5571
  if (text) {
5542
5572
  parts.push(`Assistant text: ${text}`);
5543
5573
  }
5544
5574
  if (onboardingState && onboardingState !== "onboardingFinished") {
5545
5575
  parts.push(`Build phase: ${onboardingState}`);
5546
5576
  }
5547
- if (userMessage) {
5548
- parts.push(`User request: ${userMessage.slice(-100)}`);
5577
+ const automated = parseSentinel(userMessage);
5578
+ if (automated) {
5579
+ parts.push(`Automated action: ${automated.name}`);
5580
+ } else if (userMessage) {
5581
+ parts.push(`User request: ${userMessage.slice(-500)}`);
5549
5582
  }
5550
5583
  return parts.join("\n");
5551
5584
  },
@@ -6005,19 +6038,246 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6005
6038
  }
6006
6039
  };
6007
6040
 
6041
+ // src/headless/attachments.ts
6042
+ import { mkdirSync, existsSync } from "fs";
6043
+ import { writeFile } from "fs/promises";
6044
+ import { basename, join, extname } from "path";
6045
+ var log11 = createLogger("headless:attachments");
6046
+ var UPLOADS_DIR = "src/.user-uploads";
6047
+ function filenameFromUrl(url) {
6048
+ try {
6049
+ const pathname = new URL(url).pathname;
6050
+ const name = basename(pathname);
6051
+ return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
6052
+ } catch {
6053
+ return `upload-${Date.now()}`;
6054
+ }
6055
+ }
6056
+ function resolveUniqueFilename(name) {
6057
+ if (!existsSync(join(UPLOADS_DIR, name))) {
6058
+ return name;
6059
+ }
6060
+ const ext = extname(name);
6061
+ const base = name.slice(0, name.length - ext.length);
6062
+ let counter = 1;
6063
+ while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
6064
+ counter++;
6065
+ }
6066
+ return `${base}-${counter}${ext}`;
6067
+ }
6068
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
6069
+ ".png",
6070
+ ".jpg",
6071
+ ".jpeg",
6072
+ ".gif",
6073
+ ".webp",
6074
+ ".svg",
6075
+ ".bmp",
6076
+ ".ico",
6077
+ ".tiff",
6078
+ ".tif",
6079
+ ".avif",
6080
+ ".heic",
6081
+ ".heif"
6082
+ ]);
6083
+ function isImageAttachment(att) {
6084
+ const name = att.filename || filenameFromUrl(att.url);
6085
+ return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
6086
+ }
6087
+ async function persistAttachments(attachments) {
6088
+ const nonVoice = attachments.filter((a) => !a.isVoice);
6089
+ if (nonVoice.length === 0) {
6090
+ return { documents: [], images: [] };
6091
+ }
6092
+ mkdirSync(UPLOADS_DIR, { recursive: true });
6093
+ const results = await Promise.allSettled(
6094
+ nonVoice.map(async (att) => {
6095
+ const name = resolveUniqueFilename(
6096
+ att.filename || filenameFromUrl(att.url)
6097
+ );
6098
+ const localPath = join(UPLOADS_DIR, name);
6099
+ const res = await fetch(att.url, {
6100
+ signal: AbortSignal.timeout(3e4)
6101
+ });
6102
+ if (!res.ok) {
6103
+ throw new Error(`HTTP ${res.status} downloading ${att.url}`);
6104
+ }
6105
+ const buffer = Buffer.from(await res.arrayBuffer());
6106
+ await writeFile(localPath, buffer);
6107
+ log11.info("Attachment saved", {
6108
+ filename: name,
6109
+ path: localPath,
6110
+ bytes: buffer.length
6111
+ });
6112
+ let extractedTextPath;
6113
+ if (att.extractedTextUrl) {
6114
+ try {
6115
+ const textRes = await fetch(att.extractedTextUrl, {
6116
+ signal: AbortSignal.timeout(3e4)
6117
+ });
6118
+ if (textRes.ok) {
6119
+ extractedTextPath = `${localPath}.txt`;
6120
+ await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6121
+ log11.info("Extracted text saved", { path: extractedTextPath });
6122
+ }
6123
+ } catch {
6124
+ }
6125
+ }
6126
+ return {
6127
+ filename: name,
6128
+ localPath,
6129
+ remoteUrl: att.url,
6130
+ extractedTextPath
6131
+ };
6132
+ })
6133
+ );
6134
+ const settled = results.map((r, i) => ({
6135
+ result: r.status === "fulfilled" ? r.value : null,
6136
+ isImage: isImageAttachment(nonVoice[i])
6137
+ }));
6138
+ return {
6139
+ documents: settled.filter((s) => !s.isImage).map((s) => s.result),
6140
+ images: settled.filter((s) => s.isImage).map((s) => s.result)
6141
+ };
6142
+ }
6143
+ function buildUploadHeader(results) {
6144
+ const succeeded = results.filter(Boolean);
6145
+ if (succeeded.length === 0) {
6146
+ return "";
6147
+ }
6148
+ if (succeeded.length === 1) {
6149
+ const r = succeeded[0];
6150
+ const parts = [`[Uploaded file: ${r.localPath} (CDN: ${r.remoteUrl})`];
6151
+ if (r.extractedTextPath) {
6152
+ parts.push(`extracted text: ${r.extractedTextPath}`);
6153
+ }
6154
+ return parts.join(" \u2014 ") + "]";
6155
+ }
6156
+ const lines = succeeded.map((r) => {
6157
+ const parts = [`- ${r.localPath} (CDN: ${r.remoteUrl})`];
6158
+ if (r.extractedTextPath) {
6159
+ parts.push(` extracted text: ${r.extractedTextPath}`);
6160
+ }
6161
+ return parts.join("\n");
6162
+ });
6163
+ return `[Uploaded files]
6164
+ ${lines.join("\n")}`;
6165
+ }
6166
+
6167
+ // src/headless/planFile.ts
6168
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
6169
+ var PLAN_FILE3 = ".remy-plan.md";
6170
+ function applyPlanFileSideEffect(rawText) {
6171
+ if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
6172
+ try {
6173
+ const plan = readFileSync(PLAN_FILE3, "utf-8");
6174
+ writeFileSync(
6175
+ PLAN_FILE3,
6176
+ plan.replace(/^status:\s*pending/m, "status: approved"),
6177
+ "utf-8"
6178
+ );
6179
+ } catch {
6180
+ }
6181
+ } else if (hasSentinel(rawText, "rejectPlan")) {
6182
+ try {
6183
+ unlinkSync(PLAN_FILE3);
6184
+ } catch {
6185
+ }
6186
+ }
6187
+ }
6188
+
6189
+ // src/headless/stats.ts
6190
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
6191
+ var STATS_FILE = ".remy-stats.json";
6192
+ function createSessionStats() {
6193
+ return {
6194
+ messageCount: 0,
6195
+ turns: 0,
6196
+ totalInputTokens: 0,
6197
+ totalOutputTokens: 0,
6198
+ totalCacheCreationTokens: 0,
6199
+ totalCacheReadTokens: 0,
6200
+ lastContextSize: 0,
6201
+ compactionInProgress: false,
6202
+ updatedAt: 0
6203
+ };
6204
+ }
6205
+ function loadQueue() {
6206
+ try {
6207
+ const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
6208
+ if (Array.isArray(stats.queue)) {
6209
+ return stats.queue;
6210
+ }
6211
+ } catch {
6212
+ }
6213
+ return [];
6214
+ }
6215
+ function writeStats(stats, queue) {
6216
+ try {
6217
+ writeFileSync2(
6218
+ STATS_FILE,
6219
+ JSON.stringify({
6220
+ ...stats,
6221
+ queue
6222
+ })
6223
+ );
6224
+ } catch {
6225
+ }
6226
+ }
6227
+
6228
+ // src/headless/messageQueue.ts
6229
+ var MessageQueue = class {
6230
+ items = [];
6231
+ onChange;
6232
+ constructor(initial = [], onChange) {
6233
+ this.items = [...initial];
6234
+ this.onChange = onChange;
6235
+ }
6236
+ push(item) {
6237
+ this.items.push(item);
6238
+ this.onChange?.();
6239
+ }
6240
+ shift() {
6241
+ const item = this.items.shift();
6242
+ if (item) {
6243
+ this.onChange?.();
6244
+ }
6245
+ return item;
6246
+ }
6247
+ /** Remove and return all queued items. */
6248
+ drain() {
6249
+ if (this.items.length === 0) {
6250
+ return [];
6251
+ }
6252
+ const all = this.items.splice(0);
6253
+ this.onChange?.();
6254
+ return all;
6255
+ }
6256
+ /** Copy of current queue contents (for surfacing on events). */
6257
+ snapshot() {
6258
+ return [...this.items];
6259
+ }
6260
+ /** Return the next item without removing it. */
6261
+ peek() {
6262
+ return this.items[0];
6263
+ }
6264
+ get length() {
6265
+ return this.items.length;
6266
+ }
6267
+ };
6268
+
6008
6269
  // src/automatedActions/resolve.ts
6009
6270
  var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
6010
6271
  function resolveAction(text) {
6011
- const match = text.match(/^@@automated::(\w+)@@(.*)/s);
6012
- if (!match) {
6272
+ const parsed = parseSentinel(text);
6273
+ if (!parsed) {
6013
6274
  return null;
6014
6275
  }
6015
- const triggerName = match[1];
6276
+ const { name: triggerName, remainder } = parsed;
6016
6277
  if (NON_ACTION_SENTINELS.has(triggerName)) {
6017
6278
  return null;
6018
6279
  }
6019
6280
  let params = {};
6020
- const remainder = match[2];
6021
6281
  if (remainder) {
6022
6282
  try {
6023
6283
  params = JSON.parse(remainder.split("\n")[0]);
@@ -6039,114 +6299,155 @@ function resolveAction(text) {
6039
6299
  body = body.replaceAll(`{{${key}}}`, str);
6040
6300
  }
6041
6301
  return {
6042
- message: `@@automated::${triggerName}@@
6043
- ${body}`,
6302
+ message: automatedMessage(triggerName, body),
6044
6303
  next
6045
6304
  };
6046
6305
  }
6047
6306
 
6048
- // src/headless.ts
6049
- var log11 = createLogger("headless");
6050
- function emit(event, data, requestId) {
6051
- const payload = { event, ...data };
6052
- if (requestId) {
6053
- payload.requestId = requestId;
6054
- }
6055
- process.stdout.write(JSON.stringify(payload) + "\n");
6056
- }
6057
- function handleClear(state) {
6058
- clearSession(state);
6059
- return {};
6060
- }
6061
- function handleCancel(currentAbort, pendingTools) {
6062
- if (currentAbort) {
6063
- currentAbort.abort();
6064
- }
6065
- for (const [id, pending] of pendingTools) {
6066
- clearTimeout(pending.timeout);
6067
- pending.resolve("Error: cancelled");
6068
- pendingTools.delete(id);
6069
- }
6070
- return {};
6071
- }
6072
- function dispatchSimple(requestId, eventName, handler) {
6073
- try {
6074
- const data = handler();
6075
- if (eventName) {
6076
- emit(eventName, data, requestId);
6307
+ // src/headless/index.ts
6308
+ var log12 = createLogger("headless");
6309
+ var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6310
+ var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6311
+ "promptUser",
6312
+ "confirmDestructiveAction",
6313
+ "presentPublishPlan"
6314
+ ]);
6315
+ var HeadlessSession = class {
6316
+ // Configuration
6317
+ opts;
6318
+ config;
6319
+ // Conversation state
6320
+ state = createAgentState();
6321
+ sessionStats = createSessionStats();
6322
+ // Turn lifecycle
6323
+ running = false;
6324
+ currentAbort = null;
6325
+ /** RequestId of the in-flight message command — injected into streamed events. */
6326
+ currentRequestId;
6327
+ /** Guard: track whether terminal `completed` was already sent so we emit exactly one. */
6328
+ completedEmitted = false;
6329
+ turnStart = 0;
6330
+ /**
6331
+ * Onboarding state of the currently-running turn. Captured at runSingleTurn
6332
+ * start so onBackgroundComplete can enqueue background results with the
6333
+ * right state (the triggering turn's state, not a stale one).
6334
+ */
6335
+ currentOnboardingState;
6336
+ /**
6337
+ * Unified message queue. Holds pending work to deliver after the current
6338
+ * turn completes: chained automated actions, background sub-agent results,
6339
+ * and user messages sent while a turn is running. Strict FIFO. Persisted
6340
+ * to .remy-stats.json so queued work survives process restarts.
6341
+ */
6342
+ queue;
6343
+ // External tool bridge
6344
+ pendingTools = /* @__PURE__ */ new Map();
6345
+ earlyResults = /* @__PURE__ */ new Map();
6346
+ // Tool block updates from background completions (separate from the message queue)
6347
+ pendingBlockUpdates = [];
6348
+ // Tool lifecycle management — shared across all nesting depths
6349
+ toolRegistry = new ToolRegistry();
6350
+ // IO
6351
+ readline = null;
6352
+ constructor(opts = {}) {
6353
+ this.opts = opts;
6354
+ }
6355
+ //////////////////////////////////////////////////////////////////////////////
6356
+ // Lifecycle
6357
+ //////////////////////////////////////////////////////////////////////////////
6358
+ async start() {
6359
+ const stderrWrite = (...args) => {
6360
+ process.stderr.write(args.map(String).join(" ") + "\n");
6361
+ };
6362
+ console.log = stderrWrite;
6363
+ console.warn = stderrWrite;
6364
+ console.info = stderrWrite;
6365
+ if (this.opts.lspUrl) {
6366
+ setLspBaseUrl(this.opts.lspUrl);
6367
+ }
6368
+ this.config = resolveConfig({
6369
+ apiKey: this.opts.apiKey,
6370
+ baseUrl: this.opts.baseUrl
6371
+ });
6372
+ const resumed = loadSession(this.state);
6373
+ this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
6374
+ if (resumed) {
6375
+ this.emit("session_restored", {
6376
+ messageCount: this.state.messages.length,
6377
+ ...this.queueFields()
6378
+ });
6077
6379
  }
6078
- emit("completed", { success: true }, requestId);
6079
- } catch (err) {
6080
- emit("completed", { success: false, error: err.message }, requestId);
6380
+ this.toolRegistry.onEvent = this.onEvent;
6381
+ this.readline = createInterface({ input: process.stdin });
6382
+ this.readline.on("line", this.handleStdinLine);
6383
+ this.readline.on("close", () => {
6384
+ this.emit("stopping");
6385
+ this.emit("stopped");
6386
+ process.exit(0);
6387
+ });
6388
+ process.on("SIGTERM", this.shutdown);
6389
+ process.on("SIGINT", this.shutdown);
6390
+ this.emit("ready", this.queueFields());
6081
6391
  }
6082
- }
6083
- async function startHeadless(opts = {}) {
6084
- const stderrWrite = (...args) => {
6085
- process.stderr.write(args.map(String).join(" ") + "\n");
6086
- };
6087
- console.log = stderrWrite;
6088
- console.warn = stderrWrite;
6089
- console.info = stderrWrite;
6090
- if (opts.lspUrl) {
6091
- setLspBaseUrl(opts.lspUrl);
6092
- }
6093
- const config = resolveConfig({
6094
- apiKey: opts.apiKey,
6095
- baseUrl: opts.baseUrl
6096
- });
6097
- const state = createAgentState();
6098
- const resumed = loadSession(state);
6099
- if (resumed) {
6100
- emit("session_restored", { messageCount: state.messages.length });
6101
- }
6102
- let running = false;
6103
- let currentAbort = null;
6104
- let currentRequestId;
6105
- let completedEmitted = false;
6106
- let turnStart = 0;
6107
- let pendingNextAction;
6108
- const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6109
- const pendingTools = /* @__PURE__ */ new Map();
6110
- const earlyResults = /* @__PURE__ */ new Map();
6111
- const toolRegistry = new ToolRegistry();
6112
- const sessionStats = {
6113
- messageCount: 0,
6114
- turns: 0,
6115
- totalInputTokens: 0,
6116
- totalOutputTokens: 0,
6117
- totalCacheCreationTokens: 0,
6118
- totalCacheReadTokens: 0,
6119
- lastContextSize: 0,
6120
- compactionInProgress: false,
6121
- updatedAt: 0
6392
+ shutdown = () => {
6393
+ this.emit("stopping");
6394
+ this.emit("stopped");
6395
+ process.exit(0);
6122
6396
  };
6123
- const backgroundQueue = [];
6124
- function flushBackgroundQueue() {
6125
- if (backgroundQueue.length === 0) {
6126
- return;
6397
+ //////////////////////////////////////////////////////////////////////////////
6398
+ // Wire protocol
6399
+ //////////////////////////////////////////////////////////////////////////////
6400
+ emit(event, data, requestId) {
6401
+ const payload = { event, ...data };
6402
+ if (requestId) {
6403
+ payload.requestId = requestId;
6127
6404
  }
6128
- const results = backgroundQueue.splice(0);
6129
- const xmlParts = results.map(
6130
- (r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
6131
- ${r.result}
6132
- </tool_result>`
6133
- ).join("\n\n");
6134
- const message = `@@automated::background_results@@
6135
- 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.
6136
- <background_results>
6137
- ${xmlParts}
6138
- </background_results>`;
6139
- const bgRequestId = `bg-${Date.now()}`;
6140
- handleMessage({ action: "message", text: message }, bgRequestId);
6405
+ process.stdout.write(JSON.stringify(payload) + "\n");
6141
6406
  }
6142
- const pendingBlockUpdates = [];
6143
- function applyPendingBlockUpdates() {
6144
- if (pendingBlockUpdates.length === 0) {
6407
+ /**
6408
+ * Emit a `completed` event and mark completedEmitted. Includes
6409
+ * `queuedMessages` if the queue has items (sandbox uses this to know the
6410
+ * agent is still busy with pipeline work).
6411
+ */
6412
+ emitCompleted(rid, data) {
6413
+ this.emit("completed", { ...data, ...this.queueFields() }, rid);
6414
+ this.completedEmitted = true;
6415
+ }
6416
+ /** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
6417
+ queueFields() {
6418
+ return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
6419
+ }
6420
+ /** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
6421
+ dispatchSimple(requestId, eventName, handler) {
6422
+ try {
6423
+ const data = handler();
6424
+ if (eventName) {
6425
+ this.emit(eventName, data, requestId);
6426
+ }
6427
+ this.emit("completed", { success: true }, requestId);
6428
+ } catch (err) {
6429
+ this.emit("completed", { success: false, error: err.message }, requestId);
6430
+ }
6431
+ }
6432
+ //////////////////////////////////////////////////////////////////////////////
6433
+ // Stats + queue persistence
6434
+ //////////////////////////////////////////////////////////////////////////////
6435
+ /** Persist sessionStats + queue snapshot to .remy-stats.json. */
6436
+ persistStats() {
6437
+ this.sessionStats.updatedAt = Date.now();
6438
+ writeStats(this.sessionStats, this.queue.snapshot());
6439
+ }
6440
+ //////////////////////////////////////////////////////////////////////////////
6441
+ // Background completions (tool-block mutation; message delivery via queue)
6442
+ //////////////////////////////////////////////////////////////////////////////
6443
+ /** Apply queued tool block updates to state.messages. Safe to call any time. */
6444
+ applyPendingBlockUpdates() {
6445
+ if (this.pendingBlockUpdates.length === 0) {
6145
6446
  return;
6146
6447
  }
6147
- const updates = pendingBlockUpdates.splice(0);
6448
+ const updates = this.pendingBlockUpdates.splice(0);
6148
6449
  for (const update of updates) {
6149
- for (const msg of state.messages) {
6450
+ for (const msg of this.state.messages) {
6150
6451
  if (!Array.isArray(msg.content)) {
6151
6452
  continue;
6152
6453
  }
@@ -6161,60 +6462,65 @@ ${xmlParts}
6161
6462
  }
6162
6463
  }
6163
6464
  }
6465
+ saveSession(this.state);
6164
6466
  }
6165
- function applyPendingSummaries() {
6467
+ /** Drain pending compaction summaries and insert at a safe point. */
6468
+ applyPendingSummaries() {
6166
6469
  const summaries = getPendingSummaries();
6167
6470
  if (summaries.length === 0) {
6168
6471
  return;
6169
6472
  }
6170
- const idx = findSafeInsertionPoint(state.messages);
6171
- state.messages.splice(idx, 0, ...summaries);
6172
- saveSession(state);
6473
+ const idx = findSafeInsertionPoint(this.state.messages);
6474
+ this.state.messages.splice(idx, 0, ...summaries);
6475
+ saveSession(this.state);
6173
6476
  }
6174
- function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
6175
- pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6176
- log11.info("Background complete", {
6477
+ onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
6478
+ this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6479
+ log12.info("Background complete", {
6177
6480
  toolCallId,
6178
6481
  name,
6179
- requestId: currentRequestId
6482
+ requestId: this.currentRequestId
6180
6483
  });
6181
- onEvent({
6484
+ this.onEvent({
6182
6485
  type: "tool_background_complete",
6183
6486
  id: toolCallId,
6184
6487
  name,
6185
6488
  result
6186
6489
  });
6187
- backgroundQueue.push({
6188
- toolCallId,
6189
- name,
6190
- result,
6191
- completedAt: Date.now()
6490
+ this.queue.push({
6491
+ command: {
6492
+ action: "message",
6493
+ text: buildBackgroundResultsMessage([{ toolCallId, name, result }]),
6494
+ ...this.currentOnboardingState && {
6495
+ onboardingState: this.currentOnboardingState
6496
+ }
6497
+ },
6498
+ source: "background",
6499
+ enqueuedAt: Date.now()
6192
6500
  });
6193
- if (!running) {
6194
- applyPendingBlockUpdates();
6195
- flushBackgroundQueue();
6501
+ if (!this.running) {
6502
+ this.applyPendingBlockUpdates();
6503
+ this.kickDrain();
6196
6504
  }
6197
- }
6198
- const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6199
- "promptUser",
6200
- "confirmDestructiveAction",
6201
- "presentPublishPlan"
6202
- ]);
6203
- function resolveExternalTool(id, name, _input) {
6204
- const early = earlyResults.get(id);
6505
+ };
6506
+ //////////////////////////////////////////////////////////////////////////////
6507
+ // External tool bridge
6508
+ //////////////////////////////////////////////////////////////////////////////
6509
+ resolveExternalTool = (id, name, _input) => {
6510
+ const early = this.earlyResults.get(id);
6205
6511
  if (early !== void 0) {
6206
- earlyResults.delete(id);
6512
+ this.earlyResults.delete(id);
6207
6513
  return Promise.resolve(early);
6208
6514
  }
6209
6515
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
6210
6516
  return new Promise((resolve2) => {
6211
6517
  const timeout = shouldTimeout ? setTimeout(() => {
6212
- pendingTools.delete(id);
6518
+ this.pendingTools.delete(id);
6213
6519
  resolve2(
6214
6520
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
6215
6521
  );
6216
6522
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
6217
- pendingTools.set(id, {
6523
+ this.pendingTools.set(id, {
6218
6524
  resolve: (result) => {
6219
6525
  clearTimeout(timeout);
6220
6526
  resolve2(result);
@@ -6222,57 +6528,51 @@ ${xmlParts}
6222
6528
  timeout
6223
6529
  });
6224
6530
  });
6225
- }
6226
- function onEvent(e) {
6227
- const rid = currentRequestId;
6531
+ };
6532
+ //////////////////////////////////////////////////////////////////////////////
6533
+ // AgentEvent wire protocol translation
6534
+ //////////////////////////////////////////////////////////////////////////////
6535
+ onEvent = (e) => {
6536
+ const rid = this.currentRequestId;
6228
6537
  switch (e.type) {
6229
6538
  case "turn_started":
6230
- emit("turn_started", {}, rid);
6539
+ this.emit("turn_started", {}, rid);
6540
+ return;
6541
+ case "user_message":
6542
+ this.emit("user_message", { text: e.text }, rid);
6231
6543
  return;
6232
- // Terminal events — translate to `completed`
6544
+ // Terminal events — translate to `completed`.
6545
+ // Post-turn queue drain happens in handleMessage AFTER runTurn returns,
6546
+ // so that `running` is held across the drain and no user message can
6547
+ // slip in mid-pipeline.
6233
6548
  case "turn_done":
6234
- completedEmitted = true;
6235
6549
  if (e.stats) {
6236
- sessionStats.turns++;
6237
- sessionStats.totalInputTokens += e.stats.inputTokens;
6238
- sessionStats.totalOutputTokens += e.stats.outputTokens;
6239
- sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
6240
- sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
6241
- sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
6242
- }
6243
- sessionStats.messageCount = state.messages.length;
6244
- sessionStats.updatedAt = Date.now();
6245
- try {
6246
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
6247
- } catch {
6550
+ this.sessionStats.turns++;
6551
+ this.sessionStats.totalInputTokens += e.stats.inputTokens;
6552
+ this.sessionStats.totalOutputTokens += e.stats.outputTokens;
6553
+ this.sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
6554
+ this.sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
6555
+ this.sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
6248
6556
  }
6249
- emit(
6557
+ this.sessionStats.messageCount = this.state.messages.length;
6558
+ this.persistStats();
6559
+ this.emitCompleted(rid, {
6560
+ success: true,
6561
+ durationMs: Date.now() - this.turnStart
6562
+ });
6563
+ return;
6564
+ case "turn_cancelled": {
6565
+ this.emit(
6250
6566
  "completed",
6251
- { success: true, durationMs: Date.now() - turnStart },
6567
+ { success: false, error: "cancelled", ...this.queueFields() },
6252
6568
  rid
6253
6569
  );
6254
- setTimeout(() => {
6255
- applyPendingSummaries();
6256
- applyPendingBlockUpdates();
6257
- flushBackgroundQueue();
6258
- if (pendingNextAction) {
6259
- const next = pendingNextAction;
6260
- pendingNextAction = void 0;
6261
- handleMessage(
6262
- { action: "message", text: `@@automated::${next}@@` },
6263
- `chain-${Date.now()}`
6264
- );
6265
- }
6266
- }, 0);
6267
- return;
6268
- case "turn_cancelled":
6269
- completedEmitted = true;
6270
- pendingNextAction = void 0;
6271
- emit("completed", { success: false, error: "cancelled" }, rid);
6570
+ this.completedEmitted = true;
6272
6571
  return;
6572
+ }
6273
6573
  // Streaming events — forward with requestId
6274
6574
  case "text":
6275
- emit(
6575
+ this.emit(
6276
6576
  "text",
6277
6577
  {
6278
6578
  text: e.text,
@@ -6282,7 +6582,7 @@ ${xmlParts}
6282
6582
  );
6283
6583
  return;
6284
6584
  case "thinking":
6285
- emit(
6585
+ this.emit(
6286
6586
  "thinking",
6287
6587
  {
6288
6588
  text: e.text,
@@ -6292,7 +6592,7 @@ ${xmlParts}
6292
6592
  );
6293
6593
  return;
6294
6594
  case "tool_input_delta":
6295
- emit(
6595
+ this.emit(
6296
6596
  "tool_input_delta",
6297
6597
  {
6298
6598
  id: e.id,
@@ -6304,7 +6604,7 @@ ${xmlParts}
6304
6604
  );
6305
6605
  return;
6306
6606
  case "tool_start":
6307
- emit(
6607
+ this.emit(
6308
6608
  "tool_start",
6309
6609
  {
6310
6610
  id: e.id,
@@ -6318,7 +6618,7 @@ ${xmlParts}
6318
6618
  );
6319
6619
  return;
6320
6620
  case "tool_done":
6321
- emit(
6621
+ this.emit(
6322
6622
  "tool_done",
6323
6623
  {
6324
6624
  id: e.id,
@@ -6331,7 +6631,7 @@ ${xmlParts}
6331
6631
  );
6332
6632
  return;
6333
6633
  case "tool_background_complete":
6334
- emit(
6634
+ this.emit(
6335
6635
  "tool_background_complete",
6336
6636
  {
6337
6637
  id: e.id,
@@ -6343,7 +6643,7 @@ ${xmlParts}
6343
6643
  );
6344
6644
  return;
6345
6645
  case "tool_stopped":
6346
- emit(
6646
+ this.emit(
6347
6647
  "tool_stopped",
6348
6648
  {
6349
6649
  id: e.id,
@@ -6355,7 +6655,7 @@ ${xmlParts}
6355
6655
  );
6356
6656
  return;
6357
6657
  case "tool_restarted":
6358
- emit(
6658
+ this.emit(
6359
6659
  "tool_restarted",
6360
6660
  {
6361
6661
  id: e.id,
@@ -6367,7 +6667,7 @@ ${xmlParts}
6367
6667
  );
6368
6668
  return;
6369
6669
  case "status":
6370
- emit(
6670
+ this.emit(
6371
6671
  "status",
6372
6672
  {
6373
6673
  message: e.message,
@@ -6377,147 +6677,27 @@ ${xmlParts}
6377
6677
  );
6378
6678
  return;
6379
6679
  case "error":
6380
- emit("error", { error: e.error }, rid);
6680
+ this.emit("error", { error: e.error }, rid);
6381
6681
  return;
6382
6682
  }
6383
- }
6384
- toolRegistry.onEvent = onEvent;
6385
- const UPLOADS_DIR = "src/.user-uploads";
6386
- function filenameFromUrl(url) {
6387
- try {
6388
- const pathname = new URL(url).pathname;
6389
- const name = basename(pathname);
6390
- return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
6391
- } catch {
6392
- return `upload-${Date.now()}`;
6393
- }
6394
- }
6395
- function resolveUniqueFilename(name) {
6396
- if (!existsSync(join(UPLOADS_DIR, name))) {
6397
- return name;
6398
- }
6399
- const ext = extname(name);
6400
- const base = name.slice(0, name.length - ext.length);
6401
- let counter = 1;
6402
- while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
6403
- counter++;
6404
- }
6405
- return `${base}-${counter}${ext}`;
6406
- }
6407
- const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
6408
- ".png",
6409
- ".jpg",
6410
- ".jpeg",
6411
- ".gif",
6412
- ".webp",
6413
- ".svg",
6414
- ".bmp",
6415
- ".ico",
6416
- ".tiff",
6417
- ".tif",
6418
- ".avif",
6419
- ".heic",
6420
- ".heif"
6421
- ]);
6422
- function isImageAttachment(att) {
6423
- const name = att.filename || filenameFromUrl(att.url);
6424
- return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
6425
- }
6426
- async function persistAttachments(attachments) {
6427
- const nonVoice = attachments.filter((a) => !a.isVoice);
6428
- if (nonVoice.length === 0) {
6429
- return { documents: [], images: [] };
6430
- }
6431
- mkdirSync(UPLOADS_DIR, { recursive: true });
6432
- const results = await Promise.allSettled(
6433
- nonVoice.map(async (att) => {
6434
- const name = resolveUniqueFilename(
6435
- att.filename || filenameFromUrl(att.url)
6436
- );
6437
- const localPath = join(UPLOADS_DIR, name);
6438
- const res = await fetch(att.url, {
6439
- signal: AbortSignal.timeout(3e4)
6440
- });
6441
- if (!res.ok) {
6442
- throw new Error(`HTTP ${res.status} downloading ${att.url}`);
6443
- }
6444
- const buffer = Buffer.from(await res.arrayBuffer());
6445
- await writeFile(localPath, buffer);
6446
- log11.info("Attachment saved", {
6447
- filename: name,
6448
- path: localPath,
6449
- bytes: buffer.length
6450
- });
6451
- let extractedTextPath;
6452
- if (att.extractedTextUrl) {
6453
- try {
6454
- const textRes = await fetch(att.extractedTextUrl, {
6455
- signal: AbortSignal.timeout(3e4)
6456
- });
6457
- if (textRes.ok) {
6458
- extractedTextPath = `${localPath}.txt`;
6459
- await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6460
- log11.info("Extracted text saved", { path: extractedTextPath });
6461
- }
6462
- } catch {
6463
- }
6464
- }
6465
- return { filename: name, localPath, extractedTextPath };
6466
- })
6467
- );
6468
- const settled = results.map((r, i) => ({
6469
- result: r.status === "fulfilled" ? r.value : null,
6470
- isImage: isImageAttachment(nonVoice[i])
6471
- }));
6472
- return {
6473
- documents: settled.filter((s) => !s.isImage).map((s) => s.result),
6474
- images: settled.filter((s) => s.isImage).map((s) => s.result)
6475
- };
6476
- }
6477
- function buildUploadHeader(results) {
6478
- const succeeded = results.filter(Boolean);
6479
- if (succeeded.length === 0) {
6480
- return "";
6481
- }
6482
- if (succeeded.length === 1) {
6483
- const r = succeeded[0];
6484
- const parts = [`[Uploaded file: ${r.localPath}`];
6485
- if (r.extractedTextPath) {
6486
- parts.push(`extracted text: ${r.extractedTextPath}`);
6487
- }
6488
- return parts.join(" \u2014 ") + "]";
6489
- }
6490
- const lines = succeeded.map((r) => {
6491
- if (r.extractedTextPath) {
6492
- return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
6493
- }
6494
- return `- ${r.localPath}`;
6495
- });
6496
- return `[Uploaded files]
6497
- ${lines.join("\n")}`;
6498
- }
6499
- async function handleMessage(parsed, requestId) {
6500
- if (running) {
6501
- emit(
6502
- "error",
6503
- { error: "Agent is already processing a message" },
6504
- requestId
6505
- );
6506
- emit(
6507
- "completed",
6508
- { success: false, error: "Agent is already processing a message" },
6509
- requestId
6510
- );
6511
- return;
6512
- }
6513
- running = true;
6514
- currentRequestId = requestId;
6515
- currentAbort = new AbortController();
6516
- completedEmitted = false;
6517
- turnStart = Date.now();
6683
+ };
6684
+ //////////////////////////////////////////////////////////////////////////////
6685
+ // Message command handler (long-running / streaming)
6686
+ //////////////////////////////////////////////////////////////////////////////
6687
+ /**
6688
+ * Run one turn (without acquiring the `running` lock). Called by
6689
+ * handleMessage for the initial turn, then repeatedly for each queued
6690
+ * message `running` stays held across the queue drain so no user
6691
+ * message can slip in mid-pipeline.
6692
+ */
6693
+ async runSingleTurn(parsed, requestId) {
6694
+ this.currentRequestId = requestId;
6695
+ this.currentAbort = new AbortController();
6696
+ this.completedEmitted = false;
6697
+ this.turnStart = Date.now();
6518
6698
  const attachments = parsed.attachments;
6519
6699
  if (attachments?.length) {
6520
- log11.info("Message has attachments", {
6700
+ log12.info("Message has attachments", {
6521
6701
  count: attachments.length,
6522
6702
  urls: attachments.map((a) => a.url)
6523
6703
  });
@@ -6534,143 +6714,266 @@ ${lines.join("\n")}`;
6534
6714
  ${userMessage}` : header;
6535
6715
  }
6536
6716
  } catch (err) {
6537
- log11.warn("Attachment persistence failed", { error: err.message });
6717
+ log12.warn("Attachment persistence failed", { error: err.message });
6538
6718
  }
6539
6719
  }
6540
6720
  let resolved = null;
6541
6721
  try {
6542
6722
  resolved = resolveAction(userMessage);
6543
6723
  } catch (err) {
6544
- emit(
6545
- "completed",
6546
- { success: false, error: err.message || "Failed to resolve action" },
6547
- requestId
6548
- );
6724
+ this.emitCompleted(requestId, {
6725
+ success: false,
6726
+ error: err.message || "Failed to resolve action"
6727
+ });
6549
6728
  return;
6550
6729
  }
6551
- pendingNextAction = void 0;
6552
6730
  if (resolved !== null) {
6553
6731
  userMessage = resolved.message;
6554
- pendingNextAction = resolved.next;
6555
6732
  }
6556
6733
  const isHidden = resolved !== null || !!parsed.hidden;
6557
- const rawText = parsed.text ?? "";
6558
- if (rawText.startsWith("@@automated::approvePlan@@")) {
6559
- try {
6560
- const plan = readFileSync(".remy-plan.md", "utf-8");
6561
- writeFileSync(
6562
- ".remy-plan.md",
6563
- plan.replace(/^status:\s*pending/m, "status: approved"),
6564
- "utf-8"
6565
- );
6566
- } catch {
6567
- }
6568
- } else if (rawText.startsWith("@@automated::rejectPlan@@")) {
6569
- try {
6570
- unlinkSync(".remy-plan.md");
6571
- } catch {
6572
- }
6573
- }
6734
+ applyPlanFileSideEffect(parsed.text ?? "");
6574
6735
  const onboardingState = parsed.onboardingState ?? "onboardingFinished";
6736
+ this.currentOnboardingState = onboardingState;
6575
6737
  const system = buildSystemPrompt(
6576
6738
  onboardingState,
6577
6739
  parsed.viewContext
6578
6740
  );
6741
+ if (resolved?.next) {
6742
+ this.queue.push({
6743
+ command: {
6744
+ action: "message",
6745
+ text: sentinel(resolved.next),
6746
+ onboardingState
6747
+ },
6748
+ source: "chain",
6749
+ enqueuedAt: Date.now()
6750
+ });
6751
+ }
6579
6752
  try {
6580
6753
  await runTurn({
6581
- state,
6754
+ state: this.state,
6582
6755
  userMessage,
6583
6756
  attachments,
6584
- apiConfig: config,
6757
+ apiConfig: this.config,
6585
6758
  system,
6586
- model: opts.model,
6759
+ model: this.opts.model,
6587
6760
  onboardingState,
6588
6761
  requestId,
6589
- signal: currentAbort.signal,
6590
- onEvent,
6591
- resolveExternalTool,
6762
+ signal: this.currentAbort.signal,
6763
+ onEvent: this.onEvent,
6764
+ resolveExternalTool: this.resolveExternalTool,
6592
6765
  hidden: isHidden,
6593
- toolRegistry,
6594
- onBackgroundComplete
6766
+ toolRegistry: this.toolRegistry,
6767
+ onBackgroundComplete: this.onBackgroundComplete
6595
6768
  });
6596
- if (!completedEmitted) {
6597
- emit(
6598
- "completed",
6599
- { success: false, error: "Turn ended unexpectedly" },
6600
- requestId
6601
- );
6769
+ if (!this.completedEmitted) {
6770
+ this.emitCompleted(requestId, {
6771
+ success: false,
6772
+ error: "Turn ended unexpectedly"
6773
+ });
6602
6774
  }
6603
- log11.info("Turn complete", {
6775
+ log12.info("Turn complete", {
6604
6776
  requestId,
6605
- durationMs: Date.now() - turnStart
6777
+ durationMs: Date.now() - this.turnStart
6606
6778
  });
6607
6779
  } catch (err) {
6608
- if (!completedEmitted) {
6609
- emit("error", { error: err.message }, requestId);
6610
- emit("completed", { success: false, error: err.message }, requestId);
6780
+ if (!this.completedEmitted) {
6781
+ this.emit("error", { error: err.message }, requestId);
6782
+ this.emitCompleted(requestId, {
6783
+ success: false,
6784
+ error: err.message
6785
+ });
6611
6786
  }
6612
- log11.warn("Command failed", {
6787
+ log12.warn("Command failed", {
6613
6788
  action: "message",
6614
6789
  requestId,
6615
6790
  error: err.message
6616
6791
  });
6792
+ this.queue.drain();
6793
+ }
6794
+ this.applyPendingSummaries();
6795
+ this.applyPendingBlockUpdates();
6796
+ }
6797
+ async handleMessage(parsed, requestId) {
6798
+ if (this.running) {
6799
+ const command = { ...parsed };
6800
+ if (requestId && command.requestId === void 0) {
6801
+ command.requestId = requestId;
6802
+ }
6803
+ this.queue.push({
6804
+ command,
6805
+ source: "user",
6806
+ enqueuedAt: Date.now()
6807
+ });
6808
+ this.emit(
6809
+ "queued",
6810
+ { position: this.queue.length, ...this.queueFields() },
6811
+ requestId
6812
+ );
6813
+ return;
6814
+ }
6815
+ this.running = true;
6816
+ try {
6817
+ await this.runSingleTurn(parsed, requestId);
6818
+ await this.drainQueueLoop();
6819
+ } finally {
6820
+ this.currentAbort = null;
6821
+ this.currentRequestId = void 0;
6822
+ this.running = false;
6823
+ }
6824
+ }
6825
+ /**
6826
+ * Drain the queue in strict FIFO order. Caller must hold `running = true`.
6827
+ * User messages arriving during the drain will be enqueued behind current items.
6828
+ *
6829
+ * Consecutive background-source items are coalesced into a single turn so
6830
+ * the LLM sees all the background results together and produces one
6831
+ * acknowledgment, not N separate ones.
6832
+ */
6833
+ async drainQueueLoop() {
6834
+ while (true) {
6835
+ const next = this.queue.shift();
6836
+ if (!next) {
6837
+ break;
6838
+ }
6839
+ if (next.source === "background") {
6840
+ const batch = [next];
6841
+ while (this.queue.peek()?.source === "background") {
6842
+ const more = this.queue.shift();
6843
+ if (more) {
6844
+ batch.push(more);
6845
+ }
6846
+ }
6847
+ const combinedCommand = {
6848
+ action: "message",
6849
+ text: mergeBackgroundResultsMessages(
6850
+ batch.map((b) => b.command.text ?? "")
6851
+ ),
6852
+ ...this.currentOnboardingState && {
6853
+ onboardingState: this.currentOnboardingState
6854
+ }
6855
+ };
6856
+ await this.runSingleTurn(combinedCommand, `background-${Date.now()}`);
6857
+ continue;
6858
+ }
6859
+ const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
6860
+ await this.runSingleTurn(next.command, nextRid);
6861
+ }
6862
+ }
6863
+ /**
6864
+ * Resume draining the queue when the agent is idle. Acquires the lock,
6865
+ * drains, releases. Used by the `resume` stdin action (sandbox-initiated)
6866
+ * and by kickDrain (background-completion-initiated).
6867
+ */
6868
+ async resumeQueue() {
6869
+ if (this.running || this.queue.length === 0) {
6870
+ return;
6871
+ }
6872
+ this.running = true;
6873
+ try {
6874
+ await this.drainQueueLoop();
6875
+ } finally {
6876
+ this.currentAbort = null;
6877
+ this.currentRequestId = void 0;
6878
+ this.running = false;
6617
6879
  }
6618
- currentAbort = null;
6619
- currentRequestId = void 0;
6620
- running = false;
6621
6880
  }
6622
- const rl = createInterface({ input: process.stdin });
6623
- rl.on("line", async (line) => {
6881
+ /**
6882
+ * Kick off drainage of the queue when the agent is idle. Used by
6883
+ * onBackgroundComplete (when !running) to deliver results without
6884
+ * racing any currently-synchronous path.
6885
+ */
6886
+ kickDrain() {
6887
+ if (this.running || this.queue.length === 0) {
6888
+ return;
6889
+ }
6890
+ setTimeout(() => this.resumeQueue(), 0);
6891
+ }
6892
+ //////////////////////////////////////////////////////////////////////////////
6893
+ // Simple command handlers
6894
+ //////////////////////////////////////////////////////////////////////////////
6895
+ handleClear() {
6896
+ clearSession(this.state);
6897
+ return {};
6898
+ }
6899
+ /** Cancel the running turn and drain the queue. Returns the drained items. */
6900
+ handleCancel() {
6901
+ if (this.currentAbort) {
6902
+ this.currentAbort.abort();
6903
+ }
6904
+ for (const [id, pending] of this.pendingTools) {
6905
+ clearTimeout(pending.timeout);
6906
+ pending.resolve("Error: cancelled");
6907
+ this.pendingTools.delete(id);
6908
+ }
6909
+ return this.queue.drain();
6910
+ }
6911
+ //////////////////////////////////////////////////////////////////////////////
6912
+ // Stdin router
6913
+ //////////////////////////////////////////////////////////////////////////////
6914
+ handleStdinLine = async (line) => {
6624
6915
  let parsed;
6625
6916
  try {
6626
6917
  parsed = JSON.parse(line);
6627
6918
  } catch {
6628
- emit("error", { error: "Invalid JSON on stdin" });
6919
+ this.emit("error", { error: "Invalid JSON on stdin" });
6629
6920
  return;
6630
6921
  }
6631
6922
  const { action, requestId } = parsed;
6632
- log11.info("Command received", { action, requestId });
6923
+ log12.info("Command received", { action, requestId });
6633
6924
  if (action === "tool_result" && parsed.id) {
6634
6925
  const id = parsed.id;
6635
6926
  const result = parsed.result ?? "";
6636
- const pending = pendingTools.get(id);
6927
+ const pending = this.pendingTools.get(id);
6637
6928
  if (pending) {
6638
- pendingTools.delete(id);
6929
+ this.pendingTools.delete(id);
6639
6930
  pending.resolve(result);
6640
- } else if (!running) {
6641
- log11.info("Late tool_result while idle, dismissing", { id });
6642
- emit("completed", { success: true }, requestId);
6931
+ } else if (!this.running) {
6932
+ log12.info("Late tool_result while idle, dismissing", { id });
6933
+ this.emit("completed", { success: true }, requestId);
6643
6934
  } else {
6644
- earlyResults.set(id, result);
6935
+ this.earlyResults.set(id, result);
6645
6936
  }
6646
6937
  return;
6647
6938
  }
6648
6939
  if (action === "get_history") {
6649
- applyPendingBlockUpdates();
6650
- dispatchSimple(requestId, "history", () => ({
6651
- messages: state.messages,
6652
- running,
6653
- ...running && currentRequestId ? { currentRequestId } : {}
6940
+ this.applyPendingBlockUpdates();
6941
+ this.dispatchSimple(requestId, "history", () => ({
6942
+ messages: this.state.messages,
6943
+ running: this.running,
6944
+ ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
6945
+ ...this.queueFields()
6654
6946
  }));
6655
6947
  return;
6656
6948
  }
6657
6949
  if (action === "clear") {
6658
- dispatchSimple(requestId, "session_cleared", () => handleClear(state));
6950
+ this.dispatchSimple(
6951
+ requestId,
6952
+ "session_cleared",
6953
+ () => this.handleClear()
6954
+ );
6659
6955
  return;
6660
6956
  }
6661
6957
  if (action === "cancel") {
6662
- handleCancel(currentAbort, pendingTools);
6663
- emit("completed", { success: true }, requestId);
6958
+ const cancelled = this.handleCancel();
6959
+ this.emit(
6960
+ "completed",
6961
+ {
6962
+ success: true,
6963
+ ...cancelled.length > 0 && { cancelledMessages: cancelled }
6964
+ },
6965
+ requestId
6966
+ );
6664
6967
  return;
6665
6968
  }
6666
6969
  if (action === "stop_tool") {
6667
6970
  const id = parsed.id;
6668
6971
  const mode = parsed.mode ?? "hard";
6669
- const found = toolRegistry.stop(id, mode);
6972
+ const found = this.toolRegistry.stop(id, mode);
6670
6973
  if (found) {
6671
- emit("completed", { success: true }, requestId);
6974
+ this.emit("completed", { success: true }, requestId);
6672
6975
  } else {
6673
- emit(
6976
+ this.emit(
6674
6977
  "completed",
6675
6978
  { success: false, error: "Tool not found" },
6676
6979
  requestId
@@ -6681,11 +6984,11 @@ ${userMessage}` : header;
6681
6984
  if (action === "restart_tool") {
6682
6985
  const id = parsed.id;
6683
6986
  const patchedInput = parsed.input;
6684
- const found = toolRegistry.restart(id, patchedInput);
6987
+ const found = this.toolRegistry.restart(id, patchedInput);
6685
6988
  if (found) {
6686
- emit("completed", { success: true }, requestId);
6989
+ this.emit("completed", { success: true }, requestId);
6687
6990
  } else {
6688
- emit(
6991
+ this.emit(
6689
6992
  "completed",
6690
6993
  { success: false, error: "Tool not found" },
6691
6994
  requestId
@@ -6694,64 +6997,60 @@ ${userMessage}` : header;
6694
6997
  return;
6695
6998
  }
6696
6999
  if (action === "compact") {
6697
- triggerCompaction(state, config, {
7000
+ triggerCompaction(this.state, this.config, {
6698
7001
  onStart: () => {
6699
- sessionStats.compactionInProgress = true;
6700
- sessionStats.updatedAt = Date.now();
6701
- try {
6702
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
6703
- } catch {
6704
- }
7002
+ this.sessionStats.compactionInProgress = true;
7003
+ this.persistStats();
6705
7004
  },
6706
7005
  onSummariesReady: () => {
6707
- if (!running) {
6708
- applyPendingSummaries();
7006
+ if (!this.running) {
7007
+ this.applyPendingSummaries();
6709
7008
  }
6710
- emit("compaction_complete", {}, requestId);
6711
- emit("completed", { success: true }, requestId);
7009
+ this.emit("compaction_complete", {}, requestId);
7010
+ this.emit("completed", { success: true }, requestId);
6712
7011
  },
6713
7012
  onError: (error) => {
6714
- emit("compaction_complete", { error }, requestId);
6715
- emit("completed", { success: false, error }, requestId);
7013
+ this.emit("compaction_complete", { error }, requestId);
7014
+ this.emit("completed", { success: false, error }, requestId);
6716
7015
  },
6717
7016
  onFinally: () => {
6718
- sessionStats.compactionInProgress = false;
6719
- sessionStats.lastContextSize = 0;
6720
- sessionStats.messageCount = state.messages.length;
6721
- sessionStats.updatedAt = Date.now();
6722
- try {
6723
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
6724
- } catch {
6725
- }
7017
+ this.sessionStats.compactionInProgress = false;
7018
+ this.sessionStats.lastContextSize = 0;
7019
+ this.sessionStats.messageCount = this.state.messages.length;
7020
+ this.persistStats();
6726
7021
  }
6727
7022
  });
6728
7023
  return;
6729
7024
  }
6730
7025
  if (action === "message") {
6731
- await handleMessage(parsed, requestId);
7026
+ await this.handleMessage(parsed, requestId);
6732
7027
  return;
6733
7028
  }
6734
- emit("error", { error: `Unknown action: ${action}` }, requestId);
6735
- emit(
7029
+ if (action === "resume") {
7030
+ if (this.running) {
7031
+ this.emit(
7032
+ "completed",
7033
+ { success: false, error: "already running" },
7034
+ requestId
7035
+ );
7036
+ return;
7037
+ }
7038
+ if (this.queue.length === 0) {
7039
+ this.emit("completed", { success: true }, requestId);
7040
+ return;
7041
+ }
7042
+ this.emit("completed", { success: true }, requestId);
7043
+ await this.resumeQueue();
7044
+ return;
7045
+ }
7046
+ this.emit("error", { error: `Unknown action: ${action}` }, requestId);
7047
+ this.emit(
6736
7048
  "completed",
6737
7049
  { success: false, error: `Unknown action: ${action}` },
6738
7050
  requestId
6739
7051
  );
6740
- });
6741
- rl.on("close", () => {
6742
- emit("stopping");
6743
- emit("stopped");
6744
- process.exit(0);
6745
- });
6746
- function shutdown() {
6747
- emit("stopping");
6748
- emit("stopped");
6749
- process.exit(0);
6750
- }
6751
- process.on("SIGTERM", shutdown);
6752
- process.on("SIGINT", shutdown);
6753
- emit("ready");
6754
- }
7052
+ };
7053
+ };
6755
7054
  export {
6756
- startHeadless
7055
+ HeadlessSession
6757
7056
  };