@mindstudio-ai/remy 0.1.148 → 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/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,43 @@ 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 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.
2782
+ <background_results>
2783
+ ${xml}
2784
+ </background_results>`;
2785
+ return automatedMessage("background_results", body);
2786
+ }
2787
+
2766
2788
  // src/subagents/common/cleanMessages.ts
2767
2789
  function findLastSummaryCheckpoint(messages, name) {
2768
2790
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -2856,11 +2878,8 @@ ${summaryBlock.text}
2856
2878
  }
2857
2879
  return true;
2858
2880
  }).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
- };
2881
+ if (msg.role === "user" && typeof msg.content === "string" && isAutomatedMessage(msg.content)) {
2882
+ return { ...msg, content: stripSentinelLine(msg.content) };
2864
2883
  }
2865
2884
  if (!Array.isArray(msg.content)) {
2866
2885
  return msg;
@@ -2918,7 +2937,7 @@ async function runSubAgent(config) {
2918
2937
  const agentName = subAgentId || "sub-agent";
2919
2938
  const runStart = Date.now();
2920
2939
  log5.info("Sub-agent started", { requestId, parentToolId, agentName });
2921
- const emit2 = (e) => {
2940
+ const emit = (e) => {
2922
2941
  onEvent({ ...e, parentToolId });
2923
2942
  };
2924
2943
  const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
@@ -2983,7 +3002,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
2983
3002
  }
2984
3003
  return parts.join("\n");
2985
3004
  },
2986
- onStatus: (label) => emit2({ type: "status", message: label }),
3005
+ onStatus: (label) => emit({ type: "status", message: label }),
2987
3006
  signal
2988
3007
  });
2989
3008
  try {
@@ -3000,7 +3019,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3000
3019
  signal
3001
3020
  },
3002
3021
  {
3003
- onRetry: (attempt) => emit2({
3022
+ onRetry: (attempt) => emit({
3004
3023
  type: "status",
3005
3024
  message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
3006
3025
  })
@@ -3021,14 +3040,14 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3021
3040
  startedAt: event.ts
3022
3041
  });
3023
3042
  }
3024
- emit2({ type: "text", text: event.text });
3043
+ emit({ type: "text", text: event.text });
3025
3044
  break;
3026
3045
  }
3027
3046
  case "thinking":
3028
3047
  if (!thinkingStartedAt) {
3029
3048
  thinkingStartedAt = event.ts;
3030
3049
  }
3031
- emit2({ type: "thinking", text: event.text });
3050
+ emit({ type: "thinking", text: event.text });
3032
3051
  break;
3033
3052
  case "thinking_complete":
3034
3053
  contentBlocks.push({
@@ -3048,7 +3067,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3048
3067
  input: event.input,
3049
3068
  startedAt: Date.now()
3050
3069
  });
3051
- emit2({
3070
+ emit({
3052
3071
  type: "tool_start",
3053
3072
  id: event.id,
3054
3073
  name: event.name,
@@ -3125,7 +3144,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3125
3144
  if (externalTools.has(tc.name) && resolveExternalTool) {
3126
3145
  result = await resolveExternalTool(tc.id, tc.name, input);
3127
3146
  } else {
3128
- const onLog = (line) => emit2({
3147
+ const onLog = (line) => emit({
3129
3148
  type: "tool_input_delta",
3130
3149
  id: tc.id,
3131
3150
  name: tc.name,
@@ -3176,7 +3195,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3176
3195
  durationMs: Date.now() - toolStart,
3177
3196
  isError: r.isError
3178
3197
  });
3179
- emit2({
3198
+ emit({
3180
3199
  type: "tool_done",
3181
3200
  id: tc.id,
3182
3201
  name: tc.name,
@@ -3445,7 +3464,7 @@ var browserAutomationTool = {
3445
3464
  try {
3446
3465
  const browserStatus = await checkBrowserConnected();
3447
3466
  if (!browserStatus.connected) {
3448
- return `Error: ${browserStatus.error}`;
3467
+ return browserStatus.reason ?? "Browser preview unavailable.";
3449
3468
  }
3450
3469
  try {
3451
3470
  await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
@@ -3631,7 +3650,7 @@ var screenshotTool = {
3631
3650
  try {
3632
3651
  const browserStatus = await checkBrowserConnected();
3633
3652
  if (!browserStatus.connected) {
3634
- return `Error: ${browserStatus.error}`;
3653
+ return browserStatus.reason ?? "Browser preview unavailable.";
3635
3654
  }
3636
3655
  return await captureAndAnalyzeScreenshot({
3637
3656
  prompt: input.prompt,
@@ -3969,7 +3988,7 @@ async function execute5(input, onLog, context) {
3969
3988
  try {
3970
3989
  const browserStatus = await checkBrowserConnected();
3971
3990
  if (!browserStatus.connected) {
3972
- return `Error: ${browserStatus.error}`;
3991
+ return browserStatus.reason ?? "Browser preview unavailable.";
3973
3992
  }
3974
3993
  return await captureAndAnalyzeScreenshot({
3975
3994
  prompt: input.prompt,
@@ -4402,7 +4421,7 @@ Each interface type invokes the same backend methods. Methods don't know which i
4402
4421
  TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
4403
4422
 
4404
4423
  - 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.
4424
+ - 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
4425
  - Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
4407
4426
  - Git-native deployment. Push to default branch to deploy.
4408
4427
 
@@ -4834,7 +4853,7 @@ Use <current_deck> as your starting point and replace or update the content as n
4834
4853
  - The deck must be a single HTML file \u2014 it will be rendered in an iframe.
4835
4854
  - Must look beautiful on desktop and mobile.
4836
4855
  - 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.
4856
+ - Be bold and impactful. Use images from the spec or generate new images when needed.
4838
4857
  - 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
4858
  - Keep the progress bar and edge chevrons from the shell \u2014 they are part of the navigation UX.
4840
4859
 
@@ -5532,20 +5551,23 @@ async function runTurn(params) {
5532
5551
  parts.push(`Tool: ${toolName}`);
5533
5552
  }
5534
5553
  if (lastCompletedInput) {
5535
- parts.push(`Tool input: ${lastCompletedInput.slice(-300)}`);
5554
+ parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
5536
5555
  }
5537
5556
  if (lastCompletedResult) {
5538
- parts.push(`Tool result: ${lastCompletedResult.slice(-200)}`);
5557
+ parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
5539
5558
  }
5540
- const text = subAgentText || getTextContent(contentBlocks).slice(-500);
5559
+ const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
5541
5560
  if (text) {
5542
5561
  parts.push(`Assistant text: ${text}`);
5543
5562
  }
5544
5563
  if (onboardingState && onboardingState !== "onboardingFinished") {
5545
5564
  parts.push(`Build phase: ${onboardingState}`);
5546
5565
  }
5547
- if (userMessage) {
5548
- parts.push(`User request: ${userMessage.slice(-100)}`);
5566
+ const automated = parseSentinel(userMessage);
5567
+ if (automated) {
5568
+ parts.push(`Automated action: ${automated.name}`);
5569
+ } else if (userMessage) {
5570
+ parts.push(`User request: ${userMessage.slice(-500)}`);
5549
5571
  }
5550
5572
  return parts.join("\n");
5551
5573
  },
@@ -6005,19 +6027,242 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
6005
6027
  }
6006
6028
  };
6007
6029
 
6030
+ // src/headless/attachments.ts
6031
+ import { mkdirSync, existsSync } from "fs";
6032
+ import { writeFile } from "fs/promises";
6033
+ import { basename, join, extname } from "path";
6034
+ var log11 = createLogger("headless:attachments");
6035
+ var UPLOADS_DIR = "src/.user-uploads";
6036
+ function filenameFromUrl(url) {
6037
+ try {
6038
+ const pathname = new URL(url).pathname;
6039
+ const name = basename(pathname);
6040
+ return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
6041
+ } catch {
6042
+ return `upload-${Date.now()}`;
6043
+ }
6044
+ }
6045
+ function resolveUniqueFilename(name) {
6046
+ if (!existsSync(join(UPLOADS_DIR, name))) {
6047
+ return name;
6048
+ }
6049
+ const ext = extname(name);
6050
+ const base = name.slice(0, name.length - ext.length);
6051
+ let counter = 1;
6052
+ while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
6053
+ counter++;
6054
+ }
6055
+ return `${base}-${counter}${ext}`;
6056
+ }
6057
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
6058
+ ".png",
6059
+ ".jpg",
6060
+ ".jpeg",
6061
+ ".gif",
6062
+ ".webp",
6063
+ ".svg",
6064
+ ".bmp",
6065
+ ".ico",
6066
+ ".tiff",
6067
+ ".tif",
6068
+ ".avif",
6069
+ ".heic",
6070
+ ".heif"
6071
+ ]);
6072
+ function isImageAttachment(att) {
6073
+ const name = att.filename || filenameFromUrl(att.url);
6074
+ return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
6075
+ }
6076
+ async function persistAttachments(attachments) {
6077
+ const nonVoice = attachments.filter((a) => !a.isVoice);
6078
+ if (nonVoice.length === 0) {
6079
+ return { documents: [], images: [] };
6080
+ }
6081
+ mkdirSync(UPLOADS_DIR, { recursive: true });
6082
+ const results = await Promise.allSettled(
6083
+ nonVoice.map(async (att) => {
6084
+ const name = resolveUniqueFilename(
6085
+ att.filename || filenameFromUrl(att.url)
6086
+ );
6087
+ const localPath = join(UPLOADS_DIR, name);
6088
+ const res = await fetch(att.url, {
6089
+ signal: AbortSignal.timeout(3e4)
6090
+ });
6091
+ if (!res.ok) {
6092
+ throw new Error(`HTTP ${res.status} downloading ${att.url}`);
6093
+ }
6094
+ const buffer = Buffer.from(await res.arrayBuffer());
6095
+ await writeFile(localPath, buffer);
6096
+ log11.info("Attachment saved", {
6097
+ filename: name,
6098
+ path: localPath,
6099
+ bytes: buffer.length
6100
+ });
6101
+ let extractedTextPath;
6102
+ if (att.extractedTextUrl) {
6103
+ try {
6104
+ const textRes = await fetch(att.extractedTextUrl, {
6105
+ signal: AbortSignal.timeout(3e4)
6106
+ });
6107
+ if (textRes.ok) {
6108
+ extractedTextPath = `${localPath}.txt`;
6109
+ await writeFile(extractedTextPath, await textRes.text(), "utf-8");
6110
+ log11.info("Extracted text saved", { path: extractedTextPath });
6111
+ }
6112
+ } catch {
6113
+ }
6114
+ }
6115
+ return {
6116
+ filename: name,
6117
+ localPath,
6118
+ remoteUrl: att.url,
6119
+ extractedTextPath
6120
+ };
6121
+ })
6122
+ );
6123
+ const settled = results.map((r, i) => ({
6124
+ result: r.status === "fulfilled" ? r.value : null,
6125
+ isImage: isImageAttachment(nonVoice[i])
6126
+ }));
6127
+ return {
6128
+ documents: settled.filter((s) => !s.isImage).map((s) => s.result),
6129
+ images: settled.filter((s) => s.isImage).map((s) => s.result)
6130
+ };
6131
+ }
6132
+ function buildUploadHeader(results) {
6133
+ const succeeded = results.filter(Boolean);
6134
+ if (succeeded.length === 0) {
6135
+ return "";
6136
+ }
6137
+ if (succeeded.length === 1) {
6138
+ const r = succeeded[0];
6139
+ const parts = [`[Uploaded file: ${r.localPath} (CDN: ${r.remoteUrl})`];
6140
+ if (r.extractedTextPath) {
6141
+ parts.push(`extracted text: ${r.extractedTextPath}`);
6142
+ }
6143
+ return parts.join(" \u2014 ") + "]";
6144
+ }
6145
+ const lines = succeeded.map((r) => {
6146
+ const parts = [`- ${r.localPath} (CDN: ${r.remoteUrl})`];
6147
+ if (r.extractedTextPath) {
6148
+ parts.push(` extracted text: ${r.extractedTextPath}`);
6149
+ }
6150
+ return parts.join("\n");
6151
+ });
6152
+ return `[Uploaded files]
6153
+ ${lines.join("\n")}`;
6154
+ }
6155
+
6156
+ // src/headless/planFile.ts
6157
+ import { readFileSync, writeFileSync, unlinkSync } from "fs";
6158
+ var PLAN_FILE3 = ".remy-plan.md";
6159
+ function applyPlanFileSideEffect(rawText) {
6160
+ if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
6161
+ try {
6162
+ const plan = readFileSync(PLAN_FILE3, "utf-8");
6163
+ writeFileSync(
6164
+ PLAN_FILE3,
6165
+ plan.replace(/^status:\s*pending/m, "status: approved"),
6166
+ "utf-8"
6167
+ );
6168
+ } catch {
6169
+ }
6170
+ } else if (hasSentinel(rawText, "rejectPlan")) {
6171
+ try {
6172
+ unlinkSync(PLAN_FILE3);
6173
+ } catch {
6174
+ }
6175
+ }
6176
+ }
6177
+
6178
+ // src/headless/stats.ts
6179
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
6180
+ var STATS_FILE = ".remy-stats.json";
6181
+ function createSessionStats() {
6182
+ return {
6183
+ messageCount: 0,
6184
+ turns: 0,
6185
+ totalInputTokens: 0,
6186
+ totalOutputTokens: 0,
6187
+ totalCacheCreationTokens: 0,
6188
+ totalCacheReadTokens: 0,
6189
+ lastContextSize: 0,
6190
+ compactionInProgress: false,
6191
+ updatedAt: 0
6192
+ };
6193
+ }
6194
+ function loadQueue() {
6195
+ try {
6196
+ const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
6197
+ if (Array.isArray(stats.queue)) {
6198
+ return stats.queue;
6199
+ }
6200
+ } catch {
6201
+ }
6202
+ return [];
6203
+ }
6204
+ function writeStats(stats, queue) {
6205
+ try {
6206
+ writeFileSync2(
6207
+ STATS_FILE,
6208
+ JSON.stringify({
6209
+ ...stats,
6210
+ queue
6211
+ })
6212
+ );
6213
+ } catch {
6214
+ }
6215
+ }
6216
+
6217
+ // src/headless/messageQueue.ts
6218
+ var MessageQueue = class {
6219
+ items = [];
6220
+ onChange;
6221
+ constructor(initial = [], onChange) {
6222
+ this.items = [...initial];
6223
+ this.onChange = onChange;
6224
+ }
6225
+ push(item) {
6226
+ this.items.push(item);
6227
+ this.onChange?.();
6228
+ }
6229
+ shift() {
6230
+ const item = this.items.shift();
6231
+ if (item) {
6232
+ this.onChange?.();
6233
+ }
6234
+ return item;
6235
+ }
6236
+ /** Remove and return all queued items. */
6237
+ drain() {
6238
+ if (this.items.length === 0) {
6239
+ return [];
6240
+ }
6241
+ const all = this.items.splice(0);
6242
+ this.onChange?.();
6243
+ return all;
6244
+ }
6245
+ /** Copy of current queue contents (for surfacing on events). */
6246
+ snapshot() {
6247
+ return [...this.items];
6248
+ }
6249
+ get length() {
6250
+ return this.items.length;
6251
+ }
6252
+ };
6253
+
6008
6254
  // src/automatedActions/resolve.ts
6009
6255
  var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
6010
6256
  function resolveAction(text) {
6011
- const match = text.match(/^@@automated::(\w+)@@(.*)/s);
6012
- if (!match) {
6257
+ const parsed = parseSentinel(text);
6258
+ if (!parsed) {
6013
6259
  return null;
6014
6260
  }
6015
- const triggerName = match[1];
6261
+ const { name: triggerName, remainder } = parsed;
6016
6262
  if (NON_ACTION_SENTINELS.has(triggerName)) {
6017
6263
  return null;
6018
6264
  }
6019
6265
  let params = {};
6020
- const remainder = match[2];
6021
6266
  if (remainder) {
6022
6267
  try {
6023
6268
  params = JSON.parse(remainder.split("\n")[0]);
@@ -6039,114 +6284,155 @@ function resolveAction(text) {
6039
6284
  body = body.replaceAll(`{{${key}}}`, str);
6040
6285
  }
6041
6286
  return {
6042
- message: `@@automated::${triggerName}@@
6043
- ${body}`,
6287
+ message: automatedMessage(triggerName, body),
6044
6288
  next
6045
6289
  };
6046
6290
  }
6047
6291
 
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);
6292
+ // src/headless/index.ts
6293
+ var log12 = createLogger("headless");
6294
+ var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
6295
+ var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
6296
+ "promptUser",
6297
+ "confirmDestructiveAction",
6298
+ "presentPublishPlan"
6299
+ ]);
6300
+ var HeadlessSession = class {
6301
+ // Configuration
6302
+ opts;
6303
+ config;
6304
+ // Conversation state
6305
+ state = createAgentState();
6306
+ sessionStats = createSessionStats();
6307
+ // Turn lifecycle
6308
+ running = false;
6309
+ currentAbort = null;
6310
+ /** RequestId of the in-flight message command — injected into streamed events. */
6311
+ currentRequestId;
6312
+ /** Guard: track whether terminal `completed` was already sent so we emit exactly one. */
6313
+ completedEmitted = false;
6314
+ turnStart = 0;
6315
+ /**
6316
+ * Onboarding state of the currently-running turn. Captured at runSingleTurn
6317
+ * start so onBackgroundComplete can enqueue background results with the
6318
+ * right state (the triggering turn's state, not a stale one).
6319
+ */
6320
+ currentOnboardingState;
6321
+ /**
6322
+ * Unified message queue. Holds pending work to deliver after the current
6323
+ * turn completes: chained automated actions, background sub-agent results,
6324
+ * and user messages sent while a turn is running. Strict FIFO. Persisted
6325
+ * to .remy-stats.json so queued work survives process restarts.
6326
+ */
6327
+ queue;
6328
+ // External tool bridge
6329
+ pendingTools = /* @__PURE__ */ new Map();
6330
+ earlyResults = /* @__PURE__ */ new Map();
6331
+ // Tool block updates from background completions (separate from the message queue)
6332
+ pendingBlockUpdates = [];
6333
+ // Tool lifecycle management — shared across all nesting depths
6334
+ toolRegistry = new ToolRegistry();
6335
+ // IO
6336
+ readline = null;
6337
+ constructor(opts = {}) {
6338
+ this.opts = opts;
6339
+ }
6340
+ //////////////////////////////////////////////////////////////////////////////
6341
+ // Lifecycle
6342
+ //////////////////////////////////////////////////////////////////////////////
6343
+ async start() {
6344
+ const stderrWrite = (...args) => {
6345
+ process.stderr.write(args.map(String).join(" ") + "\n");
6346
+ };
6347
+ console.log = stderrWrite;
6348
+ console.warn = stderrWrite;
6349
+ console.info = stderrWrite;
6350
+ if (this.opts.lspUrl) {
6351
+ setLspBaseUrl(this.opts.lspUrl);
6352
+ }
6353
+ this.config = resolveConfig({
6354
+ apiKey: this.opts.apiKey,
6355
+ baseUrl: this.opts.baseUrl
6356
+ });
6357
+ const resumed = loadSession(this.state);
6358
+ this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
6359
+ if (resumed) {
6360
+ this.emit("session_restored", {
6361
+ messageCount: this.state.messages.length,
6362
+ ...this.queueFields()
6363
+ });
6077
6364
  }
6078
- emit("completed", { success: true }, requestId);
6079
- } catch (err) {
6080
- emit("completed", { success: false, error: err.message }, requestId);
6365
+ this.toolRegistry.onEvent = this.onEvent;
6366
+ this.readline = createInterface({ input: process.stdin });
6367
+ this.readline.on("line", this.handleStdinLine);
6368
+ this.readline.on("close", () => {
6369
+ this.emit("stopping");
6370
+ this.emit("stopped");
6371
+ process.exit(0);
6372
+ });
6373
+ process.on("SIGTERM", this.shutdown);
6374
+ process.on("SIGINT", this.shutdown);
6375
+ this.emit("ready", this.queueFields());
6081
6376
  }
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
6377
+ shutdown = () => {
6378
+ this.emit("stopping");
6379
+ this.emit("stopped");
6380
+ process.exit(0);
6122
6381
  };
6123
- const backgroundQueue = [];
6124
- function flushBackgroundQueue() {
6125
- if (backgroundQueue.length === 0) {
6126
- return;
6382
+ //////////////////////////////////////////////////////////////////////////////
6383
+ // Wire protocol
6384
+ //////////////////////////////////////////////////////////////////////////////
6385
+ emit(event, data, requestId) {
6386
+ const payload = { event, ...data };
6387
+ if (requestId) {
6388
+ payload.requestId = requestId;
6127
6389
  }
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);
6390
+ process.stdout.write(JSON.stringify(payload) + "\n");
6391
+ }
6392
+ /**
6393
+ * Emit a `completed` event and mark completedEmitted. Includes
6394
+ * `queuedMessages` if the queue has items (sandbox uses this to know the
6395
+ * agent is still busy with pipeline work).
6396
+ */
6397
+ emitCompleted(rid, data) {
6398
+ this.emit("completed", { ...data, ...this.queueFields() }, rid);
6399
+ this.completedEmitted = true;
6400
+ }
6401
+ /** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
6402
+ queueFields() {
6403
+ return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
6141
6404
  }
6142
- const pendingBlockUpdates = [];
6143
- function applyPendingBlockUpdates() {
6144
- if (pendingBlockUpdates.length === 0) {
6405
+ /** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
6406
+ dispatchSimple(requestId, eventName, handler) {
6407
+ try {
6408
+ const data = handler();
6409
+ if (eventName) {
6410
+ this.emit(eventName, data, requestId);
6411
+ }
6412
+ this.emit("completed", { success: true }, requestId);
6413
+ } catch (err) {
6414
+ this.emit("completed", { success: false, error: err.message }, requestId);
6415
+ }
6416
+ }
6417
+ //////////////////////////////////////////////////////////////////////////////
6418
+ // Stats + queue persistence
6419
+ //////////////////////////////////////////////////////////////////////////////
6420
+ /** Persist sessionStats + queue snapshot to .remy-stats.json. */
6421
+ persistStats() {
6422
+ this.sessionStats.updatedAt = Date.now();
6423
+ writeStats(this.sessionStats, this.queue.snapshot());
6424
+ }
6425
+ //////////////////////////////////////////////////////////////////////////////
6426
+ // Background completions (tool-block mutation; message delivery via queue)
6427
+ //////////////////////////////////////////////////////////////////////////////
6428
+ /** Apply queued tool block updates to state.messages. Safe to call any time. */
6429
+ applyPendingBlockUpdates() {
6430
+ if (this.pendingBlockUpdates.length === 0) {
6145
6431
  return;
6146
6432
  }
6147
- const updates = pendingBlockUpdates.splice(0);
6433
+ const updates = this.pendingBlockUpdates.splice(0);
6148
6434
  for (const update of updates) {
6149
- for (const msg of state.messages) {
6435
+ for (const msg of this.state.messages) {
6150
6436
  if (!Array.isArray(msg.content)) {
6151
6437
  continue;
6152
6438
  }
@@ -6161,60 +6447,65 @@ ${xmlParts}
6161
6447
  }
6162
6448
  }
6163
6449
  }
6450
+ saveSession(this.state);
6164
6451
  }
6165
- function applyPendingSummaries() {
6452
+ /** Drain pending compaction summaries and insert at a safe point. */
6453
+ applyPendingSummaries() {
6166
6454
  const summaries = getPendingSummaries();
6167
6455
  if (summaries.length === 0) {
6168
6456
  return;
6169
6457
  }
6170
- const idx = findSafeInsertionPoint(state.messages);
6171
- state.messages.splice(idx, 0, ...summaries);
6172
- saveSession(state);
6458
+ const idx = findSafeInsertionPoint(this.state.messages);
6459
+ this.state.messages.splice(idx, 0, ...summaries);
6460
+ saveSession(this.state);
6173
6461
  }
6174
- function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
6175
- pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6176
- log11.info("Background complete", {
6462
+ onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
6463
+ this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
6464
+ log12.info("Background complete", {
6177
6465
  toolCallId,
6178
6466
  name,
6179
- requestId: currentRequestId
6467
+ requestId: this.currentRequestId
6180
6468
  });
6181
- onEvent({
6469
+ this.onEvent({
6182
6470
  type: "tool_background_complete",
6183
6471
  id: toolCallId,
6184
6472
  name,
6185
6473
  result
6186
6474
  });
6187
- backgroundQueue.push({
6188
- toolCallId,
6189
- name,
6190
- result,
6191
- completedAt: Date.now()
6475
+ this.queue.push({
6476
+ command: {
6477
+ action: "message",
6478
+ text: buildBackgroundResultsMessage([{ toolCallId, name, result }]),
6479
+ ...this.currentOnboardingState && {
6480
+ onboardingState: this.currentOnboardingState
6481
+ }
6482
+ },
6483
+ source: "background",
6484
+ enqueuedAt: Date.now()
6192
6485
  });
6193
- if (!running) {
6194
- applyPendingBlockUpdates();
6195
- flushBackgroundQueue();
6486
+ if (!this.running) {
6487
+ this.applyPendingBlockUpdates();
6488
+ this.kickDrain();
6196
6489
  }
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);
6490
+ };
6491
+ //////////////////////////////////////////////////////////////////////////////
6492
+ // External tool bridge
6493
+ //////////////////////////////////////////////////////////////////////////////
6494
+ resolveExternalTool = (id, name, _input) => {
6495
+ const early = this.earlyResults.get(id);
6205
6496
  if (early !== void 0) {
6206
- earlyResults.delete(id);
6497
+ this.earlyResults.delete(id);
6207
6498
  return Promise.resolve(early);
6208
6499
  }
6209
6500
  const shouldTimeout = !USER_FACING_TOOLS.has(name);
6210
6501
  return new Promise((resolve2) => {
6211
6502
  const timeout = shouldTimeout ? setTimeout(() => {
6212
- pendingTools.delete(id);
6503
+ this.pendingTools.delete(id);
6213
6504
  resolve2(
6214
6505
  "Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
6215
6506
  );
6216
6507
  }, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
6217
- pendingTools.set(id, {
6508
+ this.pendingTools.set(id, {
6218
6509
  resolve: (result) => {
6219
6510
  clearTimeout(timeout);
6220
6511
  resolve2(result);
@@ -6222,57 +6513,51 @@ ${xmlParts}
6222
6513
  timeout
6223
6514
  });
6224
6515
  });
6225
- }
6226
- function onEvent(e) {
6227
- const rid = currentRequestId;
6516
+ };
6517
+ //////////////////////////////////////////////////////////////////////////////
6518
+ // AgentEvent wire protocol translation
6519
+ //////////////////////////////////////////////////////////////////////////////
6520
+ onEvent = (e) => {
6521
+ const rid = this.currentRequestId;
6228
6522
  switch (e.type) {
6229
6523
  case "turn_started":
6230
- emit("turn_started", {}, rid);
6524
+ this.emit("turn_started", {}, rid);
6525
+ return;
6526
+ case "user_message":
6527
+ this.emit("user_message", { text: e.text }, rid);
6231
6528
  return;
6232
- // Terminal events — translate to `completed`
6529
+ // Terminal events — translate to `completed`.
6530
+ // Post-turn queue drain happens in handleMessage AFTER runTurn returns,
6531
+ // so that `running` is held across the drain and no user message can
6532
+ // slip in mid-pipeline.
6233
6533
  case "turn_done":
6234
- completedEmitted = true;
6235
6534
  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;
6535
+ this.sessionStats.turns++;
6536
+ this.sessionStats.totalInputTokens += e.stats.inputTokens;
6537
+ this.sessionStats.totalOutputTokens += e.stats.outputTokens;
6538
+ this.sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
6539
+ this.sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
6540
+ this.sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
6242
6541
  }
6243
- sessionStats.messageCount = state.messages.length;
6244
- sessionStats.updatedAt = Date.now();
6245
- try {
6246
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
6247
- } catch {
6248
- }
6249
- emit(
6542
+ this.sessionStats.messageCount = this.state.messages.length;
6543
+ this.persistStats();
6544
+ this.emitCompleted(rid, {
6545
+ success: true,
6546
+ durationMs: Date.now() - this.turnStart
6547
+ });
6548
+ return;
6549
+ case "turn_cancelled": {
6550
+ this.emit(
6250
6551
  "completed",
6251
- { success: true, durationMs: Date.now() - turnStart },
6552
+ { success: false, error: "cancelled", ...this.queueFields() },
6252
6553
  rid
6253
6554
  );
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);
6555
+ this.completedEmitted = true;
6272
6556
  return;
6557
+ }
6273
6558
  // Streaming events — forward with requestId
6274
6559
  case "text":
6275
- emit(
6560
+ this.emit(
6276
6561
  "text",
6277
6562
  {
6278
6563
  text: e.text,
@@ -6282,7 +6567,7 @@ ${xmlParts}
6282
6567
  );
6283
6568
  return;
6284
6569
  case "thinking":
6285
- emit(
6570
+ this.emit(
6286
6571
  "thinking",
6287
6572
  {
6288
6573
  text: e.text,
@@ -6292,7 +6577,7 @@ ${xmlParts}
6292
6577
  );
6293
6578
  return;
6294
6579
  case "tool_input_delta":
6295
- emit(
6580
+ this.emit(
6296
6581
  "tool_input_delta",
6297
6582
  {
6298
6583
  id: e.id,
@@ -6304,7 +6589,7 @@ ${xmlParts}
6304
6589
  );
6305
6590
  return;
6306
6591
  case "tool_start":
6307
- emit(
6592
+ this.emit(
6308
6593
  "tool_start",
6309
6594
  {
6310
6595
  id: e.id,
@@ -6318,7 +6603,7 @@ ${xmlParts}
6318
6603
  );
6319
6604
  return;
6320
6605
  case "tool_done":
6321
- emit(
6606
+ this.emit(
6322
6607
  "tool_done",
6323
6608
  {
6324
6609
  id: e.id,
@@ -6331,7 +6616,7 @@ ${xmlParts}
6331
6616
  );
6332
6617
  return;
6333
6618
  case "tool_background_complete":
6334
- emit(
6619
+ this.emit(
6335
6620
  "tool_background_complete",
6336
6621
  {
6337
6622
  id: e.id,
@@ -6343,7 +6628,7 @@ ${xmlParts}
6343
6628
  );
6344
6629
  return;
6345
6630
  case "tool_stopped":
6346
- emit(
6631
+ this.emit(
6347
6632
  "tool_stopped",
6348
6633
  {
6349
6634
  id: e.id,
@@ -6355,7 +6640,7 @@ ${xmlParts}
6355
6640
  );
6356
6641
  return;
6357
6642
  case "tool_restarted":
6358
- emit(
6643
+ this.emit(
6359
6644
  "tool_restarted",
6360
6645
  {
6361
6646
  id: e.id,
@@ -6367,7 +6652,7 @@ ${xmlParts}
6367
6652
  );
6368
6653
  return;
6369
6654
  case "status":
6370
- emit(
6655
+ this.emit(
6371
6656
  "status",
6372
6657
  {
6373
6658
  message: e.message,
@@ -6377,147 +6662,27 @@ ${xmlParts}
6377
6662
  );
6378
6663
  return;
6379
6664
  case "error":
6380
- emit("error", { error: e.error }, rid);
6665
+ this.emit("error", { error: e.error }, rid);
6381
6666
  return;
6382
6667
  }
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();
6668
+ };
6669
+ //////////////////////////////////////////////////////////////////////////////
6670
+ // Message command handler (long-running / streaming)
6671
+ //////////////////////////////////////////////////////////////////////////////
6672
+ /**
6673
+ * Run one turn (without acquiring the `running` lock). Called by
6674
+ * handleMessage for the initial turn, then repeatedly for each queued
6675
+ * message `running` stays held across the queue drain so no user
6676
+ * message can slip in mid-pipeline.
6677
+ */
6678
+ async runSingleTurn(parsed, requestId) {
6679
+ this.currentRequestId = requestId;
6680
+ this.currentAbort = new AbortController();
6681
+ this.completedEmitted = false;
6682
+ this.turnStart = Date.now();
6518
6683
  const attachments = parsed.attachments;
6519
6684
  if (attachments?.length) {
6520
- log11.info("Message has attachments", {
6685
+ log12.info("Message has attachments", {
6521
6686
  count: attachments.length,
6522
6687
  urls: attachments.map((a) => a.url)
6523
6688
  });
@@ -6534,143 +6699,242 @@ ${lines.join("\n")}`;
6534
6699
  ${userMessage}` : header;
6535
6700
  }
6536
6701
  } catch (err) {
6537
- log11.warn("Attachment persistence failed", { error: err.message });
6702
+ log12.warn("Attachment persistence failed", { error: err.message });
6538
6703
  }
6539
6704
  }
6540
6705
  let resolved = null;
6541
6706
  try {
6542
6707
  resolved = resolveAction(userMessage);
6543
6708
  } catch (err) {
6544
- emit(
6545
- "completed",
6546
- { success: false, error: err.message || "Failed to resolve action" },
6547
- requestId
6548
- );
6709
+ this.emitCompleted(requestId, {
6710
+ success: false,
6711
+ error: err.message || "Failed to resolve action"
6712
+ });
6549
6713
  return;
6550
6714
  }
6551
- pendingNextAction = void 0;
6552
6715
  if (resolved !== null) {
6553
6716
  userMessage = resolved.message;
6554
- pendingNextAction = resolved.next;
6555
6717
  }
6556
6718
  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
- }
6719
+ applyPlanFileSideEffect(parsed.text ?? "");
6574
6720
  const onboardingState = parsed.onboardingState ?? "onboardingFinished";
6721
+ this.currentOnboardingState = onboardingState;
6575
6722
  const system = buildSystemPrompt(
6576
6723
  onboardingState,
6577
6724
  parsed.viewContext
6578
6725
  );
6726
+ if (resolved?.next) {
6727
+ this.queue.push({
6728
+ command: {
6729
+ action: "message",
6730
+ text: sentinel(resolved.next),
6731
+ onboardingState
6732
+ },
6733
+ source: "chain",
6734
+ enqueuedAt: Date.now()
6735
+ });
6736
+ }
6579
6737
  try {
6580
6738
  await runTurn({
6581
- state,
6739
+ state: this.state,
6582
6740
  userMessage,
6583
6741
  attachments,
6584
- apiConfig: config,
6742
+ apiConfig: this.config,
6585
6743
  system,
6586
- model: opts.model,
6744
+ model: this.opts.model,
6587
6745
  onboardingState,
6588
6746
  requestId,
6589
- signal: currentAbort.signal,
6590
- onEvent,
6591
- resolveExternalTool,
6747
+ signal: this.currentAbort.signal,
6748
+ onEvent: this.onEvent,
6749
+ resolveExternalTool: this.resolveExternalTool,
6592
6750
  hidden: isHidden,
6593
- toolRegistry,
6594
- onBackgroundComplete
6751
+ toolRegistry: this.toolRegistry,
6752
+ onBackgroundComplete: this.onBackgroundComplete
6595
6753
  });
6596
- if (!completedEmitted) {
6597
- emit(
6598
- "completed",
6599
- { success: false, error: "Turn ended unexpectedly" },
6600
- requestId
6601
- );
6754
+ if (!this.completedEmitted) {
6755
+ this.emitCompleted(requestId, {
6756
+ success: false,
6757
+ error: "Turn ended unexpectedly"
6758
+ });
6602
6759
  }
6603
- log11.info("Turn complete", {
6760
+ log12.info("Turn complete", {
6604
6761
  requestId,
6605
- durationMs: Date.now() - turnStart
6762
+ durationMs: Date.now() - this.turnStart
6606
6763
  });
6607
6764
  } catch (err) {
6608
- if (!completedEmitted) {
6609
- emit("error", { error: err.message }, requestId);
6610
- emit("completed", { success: false, error: err.message }, requestId);
6765
+ if (!this.completedEmitted) {
6766
+ this.emit("error", { error: err.message }, requestId);
6767
+ this.emitCompleted(requestId, {
6768
+ success: false,
6769
+ error: err.message
6770
+ });
6611
6771
  }
6612
- log11.warn("Command failed", {
6772
+ log12.warn("Command failed", {
6613
6773
  action: "message",
6614
6774
  requestId,
6615
6775
  error: err.message
6616
6776
  });
6777
+ this.queue.drain();
6778
+ }
6779
+ this.applyPendingSummaries();
6780
+ this.applyPendingBlockUpdates();
6781
+ }
6782
+ async handleMessage(parsed, requestId) {
6783
+ if (this.running) {
6784
+ const command = { ...parsed };
6785
+ if (requestId && command.requestId === void 0) {
6786
+ command.requestId = requestId;
6787
+ }
6788
+ this.queue.push({
6789
+ command,
6790
+ source: "user",
6791
+ enqueuedAt: Date.now()
6792
+ });
6793
+ this.emit(
6794
+ "queued",
6795
+ { position: this.queue.length, ...this.queueFields() },
6796
+ requestId
6797
+ );
6798
+ return;
6799
+ }
6800
+ this.running = true;
6801
+ try {
6802
+ await this.runSingleTurn(parsed, requestId);
6803
+ await this.drainQueueLoop();
6804
+ } finally {
6805
+ this.currentAbort = null;
6806
+ this.currentRequestId = void 0;
6807
+ this.running = false;
6808
+ }
6809
+ }
6810
+ /**
6811
+ * Drain the queue in strict FIFO order. Caller must hold `running = true`.
6812
+ * User messages arriving during the drain will be enqueued behind current items.
6813
+ */
6814
+ async drainQueueLoop() {
6815
+ while (true) {
6816
+ const next = this.queue.shift();
6817
+ if (!next) {
6818
+ break;
6819
+ }
6820
+ const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
6821
+ await this.runSingleTurn(next.command, nextRid);
6822
+ }
6823
+ }
6824
+ /**
6825
+ * Resume draining the queue when the agent is idle. Acquires the lock,
6826
+ * drains, releases. Used by the `resume` stdin action (sandbox-initiated)
6827
+ * and by kickDrain (background-completion-initiated).
6828
+ */
6829
+ async resumeQueue() {
6830
+ if (this.running || this.queue.length === 0) {
6831
+ return;
6832
+ }
6833
+ this.running = true;
6834
+ try {
6835
+ await this.drainQueueLoop();
6836
+ } finally {
6837
+ this.currentAbort = null;
6838
+ this.currentRequestId = void 0;
6839
+ this.running = false;
6840
+ }
6841
+ }
6842
+ /**
6843
+ * Kick off drainage of the queue when the agent is idle. Used by
6844
+ * onBackgroundComplete (when !running) to deliver results without
6845
+ * racing any currently-synchronous path.
6846
+ */
6847
+ kickDrain() {
6848
+ if (this.running || this.queue.length === 0) {
6849
+ return;
6850
+ }
6851
+ setTimeout(() => this.resumeQueue(), 0);
6852
+ }
6853
+ //////////////////////////////////////////////////////////////////////////////
6854
+ // Simple command handlers
6855
+ //////////////////////////////////////////////////////////////////////////////
6856
+ handleClear() {
6857
+ clearSession(this.state);
6858
+ return {};
6859
+ }
6860
+ /** Cancel the running turn and drain the queue. Returns the drained items. */
6861
+ handleCancel() {
6862
+ if (this.currentAbort) {
6863
+ this.currentAbort.abort();
6864
+ }
6865
+ for (const [id, pending] of this.pendingTools) {
6866
+ clearTimeout(pending.timeout);
6867
+ pending.resolve("Error: cancelled");
6868
+ this.pendingTools.delete(id);
6617
6869
  }
6618
- currentAbort = null;
6619
- currentRequestId = void 0;
6620
- running = false;
6870
+ return this.queue.drain();
6621
6871
  }
6622
- const rl = createInterface({ input: process.stdin });
6623
- rl.on("line", async (line) => {
6872
+ //////////////////////////////////////////////////////////////////////////////
6873
+ // Stdin router
6874
+ //////////////////////////////////////////////////////////////////////////////
6875
+ handleStdinLine = async (line) => {
6624
6876
  let parsed;
6625
6877
  try {
6626
6878
  parsed = JSON.parse(line);
6627
6879
  } catch {
6628
- emit("error", { error: "Invalid JSON on stdin" });
6880
+ this.emit("error", { error: "Invalid JSON on stdin" });
6629
6881
  return;
6630
6882
  }
6631
6883
  const { action, requestId } = parsed;
6632
- log11.info("Command received", { action, requestId });
6884
+ log12.info("Command received", { action, requestId });
6633
6885
  if (action === "tool_result" && parsed.id) {
6634
6886
  const id = parsed.id;
6635
6887
  const result = parsed.result ?? "";
6636
- const pending = pendingTools.get(id);
6888
+ const pending = this.pendingTools.get(id);
6637
6889
  if (pending) {
6638
- pendingTools.delete(id);
6890
+ this.pendingTools.delete(id);
6639
6891
  pending.resolve(result);
6640
- } else if (!running) {
6641
- log11.info("Late tool_result while idle, dismissing", { id });
6642
- emit("completed", { success: true }, requestId);
6892
+ } else if (!this.running) {
6893
+ log12.info("Late tool_result while idle, dismissing", { id });
6894
+ this.emit("completed", { success: true }, requestId);
6643
6895
  } else {
6644
- earlyResults.set(id, result);
6896
+ this.earlyResults.set(id, result);
6645
6897
  }
6646
6898
  return;
6647
6899
  }
6648
6900
  if (action === "get_history") {
6649
- applyPendingBlockUpdates();
6650
- dispatchSimple(requestId, "history", () => ({
6651
- messages: state.messages,
6652
- running,
6653
- ...running && currentRequestId ? { currentRequestId } : {}
6901
+ this.applyPendingBlockUpdates();
6902
+ this.dispatchSimple(requestId, "history", () => ({
6903
+ messages: this.state.messages,
6904
+ running: this.running,
6905
+ ...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
6906
+ ...this.queueFields()
6654
6907
  }));
6655
6908
  return;
6656
6909
  }
6657
6910
  if (action === "clear") {
6658
- dispatchSimple(requestId, "session_cleared", () => handleClear(state));
6911
+ this.dispatchSimple(
6912
+ requestId,
6913
+ "session_cleared",
6914
+ () => this.handleClear()
6915
+ );
6659
6916
  return;
6660
6917
  }
6661
6918
  if (action === "cancel") {
6662
- handleCancel(currentAbort, pendingTools);
6663
- emit("completed", { success: true }, requestId);
6919
+ const cancelled = this.handleCancel();
6920
+ this.emit(
6921
+ "completed",
6922
+ {
6923
+ success: true,
6924
+ ...cancelled.length > 0 && { cancelledMessages: cancelled }
6925
+ },
6926
+ requestId
6927
+ );
6664
6928
  return;
6665
6929
  }
6666
6930
  if (action === "stop_tool") {
6667
6931
  const id = parsed.id;
6668
6932
  const mode = parsed.mode ?? "hard";
6669
- const found = toolRegistry.stop(id, mode);
6933
+ const found = this.toolRegistry.stop(id, mode);
6670
6934
  if (found) {
6671
- emit("completed", { success: true }, requestId);
6935
+ this.emit("completed", { success: true }, requestId);
6672
6936
  } else {
6673
- emit(
6937
+ this.emit(
6674
6938
  "completed",
6675
6939
  { success: false, error: "Tool not found" },
6676
6940
  requestId
@@ -6681,11 +6945,11 @@ ${userMessage}` : header;
6681
6945
  if (action === "restart_tool") {
6682
6946
  const id = parsed.id;
6683
6947
  const patchedInput = parsed.input;
6684
- const found = toolRegistry.restart(id, patchedInput);
6948
+ const found = this.toolRegistry.restart(id, patchedInput);
6685
6949
  if (found) {
6686
- emit("completed", { success: true }, requestId);
6950
+ this.emit("completed", { success: true }, requestId);
6687
6951
  } else {
6688
- emit(
6952
+ this.emit(
6689
6953
  "completed",
6690
6954
  { success: false, error: "Tool not found" },
6691
6955
  requestId
@@ -6694,64 +6958,60 @@ ${userMessage}` : header;
6694
6958
  return;
6695
6959
  }
6696
6960
  if (action === "compact") {
6697
- triggerCompaction(state, config, {
6961
+ triggerCompaction(this.state, this.config, {
6698
6962
  onStart: () => {
6699
- sessionStats.compactionInProgress = true;
6700
- sessionStats.updatedAt = Date.now();
6701
- try {
6702
- writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
6703
- } catch {
6704
- }
6963
+ this.sessionStats.compactionInProgress = true;
6964
+ this.persistStats();
6705
6965
  },
6706
6966
  onSummariesReady: () => {
6707
- if (!running) {
6708
- applyPendingSummaries();
6967
+ if (!this.running) {
6968
+ this.applyPendingSummaries();
6709
6969
  }
6710
- emit("compaction_complete", {}, requestId);
6711
- emit("completed", { success: true }, requestId);
6970
+ this.emit("compaction_complete", {}, requestId);
6971
+ this.emit("completed", { success: true }, requestId);
6712
6972
  },
6713
6973
  onError: (error) => {
6714
- emit("compaction_complete", { error }, requestId);
6715
- emit("completed", { success: false, error }, requestId);
6974
+ this.emit("compaction_complete", { error }, requestId);
6975
+ this.emit("completed", { success: false, error }, requestId);
6716
6976
  },
6717
6977
  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
- }
6978
+ this.sessionStats.compactionInProgress = false;
6979
+ this.sessionStats.lastContextSize = 0;
6980
+ this.sessionStats.messageCount = this.state.messages.length;
6981
+ this.persistStats();
6726
6982
  }
6727
6983
  });
6728
6984
  return;
6729
6985
  }
6730
6986
  if (action === "message") {
6731
- await handleMessage(parsed, requestId);
6987
+ await this.handleMessage(parsed, requestId);
6732
6988
  return;
6733
6989
  }
6734
- emit("error", { error: `Unknown action: ${action}` }, requestId);
6735
- emit(
6990
+ if (action === "resume") {
6991
+ if (this.running) {
6992
+ this.emit(
6993
+ "completed",
6994
+ { success: false, error: "already running" },
6995
+ requestId
6996
+ );
6997
+ return;
6998
+ }
6999
+ if (this.queue.length === 0) {
7000
+ this.emit("completed", { success: true }, requestId);
7001
+ return;
7002
+ }
7003
+ this.emit("completed", { success: true }, requestId);
7004
+ await this.resumeQueue();
7005
+ return;
7006
+ }
7007
+ this.emit("error", { error: `Unknown action: ${action}` }, requestId);
7008
+ this.emit(
6736
7009
  "completed",
6737
7010
  { success: false, error: `Unknown action: ${action}` },
6738
7011
  requestId
6739
7012
  );
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
- }
7013
+ };
7014
+ };
6755
7015
  export {
6756
- startHeadless
7016
+ HeadlessSession
6757
7017
  };