@mindstudio-ai/remy 0.1.239 → 0.1.240

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
@@ -5913,7 +5913,7 @@ var log8 = createLogger("compaction");
5913
5913
  var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
5914
5914
  var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
5915
5915
  var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
5916
- async function compactConversation(messages, apiConfig, system, tools2, model) {
5916
+ async function compactConversation(messages, apiConfig, model) {
5917
5917
  const endIndex = findSafeInsertionPoint(messages);
5918
5918
  const summaries = [];
5919
5919
  const tasks = [];
@@ -5921,6 +5921,7 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
5921
5921
  messages,
5922
5922
  endIndex
5923
5923
  );
5924
+ let conversationFailed = false;
5924
5925
  if (conversationMessages.length > 0) {
5925
5926
  tasks.push(
5926
5927
  generateSummary(
@@ -5928,12 +5929,12 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
5928
5929
  "conversation",
5929
5930
  CONVERSATION_SUMMARY_PROMPT,
5930
5931
  conversationMessages,
5931
- system,
5932
- tools2,
5933
5932
  model
5934
5933
  ).then((text) => {
5935
5934
  if (text) {
5936
5935
  summaries.push({ name: "conversation", text });
5936
+ } else {
5937
+ conversationFailed = true;
5937
5938
  }
5938
5939
  })
5939
5940
  );
@@ -5951,18 +5952,25 @@ async function compactConversation(messages, apiConfig, system, tools2, model) {
5951
5952
  name,
5952
5953
  SUBAGENT_SUMMARY_PROMPT,
5953
5954
  subagentMessages,
5954
- system,
5955
- tools2,
5956
5955
  model
5957
5956
  ).then((text) => {
5958
5957
  if (text) {
5959
5958
  summaries.push({ name, text });
5959
+ } else {
5960
+ log8.warn("Subagent summary unusable \u2014 leaving its history intact", {
5961
+ name
5962
+ });
5960
5963
  }
5961
5964
  })
5962
5965
  );
5963
5966
  }
5964
5967
  }
5965
5968
  await Promise.all(tasks);
5969
+ if (conversationFailed) {
5970
+ throw new Error(
5971
+ "Could not summarize the conversation \u2014 the model did not return a usable summary. History left intact."
5972
+ );
5973
+ }
5966
5974
  const checkpointMessages = summaries.map((s) => ({
5967
5975
  role: "user",
5968
5976
  hidden: true,
@@ -6107,66 +6115,108 @@ function serializeForSummary(messages) {
6107
6115
  }
6108
6116
  return lines.join("\n\n");
6109
6117
  }
6110
- var CHUNK_CHAR_LIMIT = 24e5;
6111
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
6118
+ var CHUNK_CHAR_LIMIT = 2e5;
6119
+ var MIN_SUMMARY_CHARS = 400;
6120
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, model, opts = {}) {
6112
6121
  const serialized = serializeForSummary(messagesToSummarize);
6113
6122
  if (!serialized.trim()) {
6114
6123
  return null;
6115
6124
  }
6116
- if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
6125
+ const splittable = messagesToSummarize.length > 1;
6126
+ const allowRetry = opts.allowRetry ?? true;
6127
+ if (splittable && (opts.forceChunk || serialized.length > CHUNK_CHAR_LIMIT)) {
6117
6128
  const mid = Math.floor(messagesToSummarize.length / 2);
6129
+ const halves = [
6130
+ messagesToSummarize.slice(0, mid),
6131
+ messagesToSummarize.slice(mid)
6132
+ ];
6118
6133
  log8.info("Chunking summary", {
6119
6134
  name,
6120
6135
  messageCount: messagesToSummarize.length,
6121
- serializedLength: serialized.length
6136
+ serializedLength: serialized.length,
6137
+ forced: !!opts.forceChunk
6122
6138
  });
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
6139
+ const results = await Promise.all(
6140
+ halves.map(
6141
+ (half, i) => generateSummary(
6142
+ apiConfig,
6143
+ `${name} [pt${i + 1}]`,
6144
+ compactionPrompt,
6145
+ half,
6146
+ model,
6147
+ // A split driven by size gets its children a retry of their own. A
6148
+ // split that IS the retry does not, or a model that will not
6149
+ // summarize at any size fans out call after call before giving up.
6150
+ { allowRetry: opts.forceChunk ? false : allowRetry }
6151
+ )
6141
6152
  )
6142
- ]);
6143
- const parts = [first, second].filter((p) => !!p);
6153
+ );
6154
+ const lost = results.some(
6155
+ (r, i) => r === null && serializeForSummary(halves[i]).trim()
6156
+ );
6157
+ if (lost) {
6158
+ return null;
6159
+ }
6160
+ const parts = results.filter((p) => p !== null);
6144
6161
  return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
6145
6162
  }
6146
6163
  log8.info("Generating summary", {
6147
6164
  name,
6148
6165
  messageCount: messagesToSummarize.length,
6149
- cacheReuse: !!mainSystem
6166
+ serializedLength: serialized.length
6150
6167
  });
6151
- let summaryText = "";
6152
- const useMainCache = !!mainSystem;
6153
- const system = useMainCache ? mainSystem : compactionPrompt;
6154
- const tools2 = [];
6155
- const userContent = useMainCache ? `${compactionPrompt}
6168
+ const summaryText = await runSummaryCall(
6169
+ apiConfig,
6170
+ name,
6171
+ compactionPrompt,
6172
+ serialized,
6173
+ model
6174
+ );
6175
+ if (summaryText === null) {
6176
+ return null;
6177
+ }
6178
+ if (summaryText.length >= MIN_SUMMARY_CHARS) {
6179
+ log8.info("Summary generated", { name, summaryLength: summaryText.length });
6180
+ return summaryText;
6181
+ }
6182
+ log8.warn("Summary too short to be real", {
6183
+ name,
6184
+ summaryLength: summaryText.length,
6185
+ minimum: MIN_SUMMARY_CHARS,
6186
+ serializedLength: serialized.length,
6187
+ retrying: splittable && allowRetry
6188
+ });
6189
+ if (!splittable || !allowRetry) {
6190
+ return null;
6191
+ }
6192
+ return generateSummary(
6193
+ apiConfig,
6194
+ name,
6195
+ compactionPrompt,
6196
+ messagesToSummarize,
6197
+ model,
6198
+ { forceChunk: true, allowRetry: false }
6199
+ );
6200
+ }
6201
+ async function runSummaryCall(apiConfig, name, compactionPrompt, serialized, model) {
6202
+ const userContent = `Conversation to summarize:
6156
6203
 
6157
- ---
6204
+ ${serialized}
6158
6205
 
6159
- Conversation to summarize:
6206
+ ---
6160
6207
 
6161
- ${serialized}` : serialized;
6208
+ Write the summary of the conversation above, following your instructions.`;
6209
+ let summaryText = "";
6162
6210
  const iterStart = Date.now();
6163
- for await (const event of streamChat({
6211
+ for await (const event of streamChatWithRetry({
6164
6212
  ...apiConfig,
6165
6213
  model,
6166
6214
  subAgentId: "conversationSummarizer",
6167
- system,
6215
+ system: compactionPrompt,
6168
6216
  messages: [{ role: "user", content: userContent }],
6169
- tools: tools2
6217
+ // Always empty. With a toolset available the model picks `tool_use` over
6218
+ // producing a summary, leaving summaryText empty.
6219
+ tools: []
6170
6220
  })) {
6171
6221
  if (event.type === "text") {
6172
6222
  summaryText += event.text;
@@ -6193,16 +6243,223 @@ ${serialized}` : serialized;
6193
6243
  log8.warn("Empty summary generated", { name });
6194
6244
  return null;
6195
6245
  }
6196
- log8.info("Summary generated", { name, summaryLength: summaryText.length });
6197
6246
  return summaryText.trim();
6198
6247
  }
6199
6248
 
6249
+ // src/session.ts
6250
+ import fs21 from "fs";
6251
+ import path10 from "path";
6252
+ var log9 = createLogger("session");
6253
+ var SESSION_FILE = ".remy-session.json";
6254
+ var ARCHIVE_DIR = ".logs/sessions";
6255
+ var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6256
+ var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6257
+ var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
6258
+ function loadSession(state) {
6259
+ pruneArchives();
6260
+ try {
6261
+ const raw = fs21.readFileSync(SESSION_FILE, "utf-8");
6262
+ const data = JSON.parse(raw);
6263
+ if (data.models && typeof data.models === "object") {
6264
+ state.models = data.models;
6265
+ }
6266
+ if (Array.isArray(data.messages) && data.messages.length > 0) {
6267
+ state.messages = sanitizeMessages(data.messages);
6268
+ log9.info("Session loaded", {
6269
+ messageCount: state.messages.length,
6270
+ ...state.models && { models: state.models }
6271
+ });
6272
+ return true;
6273
+ }
6274
+ } catch {
6275
+ }
6276
+ return false;
6277
+ }
6278
+ function capOversizedResults(msg) {
6279
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
6280
+ for (const block of msg.content) {
6281
+ if (block.type === "tool" && typeof block.result === "string") {
6282
+ block.result = capToolResult(block.result);
6283
+ }
6284
+ }
6285
+ } else if (msg.role === "user" && msg.toolCallId && typeof msg.content === "string") {
6286
+ msg.content = capToolResult(msg.content);
6287
+ }
6288
+ }
6289
+ function sanitizeMessages(messages) {
6290
+ const result = [];
6291
+ for (let i = 0; i < messages.length; i++) {
6292
+ const msg = messages[i];
6293
+ capOversizedResults(msg);
6294
+ result.push(msg);
6295
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
6296
+ continue;
6297
+ }
6298
+ const toolBlocks = msg.content.filter(
6299
+ (b) => b.type === "tool"
6300
+ );
6301
+ if (toolBlocks.length === 0) {
6302
+ continue;
6303
+ }
6304
+ const resultIds = /* @__PURE__ */ new Set();
6305
+ for (let j = i + 1; j < messages.length; j++) {
6306
+ const next = messages[j];
6307
+ if (next.role === "user" && next.toolCallId) {
6308
+ resultIds.add(next.toolCallId);
6309
+ } else {
6310
+ break;
6311
+ }
6312
+ }
6313
+ for (const tc of toolBlocks) {
6314
+ if (!resultIds.has(tc.id)) {
6315
+ result.push({
6316
+ role: "user",
6317
+ content: "Error: tool result lost (session recovered)",
6318
+ toolCallId: tc.id,
6319
+ isToolError: true
6320
+ });
6321
+ }
6322
+ }
6323
+ }
6324
+ return result;
6325
+ }
6326
+ function buildPayload(state) {
6327
+ const payload = { messages: state.messages };
6328
+ if (state.models && Object.keys(state.models).length > 0) {
6329
+ payload.models = state.models;
6330
+ }
6331
+ return payload;
6332
+ }
6333
+ function archiveMessages(messages, label, models) {
6334
+ fs21.mkdirSync(ARCHIVE_DIR, { recursive: true });
6335
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6336
+ let dest = path10.join(ARCHIVE_DIR, `${label}-${ts}.json`);
6337
+ let n = 1;
6338
+ while (fs21.existsSync(dest)) {
6339
+ dest = path10.join(ARCHIVE_DIR, `${label}-${ts}-${n++}.json`);
6340
+ }
6341
+ const payload = { messages };
6342
+ if (models && Object.keys(models).length > 0) {
6343
+ payload.models = models;
6344
+ }
6345
+ fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6346
+ log9.info("Session archived", { label, dest, messageCount: messages.length });
6347
+ pruneArchives();
6348
+ return dest;
6349
+ }
6350
+ function pruneArchives() {
6351
+ try {
6352
+ const entries = fs21.readdirSync(ARCHIVE_DIR).filter((name) => /^(cleared|rotated)-.*\.json$/.test(name));
6353
+ if (entries.length <= 1) {
6354
+ return;
6355
+ }
6356
+ const sortKey = (name) => name.replace(/^(cleared|rotated)-/, "");
6357
+ const archives = entries.map((name) => ({
6358
+ name,
6359
+ size: fs21.statSync(path10.join(ARCHIVE_DIR, name)).size
6360
+ })).sort((a, b) => sortKey(b.name).localeCompare(sortKey(a.name)));
6361
+ let kept = 0;
6362
+ let cut = archives.length;
6363
+ for (let i = 0; i < archives.length; i++) {
6364
+ if (i === 0 || kept + archives[i].size <= ARCHIVE_RETENTION_BYTES) {
6365
+ kept += archives[i].size;
6366
+ } else {
6367
+ cut = i;
6368
+ break;
6369
+ }
6370
+ }
6371
+ let removed = 0;
6372
+ let freed = 0;
6373
+ for (let i = cut; i < archives.length; i++) {
6374
+ try {
6375
+ fs21.unlinkSync(path10.join(ARCHIVE_DIR, archives[i].name));
6376
+ freed += archives[i].size;
6377
+ removed++;
6378
+ } catch {
6379
+ }
6380
+ }
6381
+ if (removed > 0) {
6382
+ log9.info("Session archives pruned", {
6383
+ removed,
6384
+ freedBytes: freed,
6385
+ keptBytes: kept
6386
+ });
6387
+ }
6388
+ } catch {
6389
+ }
6390
+ }
6391
+ function rotate(state) {
6392
+ const messages = state.messages;
6393
+ if (messages.length === 0) {
6394
+ return false;
6395
+ }
6396
+ let tailBytes = 0;
6397
+ let scrollbackStart = 0;
6398
+ for (let i = messages.length - 1; i >= 0; i--) {
6399
+ tailBytes += Buffer.byteLength(JSON.stringify(messages[i]), "utf-8") + 1;
6400
+ if (tailBytes >= RETAIN_TAIL_BYTES) {
6401
+ scrollbackStart = i;
6402
+ break;
6403
+ }
6404
+ }
6405
+ const checkpointIdx = findLastSummaryCheckpoint(messages, "conversation");
6406
+ let cut = checkpointIdx === -1 ? scrollbackStart : Math.min(scrollbackStart, checkpointIdx);
6407
+ cut = findSafeInsertionPoint(messages, cut);
6408
+ if (cut <= 0) {
6409
+ return false;
6410
+ }
6411
+ archiveMessages(messages.slice(0, cut), "rotated", state.models);
6412
+ state.messages = messages.slice(cut);
6413
+ log9.info("Session rotated", {
6414
+ archived: cut,
6415
+ retained: state.messages.length
6416
+ });
6417
+ return true;
6418
+ }
6419
+ function saveSession(state) {
6420
+ try {
6421
+ let serialized = JSON.stringify(buildPayload(state));
6422
+ if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6423
+ serialized = JSON.stringify(buildPayload(state));
6424
+ }
6425
+ fs21.writeFileSync(SESSION_FILE, serialized, "utf-8");
6426
+ log9.info("Session saved", { messageCount: state.messages.length });
6427
+ } catch (err) {
6428
+ log9.warn("Session save failed", { error: err.message });
6429
+ }
6430
+ }
6431
+ function clearSession(state) {
6432
+ try {
6433
+ if (state.messages.length > 0) {
6434
+ archiveMessages(state.messages, "cleared", state.models);
6435
+ }
6436
+ } catch (err) {
6437
+ log9.warn("Session archive on clear failed", { error: err.message });
6438
+ }
6439
+ state.messages = [];
6440
+ try {
6441
+ if (fs21.existsSync(SESSION_FILE)) {
6442
+ fs21.unlinkSync(SESSION_FILE);
6443
+ }
6444
+ } catch (err) {
6445
+ log9.warn("Session clear: could not remove live file", {
6446
+ error: err.message
6447
+ });
6448
+ }
6449
+ }
6450
+
6200
6451
  // src/compaction/trigger.ts
6201
- var log9 = createLogger("compaction:trigger");
6452
+ var log10 = createLogger("compaction:trigger");
6202
6453
  var pendingSummaries = [];
6203
6454
  var inflightCompaction = null;
6204
- function getPendingSummaries() {
6205
- return pendingSummaries.splice(0);
6455
+ function applyPendingSummaries(state) {
6456
+ const summaries = pendingSummaries.splice(0);
6457
+ if (summaries.length === 0) {
6458
+ return;
6459
+ }
6460
+ const idx = findSafeInsertionPoint(state.messages);
6461
+ state.messages.splice(idx, 0, ...summaries);
6462
+ saveSession(state);
6206
6463
  }
6207
6464
  var listener = null;
6208
6465
  function setCompactionListener(l) {
@@ -6214,22 +6471,18 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6214
6471
  }
6215
6472
  const { blocking = false, requestId, model } = opts;
6216
6473
  listener?.({ type: "started", blocking, requestId });
6217
- const system = buildSystemPrompt("onboardingFinished");
6218
- const tools2 = getToolDefinitions("onboardingFinished");
6219
6474
  inflightCompaction = compactConversation(
6220
6475
  state.messages,
6221
6476
  apiConfig,
6222
- system,
6223
- tools2,
6224
6477
  resolveModel("conversationSummarizer", state.models, model)
6225
6478
  ).then((summaries) => {
6226
6479
  pendingSummaries.push(...summaries);
6227
6480
  listener?.({ type: "complete", requestId });
6228
- log9.info("Compaction complete");
6481
+ log10.info("Compaction complete");
6229
6482
  }).catch((err) => {
6230
6483
  const message = err.message || "Compaction failed";
6231
6484
  listener?.({ type: "complete", error: message, requestId });
6232
- log9.error("Compaction failed", { error: message });
6485
+ log10.error("Compaction failed", { error: message });
6233
6486
  throw err;
6234
6487
  }).finally(() => {
6235
6488
  inflightCompaction = null;
@@ -6238,10 +6491,10 @@ function triggerCompaction(state, apiConfig, opts = {}) {
6238
6491
  }
6239
6492
 
6240
6493
  // src/brandExtraction/index.ts
6241
- import fs21 from "fs";
6242
- import path10 from "path";
6494
+ import fs22 from "fs";
6495
+ import path11 from "path";
6243
6496
  import { createHash } from "crypto";
6244
- var log10 = createLogger("brandExtraction");
6497
+ var log11 = createLogger("brandExtraction");
6245
6498
  var EXTRACT_PROMPT = readAsset("brandExtraction", "extract.md");
6246
6499
  var BRAND_FILE = ".remy-brand.json";
6247
6500
  var CACHE_FILE = ".remy-brand.cache.json";
@@ -6249,21 +6502,21 @@ async function runExtraction(apiConfig, model) {
6249
6502
  const inputHash = computeInputHash();
6250
6503
  const cached3 = readCache();
6251
6504
  if (cached3 && cached3.inputHash === inputHash) {
6252
- log10.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6505
+ log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
6253
6506
  return null;
6254
6507
  }
6255
- log10.info("Extracting brand", { inputHash });
6508
+ log11.info("Extracting brand", { inputHash });
6256
6509
  const brand = await extractBrand(apiConfig, model);
6257
6510
  if (!brand) {
6258
- log10.warn("Brand extraction failed \u2014 leaving cache untouched");
6511
+ log11.warn("Brand extraction failed \u2014 leaving cache untouched");
6259
6512
  return null;
6260
6513
  }
6261
6514
  persistBrand(brand, inputHash);
6262
- log10.info("Brand persisted", { inputHash });
6515
+ log11.info("Brand persisted", { inputHash });
6263
6516
  return brand;
6264
6517
  }
6265
6518
  function isBrandRelevant(filePath) {
6266
- if (filePath === path10.join("src", "app.md")) {
6519
+ if (filePath === path11.join("src", "app.md")) {
6267
6520
  return true;
6268
6521
  }
6269
6522
  const { type } = parseFrontmatter3(filePath);
@@ -6289,7 +6542,7 @@ function sha256(input) {
6289
6542
  }
6290
6543
  function readSafe(filePath) {
6291
6544
  try {
6292
- return fs21.readFileSync(filePath, "utf-8");
6545
+ return fs22.readFileSync(filePath, "utf-8");
6293
6546
  } catch {
6294
6547
  return "";
6295
6548
  }
@@ -6297,9 +6550,9 @@ function readSafe(filePath) {
6297
6550
  function walkMdFiles3(dir) {
6298
6551
  const results = [];
6299
6552
  try {
6300
- const entries = fs21.readdirSync(dir, { withFileTypes: true });
6553
+ const entries = fs22.readdirSync(dir, { withFileTypes: true });
6301
6554
  for (const entry of entries) {
6302
- const full = path10.join(dir, entry.name);
6555
+ const full = path11.join(dir, entry.name);
6303
6556
  if (entry.isDirectory()) {
6304
6557
  results.push(...walkMdFiles3(full));
6305
6558
  } else if (entry.name.endsWith(".md")) {
@@ -6312,7 +6565,7 @@ function walkMdFiles3(dir) {
6312
6565
  }
6313
6566
  function parseFrontmatter3(filePath) {
6314
6567
  try {
6315
- const content = fs21.readFileSync(filePath, "utf-8");
6568
+ const content = fs22.readFileSync(filePath, "utf-8");
6316
6569
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6317
6570
  if (!match) {
6318
6571
  return { type: "" };
@@ -6327,7 +6580,7 @@ function parseFrontmatter3(filePath) {
6327
6580
  async function extractBrand(apiConfig, model) {
6328
6581
  const corpus = buildCorpus();
6329
6582
  if (!corpus.trim()) {
6330
- log10.debug("No spec corpus \u2014 emitting empty brand");
6583
+ log11.debug("No spec corpus \u2014 emitting empty brand");
6331
6584
  return { version: 1 };
6332
6585
  }
6333
6586
  let responseText = "";
@@ -6358,17 +6611,17 @@ async function extractBrand(apiConfig, model) {
6358
6611
  toolNames: []
6359
6612
  });
6360
6613
  } else if (event.type === "error") {
6361
- log10.error("Brand extraction stream error", { error: event.error });
6614
+ log11.error("Brand extraction stream error", { error: event.error });
6362
6615
  return null;
6363
6616
  }
6364
6617
  }
6365
6618
  } catch (err) {
6366
- log10.error("Brand extraction threw", { error: err?.message });
6619
+ log11.error("Brand extraction threw", { error: err?.message });
6367
6620
  return null;
6368
6621
  }
6369
6622
  const parsed = parseJsonResponse(responseText);
6370
6623
  if (!parsed) {
6371
- log10.warn("Brand extraction returned unparseable JSON", {
6624
+ log11.warn("Brand extraction returned unparseable JSON", {
6372
6625
  preview: responseText.slice(0, 200)
6373
6626
  });
6374
6627
  return null;
@@ -6510,14 +6763,14 @@ function pickFont(raw) {
6510
6763
  }
6511
6764
  function persistBrand(brand, inputHash) {
6512
6765
  const tmp = `${BRAND_FILE}.tmp`;
6513
- fs21.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6514
- fs21.renameSync(tmp, BRAND_FILE);
6766
+ fs22.writeFileSync(tmp, JSON.stringify(brand, null, 2), "utf-8");
6767
+ fs22.renameSync(tmp, BRAND_FILE);
6515
6768
  const cache = { inputHash, generatedAt: Date.now() };
6516
- fs21.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6769
+ fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
6517
6770
  }
6518
6771
  function readCache() {
6519
6772
  try {
6520
- const raw = fs21.readFileSync(CACHE_FILE, "utf-8");
6773
+ const raw = fs22.readFileSync(CACHE_FILE, "utf-8");
6521
6774
  const parsed = JSON.parse(raw);
6522
6775
  if (parsed && typeof parsed.inputHash === "string" && typeof parsed.generatedAt === "number") {
6523
6776
  return parsed;
@@ -6529,7 +6782,7 @@ function readCache() {
6529
6782
  }
6530
6783
 
6531
6784
  // src/brandExtraction/trigger.ts
6532
- var log11 = createLogger("brandExtraction:trigger");
6785
+ var log12 = createLogger("brandExtraction:trigger");
6533
6786
  var inflight = false;
6534
6787
  var dirty = false;
6535
6788
  function triggerBrandExtraction(apiConfig, model) {
@@ -6539,7 +6792,7 @@ function triggerBrandExtraction(apiConfig, model) {
6539
6792
  }
6540
6793
  inflight = true;
6541
6794
  void runExtraction(apiConfig, model).catch((err) => {
6542
- log11.error("Brand extraction failed", { error: err?.message });
6795
+ log12.error("Brand extraction failed", { error: err?.message });
6543
6796
  }).finally(() => {
6544
6797
  inflight = false;
6545
6798
  if (dirty) {
@@ -6549,208 +6802,6 @@ function triggerBrandExtraction(apiConfig, model) {
6549
6802
  });
6550
6803
  }
6551
6804
 
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
6805
  // src/parsePartialJson.ts
6755
6806
  var PartialJSON = class extends Error {
6756
6807
  };
@@ -7950,7 +8001,9 @@ var HeadlessSession = class {
7950
8001
  const data = event.error ? { error: event.error } : {};
7951
8002
  this.emit("compaction_complete", data, event.requestId);
7952
8003
  this.sessionStats.compactionInProgress = false;
7953
- this.sessionStats.lastContextSize = 0;
8004
+ if (!event.error) {
8005
+ this.sessionStats.lastContextSize = 0;
8006
+ }
7954
8007
  this.sessionStats.messageCount = this.state.messages.length;
7955
8008
  this.persistStats();
7956
8009
  }
@@ -8084,20 +8137,10 @@ var HeadlessSession = class {
8084
8137
  requestId,
8085
8138
  model: this.opts.model
8086
8139
  });
8087
- this.applyPendingSummaries();
8140
+ applyPendingSummaries(this.state);
8088
8141
  } catch {
8089
8142
  }
8090
8143
  }
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
8144
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
8102
8145
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
8103
8146
  log15.info("Background complete", {
@@ -8426,7 +8469,7 @@ var HeadlessSession = class {
8426
8469
  error: err.message
8427
8470
  });
8428
8471
  }
8429
- this.applyPendingSummaries();
8472
+ applyPendingSummaries(this.state);
8430
8473
  this.applyPendingBlockUpdates();
8431
8474
  }
8432
8475
  async handleMessage(parsed, requestId) {
@@ -8730,7 +8773,7 @@ var HeadlessSession = class {
8730
8773
  model: this.opts.model
8731
8774
  });
8732
8775
  if (!this.running) {
8733
- this.applyPendingSummaries();
8776
+ applyPendingSummaries(this.state);
8734
8777
  }
8735
8778
  this.emit("completed", { success: true }, requestId);
8736
8779
  } catch (err) {