@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/compaction/conversation.md +1 -1
- package/dist/headless.d.ts +0 -2
- package/dist/headless.js +361 -302
- package/dist/index.js +1272 -1207
- package/dist/prompt/compiled/dev-and-deploy.md +4 -0
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
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 =
|
|
6111
|
-
|
|
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
|
-
|
|
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
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
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
|
|
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
|
-
|
|
6182
|
+
serializedLength: serialized.length
|
|
6150
6183
|
});
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
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
|
-
|
|
6222
|
+
---
|
|
6160
6223
|
|
|
6161
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6468
|
+
var log10 = createLogger("compaction:trigger");
|
|
6202
6469
|
var pendingSummaries = [];
|
|
6203
6470
|
var inflightCompaction = null;
|
|
6204
|
-
function
|
|
6205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6242
|
-
import
|
|
6510
|
+
import fs22 from "fs";
|
|
6511
|
+
import path12 from "path";
|
|
6243
6512
|
import { createHash } from "crypto";
|
|
6244
|
-
var
|
|
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
|
-
|
|
6521
|
+
log11.debug("Brand inputs unchanged \u2014 skipping extraction", { inputHash });
|
|
6253
6522
|
return null;
|
|
6254
6523
|
}
|
|
6255
|
-
|
|
6524
|
+
log11.info("Extracting brand", { inputHash });
|
|
6256
6525
|
const brand = await extractBrand(apiConfig, model);
|
|
6257
6526
|
if (!brand) {
|
|
6258
|
-
|
|
6527
|
+
log11.warn("Brand extraction failed \u2014 leaving cache untouched");
|
|
6259
6528
|
return null;
|
|
6260
6529
|
}
|
|
6261
6530
|
persistBrand(brand, inputHash);
|
|
6262
|
-
|
|
6531
|
+
log11.info("Brand persisted", { inputHash });
|
|
6263
6532
|
return brand;
|
|
6264
6533
|
}
|
|
6265
6534
|
function isBrandRelevant(filePath) {
|
|
6266
|
-
if (filePath ===
|
|
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
|
|
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 =
|
|
6569
|
+
const entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
6301
6570
|
for (const entry of entries) {
|
|
6302
|
-
const full =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
6630
|
+
log11.error("Brand extraction stream error", { error: event.error });
|
|
6362
6631
|
return null;
|
|
6363
6632
|
}
|
|
6364
6633
|
}
|
|
6365
6634
|
} catch (err) {
|
|
6366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6514
|
-
|
|
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
|
-
|
|
6785
|
+
fs22.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf-8");
|
|
6517
6786
|
}
|
|
6518
6787
|
function readCache() {
|
|
6519
6788
|
try {
|
|
6520
|
-
const raw =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
8792
|
+
applyPendingSummaries(this.state);
|
|
8734
8793
|
}
|
|
8735
8794
|
this.emit("completed", { success: true }, requestId);
|
|
8736
8795
|
} catch (err) {
|