@mindstudio-ai/remy 0.1.239 → 0.1.241

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
@@ -618,6 +618,18 @@ function readJsonAsset(fallback, ...segments) {
618
618
  // src/prompt/static/projectContext.ts
619
619
  import fs4 from "fs";
620
620
  import path3 from "path";
621
+
622
+ // src/projectRoot.ts
623
+ var PROJECT_ROOT = process.cwd();
624
+
625
+ // src/prompt/static/projectContext.ts
626
+ function loadProjectRoot() {
627
+ return `
628
+ ## Project Root
629
+ \`${PROJECT_ROOT}\`
630
+
631
+ File paths are relative to this directory. Every tool operates here and bash commands run it it.`;
632
+ }
621
633
  function loadProjectManifest() {
622
634
  try {
623
635
  const manifest = fs4.readFileSync("mindstudio.json", "utf-8");
@@ -750,6 +762,7 @@ function resolveIncludes(template) {
750
762
  }
751
763
  function buildSystemPrompt(onboardingState, viewContext) {
752
764
  const projectContext = [
765
+ loadProjectRoot(),
753
766
  loadProjectManifest(),
754
767
  loadSpecFileMetadata(),
755
768
  loadProjectFileListing()
@@ -2148,6 +2161,7 @@ ${unifiedDiff(input.path, content, updated)}`;
2148
2161
 
2149
2162
  // src/tools/code/bash.ts
2150
2163
  import { spawn as spawn2 } from "child_process";
2164
+ import path7 from "path";
2151
2165
  var DEFAULT_TIMEOUT_MS = 12e4;
2152
2166
  var DEFAULT_MAX_LINES2 = 500;
2153
2167
  var MAX_OUTPUT_BYTES = 3e4;
@@ -2165,7 +2179,7 @@ var bashTool = {
2165
2179
  },
2166
2180
  cwd: {
2167
2181
  type: "string",
2168
- description: "Working directory to run the command in. Defaults to the project root."
2182
+ description: "Working directory, relative to the project root. Defaults to the project root itself."
2169
2183
  },
2170
2184
  timeout: {
2171
2185
  type: "number",
@@ -2184,7 +2198,9 @@ var bashTool = {
2184
2198
  const timeoutMs = input.timeout ? input.timeout * 1e3 : DEFAULT_TIMEOUT_MS;
2185
2199
  return new Promise((resolve2) => {
2186
2200
  const child = spawn2("sh", ["-c", input.command], {
2187
- cwd: input.cwd || void 0,
2201
+ // Pinned rather than inherited. `undefined` here means "wherever the
2202
+ // process happens to be", which is the project root only by luck.
2203
+ cwd: input.cwd ? path7.resolve(PROJECT_ROOT, input.cwd) : PROJECT_ROOT,
2188
2204
  env: { ...process.env, FORCE_COLOR: "1" }
2189
2205
  });
2190
2206
  let output = "";
@@ -2430,7 +2446,7 @@ var globTool = {
2430
2446
 
2431
2447
  // src/tools/code/listDir.ts
2432
2448
  import fs15 from "fs/promises";
2433
- import path7 from "path";
2449
+ import path8 from "path";
2434
2450
  var EXCLUDE = /* @__PURE__ */ new Set([".git", "node_modules"]);
2435
2451
  var MAX_CHILDREN = 15;
2436
2452
  async function readAndSort(dirPath) {
@@ -2447,7 +2463,7 @@ async function readAndSort(dirPath) {
2447
2463
  }
2448
2464
  async function collapsePath(basePath, name) {
2449
2465
  let display = name;
2450
- let current = path7.join(basePath, name);
2466
+ let current = path8.join(basePath, name);
2451
2467
  for (; ; ) {
2452
2468
  let children;
2453
2469
  try {
@@ -2457,7 +2473,7 @@ async function collapsePath(basePath, name) {
2457
2473
  }
2458
2474
  if (children.length === 1 && children[0].isDirectory()) {
2459
2475
  display += "/" + children[0].name;
2460
- current = path7.join(current, children[0].name);
2476
+ current = path8.join(current, children[0].name);
2461
2477
  } else {
2462
2478
  break;
2463
2479
  }
@@ -2475,7 +2491,7 @@ function formatSize(bytes) {
2475
2491
  }
2476
2492
  async function formatFile(dirPath, name, indent) {
2477
2493
  try {
2478
- const stat = await fs15.stat(path7.join(dirPath, name));
2494
+ const stat = await fs15.stat(path8.join(dirPath, name));
2479
2495
  return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat.size)}`;
2480
2496
  } catch {
2481
2497
  return `${indent}${name}`;
@@ -2836,12 +2852,12 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2836
2852
  let existingUrl;
2837
2853
  let onLog;
2838
2854
  let model;
2839
- let path12;
2855
+ let path13;
2840
2856
  let fullPage = true;
2841
2857
  if (typeof promptOrOptions === "object" && promptOrOptions !== null) {
2842
2858
  prompt = promptOrOptions.prompt;
2843
2859
  existingUrl = promptOrOptions.imageUrl;
2844
- path12 = promptOrOptions.path;
2860
+ path13 = promptOrOptions.path;
2845
2861
  if (promptOrOptions.fullPage !== void 0) {
2846
2862
  fullPage = promptOrOptions.fullPage;
2847
2863
  }
@@ -2857,7 +2873,7 @@ async function captureAndAnalyzeScreenshot(promptOrOptions) {
2857
2873
  } else {
2858
2874
  const ssResult = await sidecarRequest(
2859
2875
  fullPage ? "/screenshot-full-page" : "/screenshot-viewport",
2860
- path12 ? { path: path12 } : void 0,
2876
+ path13 ? { path: path13 } : void 0,
2861
2877
  { timeout: fullPage ? 12e4 : 3e4 }
2862
2878
  );
2863
2879
  url = ssResult?.url || ssResult?.screenshotUrl;
@@ -4662,12 +4678,12 @@ __export(polishCopy_exports, {
4662
4678
 
4663
4679
  // src/subagents/common/context.ts
4664
4680
  import fs17 from "fs";
4665
- import path8 from "path";
4681
+ import path9 from "path";
4666
4682
  function walkMdFiles2(dir, skip) {
4667
4683
  const files = [];
4668
4684
  try {
4669
4685
  for (const entry of fs17.readdirSync(dir, { withFileTypes: true })) {
4670
- const full = path8.join(dir, entry.name);
4686
+ const full = path9.join(dir, entry.name);
4671
4687
  if (entry.isDirectory()) {
4672
4688
  if (!skip?.has(entry.name)) {
4673
4689
  files.push(...walkMdFiles2(full, skip));
@@ -5310,14 +5326,14 @@ var VISION_TOOLS = [
5310
5326
 
5311
5327
  // src/subagents/productVision/executor.ts
5312
5328
  import fs19 from "fs";
5313
- import path9 from "path";
5329
+ import path10 from "path";
5314
5330
  var ROADMAP_DIR = "src/roadmap";
5315
5331
  var PITCH_DECK_SHELL = readAsset(
5316
5332
  "subagents/productVision",
5317
5333
  "pitch-deck-shell.html"
5318
5334
  );
5319
5335
  function resolve(filePath) {
5320
- return path9.join(ROADMAP_DIR, filePath);
5336
+ return path10.join(ROADMAP_DIR, filePath);
5321
5337
  }
5322
5338
  async function executeVisionTool(name, input, context) {
5323
5339
  switch (name) {
@@ -5913,7 +5929,7 @@ var log8 = createLogger("compaction");
5913
5929
  var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
5914
5930
  var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
5915
5931
  var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
5916
- async function compactConversation(messages, apiConfig, system, tools2, model) {
5932
+ async function compactConversation(messages, apiConfig, model) {
5917
5933
  const endIndex = findSafeInsertionPoint(messages);
5918
5934
  const summaries = [];
5919
5935
  const tasks = [];
@@ -5921,6 +5937,7 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
5921
5937
  messages,
5922
5938
  endIndex
5923
5939
  );
5940
+ let conversationFailed = false;
5924
5941
  if (conversationMessages.length > 0) {
5925
5942
  tasks.push(
5926
5943
  generateSummary(
@@ -5928,12 +5945,12 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
5928
5945
  "conversation",
5929
5946
  CONVERSATION_SUMMARY_PROMPT,
5930
5947
  conversationMessages,
5931
- system,
5932
- tools2,
5933
5948
  model
5934
5949
  ).then((text) => {
5935
5950
  if (text) {
5936
5951
  summaries.push({ name: "conversation", text });
5952
+ } else {
5953
+ conversationFailed = true;
5937
5954
  }
5938
5955
  })
5939
5956
  );
@@ -5951,18 +5968,25 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
5951
5968
  name,
5952
5969
  SUBAGENT_SUMMARY_PROMPT,
5953
5970
  subagentMessages,
5954
- system,
5955
- tools2,
5956
5971
  model
5957
5972
  ).then((text) => {
5958
5973
  if (text) {
5959
5974
  summaries.push({ name, text });
5975
+ } else {
5976
+ log8.warn("Subagent summary unusable \u2014 leaving its history intact", {
5977
+ name
5978
+ });
5960
5979
  }
5961
5980
  })
5962
5981
  );
5963
5982
  }
5964
5983
  }
5965
5984
  await Promise.all(tasks);
5985
+ if (conversationFailed) {
5986
+ throw new Error(
5987
+ "Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
5988
+ );
5989
+ }
5966
5990
  const checkpointMessages = summaries.map((s) => ({
5967
5991
  role: "user",
5968
5992
  hidden: true,
@@ -6107,66 +6131,108 @@ function serializeForSummary(messages) {
6107
6131
  }
6108
6132
  return lines.join("\n\n");
6109
6133
  }
6110
- var CHUNK_CHAR_LIMIT = 24e5;
6111
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
6134
+ var CHUNK_CHAR_LIMIT = 2e5;
6135
+ var MIN_SUMMARY_CHARS = 400;
6136
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, model, opts = {}) {
6112
6137
  const serialized = serializeForSummary(messagesToSummarize);
6113
6138
  if (!serialized.trim()) {
6114
6139
  return null;
6115
6140
  }
6116
- if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
6141
+ const splittable = messagesToSummarize.length > 1;
6142
+ const allowRetry = opts.allowRetry ?? true;
6143
+ if (splittable && (opts.forceChunk || serialized.length > CHUNK_CHAR_LIMIT)) {
6117
6144
  const mid = Math.floor(messagesToSummarize.length / 2);
6145
+ const halves = [
6146
+ messagesToSummarize.slice(0, mid),
6147
+ messagesToSummarize.slice(mid)
6148
+ ];
6118
6149
  log8.info("Chunking summary", {
6119
6150
  name,
6120
6151
  messageCount: messagesToSummarize.length,
6121
- serializedLength: serialized.length
6152
+ serializedLength: serialized.length,
6153
+ forced: !!opts.forceChunk
6122
6154
  });
6123
- const [first, second] = await Promise.all([
6124
- generateSummary(
6125
- apiConfig,
6126
- `${name} [pt1]`,
6127
- compactionPrompt,
6128
- messagesToSummarize.slice(0, mid),
6129
- mainSystem,
6130
- mainTools,
6131
- model
6132
- ),
6133
- generateSummary(
6134
- apiConfig,
6135
- `${name} [pt2]`,
6136
- compactionPrompt,
6137
- messagesToSummarize.slice(mid),
6138
- mainSystem,
6139
- mainTools,
6140
- model
6155
+ const results = await Promise.all(
6156
+ halves.map(
6157
+ (half, i) => generateSummary(
6158
+ apiConfig,
6159
+ `${name} [pt${i + 1}]`,
6160
+ compactionPrompt,
6161
+ half,
6162
+ model,
6163
+ // A split driven by size gets its children a retry of their own. A
6164
+ // split that IS the retry does not, or a model that will not
6165
+ // summarize at any size fans out call after call before giving up.
6166
+ { allowRetry: opts.forceChunk ? false : allowRetry }
6167
+ )
6141
6168
  )
6142
- ]);
6143
- const parts = [first, second].filter((p) => !!p);
6169
+ );
6170
+ const lost = results.some(
6171
+ (r, i) => r === null && serializeForSummary(halves[i]).trim()
6172
+ );
6173
+ if (lost) {
6174
+ return null;
6175
+ }
6176
+ const parts = results.filter((p) => p !== null);
6144
6177
  return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
6145
6178
  }
6146
6179
  log8.info("Generating summary", {
6147
6180
  name,
6148
6181
  messageCount: messagesToSummarize.length,
6149
- cacheReuse: !!mainSystem
6182
+ serializedLength: serialized.length
6150
6183
  });
6151
- let summaryText = "";
6152
- const useMainCache = !!mainSystem;
6153
- const system = useMainCache ? mainSystem : compactionPrompt;
6154
- const tools2 = [];
6155
- const userContent = useMainCache ? `${compactionPrompt}
6184
+ const summaryText = await runSummaryCall(
6185
+ apiConfig,
6186
+ name,
6187
+ compactionPrompt,
6188
+ serialized,
6189
+ model
6190
+ );
6191
+ if (summaryText === null) {
6192
+ return null;
6193
+ }
6194
+ if (summaryText.length >= MIN_SUMMARY_CHARS) {
6195
+ log8.info("Summary generated", { name, summaryLength: summaryText.length });
6196
+ return summaryText;
6197
+ }
6198
+ log8.warn("Summary too short to be real", {
6199
+ name,
6200
+ summaryLength: summaryText.length,
6201
+ minimum: MIN_SUMMARY_CHARS,
6202
+ serializedLength: serialized.length,
6203
+ retrying: splittable && allowRetry
6204
+ });
6205
+ if (!splittable || !allowRetry) {
6206
+ return null;
6207
+ }
6208
+ return generateSummary(
6209
+ apiConfig,
6210
+ name,
6211
+ compactionPrompt,
6212
+ messagesToSummarize,
6213
+ model,
6214
+ { forceChunk: true, allowRetry: false }
6215
+ );
6216
+ }
6217
+ async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model) {
6218
+ const userContent = `Conversation to summarize:
6156
6219
 
6157
- ---
6220
+ ${serialized}
6158
6221
 
6159
- Conversation to summarize:
6222
+ ---
6160
6223
 
6161
- ${serialized}` : serialized;
6224
+ Write the summary of the conversation above, following your instructions.`;
6225
+ let summaryText = "";
6162
6226
  const iterStart = Date.now();
6163
- for await (const event of streamChat({
6227
+ for await (const event of streamChatWithRetry({
6164
6228
  ...apiConfig,
6165
6229
  model,
6166
6230
  subAgentId: "conversationSummarizer",
6167
- system,
6231
+ system: compactionPrompt,
6168
6232
  messages: [{ role: "user", content: userContent }],
6169
- tools: tools2
6233
+ // Always empty. With a toolset available the model picks `tool_use` over
6234
+ // producing a summary, leaving summaryText empty.
6235
+ tools: []
6170
6236
  })) {
6171
6237
  if (event.type === "text") {
6172
6238
  summaryText += event.text;
@@ -6193,16 +6259,223 @@ ${serialized}` : serialized;
6193
6259
  log8.warn("Empty summary generated", { name });
6194
6260
  return null;
6195
6261
  }
6196
- log8.info("Summary generated", { name, summaryLength: summaryText.length });
6197
6262
  return summaryText.trim();
6198
6263
  }
6199
6264
 
6265
+ // src/session.ts
6266
+ import fs21 from "fs";
6267
+ import path11 from "path";
6268
+ var log9 = createLogger("session");
6269
+ var SESSION_FILE = ".remy-session.json";
6270
+ var ARCHIVE_DIR = ".logs/sessions";
6271
+ var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6272
+ var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6273
+ var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
6274
+ function loadSession(state) {
6275
+ pruneArchives();
6276
+ try {
6277
+ const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
6278
+ const data = JSON.parse(raw);
6279
+ if (data.models && typeof data.models === "object") {
6280
+ state.models = data.models;
6281
+ }
6282
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
6283
+ state.messages = sanitizeMessages(data.messages);
6284
+ log9.info("Session loaded", {
6285
+ messageCount: state.messages.length,
6286
+ ...state.models && { models: state.models }
6287
+ });
6288
+ return true;
6289
+ }
6290
+ } catch {
6291
+ }
6292
+ return false;
6293
+ }
6294
+ function capOversizedResults(msg) {
6295
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
6296
+ for (const block of msg.content) {
6297
+ if (block.type === "tool" && typeof block.result === "string") {
6298
+ block.result = capToolResult(block.result);
6299
+ }
6300
+ }
6301
+ } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
6302
+ msg.content = capToolResult(msg.content);
6303
+ }
6304
+ }
6305
+ function sanitizeMessages(messages) {
6306
+ const result = [];
6307
+ for (let i = 0; i < messages.length; i++) {
6308
+ const msg = messages[i];
6309
+ capOversizedResults(msg);
6310
+ result.push(msg);
6311
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
6312
+ continue;
6313
+ }
6314
+ const toolBlocks = msg.content.filter(
6315
+ (b) => b.type === "tool"
6316
+ );
6317
+ if (toolBlocks.length === 0) {
6318
+ continue;
6319
+ }
6320
+ const resultIds = /* @__PURE__ */ new Set();
6321
+ for (let j = i + 1; j < messages.length; j++) {
6322
+ const next = messages[j];
6323
+ if (next.role === "user" && next.toolCallId) {
6324
+ resultIds.add(next.toolCallId);
6325
+ } else {
6326
+ break;
6327
+ }
6328
+ }
6329
+ for (const tc of toolBlocks) {
6330
+ if (!resultIds.has(tc.id)) {
6331
+ result.push({
6332
+ role: "user",
6333
+ content: "Error: tool result lost (session recovered)",
6334
+ toolCallId: tc.id,
6335
+ isToolError: true
6336
+ });
6337
+ }
6338
+ }
6339
+ }
6340
+ return result;
6341
+ }
6342
+ function buildPayload(state) {
6343
+ const payload = { messages: state.messages };
6344
+ if (state.models && Object.keys(state.models).length > 0) {
6345
+ payload.models = state.models;
6346
+ }
6347
+ return payload;
6348
+ }
6349
+ function archiveMessages(messages, label, models) {
6350
+ fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
6351
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6352
+ let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.json`);
6353
+ let n = 1;
6354
+ while (fs21.existsSync(dest)) {
6355
+ dest = path11.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
6356
+ }
6357
+ const payload = { messages };
6358
+ if (models && Object.keys(models).length > 0) {
6359
+ payload.models = models;
6360
+ }
6361
+ fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6362
+ log9.info("Session archived", { label, dest, messageCount: messages.length });
6363
+ pruneArchives();
6364
+ return dest;
6365
+ }
6366
+ function pruneArchives() {
6367
+ try {
6368
+ const entries = fs21.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
6369
+ if (entries.length <= 1) {
6370
+ return;
6371
+ }
6372
+ const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
6373
+ const archives = entries.map((name) => ({
6374
+ name,
6375
+ size: fs21.statSync(path11.join(ARCHIVE_DIR, name)).size
6376
+ })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
6377
+ let kept = 0;
6378
+ let cut = archives.length;
6379
+ for (let i = 0; i < archives.length; i++) {
6380
+ if (i === 0 || kept + archives[i].size <= ARCHIVE_RETENTION_BYTES) {
6381
+ kept += archives[i].size;
6382
+ } else {
6383
+ cut = i;
6384
+ break;
6385
+ }
6386
+ }
6387
+ let removed = 0;
6388
+ let freed = 0;
6389
+ for (let i = cut; i < archives.length; i++) {
6390
+ try {
6391
+ fs21.unlinkSync(path11.join(ARCHIVE_DIR, archives[i].name));
6392
+ freed += archives[i].size;
6393
+ removed++;
6394
+ } catch {
6395
+ }
6396
+ }
6397
+ if (removed > 0) {
6398
+ log9.info("Session archives pruned", {
6399
+ removed,
6400
+ freedBytes: freed,
6401
+ keptBytes: kept
6402
+ });
6403
+ }
6404
+ } catch {
6405
+ }
6406
+ }
6407
+ function rotate(state) {
6408
+ const messages = state.messages;
6409
+ if (messages.length === 0) {
6410
+ return false;
6411
+ }
6412
+ let tailBytes = 0;
6413
+ let scrollbackStart = 0;
6414
+ for (let i = messages.length - 1; i >= 0; i--) {
6415
+ tailBytes += Buffer.byteLength(JSON.stringify(messages[i]), "utf-8") + 1;
6416
+ if (tailBytes >= RETAIN_TAIL_BYTES) {
6417
+ scrollbackStart = i;
6418
+ break;
6419
+ }
6420
+ }
6421
+ const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
6422
+ let cut = checkpointIdx === -1 ? scrollbackStart : Math.min(scrollbackStart, checkpointIdx);
6423
+ cut = findSafeInsertionPoint(messages, cut);
6424
+ if (cut <= 0) {
6425
+ return false;
6426
+ }
6427
+ archiveMessages(messages.slice(0, cut), "rotated", state.models);
6428
+ state.messages = messages.slice(cut);
6429
+ log9.info("Session rotated", {
6430
+ archived: cut,
6431
+ retained: state.messages.length
6432
+ });
6433
+ return true;
6434
+ }
6435
+ function saveSession(state) {
6436
+ try {
6437
+ let serialized = JSON.stringify(buildPayload(state));
6438
+ if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6439
+ serialized = JSON.stringify(buildPayload(state));
6440
+ }
6441
+ fs21.writeFileSync(SESSION_FILE, serialized, "utf-8");
6442
+ log9.info("Session saved", { messageCount: state.messages.length });
6443
+ } catch (err) {
6444
+ log9.warn("Session save failed", { error: err.message });
6445
+ }
6446
+ }
6447
+ function clearSession(state) {
6448
+ try {
6449
+ if (state.messages.length > 0) {
6450
+ archiveMessages(state.messages, "cleared", state.models);
6451
+ }
6452
+ } catch (err) {
6453
+ log9.warn("Session archive on clear failed", { error: err.message });
6454
+ }
6455
+ state.messages = [];
6456
+ try {
6457
+ if (fs21.existsSync(SESSION_FILE)) {
6458
+ fs21.unlinkSync(SESSION_FILE);
6459
+ }
6460
+ } catch (err) {
6461
+ log9.warn("Session clear: could not remove live file", {
6462
+ error: err.message
6463
+ });
6464
+ }
6465
+ }
6466
+
6200
6467
  // src/compaction/trigger.ts
6201
- var log9 = createLogger("compaction:trigger");
6468
+ var log10 = createLogger("compaction:trigger");
6202
6469
  var pendingSummaries = [];
6203
6470
  var inflightCompaction = null;
6204
- function getPendingSummaries() {
6205
- return pendingSummaries.splice(0);
6471
+ function applyPendingSummaries(state) {
6472
+ const summaries = pendingSummaries.splice(0);
6473
+ if (summaries.length === 0) {
6474
+ return;
6475
+ }
6476
+ const idx = findSafeInsertionPoint(state.messages);
6477
+ state.messages.splice(idx, 0, ...summaries);
6478
+ saveSession(state);
6206
6479
  }
6207
6480
  var listener = null;
6208
6481
  function setCompactionListener(l) {
@@ -6214,22 +6487,18 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6214
6487
  }
6215
6488
  const { blocking = false, requestId, model } = opts;
6216
6489
  listener?.({ type: "started", blocking, requestId });
6217
- const system = buildSystemPrompt("onboardingFinished");
6218
- const tools2 = getToolDefinitions("onboardingFinished");
6219
6490
  inflightCompaction = compactConversation(
6220
6491
  state.messages,
6221
6492
  apiConfig,
6222
- system,
6223
- tools2,
6224
6493
  resolveModel("conversationSummarizer", state.models, model)
6225
6494
  ).then((summaries) => {
6226
6495
  pendingSummaries.push(...summaries);
6227
6496
  listener?.({ type: "complete", requestId });
6228
- log9.info("Compaction complete");
6497
+ log10.info("Compaction complete");
6229
6498
  }).catch((err) => {
6230
6499
  const message = err.message || "Compaction failed";
6231
6500
  listener?.({ type: "complete", error: message, requestId });
6232
- log9.error("Compaction failed", { error: message });
6501
+ log10.error("Compaction failed", { error: message });
6233
6502
  throw err;
6234
6503
  }).finally(() => {
6235
6504
  inflightCompaction = null;
@@ -6238,10 +6507,10 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6238
6507
  }
6239
6508
 
6240
6509
  // src/brandExtraction/index.ts
6241
- import fs21 from "fs";
6242
- import path10 from "path";
6510
+ import fs22 from "fs";
6511
+ import path12 from "path";
6243
6512
  import { createHash } from "crypto";
6244
- var log10 = createLogger("brandExtraction");
6513
+ var log11 = createLogger("brandExtraction");
6245
6514
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6246
6515
  var BRAND_FILE = ".remy-brand.json";
6247
6516
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -6249,21 +6518,21 @@ async function runExtraction(apiConfig, model) {
6249
6518
  const inputHash = computeInputHash();
6250
6519
  const cached3 = readCache();
6251
6520
  if (cached3 && cached3.inputHash === inputHash) {
6252
- log10.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6521
+ log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6253
6522
  return null;
6254
6523
  }
6255
- log10.info("Extracting brand", { inputHash });
6524
+ log11.info("Extracting brand", { inputHash });
6256
6525
  const brand = await extractBrand(apiConfig, model);
6257
6526
  if (!brand) {
6258
- log10.warn("Brand extraction failed \u2014 leaving cache untouched");
6527
+ log11.warn("Brand extraction failed \u2014 leaving cache untouched");
6259
6528
  return null;
6260
6529
  }
6261
6530
  persistBrand(brand, inputHash);
6262
- log10.info("Brand persisted", { inputHash });
6531
+ log11.info("Brand persisted", { inputHash });
6263
6532
  return brand;
6264
6533
  }
6265
6534
  function isBrandRelevant(filePath) {
6266
- if (filePath === path10.join("src", "app.md")) {
6535
+ if (filePath === path12.join("src", "app.md")) {
6267
6536
  return true;
6268
6537
  }
6269
6538
  const { type } = parseFrontmatter3(filePath);
@@ -6289,7 +6558,7 @@ function sha256(input) {
6289
6558
  }
6290
6559
  function readSafe(filePath) {
6291
6560
  try {
6292
- return fs21.readFileSync(filePath, "utf-8");
6561
+ return fs22.readFileSync(filePath, "utf-8");
6293
6562
  } catch {
6294
6563
  return "";
6295
6564
  }
@@ -6297,9 +6566,9 @@ function readSafe(filePath) {
6297
6566
  function walkMdFiles3(dir) {
6298
6567
  const results = [];
6299
6568
  try {
6300
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
6569
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
6301
6570
  for (const entry of entries) {
6302
- const full = path10.join(dir, entry.name);
6571
+ const full = path12.join(dir, entry.name);
6303
6572
  if (entry.isDirectory()) {
6304
6573
  results.push(...walkMdFiles3(full));
6305
6574
  } else if (entry.name.endsWith(".md")) {
@@ -6312,7 +6581,7 @@ function walkMdFiles3(dir) {
6312
6581
  }
6313
6582
  function parseFrontmatter3(filePath) {
6314
6583
  try {
6315
- const content = fs21.readFileSync(filePath, "utf-8");
6584
+ const content = fs22.readFileSync(filePath, "utf-8");
6316
6585
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6317
6586
  if (!match) {
6318
6587
  return { type: "" };
@@ -6327,7 +6596,7 @@ function parseFrontmatter3(filePath) {
6327
6596
  async function extractBrand(apiConfig, model) {
6328
6597
  const corpus = buildCorpus();
6329
6598
  if (!corpus.trim()) {
6330
- log10.debug("No spec corpus \u2014 emitting empty brand");
6599
+ log11.debug("No spec corpus \u2014 emitting empty brand");
6331
6600
  return { version: 1 };
6332
6601
  }
6333
6602
  let responseText = "";
@@ -6358,17 +6627,17 @@ async function extractBrand(apiConfig, model) {
6358
6627
  toolNames: []
6359
6628
  });
6360
6629
  } else if (event.type === "error") {
6361
- log10.error("Brand extraction stream error", { error: event.error });
6630
+ log11.error("Brand extraction stream error", { error: event.error });
6362
6631
  return null;
6363
6632
  }
6364
6633
  }
6365
6634
  } catch (err) {
6366
- log10.error("Brand extraction threw", { error: err?.message });
6635
+ log11.error("Brand extraction threw", { error: err?.message });
6367
6636
  return null;
6368
6637
  }
6369
6638
  const parsed = parseJsonResponse(responseText);
6370
6639
  if (!parsed) {
6371
- log10.warn("Brand extraction returned unparseable JSON", {
6640
+ log11.warn("Brand extraction returned unparseable JSON", {
6372
6641
  preview: responseText.slice(0, 200)
6373
6642
  });
6374
6643
  return null;
@@ -6510,14 +6779,14 @@ function pickFont(raw) {
6510
6779
  }
6511
6780
  function persistBrand(brand, inputHash) {
6512
6781
  const tmp = `${BRAND_FILE}.tmp`;
6513
- fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6514
- fs21.renameSync(tmp, BRAND_FILE);
6782
+ fs22.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6783
+ fs22.renameSync(tmp, BRAND_FILE);
6515
6784
  const cache = { inputHash, generatedAt: Date.now() };
6516
- fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6785
+ fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6517
6786
  }
6518
6787
  function readCache() {
6519
6788
  try {
6520
- const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
6789
+ const raw = fs22.readFileSync(CACHE_FILE, "utf-8");
6521
6790
  const parsed = JSON.parse(raw);
6522
6791
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
6523
6792
  return parsed;
@@ -6529,7 +6798,7 @@ function readCache() {
6529
6798
  }
6530
6799
 
6531
6800
  // src/brandExtraction/trigger.ts
6532
- var log11 = createLogger("brandExtraction:trigger");
6801
+ var log12 = createLogger("brandExtraction:trigger");
6533
6802
  var inflight = false;
6534
6803
  var dirty = false;
6535
6804
  function triggerBrandExtraction(apiConfig, model) {
@@ -6539,7 +6808,7 @@ function triggerBrandExtraction(apiConfig, model) {
6539
6808
  }
6540
6809
  inflight = true;
6541
6810
  void runExtraction(apiConfig, model).catch((err) => {
6542
- log11.error("Brand extraction failed", { error: err?.message });
6811
+ log12.error("Brand extraction failed", { error: err?.message });
6543
6812
  }).finally(() => {
6544
6813
  inflight = false;
6545
6814
  if (dirty) {
@@ -6549,208 +6818,6 @@ function triggerBrandExtraction(apiConfig, model) {
6549
6818
  });
6550
6819
  }
6551
6820
 
6552
- // src/session.ts
6553
- import fs22 from "fs";
6554
- import path11 from "path";
6555
- var log12 = createLogger("session");
6556
- var SESSION_FILE = ".remy-session.json";
6557
- var ARCHIVE_DIR = ".logs/sessions";
6558
- var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6559
- var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6560
- var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
6561
- function loadSession(state) {
6562
- pruneArchives();
6563
- try {
6564
- const raw = fs22.readFileSync(SESSION_FILE, "utf-8");
6565
- const data = JSON.parse(raw);
6566
- if (data.models && typeof data.models === "object") {
6567
- state.models = data.models;
6568
- }
6569
- if (Array.isArray(data.messages) && data.messages.length > 0) {
6570
- state.messages = sanitizeMessages(data.messages);
6571
- log12.info("Session loaded", {
6572
- messageCount: state.messages.length,
6573
- ...state.models && { models: state.models }
6574
- });
6575
- return true;
6576
- }
6577
- } catch {
6578
- }
6579
- return false;
6580
- }
6581
- function capOversizedResults(msg) {
6582
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
6583
- for (const block of msg.content) {
6584
- if (block.type === "tool" && typeof block.result === "string") {
6585
- block.result = capToolResult(block.result);
6586
- }
6587
- }
6588
- } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
6589
- msg.content = capToolResult(msg.content);
6590
- }
6591
- }
6592
- function sanitizeMessages(messages) {
6593
- const result = [];
6594
- for (let i = 0; i < messages.length; i++) {
6595
- const msg = messages[i];
6596
- capOversizedResults(msg);
6597
- result.push(msg);
6598
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
6599
- continue;
6600
- }
6601
- const toolBlocks = msg.content.filter(
6602
- (b) => b.type === "tool"
6603
- );
6604
- if (toolBlocks.length === 0) {
6605
- continue;
6606
- }
6607
- const resultIds = /* @__PURE__ */ new Set();
6608
- for (let j = i + 1; j < messages.length; j++) {
6609
- const next = messages[j];
6610
- if (next.role === "user" && next.toolCallId) {
6611
- resultIds.add(next.toolCallId);
6612
- } else {
6613
- break;
6614
- }
6615
- }
6616
- for (const tc of toolBlocks) {
6617
- if (!resultIds.has(tc.id)) {
6618
- result.push({
6619
- role: "user",
6620
- content: "Error: tool result lost (session recovered)",
6621
- toolCallId: tc.id,
6622
- isToolError: true
6623
- });
6624
- }
6625
- }
6626
- }
6627
- return result;
6628
- }
6629
- function buildPayload(state) {
6630
- const payload = { messages: state.messages };
6631
- if (state.models && Object.keys(state.models).length > 0) {
6632
- payload.models = state.models;
6633
- }
6634
- return payload;
6635
- }
6636
- function archiveMessages(messages, label, models) {
6637
- fs22.mkdirSync(ARCHIVE_DIR, { recursive: true });
6638
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6639
- let dest = path11.join(ARCHIVE_DIR, `${label}-${ts}.json`);
6640
- let n = 1;
6641
- while (fs22.existsSync(dest)) {
6642
- dest = path11.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
6643
- }
6644
- const payload = { messages };
6645
- if (models && Object.keys(models).length > 0) {
6646
- payload.models = models;
6647
- }
6648
- fs22.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6649
- log12.info("Session archived", { label, dest, messageCount: messages.length });
6650
- pruneArchives();
6651
- return dest;
6652
- }
6653
- function pruneArchives() {
6654
- try {
6655
- const entries = fs22.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
6656
- if (entries.length <= 1) {
6657
- return;
6658
- }
6659
- const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
6660
- const archives = entries.map((name) => ({
6661
- name,
6662
- size: fs22.statSync(path11.join(ARCHIVE_DIR, name)).size
6663
- })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
6664
- let kept = 0;
6665
- let cut = archives.length;
6666
- for (let i = 0; i < archives.length; i++) {
6667
- if (i === 0 || kept + archives[i].size <= ARCHIVE_RETENTION_BYTES) {
6668
- kept += archives[i].size;
6669
- } else {
6670
- cut = i;
6671
- break;
6672
- }
6673
- }
6674
- let removed = 0;
6675
- let freed = 0;
6676
- for (let i = cut; i < archives.length; i++) {
6677
- try {
6678
- fs22.unlinkSync(path11.join(ARCHIVE_DIR, archives[i].name));
6679
- freed += archives[i].size;
6680
- removed++;
6681
- } catch {
6682
- }
6683
- }
6684
- if (removed > 0) {
6685
- log12.info("Session archives pruned", {
6686
- removed,
6687
- freedBytes: freed,
6688
- keptBytes: kept
6689
- });
6690
- }
6691
- } catch {
6692
- }
6693
- }
6694
- function rotate(state) {
6695
- const messages = state.messages;
6696
- if (messages.length === 0) {
6697
- return false;
6698
- }
6699
- let tailBytes = 0;
6700
- let scrollbackStart = 0;
6701
- for (let i = messages.length - 1; i >= 0; i--) {
6702
- tailBytes += Buffer.byteLength(JSON.stringify(messages[i]), "utf-8") + 1;
6703
- if (tailBytes >= RETAIN_TAIL_BYTES) {
6704
- scrollbackStart = i;
6705
- break;
6706
- }
6707
- }
6708
- const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
6709
- let cut = checkpointIdx === -1 ? scrollbackStart : Math.min(scrollbackStart, checkpointIdx);
6710
- cut = findSafeInsertionPoint(messages, cut);
6711
- if (cut <= 0) {
6712
- return false;
6713
- }
6714
- archiveMessages(messages.slice(0, cut), "rotated", state.models);
6715
- state.messages = messages.slice(cut);
6716
- log12.info("Session rotated", {
6717
- archived: cut,
6718
- retained: state.messages.length
6719
- });
6720
- return true;
6721
- }
6722
- function saveSession(state) {
6723
- try {
6724
- let serialized = JSON.stringify(buildPayload(state));
6725
- if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6726
- serialized = JSON.stringify(buildPayload(state));
6727
- }
6728
- fs22.writeFileSync(SESSION_FILE, serialized, "utf-8");
6729
- log12.info("Session saved", { messageCount: state.messages.length });
6730
- } catch (err) {
6731
- log12.warn("Session save failed", { error: err.message });
6732
- }
6733
- }
6734
- function clearSession(state) {
6735
- try {
6736
- if (state.messages.length > 0) {
6737
- archiveMessages(state.messages, "cleared", state.models);
6738
- }
6739
- } catch (err) {
6740
- log12.warn("Session archive on clear failed", { error: err.message });
6741
- }
6742
- state.messages = [];
6743
- try {
6744
- if (fs22.existsSync(SESSION_FILE)) {
6745
- fs22.unlinkSync(SESSION_FILE);
6746
- }
6747
- } catch (err) {
6748
- log12.warn("Session clear: could not remove live file", {
6749
- error: err.message
6750
- });
6751
- }
6752
- }
6753
-
6754
6821
  // src/parsePartialJson.ts
6755
6822
  var PartialJSON = class extends Error {
6756
6823
  };
@@ -7950,7 +8017,9 @@ var HeadlessSession = class {
7950
8017
  const data = event.error ? { error: event.error } : {};
7951
8018
  this.emit("compaction_complete", data, event.requestId);
7952
8019
  this.sessionStats.compactionInProgress = false;
7953
- this.sessionStats.lastContextSize = 0;
8020
+ if (!event.error) {
8021
+ this.sessionStats.lastContextSize = 0;
8022
+ }
7954
8023
  this.sessionStats.messageCount = this.state.messages.length;
7955
8024
  this.persistStats();
7956
8025
  }
@@ -8084,20 +8153,10 @@ var HeadlessSession = class {
8084
8153
  requestId,
8085
8154
  model: this.opts.model
8086
8155
  });
8087
- this.applyPendingSummaries();
8156
+ applyPendingSummaries(this.state);
8088
8157
  } catch {
8089
8158
  }
8090
8159
  }
8091
- /** Drain pending compaction summaries and insert at a safe point. */
8092
- applyPendingSummaries() {
8093
- const summaries = getPendingSummaries();
8094
- if (summaries.length === 0) {
8095
- return;
8096
- }
8097
- const idx = findSafeInsertionPoint(this.state.messages);
8098
- this.state.messages.splice(idx, 0, ...summaries);
8099
- saveSession(this.state);
8100
- }
8101
8160
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
8102
8161
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
8103
8162
  log15.info("Background complete", {
@@ -8426,7 +8485,7 @@ var HeadlessSession = class {
8426
8485
  error: err.message
8427
8486
  });
8428
8487
  }
8429
- this.applyPendingSummaries();
8488
+ applyPendingSummaries(this.state);
8430
8489
  this.applyPendingBlockUpdates();
8431
8490
  }
8432
8491
  async handleMessage(parsed, requestId) {
@@ -8730,7 +8789,7 @@ var HeadlessSession = class {
8730
8789
  model: this.opts.model
8731
8790
  });
8732
8791
  if (!this.running) {
8733
- this.applyPendingSummaries();
8792
+ applyPendingSummaries(this.state);
8734
8793
  }
8735
8794
  this.emit("completed", { success: true }, requestId);
8736
8795
  } catch (err) {