@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/compaction/conversation.md +1 -1
- package/dist/headless.d.ts +0 -2
- package/dist/headless.js +332 -289
- package/dist/index.js +1239 -1196
- package/dist/prompt/compiled/dev-and-deploy.md +4 -0
- package/package.json +1 -1
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,
|
|
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 =
|
|
6111
|
-
|
|
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
|
-
|
|
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
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
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
|
|
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
|
-
|
|
6166
|
+
serializedLength: serialized.length
|
|
6150
6167
|
});
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
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
|
-
|
|
6206
|
+
---
|
|
6160
6207
|
|
|
6161
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6452
|
+
var log10 = createLogger("compaction:trigger");
|
|
6202
6453
|
var pendingSummaries = [];
|
|
6203
6454
|
var inflightCompaction = null;
|
|
6204
|
-
function
|
|
6205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6242
|
-
import
|
|
6494
|
+
import fs22 from "fs";
|
|
6495
|
+
import path11 from "path";
|
|
6243
6496
|
import { createHash } from "crypto";
|
|
6244
|
-
var
|
|
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
|
-
|
|
6505
|
+
log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
|
|
6253
6506
|
return null;
|
|
6254
6507
|
}
|
|
6255
|
-
|
|
6508
|
+
log11.info("Extracting brand", { inputHash });
|
|
6256
6509
|
const brand = await extractBrand(apiConfig, model);
|
|
6257
6510
|
if (!brand) {
|
|
6258
|
-
|
|
6511
|
+
log11.warn("Brand extraction failed \u2014 leaving cache untouched");
|
|
6259
6512
|
return null;
|
|
6260
6513
|
}
|
|
6261
6514
|
persistBrand(brand, inputHash);
|
|
6262
|
-
|
|
6515
|
+
log11.info("Brand persisted", { inputHash });
|
|
6263
6516
|
return brand;
|
|
6264
6517
|
}
|
|
6265
6518
|
function isBrandRelevant(filePath) {
|
|
6266
|
-
if (filePath ===
|
|
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
|
|
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 =
|
|
6553
|
+
const entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
6301
6554
|
for (const entry of entries) {
|
|
6302
|
-
const full =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
6614
|
+
log11.error("Brand extraction stream error", { error: event.error });
|
|
6362
6615
|
return null;
|
|
6363
6616
|
}
|
|
6364
6617
|
}
|
|
6365
6618
|
} catch (err) {
|
|
6366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6514
|
-
|
|
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
|
-
|
|
6769
|
+
fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
6517
6770
|
}
|
|
6518
6771
|
function readCache() {
|
|
6519
6772
|
try {
|
|
6520
|
-
const raw =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
8776
|
+
applyPendingSummaries(this.state);
|
|
8734
8777
|
}
|
|
8735
8778
|
this.emit("completed", { success: true }, requestId);
|
|
8736
8779
|
} catch (err) {
|