@mindstudio-ai/remy 0.1.269 → 0.1.270

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
@@ -43,13 +43,13 @@ Remy saves conversation history to `.remy-session.json` in the working directory
43
43
 
44
44
  ## Tools
45
45
 
46
- Tool availability depends on the project's onboarding state, sent by the sandbox on each message.
46
+ The full tool set is always available — it is deliberately invariant (no onboarding-state gating) so the provider's tools-tier cache prefix stays identical across turns and sessions. The sections below are thematic groupings only.
47
47
 
48
- ### Common Tools (all onboarding states)
48
+ ### Common Tools
49
49
 
50
50
  | Tool | Description |
51
51
  |------|-------------|
52
- | `setProjectOnboardingState` | Advance the onboarding flow (intake → initialSpecReviewinitialCodegen → onboardingFinished) |
52
+ | `setProjectOnboardingState` | Advance the onboarding flow (intake → buildingbuildComplete → onboardingFinished) |
53
53
  | `setProjectName` | Set the project name |
54
54
  | `promptUser` | Ask the user structured questions (form or inline display) |
55
55
  | `confirmDestructiveAction` | Confirm a destructive or irreversible action with the user |
@@ -74,8 +74,6 @@ Available in all onboarding states. Used for authoring and editing MSFM specs in
74
74
 
75
75
  ### Code Tools
76
76
 
77
- Available from `initialCodegen` onward.
78
-
79
77
  | Tool | Description |
80
78
  |------|-------------|
81
79
  | `readFile` | Read a file with line numbers |
@@ -100,9 +98,7 @@ Available when `--lsp-url` is passed.
100
98
  | `lspDiagnostics` | Type errors and warnings for a file, with suggested quick fixes |
101
99
  | `restartProcess` | Restart a managed sandbox process (e.g., dev server after npm install) |
102
100
 
103
- ### Post-Onboarding Tools
104
-
105
- Available only when `onboardingState` is `onboardingFinished`.
101
+ ### Development & Publishing Tools
106
102
 
107
103
  | Tool | Description |
108
104
  |------|-------------|
@@ -174,11 +170,7 @@ src/
174
170
  logger.ts Structured logging
175
171
 
176
172
  prompt/
177
- index.ts System prompt builder (onboarding-state-aware)
178
- actions/ Built-in prompts for runCommand actions
179
- sync.md
180
- publish.md
181
- buildFromInitialSpec.md
173
+ index.ts System prompt builder (static prefix + small dynamic tail)
182
174
  static/ Behavioral instruction fragments
183
175
  identity.md
184
176
  intake.md
@@ -187,7 +179,7 @@ src/
187
179
  instructions.md
188
180
  team.md
189
181
  lsp.md
190
- projectContext.ts Reads manifest, spec metadata, file listing at runtime
182
+ projectContext.ts Reads project root, app identity, plan status at runtime
191
183
  compiled/ Platform docs distilled for agent consumption
192
184
  sources/ Prompt source material (hand-maintained)
193
185
 
@@ -267,7 +259,7 @@ The headless IPC protocol uses request correlation and a unified response patter
267
259
  - Messages sent while a turn is running are queued. When the turn ends, all contiguous queued user messages and background results are delivered together as **one merged turn**: the first queued message's `requestId` becomes the turn's primary id (stamped on `turn_started` and all streaming events), each absorbed message echoes its own `user_message` with its original `requestId` and `queued: true`, and at turn end the primary `completed` is emitted first, followed immediately by one `completed {…same outcome, absorbed: true}` per other absorbed `requestId`. Automated-action (`@@automated::…@@`) messages and chain steps never merge — they always run one turn each.
268
260
  - A queued **user** message can be promoted to ASAP delivery via `setQueuedDelivery`. ASAP items are pulled into the **running** turn at its next tool boundary (injected as plain user messages — no abort, no restart), echo `user_message` with `queued: true` and their own `requestId`, and get a `{…, absorbed: true}` completed with the turn's outcome at turn end. Promotion deliberately jumps ahead of anything else in the queue, including chain steps. If the turn ends before injection, the tag is ignored and the item drains in normal FIFO order.
269
261
  - Background tool completions have three delivery classes (per-tool `backgroundNotify` on the tool definition). `wake` (default): the result is queued as a `background_results` message and may start a turn when the agent is idle. `passive` (e.g. `specSync`): the result never enters the queue and never wakes the agent — it parks in a persisted holding pen and rides the next real turn as a hidden `background_results` entry (so it never appears in `queuedMessages`, never affects queue-derived busy state, and never triggers resume-on-restart). `silent` (e.g. `compactConversation`): the tool block is updated for the UI and the model is never told — its outcome reaches the model by another mechanism. All classes emit `tool_background_complete` immediately. `specSync` also takes a `refreshBuildOverview` flag (set post-deploy / post-milestone, prompted via the publish flow): the run gains the `writeBuildOverview` tool and re-authors the Build Overview from the freshly-reconciled spec — the design-expert render runs foreground within specSync's already-detached run, never nested-background.
270
- - Compaction lifecycle rides `compaction_started {blocking}` / `compaction_complete {error?}` system events. Every compaction also renders as a normal `compactConversation` tool call: the model-invoked tool has its own block, and user (`/compact`) or gate-initiated compactions get a **synthesized UI-only tool block** in history (standard `tool_start {background: true}` `tool_background_complete` lifecycle; the summary lands in the block's `backgroundResult`, and the block is excluded from every API payload — the model receives the summary via the checkpoint prefix instead). Every turn passes a **compaction gate**: it waits for any in-flight compaction and applies the finished checkpoint before running, so no turn ever bills at the uncompacted context. Messages received while a compaction is in flight are queued exactly like mid-turn messages (they appear in `queuedMessages` and drain — with merged-turn semantics — when the compaction completes, on success and failure alike). A `compact` command received **mid-turn** queues too: it's acked immediately with `completed {success: true, queued: true}` (duplicate clicks coalesce onto the queued item), appears in `queuedMessages` as a removable `@@automated::compact@@` user item, and runs as its own drain step when the turn ends — everything queued behind it runs against the compacted history.
262
+ - Compaction lifecycle rides `compaction_started {blocking}` / `compaction_complete {error?}` system events. Every compaction also renders as a normal `compactConversation` tool call. The **model-invoked** tool is a real background tool (its block completes via `tool_background_complete`; the summary lands in `backgroundResult`). User (`/compact`) and gate-initiated compactions instead get a **synthesized UI-only foreground block** in history the user is actively waiting on these, so they follow the normal foreground lifecycle: `tool_start` with no result, then a (late) `tool_done {result, isError}` carrying the summary or the error when the compaction finishes. Either way the block is excluded from every API payload — the model receives the summary via the checkpoint prefix instead. Every turn passes a **compaction gate**: it waits for any in-flight compaction and applies the finished checkpoint before running, so no turn ever bills at the uncompacted context. Messages received while a compaction is in flight are queued exactly like mid-turn messages (they appear in `queuedMessages` and drain — with merged-turn semantics — when the compaction completes, on success and failure alike). A `compact` command received **mid-turn** queues too: it's acked immediately with `completed {success: true, queued: true}` (duplicate clicks coalesce onto the queued item), appears in `queuedMessages` as a removable `@@automated::compact@@` user item, and runs as its own drain step when the turn ends — everything queued behind it runs against the compacted history.
271
263
  - The caller distinguishes command responses from system events with a single check: `if (msg.requestId)`
272
264
 
273
265
  This enables a simple promise-based RPC layer: send a command with a unique ID, store a pending promise keyed by that ID, resolve it when you see `completed` with the matching ID.
@@ -287,8 +279,7 @@ Send a user message to the agent.
287
279
  Fields:
288
280
  - `requestId` — caller-provided correlation ID (echoed on all response events)
289
281
  - `text` — the user message (required unless `runCommand` is set)
290
- - `onboardingState` — controls tool availability and prompt context. One of: `intake`, `initialSpecAuthoring`, `initialCodegen`, `onboardingFinished` (default: `onboardingFinished`)
291
- - `viewContext` — `{ mode, openFiles?, activeFile? }` for prompt context
282
+ - `onboardingState` — the project's onboarding phase, reflected in the system prompt's dynamic tail and plan-status behavior. One of: `intake`, `building`, `buildComplete`, `onboardingFinished` (default: `onboardingFinished`)
292
283
  - `attachments` — array of `{ url, extractedTextUrl? }` for file attachments
293
284
  - `runCommand` — triggers a built-in action prompt (`"sync"`, `"publish"`, `"buildFromInitialSpec"`)
294
285
 
@@ -379,7 +370,8 @@ All command responses include the `requestId` from the originating command.
379
370
  | `user_message` | `text`, `attachments?`, `queued?`, `hidden?` | Echo of a user message entering the turn. Queue-delivered messages (including ASAP items injected mid-turn) carry `queued: true` and their own original `requestId` (a merged turn emits one per absorbed message); idle sends echo with the turn's requestId and no `queued` flag. `hidden: true` marks internal entries (e.g. passive background results) that should not render. |
380
371
  | `tool_start` | `id`, `name`, `input`, `partial?`, `parentToolId?` | Tool execution started. `partial: true` means more `tool_start` events will follow for this id (progressive input streaming). |
381
372
  | `tool_input_delta` | `id`, `name`, `result`, `parentToolId?` | Progressive tool content (streaming tools only) |
382
- | `tool_done` | `id`, `name`, `result`, `isError`, `parentToolId?` | Tool execution completed |
373
+ | `tool_done` | `id`, `name`, `result`, `isError`, `parentToolId?` | Tool execution completed. For a background tool this carries the synchronous ack; for a synthesized user/gate compaction block it arrives late, when the compaction finishes, carrying the summary (or error). |
374
+ | `tool_background_complete` | `id`, `name`, `result`, `parentToolId?` | A background tool's detached work finished; `result` belongs in the block's `backgroundResult`. Emitted for all `backgroundNotify` classes. |
383
375
  | `status` | `message` | Contextual status label (e.g., "Writing files...") |
384
376
  | `error` | `error` | Error message (may precede `completed`) |
385
377
  | `history` | `messages` | Response to `get_history` |
package/dist/headless.js CHANGED
@@ -628,7 +628,7 @@ function loadProjectRoot() {
628
628
  ## Project Root
629
629
  \`${PROJECT_ROOT}\`
630
630
 
631
- File paths are relative to this directory. Every tool operates here and bash commands run it it.`;
631
+ File paths are relative to this directory. Every tool operates here and bash commands run in it.`;
632
632
  }
633
633
  function loadAppIdentity() {
634
634
  try {
@@ -797,21 +797,9 @@ function buildSystemPrompt(onboardingState) {
797
797
  {{compiled/design.md}}
798
798
  </design>
799
799
 
800
- <app_files>
801
- {{compiled/files.md}}
802
- </app_files>
803
-
804
800
  <interfaces>
805
801
  {{compiled/interfaces.md}}
806
802
  </interfaces>
807
-
808
- <scenarios>
809
- {{compiled/scenarios.md}}
810
- </scenarios>
811
-
812
- <secrets>
813
- {{compiled/secrets.md}}
814
- </secrets>
815
803
  </platform_docs>
816
804
 
817
805
  ${loadSkillsCatalog()}
@@ -824,12 +812,12 @@ ${loadSkillsCatalog()}
824
812
  {{compiled/msfm.md}}
825
813
  </mindstudio_flavored_markdown_spec_docs>
826
814
 
827
- <intake_mode_instructions>
815
+ ${onboardingState === "intake" ? `<intake_mode_instructions>
828
816
  {{static/intake.md}}
829
- </intake_mode_instructions>
817
+ </intake_mode_instructions>` : ""}
830
818
 
831
819
  <spec_authoring_instructions>
832
- {{static/authoring.md}}
820
+ ${onboardingState !== void 0 && onboardingState !== "onboardingFinished" ? "{{static/authoring.md}}" : "{{static/spec-maintenance.md}}"}
833
821
  </spec_authoring_instructions>
834
822
 
835
823
  <team>
@@ -851,7 +839,7 @@ Tool results generally persist in the conversation until a compaction summarizes
851
839
  </conversation_summaries>
852
840
 
853
841
  <project_onboarding>
854
- 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:
842
+ 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:
855
843
 
856
844
  - **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.
857
845
  - **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).
@@ -1378,8 +1366,33 @@ var presentPublishPlanTool = {
1378
1366
  }
1379
1367
  };
1380
1368
 
1369
+ // src/atomicWrite.ts
1370
+ import fs10 from "fs";
1371
+ import fsp from "fs/promises";
1372
+ var writeFileAtomicSync = (file, data) => {
1373
+ const tmp = `${file}.${process.pid}.tmp`;
1374
+ const fd2 = fs10.openSync(tmp, "w");
1375
+ try {
1376
+ fs10.writeSync(fd2, data);
1377
+ fs10.fsyncSync(fd2);
1378
+ } finally {
1379
+ fs10.closeSync(fd2);
1380
+ }
1381
+ fs10.renameSync(tmp, file);
1382
+ };
1383
+ var writeFileAtomic = async (file, data) => {
1384
+ const tmp = `${file}.${process.pid}.tmp`;
1385
+ const handle = await fsp.open(tmp, "w");
1386
+ try {
1387
+ await handle.writeFile(data);
1388
+ await handle.sync();
1389
+ } finally {
1390
+ await handle.close();
1391
+ }
1392
+ await fsp.rename(tmp, file);
1393
+ };
1394
+
1381
1395
  // src/tools/spec/writePlan.ts
1382
- import fs10 from "fs/promises";
1383
1396
  var PLAN_FILE = ".remy-plan.md";
1384
1397
  var writePlanTool = {
1385
1398
  definition: {
@@ -1403,7 +1416,7 @@ status: pending
1403
1416
  ---
1404
1417
 
1405
1418
  ${content}`;
1406
- await fs10.writeFile(PLAN_FILE, file, "utf-8");
1419
+ await writeFileAtomic(PLAN_FILE, file);
1407
1420
  return "Plan written to .remy-plan.md. Waiting for user approval.";
1408
1421
  }
1409
1422
  };
@@ -1439,13 +1452,13 @@ var updatePlanStatusTool = {
1439
1452
  return "No plan file found.";
1440
1453
  }
1441
1454
  if (status === "rejected") {
1442
- await fs11.unlink(PLAN_FILE2);
1455
+ await fs11.unlink(PLAN_FILE2).catch(() => {
1456
+ });
1443
1457
  return "Plan rejected and removed.";
1444
1458
  }
1445
- await fs11.writeFile(
1459
+ await writeFileAtomic(
1446
1460
  PLAN_FILE2,
1447
- content.replace(/^status:\s*\w+/m, `status: ${status}`),
1448
- "utf-8"
1461
+ content.replace(/^status:\s*\w+/m, `status: ${status}`)
1449
1462
  );
1450
1463
  return "Plan approved. Proceeding with implementation.";
1451
1464
  }
@@ -2975,7 +2988,8 @@ var CONTENT_TYPES = {
2975
2988
  ".jpg": "image/jpeg",
2976
2989
  ".jpeg": "image/jpeg",
2977
2990
  ".gif": "image/gif",
2978
- ".webp": "image/webp"
2991
+ ".webp": "image/webp",
2992
+ ".svg": "image/svg+xml"
2979
2993
  };
2980
2994
  var hosted = /* @__PURE__ */ new Map();
2981
2995
  function isFetchableUrl(ref) {
@@ -3582,7 +3596,8 @@ async function runSubAgent(config) {
3582
3596
  background,
3583
3597
  acquireLock,
3584
3598
  onBackgroundComplete,
3585
- captureArtifacts
3599
+ captureArtifacts,
3600
+ cachePolicy = "run"
3586
3601
  } = config;
3587
3602
  const artifacts = {};
3588
3603
  const bgAbort = background ? new AbortController() : null;
@@ -3677,6 +3692,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3677
3692
  system: fullSystem,
3678
3693
  messages: cleanMessagesForApi(messages),
3679
3694
  tools: tools2,
3695
+ cachePolicy,
3680
3696
  signal
3681
3697
  },
3682
3698
  {
@@ -5243,7 +5259,7 @@ function load() {
5243
5259
  }
5244
5260
  function save(indices) {
5245
5261
  try {
5246
- fs18.writeFileSync(SAMPLE_FILE, JSON.stringify(indices));
5262
+ writeFileAtomicSync(SAMPLE_FILE, JSON.stringify(indices));
5247
5263
  } catch {
5248
5264
  }
5249
5265
  }
@@ -5494,6 +5510,8 @@ async function runDesignExpert(opts, context) {
5494
5510
  }),
5495
5511
  task: opts.task,
5496
5512
  history: history.length > 0 ? history : void 0,
5513
+ // History-carrying thread re-sent across parent turns — 1h-TTL profile.
5514
+ cachePolicy: "conversation",
5497
5515
  tools: opts.render ? DESIGN_EXPERT_RENDER_TOOLS : DESIGN_EXPERT_TOOLS,
5498
5516
  externalTools: /* @__PURE__ */ new Set(),
5499
5517
  executeTool: (name, input, toolCallId, onLog, sams) => {
@@ -5761,6 +5779,8 @@ var productVisionTool = {
5761
5779
  system: getProductVisionPrompt(),
5762
5780
  task: input.task,
5763
5781
  history: history.length > 0 ? history : void 0,
5782
+ // History-carrying thread re-sent across parent turns — 1h-TTL profile.
5783
+ cachePolicy: "conversation",
5764
5784
  tools: VISION_TOOLS,
5765
5785
  externalTools: /* @__PURE__ */ new Set(),
5766
5786
  executeTool: (name, input2, toolCallId, _onLog, sams) => {
@@ -6228,7 +6248,7 @@ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
6228
6248
  "runAutomatedBrowserTest",
6229
6249
  "askMindStudioSdk"
6230
6250
  ]);
6231
- function getToolDefinitions(_onboardingState) {
6251
+ function getToolDefinitions() {
6232
6252
  return ALL_TOOLS.map((t) => t.definition);
6233
6253
  }
6234
6254
  function getToolByName(name) {
@@ -6612,7 +6632,11 @@ Write the summary of the conversation above, following your instructions.`;
6612
6632
  messages: [{ role: "user", content: userContent }],
6613
6633
  // Always empty. With a toolset available the model picks `tool_use` over
6614
6634
  // producing a summary, leaving summaryText empty.
6615
- tools: []
6635
+ tools: [],
6636
+ // Each summary call carries a unique 100-200KB chunk that is never
6637
+ // re-read (parallel siblings can't read each other's in-flight writes
6638
+ // either) — a cache write here is pure waste.
6639
+ cachePolicy: "oneshot"
6616
6640
  })) {
6617
6641
  if (event.type === "text") {
6618
6642
  summaryText += event.text;
@@ -6648,11 +6672,6 @@ import path11 from "path";
6648
6672
  var log10 = createLogger("session");
6649
6673
  var SESSION_FILE = ".remy-session.json";
6650
6674
  var ARCHIVE_DIR = ".logs/sessions";
6651
- function writeFileAtomicSync(file, data) {
6652
- const tmp = `${file}.tmp`;
6653
- fs21.writeFileSync(tmp, data, "utf-8");
6654
- fs21.renameSync(tmp, file);
6655
- }
6656
6675
  var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6657
6676
  var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6658
6677
  var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
@@ -6681,6 +6700,12 @@ function loadSession(state) {
6681
6700
  return true;
6682
6701
  }
6683
6702
  } catch {
6703
+ try {
6704
+ const quarantine = `${SESSION_FILE}.corrupt-${Date.now()}`;
6705
+ fs21.renameSync(SESSION_FILE, quarantine);
6706
+ log10.warn(`Session file unreadable \u2014 quarantined to ${quarantine}`);
6707
+ } catch {
6708
+ }
6684
6709
  }
6685
6710
  return false;
6686
6711
  }
@@ -7210,7 +7235,10 @@ async function extractBrand(apiConfig, model) {
7210
7235
  subAgentId: "brandExtractor",
7211
7236
  system: EXTRACT_PROMPT,
7212
7237
  messages: [{ role: "user", content: corpus }],
7213
- tools: []
7238
+ tools: [],
7239
+ // One call per extraction over a corpus that changes with every spec
7240
+ // edit — never re-read, so a cache write is pure waste.
7241
+ cachePolicy: "oneshot"
7214
7242
  })) {
7215
7243
  if (event.type === "text") {
7216
7244
  responseText += event.text;
@@ -7392,11 +7420,9 @@ function pickFont(raw) {
7392
7420
  return out;
7393
7421
  }
7394
7422
  function persistBrand(brand, inputHash) {
7395
- const tmp = `${BRAND_FILE}.tmp`;
7396
- fs22.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
7397
- fs22.renameSync(tmp, BRAND_FILE);
7423
+ writeFileAtomicSync(BRAND_FILE, JSON.stringify(brand, null, 2));
7398
7424
  const cache = { inputHash, generatedAt: Date.now() };
7399
- fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
7425
+ writeFileAtomicSync(CACHE_FILE, JSON.stringify(cache, null, 2));
7400
7426
  }
7401
7427
  function readCache() {
7402
7428
  try {
@@ -7737,7 +7763,7 @@ async function runTurn(params) {
7737
7763
  toolRegistry,
7738
7764
  onBackgroundComplete
7739
7765
  } = params;
7740
- const tools2 = getToolDefinitions(onboardingState);
7766
+ const tools2 = getToolDefinitions();
7741
7767
  const buildModelOverride = buildModel ? filterModelPicks({ parent: buildModel }).parent : void 0;
7742
7768
  const baseline = resolveModel("parent", state.models, model);
7743
7769
  const parentModel = buildModelOverride ?? baseline;
@@ -7945,6 +7971,9 @@ async function runTurn(params) {
7945
7971
  system,
7946
7972
  messages: cleanMessagesForApi(state.messages),
7947
7973
  tools: tools2,
7974
+ // The parent is one long-lived conversation re-read across turns
7975
+ // with multi-minute gaps — the 1h-TTL profile.
7976
+ cachePolicy: "conversation",
7948
7977
  signal
7949
7978
  },
7950
7979
  {
@@ -8481,16 +8510,15 @@ ${lines.join("\n")}`;
8481
8510
  }
8482
8511
 
8483
8512
  // src/headless/planFile.ts
8484
- import { readFileSync, writeFileSync, unlinkSync } from "fs";
8513
+ import { readFileSync, unlinkSync } from "fs";
8485
8514
  var PLAN_FILE3 = ".remy-plan.md";
8486
8515
  function applyPlanFileSideEffect(rawText) {
8487
8516
  if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
8488
8517
  try {
8489
8518
  const plan = readFileSync(PLAN_FILE3, "utf-8");
8490
- writeFileSync(
8519
+ writeFileAtomicSync(
8491
8520
  PLAN_FILE3,
8492
- plan.replace(/^status:\s*pending/m, "status: approved"),
8493
- "utf-8"
8521
+ plan.replace(/^status:\s*pending/m, "status: approved")
8494
8522
  );
8495
8523
  } catch {
8496
8524
  }
@@ -8503,7 +8531,7 @@ function applyPlanFileSideEffect(rawText) {
8503
8531
  }
8504
8532
 
8505
8533
  // src/headless/stats.ts
8506
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
8534
+ import { readFileSync as readFileSync2 } from "fs";
8507
8535
  var STATS_FILE = ".remy-stats.json";
8508
8536
  function createSessionStats() {
8509
8537
  return {
@@ -8540,7 +8568,7 @@ function loadPassiveResults() {
8540
8568
  }
8541
8569
  function writeStats(stats, queue, passiveResults) {
8542
8570
  try {
8543
- writeFileSync2(
8571
+ writeFileAtomicSync(
8544
8572
  STATS_FILE,
8545
8573
  JSON.stringify({
8546
8574
  ...stats,
@@ -8769,9 +8797,7 @@ var HeadlessSession = class {
8769
8797
  name: "compactConversation",
8770
8798
  input: {},
8771
8799
  startedAt: Date.now(),
8772
- background: true,
8773
- uiOnly: true,
8774
- result: "Compaction started in the background."
8800
+ uiOnly: true
8775
8801
  }
8776
8802
  ]
8777
8803
  });
@@ -8780,15 +8806,7 @@ var HeadlessSession = class {
8780
8806
  type: "tool_start",
8781
8807
  id,
8782
8808
  name: "compactConversation",
8783
- input: {},
8784
- background: true
8785
- });
8786
- this.onEvent({
8787
- type: "tool_done",
8788
- id,
8789
- name: "compactConversation",
8790
- result: "Compaction started in the background.",
8791
- isError: false
8809
+ input: {}
8792
8810
  });
8793
8811
  }
8794
8812
  } else {
@@ -8797,11 +8815,31 @@ var HeadlessSession = class {
8797
8815
  if (this.syntheticCompactionId) {
8798
8816
  const id = this.syntheticCompactionId;
8799
8817
  this.syntheticCompactionId = null;
8800
- this.onBackgroundComplete(
8818
+ const result = event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? []);
8819
+ const isError = !!event.error;
8820
+ for (let i = this.state.messages.length - 1; i >= 0; i--) {
8821
+ const msg = this.state.messages[i];
8822
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
8823
+ continue;
8824
+ }
8825
+ const block = msg.content.find(
8826
+ (b) => b.type === "tool" && b.id === id
8827
+ );
8828
+ if (block && block.type === "tool") {
8829
+ block.result = result;
8830
+ block.isError = isError;
8831
+ block.completedAt = Date.now();
8832
+ saveSession(this.state);
8833
+ break;
8834
+ }
8835
+ }
8836
+ this.onEvent({
8837
+ type: "tool_done",
8801
8838
  id,
8802
- "compactConversation",
8803
- event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? [])
8804
- );
8839
+ name: "compactConversation",
8840
+ result,
8841
+ isError
8842
+ });
8805
8843
  }
8806
8844
  this.sessionStats.compactionInProgress = false;
8807
8845
  if (!event.error) {