@mindstudio-ai/remy 0.1.199 → 0.1.201

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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Remy
2
2
 
3
- A spec-building and coding agent for MindStudio apps.
3
+ A spec-building and coding agent for building apps.
4
4
 
5
- Remy helps users design, spec, build, and iterate on MindStudio projects. It runs locally in a terminal or as a headless subprocess in the MindStudio sandbox. It has tools for reading/writing specs and code, running shell commands, searching code, prompting users with structured forms, and (in the sandbox) TypeScript language server integration. LLM calls are routed through the MindStudio platform for billing and model routing.
5
+ Remy helps users design, spec, build, and iterate on app projects. It runs locally in a terminal or as a headless subprocess in the sandbox. It has tools for reading/writing specs and code, running shell commands, searching code, prompting users with structured forms, and (in the sandbox) TypeScript language server integration. LLM calls are routed through the Remy platform for billing and model routing.
6
6
 
7
7
  ## Quick Start
8
8
 
@@ -11,7 +11,7 @@ Remy helps users design, spec, build, and iterate on MindStudio projects. It run
11
11
  mindstudio login
12
12
 
13
13
  # Navigate to your project
14
- cd my-mindstudio-app
14
+ cd my-app
15
15
 
16
16
  # Run remy
17
17
  npx remy
@@ -59,6 +59,7 @@ Tool availability depends on the project's onboarding state, sent by the sandbox
59
59
  | `visualDesignExpert` | Visual design expert for fonts, colors, palettes, gradients, layouts, imagery, and icons (sub-agent) |
60
60
  | `productVision` | Owns the product roadmap — creates/updates/deletes roadmap items in `src/roadmap/` (sub-agent) |
61
61
  | `codeSanityCheck` | Quick readonly sanity check on architecture and package choices before building (sub-agent) |
62
+ | `writeBuildOverview` | Generate/refresh the Build Overview (`src/overview.html`) — a single-page plain-language reference of everything the app contains; you author the copy, the design expert lays it out |
62
63
 
63
64
  ### Spec Tools
64
65
 
@@ -307,14 +308,24 @@ Return the full conversation history.
307
308
 
308
309
  Messages with `hidden: true` were generated by `runCommand` actions and should not be displayed in the UI.
309
310
 
311
+ The response also includes `queuedMessages` — the current pending-queue snapshot (possibly empty) — so a client can render the queue immediately on connect/reconnect. Live changes after that arrive via `queue_changed`.
312
+
310
313
  #### `cancel`
311
314
 
312
- Cancel the current turn. The cancel command gets `completed(success:true)`. The in-flight message command (if any) gets its own `completed(success:false, error:"cancelled")`.
315
+ Cancel the current turn. The cancel command gets `completed(success:true)`; any queued messages are flushed and returned on it as `cancelledMessages`. The in-flight message command (if any) gets its own `completed(success:false, error:"cancelled")`.
313
316
 
314
317
  ```json
315
318
  {"action": "cancel", "requestId": "r3"}
316
319
  ```
317
320
 
321
+ #### `cancelQueued`
322
+
323
+ Cancel pending **queued** messages without touching the in-flight turn. Omit `id` to cancel all queued user messages, or pass `id` (the `requestId` of a queued message) to cancel just that one. Only user messages are cancellable — chained and background messages are part of a system chain and are never removed. Responds with `completed(success:true, cancelledQueued:[...])` listing what was removed; a `queue_changed` event also fires with the updated queue.
324
+
325
+ ```json
326
+ {"action": "cancelQueued", "requestId": "r6", "id": "r2"}
327
+ ```
328
+
318
329
  #### `clear`
319
330
 
320
331
  Clear conversation history and delete the session file.
@@ -341,6 +352,7 @@ Events are emitted as newline-delimited JSON. Command responses include `request
341
352
  |-------|--------|-------------|
342
353
  | `ready` | | Headless mode initialized, ready for input |
343
354
  | `session_restored` | `messageCount` | Previous session loaded |
355
+ | `queue_changed` | `queuedMessages` | Queue contents changed (any push/shift/drain/cancel). Carries the full current snapshot — an empty array when the queue drains. The live queue-state signal; for the initial snapshot on connect/reconnect, read `queuedMessages` from `get_history`. |
344
356
  | `stopping` | | Shutdown initiated |
345
357
  | `stopped` | | Shutdown complete |
346
358
 
@@ -2,7 +2,7 @@
2
2
  trigger: debugRequest
3
3
  ---
4
4
 
5
- This is an automated message triggered by the user having clicked "Debug" in the "Request" log detail in MindStudio UI. Find the request in .logs/requests.ndjson by its ID. If there is an error, fix it immediately - otherwise, explain the request at a high-level in non-technical/natural language and see what the user wishes to do with it. Remember, the user can't see this message, so keep that in mind when responding.
5
+ This is an automated message triggered by the user having clicked "Debug" in the "Request" log detail in the Remy UI. Find the request in .logs/requests.ndjson by its ID. If there is an error, fix it immediately - otherwise, explain the request at a high-level in non-technical/natural language and see what the user wishes to do with it. Remember, the user can't see this message, so keep that in mind when responding.
6
6
 
7
7
  <request_id>
8
8
  {{requestId}}
@@ -2,7 +2,7 @@
2
2
  trigger: feelingLucky
3
3
  ---
4
4
 
5
- This is an automated message triggered by the user having clicked "I'm feeling lucky" in the MindStudio UI and selecting a category from a list of app archetypes. They can't see this message. They're likely new to MindStudio and Remy - this might even be their first time using it. They haven't described a specific idea yet and more than likely don't actually have anything specific in mind — they're exploring what they can build and clicked on something that looked interesting. Your job is to ask questions to help them conjure the seed of an idea that is unique, specific, and interesting, and will be compelling to build as an MVP. Don't build something generic - help the user make something unique that allows you to truly show off the power of what you can build. Stay focused on web interfaces for now, unless the user specifically wants a different interface type. When you've aligned on something to build, focus on making it visually beautiful and highly usable from a UX perspective - at this point in the user's experience they're going to be much more "wowed" by something that looks pretty than by something with a ton of backend complexity.
5
+ This is an automated message triggered by the user having clicked "I'm feeling lucky" in the Remy UI and selecting a category from a list of app archetypes. They can't see this message. They're likely new to Remy - this might even be their first time using it. They haven't described a specific idea yet and more than likely don't actually have anything specific in mind — they're exploring what they can build and clicked on something that looked interesting. Your job is to ask questions to help them conjure the seed of an idea that is unique, specific, and interesting, and will be compelling to build as an MVP. Don't build something generic - help the user make something unique that allows you to truly show off the power of what you can build. Stay focused on web interfaces for now, unless the user specifically wants a different interface type. When you've aligned on something to build, focus on making it visually beautiful and highly usable from a UX perspective - at this point in the user's experience they're going to be much more "wowed" by something that looks pretty than by something with a ton of backend complexity.
6
6
 
7
7
  <selected_category>
8
8
  {{title}}
@@ -13,6 +13,7 @@ The visual assets — photography, generated images, brand colors, typography
13
13
 
14
14
  ## Finalizing
15
15
  When everything is working and polished:
16
- 1. Use `productVision` to mark the MVP roadmap item as done.
17
- 2. Call `compactConversation` to summarize the build session and free up context for the next phase of work.
18
- 3. Call `setProjectOnboardingState({ state: "buildComplete" })`. This triggers the reveal experience on the frontend, where the user sees the pitch deck and their finished app for the first time.
16
+ 1. Write the Build Overview. Author the full copy of everything this build actually produced — the data stores and what each holds, the backend operations, the interfaces, any access and role boundaries, background jobs, and seeded scenarios, including the parts the user can't see — in plain, calm, present-tense language, with real names and exact counts. Then call `writeBuildOverview` to render it. Do this now, before compacting, while the build is still fresh in context.
17
+ 2. Use `productVision` to mark the MVP roadmap item as done.
18
+ 3. Call `compactConversation` to summarize the build session and free up context for the next phase of work.
19
+ 4. Call `setProjectOnboardingState({ state: "buildComplete" })`. This triggers the reveal experience on the frontend, where the user sees their finished app and its Build Overview for the first time.
@@ -1,4 +1,4 @@
1
- You extract a structured `AppBrand` JSON object from the spec files of a MindStudio app project.
1
+ You extract a structured `AppBrand` JSON object from the spec files of an app project.
2
2
 
3
3
  Your output is read by a frontend renderer that uses the brand to style internal documents (implementation plans, sync plans, publish plans) with a "letterhead" treatment — a wordmark, accent color, paper-tone background, and branded fonts. Every field is optional. The renderer falls back to generic styling when a field is missing or invalid.
4
4
 
@@ -1,4 +1,4 @@
1
- You are summarizing a conversation between a user and an AI coding agent called Remy that builds MindStudio apps. Your summary will replace the conversation history — the agent will only see your summary plus any new messages, so it must capture everything needed to continue working.
1
+ You are summarizing a conversation between a user and an AI coding agent called Remy that builds apps. Your summary will replace the conversation history — the agent will only see your summary plus any new messages, so it must capture everything needed to continue working.
2
2
 
3
3
  Write a tight, factual summary in bullet points. Cover what matters for picking the work back up:
4
4
  - What the user is trying to do, and any key decisions, constraints, or preferences they've stated.
@@ -68,14 +68,9 @@ declare class HeadlessSession {
68
68
  start(): Promise<void>;
69
69
  private shutdown;
70
70
  private emit;
71
- /**
72
- * Emit a `completed` event and mark completedEmitted. Includes
73
- * `queuedMessages` if the queue has items (sandbox uses this to know the
74
- * agent is still busy with pipeline work).
75
- */
71
+ /** Emit a `completed` event and mark completedEmitted. Queue state is
72
+ * surfaced separately via the `queue_changed` event, not on `completed`. */
76
73
  private emitCompleted;
77
- /** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
78
- private queueFields;
79
74
  /** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
80
75
  private dispatchSimple;
81
76
  /** Persist sessionStats + queue snapshot to .remy-stats.json. */
@@ -138,6 +133,13 @@ declare class HeadlessSession {
138
133
  private handleChangeModels;
139
134
  /** Cancel the running turn and drain the queue. Returns the drained items. */
140
135
  private handleCancel;
136
+ /**
137
+ * Remove pending queued messages — all user messages, or one by id.
138
+ * Only `source: 'user'` items are removable; chained and background
139
+ * messages are part of a system chain and are never cancellable. Does
140
+ * not affect the in-flight turn (use `cancel` for that).
141
+ */
142
+ private handleCancelQueued;
141
143
  private handleStdinLine;
142
144
  }
143
145
 
package/dist/headless.js CHANGED
@@ -4680,9 +4680,9 @@ ${parts.join("\n\n")}
4680
4680
  }
4681
4681
  function loadPlatformBrief() {
4682
4682
  return `<platform_brief>
4683
- ## What is a MindStudio app?
4683
+ ## What is a Remy app?
4684
4684
 
4685
- A MindStudio app is a managed full-stack TypeScript project with three layers: a spec (natural language in src/), a backend contract (methods, tables, roles in dist/), and one or more interfaces (web, API, bots, cron, etc.). The spec is the source of truth; code is derived from it.
4685
+ A Remy app is a managed full-stack TypeScript project with three layers: a spec (natural language in src/), a backend contract (methods, tables, roles in dist/), and one or more interfaces (web, API, bots, cron, etc.). The spec is the source of truth; code is derived from it.
4686
4686
 
4687
4687
  This is a capable, stable platform used in production by 100k+ users. Build with confidence \u2014 you're building production-grade apps, not fragile prototypes.
4688
4688
 
@@ -4727,7 +4727,7 @@ TypeScript running in a sandboxed environment. Any npm package can be installed.
4727
4727
 
4728
4728
  The first-party SDK (@mindstudio-ai/agent) provides access to 200+ AI models (OpenAI, Anthropic, Google, Meta, Mistral, and more) and 1000+ integrations (email, SMS, Slack, HubSpot, Google Workspace, web scraping, image/video generation, media processing, and much more) with zero configuration \u2014 credentials are handled automatically in the execution environment. No API keys needed. This SDK is robust and battle-tested in production.
4729
4729
 
4730
- ## What MindStudio apps are NOT good for
4730
+ ## What Remy apps are NOT good for
4731
4731
 
4732
4732
  - Native mobile apps (iOS/Android). Mobile-responsive web apps are fine.
4733
4733
  - Real-time multiplayer with persistent connections (no WebSocket support). Turn-based or async patterns work.
@@ -5391,6 +5391,94 @@ var scrapeWebUrlTool = {
5391
5391
  }
5392
5392
  };
5393
5393
 
5394
+ // src/tools/spec/writeBuildOverview.ts
5395
+ import fs20 from "fs";
5396
+ var OVERVIEW_FILE = "src/overview.html";
5397
+ var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
5398
+
5399
+ Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand. If <current_overview> is non-empty, use it as your starting point and preserve its established skin, updating only what the copy changed.
5400
+
5401
+ ### The single hard rule
5402
+ Preserve every fact in <overview_copy> exactly \u2014 every number, name, label, and claim. Do not add, drop, soften, reword, or invent any fact. You own layout, typography, and visual design only; the substance and the words are fixed. A single wrong number breaks this document's entire purpose.
5403
+
5404
+ ### What it is (and is not)
5405
+ - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
5406
+ - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
5407
+ - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
5408
+ - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
5409
+ - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
5410
+ - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
5411
+
5412
+ ### Constraints
5413
+ - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
5414
+ - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.
5415
+
5416
+ Respond only with the complete HTML file and absolutely no other text. Your response will be written directly to src/overview.html.`;
5417
+ var OVERVIEW_SHELL = `<!DOCTYPE html>
5418
+ <html lang="en">
5419
+ <head>
5420
+ <meta charset="utf-8">
5421
+ <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
5422
+ static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
5423
+ The user-scalable=no viewport is intentional; leave it. -->
5424
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
5425
+ <title>Build Overview</title>
5426
+ <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
5427
+ <style>
5428
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
5429
+ </style>
5430
+ </head>
5431
+ <body>
5432
+ <!-- Compose the Build Overview here. Layout, sections, type, and styling are
5433
+ yours \u2014 keep everything inline and self-contained, brand-skinned from the
5434
+ app's spec. -->
5435
+ </body>
5436
+ </html>`;
5437
+ var buildOverviewTool = {
5438
+ clearable: false,
5439
+ definition: {
5440
+ name: "writeBuildOverview",
5441
+ description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand without altering any of your facts. Generate it at the end of a build and refresh it after meaningful work.",
5442
+ inputSchema: {
5443
+ type: "object",
5444
+ properties: {
5445
+ content: {
5446
+ type: "string",
5447
+ description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand without changing any fact."
5448
+ }
5449
+ },
5450
+ required: ["content"]
5451
+ }
5452
+ },
5453
+ async execute(input, context) {
5454
+ if (!context) {
5455
+ return "Error: writeBuildOverview requires execution context for design expert delegation";
5456
+ }
5457
+ const content = (input.content ?? "").trim();
5458
+ if (!content) {
5459
+ return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
5460
+ }
5461
+ try {
5462
+ const existing = fs20.existsSync(OVERVIEW_FILE) ? fs20.readFileSync(OVERVIEW_FILE, "utf-8").trim() : "";
5463
+ const currentOverview = existing || OVERVIEW_SHELL;
5464
+ const task = `<overview_copy>${content}</overview_copy>
5465
+
5466
+ <current_overview>${currentOverview}</current_overview>
5467
+
5468
+ ${DESIGN_BRIEF}`;
5469
+ const result = await designExpertTool.execute({ task }, context);
5470
+ const htmlMatch = result.match(
5471
+ /```(?:html|wireframe)\n([\s\S]*?)```/
5472
+ );
5473
+ const html = htmlMatch ? htmlMatch[1].trim() : result;
5474
+ fs20.writeFileSync(OVERVIEW_FILE, html, "utf-8");
5475
+ return "Build overview written successfully to src/overview.html.";
5476
+ } catch (err) {
5477
+ return `Error generating build overview: ${err.message}`;
5478
+ }
5479
+ }
5480
+ };
5481
+
5394
5482
  // src/tools/index.ts
5395
5483
  function deriveContext(parent, toolCallId) {
5396
5484
  return { ...parent, toolCallId };
@@ -5407,6 +5495,7 @@ var ALL_TOOLS = [
5407
5495
  designExpertTool,
5408
5496
  productVisionTool,
5409
5497
  codeSanityCheckTool,
5498
+ buildOverviewTool,
5410
5499
  compactConversationTool,
5411
5500
  // Post-onboarding
5412
5501
  presentPublishPlanTool,
@@ -5789,7 +5878,7 @@ function triggerCompaction(state, apiConfig, opts = {}) {
5789
5878
  }
5790
5879
 
5791
5880
  // src/brandExtraction/index.ts
5792
- import fs20 from "fs";
5881
+ import fs21 from "fs";
5793
5882
  import path10 from "path";
5794
5883
  import { createHash } from "crypto";
5795
5884
  var log9 = createLogger("brandExtraction");
@@ -5838,7 +5927,7 @@ function sha256(input) {
5838
5927
  }
5839
5928
  function readSafe(filePath) {
5840
5929
  try {
5841
- return fs20.readFileSync(filePath, "utf-8");
5930
+ return fs21.readFileSync(filePath, "utf-8");
5842
5931
  } catch {
5843
5932
  return "";
5844
5933
  }
@@ -5846,7 +5935,7 @@ function readSafe(filePath) {
5846
5935
  function walkMdFiles3(dir) {
5847
5936
  const results = [];
5848
5937
  try {
5849
- const entries = fs20.readdirSync(dir, { withFileTypes: true });
5938
+ const entries = fs21.readdirSync(dir, { withFileTypes: true });
5850
5939
  for (const entry of entries) {
5851
5940
  const full = path10.join(dir, entry.name);
5852
5941
  if (entry.isDirectory()) {
@@ -5861,7 +5950,7 @@ function walkMdFiles3(dir) {
5861
5950
  }
5862
5951
  function parseFrontmatter3(filePath) {
5863
5952
  try {
5864
- const content = fs20.readFileSync(filePath, "utf-8");
5953
+ const content = fs21.readFileSync(filePath, "utf-8");
5865
5954
  const match = content.match(/^---\n([\s\S]*?)\n---/);
5866
5955
  if (!match) {
5867
5956
  return { type: "" };
@@ -6040,14 +6129,14 @@ function pickFont(raw) {
6040
6129
  }
6041
6130
  function persistBrand(brand, inputHash) {
6042
6131
  const tmp = `${BRAND_FILE}.tmp`;
6043
- fs20.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6044
- fs20.renameSync(tmp, BRAND_FILE);
6132
+ fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6133
+ fs21.renameSync(tmp, BRAND_FILE);
6045
6134
  const cache = { inputHash, generatedAt: Date.now() };
6046
- fs20.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6135
+ fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6047
6136
  }
6048
6137
  function readCache() {
6049
6138
  try {
6050
- const raw = fs20.readFileSync(CACHE_FILE, "utf-8");
6139
+ const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
6051
6140
  const parsed = JSON.parse(raw);
6052
6141
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
6053
6142
  return parsed;
@@ -6080,14 +6169,14 @@ function triggerBrandExtraction(apiConfig, model) {
6080
6169
  }
6081
6170
 
6082
6171
  // src/session.ts
6083
- import fs21 from "fs";
6172
+ import fs22 from "fs";
6084
6173
  import path11 from "path";
6085
6174
  var log11 = createLogger("session");
6086
6175
  var SESSION_FILE = ".remy-session.json";
6087
6176
  var ARCHIVE_DIR = ".logs/sessions";
6088
6177
  function loadSession(state) {
6089
6178
  try {
6090
- const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
6179
+ const raw = fs22.readFileSync(SESSION_FILE, "utf-8");
6091
6180
  const data = JSON.parse(raw);
6092
6181
  if (data.models && typeof data.models === "object") {
6093
6182
  state.models = data.models;
@@ -6146,7 +6235,7 @@ function saveSession(state) {
6146
6235
  if (state.models && Object.keys(state.models).length > 0) {
6147
6236
  payload.models = state.models;
6148
6237
  }
6149
- fs21.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
6238
+ fs22.writeFileSync(SESSION_FILE, JSON.stringify(payload, null, 2), "utf-8");
6150
6239
  log11.info("Session saved", { messageCount: state.messages.length });
6151
6240
  } catch (err) {
6152
6241
  log11.warn("Session save failed", { error: err.message });
@@ -6155,11 +6244,11 @@ function saveSession(state) {
6155
6244
  function clearSession(state) {
6156
6245
  state.messages = [];
6157
6246
  try {
6158
- if (fs21.existsSync(SESSION_FILE)) {
6159
- fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
6247
+ if (fs22.existsSync(SESSION_FILE)) {
6248
+ fs22.mkdirSync(ARCHIVE_DIR, { recursive: true });
6160
6249
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6161
6250
  const dest = path11.join(ARCHIVE_DIR, `cleared-${ts}.json`);
6162
- fs21.renameSync(SESSION_FILE, dest);
6251
+ fs22.renameSync(SESSION_FILE, dest);
6163
6252
  log11.info("Session archived on clear", { dest });
6164
6253
  }
6165
6254
  } catch (err) {
@@ -6167,7 +6256,7 @@ function clearSession(state) {
6167
6256
  error: err.message
6168
6257
  });
6169
6258
  try {
6170
- fs21.unlinkSync(SESSION_FILE);
6259
+ fs22.unlinkSync(SESSION_FILE);
6171
6260
  } catch {
6172
6261
  }
6173
6262
  }
@@ -7159,6 +7248,24 @@ var MessageQueue = class {
7159
7248
  this.onChange?.();
7160
7249
  return all;
7161
7250
  }
7251
+ /**
7252
+ * Remove all items matching `predicate`. Fires onChange only if something
7253
+ * was removed. Returns the removed items.
7254
+ */
7255
+ removeWhere(predicate) {
7256
+ const removed = [];
7257
+ this.items = this.items.filter((item) => {
7258
+ if (predicate(item)) {
7259
+ removed.push(item);
7260
+ return false;
7261
+ }
7262
+ return true;
7263
+ });
7264
+ if (removed.length > 0) {
7265
+ this.onChange?.();
7266
+ }
7267
+ return removed;
7268
+ }
7162
7269
  /** Copy of current queue contents (for surfacing on events). */
7163
7270
  snapshot() {
7164
7271
  return [...this.items];
@@ -7209,6 +7316,24 @@ function resolveAction(text) {
7209
7316
  next
7210
7317
  };
7211
7318
  }
7319
+ function getActionChain(startName) {
7320
+ const chain = [];
7321
+ const seen = /* @__PURE__ */ new Set();
7322
+ let name = startName;
7323
+ while (name && !seen.has(name)) {
7324
+ seen.add(name);
7325
+ chain.push(name);
7326
+ let body;
7327
+ try {
7328
+ body = readAsset("automatedActions", `${name}.md`);
7329
+ } catch {
7330
+ break;
7331
+ }
7332
+ const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
7333
+ name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
7334
+ }
7335
+ return chain;
7336
+ }
7212
7337
 
7213
7338
  // src/headless/index.ts
7214
7339
  var log14 = createLogger("headless");
@@ -7279,14 +7404,16 @@ var HeadlessSession = class {
7279
7404
  baseUrl: this.opts.baseUrl
7280
7405
  });
7281
7406
  const resumed = loadSession(this.state);
7282
- this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
7407
+ this.queue = new MessageQueue(loadQueue(), () => {
7408
+ this.persistStats();
7409
+ this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
7410
+ });
7283
7411
  if (resumed) {
7284
7412
  this.emit("session_restored", {
7285
7413
  messageCount: this.state.messages.length,
7286
7414
  ...this.state.models && { models: this.state.models },
7287
7415
  modelSurfaces: MODEL_SURFACES,
7288
- allowedModelsByType: ALLOWED_MODELS_BY_TYPE,
7289
- ...this.queueFields()
7416
+ allowedModelsByType: ALLOWED_MODELS_BY_TYPE
7290
7417
  });
7291
7418
  }
7292
7419
  triggerBrandExtraction(
@@ -7336,7 +7463,7 @@ var HeadlessSession = class {
7336
7463
  });
7337
7464
  process.on("SIGTERM", this.shutdown);
7338
7465
  process.on("SIGINT", this.shutdown);
7339
- this.emit("ready", this.queueFields());
7466
+ this.emit("ready");
7340
7467
  }
7341
7468
  shutdown = () => {
7342
7469
  this.emit("stopping");
@@ -7360,19 +7487,12 @@ var HeadlessSession = class {
7360
7487
  }
7361
7488
  process.stdout.write(line);
7362
7489
  }
7363
- /**
7364
- * Emit a `completed` event and mark completedEmitted. Includes
7365
- * `queuedMessages` if the queue has items (sandbox uses this to know the
7366
- * agent is still busy with pipeline work).
7367
- */
7490
+ /** Emit a `completed` event and mark completedEmitted. Queue state is
7491
+ * surfaced separately via the `queue_changed` event, not on `completed`. */
7368
7492
  emitCompleted(rid, data) {
7369
- this.emit("completed", { ...data, ...this.queueFields() }, rid);
7493
+ this.emit("completed", { ...data }, rid);
7370
7494
  this.completedEmitted = true;
7371
7495
  }
7372
- /** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
7373
- queueFields() {
7374
- return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
7375
- }
7376
7496
  /** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
7377
7497
  dispatchSimple(requestId, eventName, handler) {
7378
7498
  try {
@@ -7550,11 +7670,7 @@ var HeadlessSession = class {
7550
7670
  });
7551
7671
  return;
7552
7672
  case "turn_cancelled": {
7553
- this.emit(
7554
- "completed",
7555
- { success: false, error: "cancelled", ...this.queueFields() },
7556
- rid
7557
- );
7673
+ this.emit("completed", { success: false, error: "cancelled" }, rid);
7558
7674
  this.completedEmitted = true;
7559
7675
  return;
7560
7676
  }
@@ -7678,7 +7794,7 @@ var HeadlessSession = class {
7678
7794
  * message — `running` stays held across the queue drain so no user
7679
7795
  * message can slip in mid-pipeline.
7680
7796
  */
7681
- async runSingleTurn(parsed, requestId) {
7797
+ async runSingleTurn(parsed, requestId, fromChain = false) {
7682
7798
  this.currentRequestId = requestId;
7683
7799
  this.currentAbort = new AbortController();
7684
7800
  this.completedEmitted = false;
@@ -7726,16 +7842,18 @@ var HeadlessSession = class {
7726
7842
  onboardingState,
7727
7843
  parsed.viewContext
7728
7844
  );
7729
- if (resolved?.next) {
7730
- this.queue.push({
7731
- command: {
7732
- action: "message",
7733
- text: sentinel(resolved.next),
7734
- onboardingState
7735
- },
7736
- source: "chain",
7737
- enqueuedAt: Date.now()
7738
- });
7845
+ if (resolved?.next && !fromChain) {
7846
+ for (const step of getActionChain(resolved.next)) {
7847
+ this.queue.push({
7848
+ command: {
7849
+ action: "message",
7850
+ text: sentinel(step),
7851
+ onboardingState
7852
+ },
7853
+ source: "chain",
7854
+ enqueuedAt: Date.now()
7855
+ });
7856
+ }
7739
7857
  }
7740
7858
  try {
7741
7859
  await runTurn({
@@ -7793,11 +7911,6 @@ var HeadlessSession = class {
7793
7911
  source: "user",
7794
7912
  enqueuedAt: Date.now()
7795
7913
  });
7796
- this.emit(
7797
- "queued",
7798
- { position: this.queue.length, ...this.queueFields() },
7799
- requestId
7800
- );
7801
7914
  return;
7802
7915
  }
7803
7916
  this.running = true;
@@ -7845,7 +7958,7 @@ var HeadlessSession = class {
7845
7958
  continue;
7846
7959
  }
7847
7960
  const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
7848
- await this.runSingleTurn(next.command, nextRid);
7961
+ await this.runSingleTurn(next.command, nextRid, next.source === "chain");
7849
7962
  }
7850
7963
  }
7851
7964
  /**
@@ -7909,6 +8022,17 @@ var HeadlessSession = class {
7909
8022
  }
7910
8023
  return this.queue.drain();
7911
8024
  }
8025
+ /**
8026
+ * Remove pending queued messages — all user messages, or one by id.
8027
+ * Only `source: 'user'` items are removable; chained and background
8028
+ * messages are part of a system chain and are never cancellable. Does
8029
+ * not affect the in-flight turn (use `cancel` for that).
8030
+ */
8031
+ handleCancelQueued(id) {
8032
+ return this.queue.removeWhere(
8033
+ (item) => item.source === "user" && (id === void 0 || item.command.requestId === id)
8034
+ );
8035
+ }
7912
8036
  //////////////////////////////////////////////////////////////////////////////
7913
8037
  // Stdin router
7914
8038
  //////////////////////////////////////////////////////////////////////////////
@@ -7973,7 +8097,11 @@ var HeadlessSession = class {
7973
8097
  ...this.state.models && { models: this.state.models },
7974
8098
  modelSurfaces: MODEL_SURFACES,
7975
8099
  allowedModelsByType: ALLOWED_MODELS_BY_TYPE,
7976
- ...this.queueFields()
8100
+ // Current queue snapshot for connect/reconnect — get_history is the
8101
+ // on-demand "current state" query. Always an array (possibly empty),
8102
+ // matching the queue_changed convention so the client reconciles the
8103
+ // same way from both; live mutations are pushed via queue_changed.
8104
+ queuedMessages: this.queue.snapshot()
7977
8105
  }));
7978
8106
  return;
7979
8107
  }
@@ -8017,6 +8145,16 @@ var HeadlessSession = class {
8017
8145
  );
8018
8146
  return;
8019
8147
  }
8148
+ if (action === "cancelQueued") {
8149
+ const id = parsed.id;
8150
+ const removed = this.handleCancelQueued(id);
8151
+ this.emit(
8152
+ "completed",
8153
+ { success: true, cancelledQueued: removed },
8154
+ requestId
8155
+ );
8156
+ return;
8157
+ }
8020
8158
  if (action === "stop_tool") {
8021
8159
  const id = parsed.id;
8022
8160
  const mode = parsed.mode ?? "hard";