clawmux 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.cjs +228 -36
- package/dist/index.cjs +199 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,8 +138,8 @@ Tests are co-located with source files as `*.test.ts`.
|
|
|
138
138
|
|
|
139
139
|
## Uninstall
|
|
140
140
|
|
|
141
|
+
Fetch the uninstallation guide and follow it:
|
|
142
|
+
|
|
141
143
|
```bash
|
|
142
|
-
|
|
144
|
+
curl -fsSL https://raw.githubusercontent.com/HyeokjaeLee/ClawMux/refs/heads/main/docs/guide/uninstallation.md
|
|
143
145
|
```
|
|
144
|
-
|
|
145
|
-
Stops the system service, removes the service file, and removes the `clawmux` provider from your OpenClaw config. A backup is created before any changes.
|
package/dist/cli.cjs
CHANGED
|
@@ -3313,39 +3313,155 @@ function createSessionStore(maxSessions = 500) {
|
|
|
3313
3313
|
};
|
|
3314
3314
|
}
|
|
3315
3315
|
|
|
3316
|
-
// src/compression/
|
|
3317
|
-
var
|
|
3318
|
-
|
|
3316
|
+
// src/compression/prompt.ts
|
|
3317
|
+
var MAX_MESSAGE_CHARS = 2000;
|
|
3318
|
+
var SUMMARY_TEMPLATE = `## Goal
|
|
3319
|
+
[What the user is trying to accomplish]
|
|
3320
|
+
|
|
3321
|
+
## Constraints & Preferences
|
|
3322
|
+
- [User's stated requirements and preferences, or "(none)"]
|
|
3323
|
+
|
|
3324
|
+
## Progress
|
|
3325
|
+
### Done
|
|
3326
|
+
- [x] [Completed items]
|
|
3327
|
+
|
|
3328
|
+
### In Progress
|
|
3329
|
+
- [ ] [Current work]
|
|
3330
|
+
|
|
3331
|
+
### Blocked
|
|
3332
|
+
- [Issues preventing progress, if any]
|
|
3333
|
+
|
|
3334
|
+
## Key Decisions
|
|
3335
|
+
- **[Decision]**: [Brief rationale]
|
|
3336
|
+
|
|
3337
|
+
## Next Steps
|
|
3338
|
+
1. [Ordered list of what should happen next]
|
|
3339
|
+
|
|
3340
|
+
## Critical Context
|
|
3341
|
+
- [Any data, examples, or references needed to continue, or "(none)"]`;
|
|
3342
|
+
var REQUIRED_SUMMARY_SECTIONS = [
|
|
3343
|
+
"## Goal",
|
|
3344
|
+
"## Constraints & Preferences",
|
|
3345
|
+
"## Progress",
|
|
3346
|
+
"## Key Decisions",
|
|
3347
|
+
"## Next Steps",
|
|
3348
|
+
"## Critical Context"
|
|
3349
|
+
];
|
|
3350
|
+
var SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, " + `then produce a structured summary following the exact format specified.
|
|
3351
|
+
|
|
3352
|
+
` + "Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
|
|
3353
|
+
var IDENTIFIER_INSTRUCTIONS = "Preserve all opaque identifiers exactly as written (no shortening or reconstruction), " + "including UUIDs, hashes, IDs, tokens, hostnames, IPs, ports, URLs, and file paths.";
|
|
3354
|
+
var LANGUAGE_INSTRUCTIONS = "Write the summary body in the primary language used in the conversation. " + "Keep the required summary structure and section headers unchanged. " + "Do not translate or alter code, file paths, identifiers, or error messages.";
|
|
3355
|
+
var INITIAL_SUMMARIZATION_INSTRUCTIONS = `Compress the conversation above into a structured summary that another AI assistant will use to continue the work.
|
|
3356
|
+
Preserve: file paths, tool call names/results, error messages, URLs.
|
|
3357
|
+
Skip: base64 images, thinking blocks, redundant greetings.
|
|
3358
|
+
|
|
3359
|
+
${IDENTIFIER_INSTRUCTIONS}
|
|
3360
|
+
${LANGUAGE_INSTRUCTIONS}
|
|
3361
|
+
|
|
3362
|
+
Use this EXACT format:
|
|
3363
|
+
|
|
3364
|
+
${SUMMARY_TEMPLATE}`;
|
|
3365
|
+
var UPDATE_SUMMARIZATION_INSTRUCTIONS = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
|
3366
|
+
|
|
3367
|
+
Update the existing structured summary with new information. RULES:
|
|
3368
|
+
- PRESERVE all existing information from the previous summary
|
|
3369
|
+
- ADD new progress, decisions, and context from the new messages
|
|
3370
|
+
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
|
|
3371
|
+
- UPDATE "Next Steps" based on what was accomplished
|
|
3372
|
+
- PRESERVE exact file paths, function names, and error messages
|
|
3373
|
+
- If something is no longer relevant, you may remove it
|
|
3374
|
+
|
|
3375
|
+
${IDENTIFIER_INSTRUCTIONS}
|
|
3376
|
+
${LANGUAGE_INSTRUCTIONS}
|
|
3377
|
+
|
|
3378
|
+
Use this EXACT format:
|
|
3379
|
+
|
|
3380
|
+
${SUMMARY_TEMPLATE}`;
|
|
3381
|
+
function isContentBlockArray(content) {
|
|
3382
|
+
return Array.isArray(content) && content.length > 0 && typeof content[0] === "object" && content[0] !== null && "type" in content[0];
|
|
3383
|
+
}
|
|
3384
|
+
function extractTextFromBlock(block) {
|
|
3385
|
+
if (block.type === "thinking")
|
|
3386
|
+
return "[thinking]";
|
|
3387
|
+
if (block.type === "image" || block.type === "image_url")
|
|
3388
|
+
return "[image]";
|
|
3389
|
+
if (block.type === "text" && block.text !== undefined)
|
|
3390
|
+
return block.text;
|
|
3391
|
+
if (block.type === "tool_result")
|
|
3392
|
+
return extractToolResultText(block);
|
|
3393
|
+
if (block.type === "tool_use")
|
|
3394
|
+
return `[tool: ${String(block.name ?? block.type)}]`;
|
|
3395
|
+
return "";
|
|
3396
|
+
}
|
|
3397
|
+
function extractToolResultText(block) {
|
|
3398
|
+
const { content } = block;
|
|
3319
3399
|
if (typeof content === "string")
|
|
3320
3400
|
return content;
|
|
3321
3401
|
if (Array.isArray(content)) {
|
|
3322
|
-
return content.filter((
|
|
3323
|
-
`);
|
|
3402
|
+
return content.filter((sub) => sub.type === "text" && sub.text !== undefined).map((sub) => sub.text).join(" ");
|
|
3324
3403
|
}
|
|
3325
|
-
return
|
|
3404
|
+
return "";
|
|
3326
3405
|
}
|
|
3327
|
-
function
|
|
3328
|
-
|
|
3329
|
-
|
|
3406
|
+
function extractContentText(content) {
|
|
3407
|
+
if (typeof content === "string")
|
|
3408
|
+
return content;
|
|
3409
|
+
if (isContentBlockArray(content)) {
|
|
3410
|
+
return content.map(extractTextFromBlock).filter(Boolean).join(" ");
|
|
3411
|
+
}
|
|
3412
|
+
return String(content);
|
|
3413
|
+
}
|
|
3414
|
+
function truncate(text) {
|
|
3415
|
+
if (text.length <= MAX_MESSAGE_CHARS)
|
|
3416
|
+
return text;
|
|
3417
|
+
return text.slice(0, MAX_MESSAGE_CHARS) + "... [truncated]";
|
|
3418
|
+
}
|
|
3419
|
+
function messagesToText(messages) {
|
|
3420
|
+
if (messages.length === 0)
|
|
3421
|
+
return "";
|
|
3422
|
+
return messages.map((msg) => {
|
|
3423
|
+
const label = msg.role === "user" ? "[User]" : msg.role === "assistant" ? "[Assistant]" : `[${msg.role}]`;
|
|
3424
|
+
const text = truncate(extractContentText(msg.content));
|
|
3425
|
+
return `${label}: ${text}`;
|
|
3426
|
+
}).join(`
|
|
3427
|
+
`) + `
|
|
3428
|
+
`;
|
|
3330
3429
|
}
|
|
3331
|
-
function buildCompressionPrompt(messages, targetTokens) {
|
|
3332
|
-
const conversationText = messages
|
|
3333
|
-
|
|
3334
|
-
|
|
3430
|
+
function buildCompressionPrompt(messages, targetTokens, previousSummary) {
|
|
3431
|
+
const conversationText = messagesToText(messages);
|
|
3432
|
+
const parts = [
|
|
3433
|
+
`<conversation>`,
|
|
3434
|
+
conversationText.trimEnd(),
|
|
3435
|
+
`</conversation>`,
|
|
3436
|
+
``,
|
|
3437
|
+
`Target approximately ${targetTokens} tokens.`,
|
|
3438
|
+
``
|
|
3439
|
+
];
|
|
3440
|
+
if (previousSummary) {
|
|
3441
|
+
parts.push(`<previous-summary>`, previousSummary, `</previous-summary>`, ``);
|
|
3442
|
+
parts.push(UPDATE_SUMMARIZATION_INSTRUCTIONS);
|
|
3443
|
+
} else {
|
|
3444
|
+
parts.push(INITIAL_SUMMARIZATION_INSTRUCTIONS);
|
|
3445
|
+
}
|
|
3335
3446
|
return [
|
|
3336
|
-
{
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3447
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
3448
|
+
{ role: "user", content: parts.join(`
|
|
3449
|
+
`) }
|
|
3450
|
+
];
|
|
3451
|
+
}
|
|
3452
|
+
function validateSummary(summary) {
|
|
3453
|
+
const missingSections = REQUIRED_SUMMARY_SECTIONS.filter((section) => !summary.includes(section));
|
|
3454
|
+
return { valid: missingSections.length === 0, missingSections };
|
|
3455
|
+
}
|
|
3456
|
+
function buildQualityFeedbackPrompt(summary, missingSections) {
|
|
3457
|
+
return [
|
|
3458
|
+
{ role: "assistant", content: summary },
|
|
3346
3459
|
{
|
|
3347
3460
|
role: "user",
|
|
3348
|
-
content:
|
|
3461
|
+
content: `The summary is missing required sections: ${missingSections.join(", ")}.
|
|
3462
|
+
` + `Please regenerate the summary including ALL required sections with this EXACT format:
|
|
3463
|
+
|
|
3464
|
+
` + SUMMARY_TEMPLATE
|
|
3349
3465
|
}
|
|
3350
3466
|
];
|
|
3351
3467
|
}
|
|
@@ -3353,7 +3469,7 @@ function buildCompressedMessages(summary) {
|
|
|
3353
3469
|
return [
|
|
3354
3470
|
{
|
|
3355
3471
|
role: "user",
|
|
3356
|
-
content:
|
|
3472
|
+
content: `[Summary of previous conversation]
|
|
3357
3473
|
${summary}`
|
|
3358
3474
|
},
|
|
3359
3475
|
{
|
|
@@ -3362,6 +3478,46 @@ ${summary}`
|
|
|
3362
3478
|
}
|
|
3363
3479
|
];
|
|
3364
3480
|
}
|
|
3481
|
+
|
|
3482
|
+
// src/compression/worker.ts
|
|
3483
|
+
var MAX_RETRIES = 3;
|
|
3484
|
+
var MAX_QUALITY_RETRIES = 3;
|
|
3485
|
+
var BASE_RETRY_DELAY_MS = 500;
|
|
3486
|
+
var MAX_RETRY_DELAY_MS = 5000;
|
|
3487
|
+
function retryDelayMs(attempt) {
|
|
3488
|
+
const jitter = Math.random() * 500;
|
|
3489
|
+
return Math.min(BASE_RETRY_DELAY_MS * 2 ** attempt + jitter, MAX_RETRY_DELAY_MS);
|
|
3490
|
+
}
|
|
3491
|
+
async function withRetry(fn, maxRetries) {
|
|
3492
|
+
let lastError = new Error("unknown");
|
|
3493
|
+
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
3494
|
+
try {
|
|
3495
|
+
return await fn();
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
3498
|
+
if (lastError.message === "compression_timeout")
|
|
3499
|
+
throw lastError;
|
|
3500
|
+
if (attempt < maxRetries - 1) {
|
|
3501
|
+
await new Promise((r) => setTimeout(r, retryDelayMs(attempt)));
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
throw lastError;
|
|
3506
|
+
}
|
|
3507
|
+
var SUMMARY_PREFIX = "[Summary of previous conversation]";
|
|
3508
|
+
function messageContentToString2(content) {
|
|
3509
|
+
if (typeof content === "string")
|
|
3510
|
+
return content;
|
|
3511
|
+
if (Array.isArray(content)) {
|
|
3512
|
+
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
|
|
3513
|
+
`);
|
|
3514
|
+
}
|
|
3515
|
+
return JSON.stringify(content);
|
|
3516
|
+
}
|
|
3517
|
+
function estimateMessageTokens(msg) {
|
|
3518
|
+
const MESSAGE_OVERHEAD2 = 4;
|
|
3519
|
+
return MESSAGE_OVERHEAD2 + estimateTokens(messageContentToString2(msg.content));
|
|
3520
|
+
}
|
|
3365
3521
|
function truncateToFit(messages, targetTokens) {
|
|
3366
3522
|
const result = [];
|
|
3367
3523
|
let usedTokens = 0;
|
|
@@ -3393,6 +3549,21 @@ function createCompressionWorker(config) {
|
|
|
3393
3549
|
const thresholdTokens = config.threshold * config.contextWindow;
|
|
3394
3550
|
return session.tokenCount >= thresholdTokens && session.compressionState === "idle";
|
|
3395
3551
|
}
|
|
3552
|
+
async function summarizeWithQualityGuard(messages, targetTokens, previousSummary, makeApiCall) {
|
|
3553
|
+
const initialPrompt = buildCompressionPrompt(messages, targetTokens, previousSummary);
|
|
3554
|
+
let summaryText = await makeApiCall(config.compressionModel, initialPrompt);
|
|
3555
|
+
for (let attempt = 0;attempt < MAX_QUALITY_RETRIES - 1; attempt++) {
|
|
3556
|
+
const { valid, missingSections } = validateSummary(summaryText);
|
|
3557
|
+
if (valid)
|
|
3558
|
+
break;
|
|
3559
|
+
const feedbackMessages = [
|
|
3560
|
+
...initialPrompt,
|
|
3561
|
+
...buildQualityFeedbackPrompt(summaryText, missingSections)
|
|
3562
|
+
];
|
|
3563
|
+
summaryText = await makeApiCall(config.compressionModel, feedbackMessages);
|
|
3564
|
+
}
|
|
3565
|
+
return summaryText;
|
|
3566
|
+
}
|
|
3396
3567
|
function triggerCompression(session, sessionStore, makeApiCall) {
|
|
3397
3568
|
if (activeJobs >= config.maxConcurrent)
|
|
3398
3569
|
return;
|
|
@@ -3404,11 +3575,11 @@ function createCompressionWorker(config) {
|
|
|
3404
3575
|
});
|
|
3405
3576
|
activeJobs++;
|
|
3406
3577
|
const targetTokens = config.targetRatio * config.contextWindow;
|
|
3407
|
-
const promptMessages = buildCompressionPrompt(session.messages, targetTokens);
|
|
3408
3578
|
const sessionId = session.id;
|
|
3409
3579
|
const originalMessages = [...session.messages];
|
|
3580
|
+
const previousSummary = session.compressedSummary;
|
|
3410
3581
|
const jobPromise = Promise.race([
|
|
3411
|
-
|
|
3582
|
+
withRetry(() => summarizeWithQualityGuard(originalMessages, targetTokens, previousSummary, makeApiCall), MAX_RETRIES),
|
|
3412
3583
|
new Promise((_resolve, reject) => {
|
|
3413
3584
|
setTimeout(() => reject(new Error("compression_timeout")), config.timeoutMs);
|
|
3414
3585
|
})
|
|
@@ -3545,7 +3716,7 @@ function createCompressionMiddleware(config) {
|
|
|
3545
3716
|
compressionModel: config.compressionModel,
|
|
3546
3717
|
contextWindow,
|
|
3547
3718
|
maxConcurrent: 2,
|
|
3548
|
-
timeoutMs:
|
|
3719
|
+
timeoutMs: 900000
|
|
3549
3720
|
});
|
|
3550
3721
|
function beforeForward(parsed, adapter) {
|
|
3551
3722
|
const messages = parsed.messages;
|
|
@@ -3945,7 +4116,7 @@ function getLogDir() {
|
|
|
3945
4116
|
}
|
|
3946
4117
|
|
|
3947
4118
|
// src/cli.ts
|
|
3948
|
-
var VERSION2 = process.env.npm_package_version ?? "0.3.
|
|
4119
|
+
var VERSION2 = process.env.npm_package_version ?? "0.3.6";
|
|
3949
4120
|
var SERVICE_NAME = "clawmux";
|
|
3950
4121
|
var HELP = `Usage: clawmux <command>
|
|
3951
4122
|
|
|
@@ -3986,23 +4157,30 @@ async function fileExistsLocal(path) {
|
|
|
3986
4157
|
return false;
|
|
3987
4158
|
}
|
|
3988
4159
|
}
|
|
3989
|
-
function
|
|
4160
|
+
function detectInstallMethod() {
|
|
4161
|
+
try {
|
|
4162
|
+
const bin = import_node_child_process.execSync("which clawmux", { encoding: "utf-8" }).trim();
|
|
4163
|
+
if (bin.includes(".bun/"))
|
|
4164
|
+
return "bun";
|
|
4165
|
+
if (bin.includes(".npm/") || bin.includes("lib/node_modules/"))
|
|
4166
|
+
return "npm";
|
|
4167
|
+
} catch {}
|
|
3990
4168
|
try {
|
|
3991
4169
|
import_node_child_process.execSync("which bun", { stdio: "pipe" });
|
|
3992
|
-
return "
|
|
4170
|
+
return "bun";
|
|
3993
4171
|
} catch {
|
|
3994
|
-
return "
|
|
4172
|
+
return "npm";
|
|
3995
4173
|
}
|
|
3996
4174
|
}
|
|
3997
4175
|
function resolveClawmuxBin() {
|
|
3998
4176
|
try {
|
|
3999
4177
|
const bin = import_node_child_process.execSync("which clawmux", { encoding: "utf-8" }).trim();
|
|
4000
4178
|
if (bin.includes("/tmp/") || bin.includes("bunx-") || bin.includes("npx-")) {
|
|
4001
|
-
return
|
|
4179
|
+
return detectInstallMethod() === "bun" ? "bunx clawmux" : "npx clawmux";
|
|
4002
4180
|
}
|
|
4003
4181
|
return bin;
|
|
4004
4182
|
} catch {
|
|
4005
|
-
return
|
|
4183
|
+
return detectInstallMethod() === "bun" ? "bunx clawmux" : "npx clawmux";
|
|
4006
4184
|
}
|
|
4007
4185
|
}
|
|
4008
4186
|
function getHomeDir3() {
|
|
@@ -4185,7 +4363,7 @@ async function checkForUpdate() {
|
|
|
4185
4363
|
} catch (_) {}
|
|
4186
4364
|
}
|
|
4187
4365
|
async function update() {
|
|
4188
|
-
const pm =
|
|
4366
|
+
const pm = detectInstallMethod();
|
|
4189
4367
|
console.log(`[clawmux] Checking for updates...`);
|
|
4190
4368
|
try {
|
|
4191
4369
|
const res = await fetch("https://registry.npmjs.org/clawmux/latest", {
|
|
@@ -4206,7 +4384,7 @@ async function update() {
|
|
|
4206
4384
|
return;
|
|
4207
4385
|
}
|
|
4208
4386
|
console.log(`[clawmux] Updating ${VERSION2} → ${latest}...`);
|
|
4209
|
-
if (pm === "
|
|
4387
|
+
if (pm === "bun") {
|
|
4210
4388
|
import_node_child_process.execSync("bun pm cache rm clawmux 2>/dev/null; bunx clawmux@latest version", { stdio: "inherit" });
|
|
4211
4389
|
} else {
|
|
4212
4390
|
import_node_child_process.execSync("npx clawmux@latest version", { stdio: "inherit" });
|
|
@@ -4362,7 +4540,21 @@ async function uninstall() {
|
|
|
4362
4540
|
console.log(`[info] Removed ${removed} ClawMux provider(s) from openclaw.json`);
|
|
4363
4541
|
}
|
|
4364
4542
|
}
|
|
4543
|
+
const clawmuxDir = import_node_path5.join(homeDir, ".openclaw", "clawmux");
|
|
4544
|
+
if (await fileExistsLocal(clawmuxDir)) {
|
|
4545
|
+
const { rm } = await import("node:fs/promises");
|
|
4546
|
+
await rm(clawmuxDir, { recursive: true, force: true });
|
|
4547
|
+
console.log("[info] Removed ~/.openclaw/clawmux (config and logs)");
|
|
4548
|
+
}
|
|
4365
4549
|
console.log("[info] ClawMux uninstalled");
|
|
4550
|
+
const pm = detectInstallMethod();
|
|
4551
|
+
console.log(`
|
|
4552
|
+
To remove the clawmux command, run:`);
|
|
4553
|
+
if (pm === "bun") {
|
|
4554
|
+
console.log(" bun remove -g clawmux");
|
|
4555
|
+
} else {
|
|
4556
|
+
console.log(" npm uninstall -g clawmux");
|
|
4557
|
+
}
|
|
4366
4558
|
}
|
|
4367
4559
|
var command = process.argv[2];
|
|
4368
4560
|
switch (command) {
|
package/dist/index.cjs
CHANGED
|
@@ -3336,39 +3336,155 @@ function createSessionStore(maxSessions = 500) {
|
|
|
3336
3336
|
};
|
|
3337
3337
|
}
|
|
3338
3338
|
|
|
3339
|
-
// src/compression/
|
|
3340
|
-
var
|
|
3341
|
-
|
|
3339
|
+
// src/compression/prompt.ts
|
|
3340
|
+
var MAX_MESSAGE_CHARS = 2000;
|
|
3341
|
+
var SUMMARY_TEMPLATE = `## Goal
|
|
3342
|
+
[What the user is trying to accomplish]
|
|
3343
|
+
|
|
3344
|
+
## Constraints & Preferences
|
|
3345
|
+
- [User's stated requirements and preferences, or "(none)"]
|
|
3346
|
+
|
|
3347
|
+
## Progress
|
|
3348
|
+
### Done
|
|
3349
|
+
- [x] [Completed items]
|
|
3350
|
+
|
|
3351
|
+
### In Progress
|
|
3352
|
+
- [ ] [Current work]
|
|
3353
|
+
|
|
3354
|
+
### Blocked
|
|
3355
|
+
- [Issues preventing progress, if any]
|
|
3356
|
+
|
|
3357
|
+
## Key Decisions
|
|
3358
|
+
- **[Decision]**: [Brief rationale]
|
|
3359
|
+
|
|
3360
|
+
## Next Steps
|
|
3361
|
+
1. [Ordered list of what should happen next]
|
|
3362
|
+
|
|
3363
|
+
## Critical Context
|
|
3364
|
+
- [Any data, examples, or references needed to continue, or "(none)"]`;
|
|
3365
|
+
var REQUIRED_SUMMARY_SECTIONS = [
|
|
3366
|
+
"## Goal",
|
|
3367
|
+
"## Constraints & Preferences",
|
|
3368
|
+
"## Progress",
|
|
3369
|
+
"## Key Decisions",
|
|
3370
|
+
"## Next Steps",
|
|
3371
|
+
"## Critical Context"
|
|
3372
|
+
];
|
|
3373
|
+
var SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, " + `then produce a structured summary following the exact format specified.
|
|
3374
|
+
|
|
3375
|
+
` + "Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
|
|
3376
|
+
var IDENTIFIER_INSTRUCTIONS = "Preserve all opaque identifiers exactly as written (no shortening or reconstruction), " + "including UUIDs, hashes, IDs, tokens, hostnames, IPs, ports, URLs, and file paths.";
|
|
3377
|
+
var LANGUAGE_INSTRUCTIONS = "Write the summary body in the primary language used in the conversation. " + "Keep the required summary structure and section headers unchanged. " + "Do not translate or alter code, file paths, identifiers, or error messages.";
|
|
3378
|
+
var INITIAL_SUMMARIZATION_INSTRUCTIONS = `Compress the conversation above into a structured summary that another AI assistant will use to continue the work.
|
|
3379
|
+
Preserve: file paths, tool call names/results, error messages, URLs.
|
|
3380
|
+
Skip: base64 images, thinking blocks, redundant greetings.
|
|
3381
|
+
|
|
3382
|
+
${IDENTIFIER_INSTRUCTIONS}
|
|
3383
|
+
${LANGUAGE_INSTRUCTIONS}
|
|
3384
|
+
|
|
3385
|
+
Use this EXACT format:
|
|
3386
|
+
|
|
3387
|
+
${SUMMARY_TEMPLATE}`;
|
|
3388
|
+
var UPDATE_SUMMARIZATION_INSTRUCTIONS = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
|
3389
|
+
|
|
3390
|
+
Update the existing structured summary with new information. RULES:
|
|
3391
|
+
- PRESERVE all existing information from the previous summary
|
|
3392
|
+
- ADD new progress, decisions, and context from the new messages
|
|
3393
|
+
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
|
|
3394
|
+
- UPDATE "Next Steps" based on what was accomplished
|
|
3395
|
+
- PRESERVE exact file paths, function names, and error messages
|
|
3396
|
+
- If something is no longer relevant, you may remove it
|
|
3397
|
+
|
|
3398
|
+
${IDENTIFIER_INSTRUCTIONS}
|
|
3399
|
+
${LANGUAGE_INSTRUCTIONS}
|
|
3400
|
+
|
|
3401
|
+
Use this EXACT format:
|
|
3402
|
+
|
|
3403
|
+
${SUMMARY_TEMPLATE}`;
|
|
3404
|
+
function isContentBlockArray(content) {
|
|
3405
|
+
return Array.isArray(content) && content.length > 0 && typeof content[0] === "object" && content[0] !== null && "type" in content[0];
|
|
3406
|
+
}
|
|
3407
|
+
function extractTextFromBlock(block) {
|
|
3408
|
+
if (block.type === "thinking")
|
|
3409
|
+
return "[thinking]";
|
|
3410
|
+
if (block.type === "image" || block.type === "image_url")
|
|
3411
|
+
return "[image]";
|
|
3412
|
+
if (block.type === "text" && block.text !== undefined)
|
|
3413
|
+
return block.text;
|
|
3414
|
+
if (block.type === "tool_result")
|
|
3415
|
+
return extractToolResultText(block);
|
|
3416
|
+
if (block.type === "tool_use")
|
|
3417
|
+
return `[tool: ${String(block.name ?? block.type)}]`;
|
|
3418
|
+
return "";
|
|
3419
|
+
}
|
|
3420
|
+
function extractToolResultText(block) {
|
|
3421
|
+
const { content } = block;
|
|
3342
3422
|
if (typeof content === "string")
|
|
3343
3423
|
return content;
|
|
3344
3424
|
if (Array.isArray(content)) {
|
|
3345
|
-
return content.filter((
|
|
3346
|
-
`);
|
|
3425
|
+
return content.filter((sub) => sub.type === "text" && sub.text !== undefined).map((sub) => sub.text).join(" ");
|
|
3347
3426
|
}
|
|
3348
|
-
return
|
|
3427
|
+
return "";
|
|
3349
3428
|
}
|
|
3350
|
-
function
|
|
3351
|
-
|
|
3352
|
-
|
|
3429
|
+
function extractContentText(content) {
|
|
3430
|
+
if (typeof content === "string")
|
|
3431
|
+
return content;
|
|
3432
|
+
if (isContentBlockArray(content)) {
|
|
3433
|
+
return content.map(extractTextFromBlock).filter(Boolean).join(" ");
|
|
3434
|
+
}
|
|
3435
|
+
return String(content);
|
|
3436
|
+
}
|
|
3437
|
+
function truncate(text) {
|
|
3438
|
+
if (text.length <= MAX_MESSAGE_CHARS)
|
|
3439
|
+
return text;
|
|
3440
|
+
return text.slice(0, MAX_MESSAGE_CHARS) + "... [truncated]";
|
|
3441
|
+
}
|
|
3442
|
+
function messagesToText(messages) {
|
|
3443
|
+
if (messages.length === 0)
|
|
3444
|
+
return "";
|
|
3445
|
+
return messages.map((msg) => {
|
|
3446
|
+
const label = msg.role === "user" ? "[User]" : msg.role === "assistant" ? "[Assistant]" : `[${msg.role}]`;
|
|
3447
|
+
const text = truncate(extractContentText(msg.content));
|
|
3448
|
+
return `${label}: ${text}`;
|
|
3449
|
+
}).join(`
|
|
3450
|
+
`) + `
|
|
3451
|
+
`;
|
|
3353
3452
|
}
|
|
3354
|
-
function buildCompressionPrompt(messages, targetTokens) {
|
|
3355
|
-
const conversationText = messages
|
|
3356
|
-
|
|
3357
|
-
|
|
3453
|
+
function buildCompressionPrompt(messages, targetTokens, previousSummary) {
|
|
3454
|
+
const conversationText = messagesToText(messages);
|
|
3455
|
+
const parts = [
|
|
3456
|
+
`<conversation>`,
|
|
3457
|
+
conversationText.trimEnd(),
|
|
3458
|
+
`</conversation>`,
|
|
3459
|
+
``,
|
|
3460
|
+
`Target approximately ${targetTokens} tokens.`,
|
|
3461
|
+
``
|
|
3462
|
+
];
|
|
3463
|
+
if (previousSummary) {
|
|
3464
|
+
parts.push(`<previous-summary>`, previousSummary, `</previous-summary>`, ``);
|
|
3465
|
+
parts.push(UPDATE_SUMMARIZATION_INSTRUCTIONS);
|
|
3466
|
+
} else {
|
|
3467
|
+
parts.push(INITIAL_SUMMARIZATION_INSTRUCTIONS);
|
|
3468
|
+
}
|
|
3358
3469
|
return [
|
|
3359
|
-
{
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3470
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
3471
|
+
{ role: "user", content: parts.join(`
|
|
3472
|
+
`) }
|
|
3473
|
+
];
|
|
3474
|
+
}
|
|
3475
|
+
function validateSummary(summary) {
|
|
3476
|
+
const missingSections = REQUIRED_SUMMARY_SECTIONS.filter((section) => !summary.includes(section));
|
|
3477
|
+
return { valid: missingSections.length === 0, missingSections };
|
|
3478
|
+
}
|
|
3479
|
+
function buildQualityFeedbackPrompt(summary, missingSections) {
|
|
3480
|
+
return [
|
|
3481
|
+
{ role: "assistant", content: summary },
|
|
3369
3482
|
{
|
|
3370
3483
|
role: "user",
|
|
3371
|
-
content:
|
|
3484
|
+
content: `The summary is missing required sections: ${missingSections.join(", ")}.
|
|
3485
|
+
` + `Please regenerate the summary including ALL required sections with this EXACT format:
|
|
3486
|
+
|
|
3487
|
+
` + SUMMARY_TEMPLATE
|
|
3372
3488
|
}
|
|
3373
3489
|
];
|
|
3374
3490
|
}
|
|
@@ -3376,7 +3492,7 @@ function buildCompressedMessages(summary) {
|
|
|
3376
3492
|
return [
|
|
3377
3493
|
{
|
|
3378
3494
|
role: "user",
|
|
3379
|
-
content:
|
|
3495
|
+
content: `[Summary of previous conversation]
|
|
3380
3496
|
${summary}`
|
|
3381
3497
|
},
|
|
3382
3498
|
{
|
|
@@ -3385,6 +3501,46 @@ ${summary}`
|
|
|
3385
3501
|
}
|
|
3386
3502
|
];
|
|
3387
3503
|
}
|
|
3504
|
+
|
|
3505
|
+
// src/compression/worker.ts
|
|
3506
|
+
var MAX_RETRIES = 3;
|
|
3507
|
+
var MAX_QUALITY_RETRIES = 3;
|
|
3508
|
+
var BASE_RETRY_DELAY_MS = 500;
|
|
3509
|
+
var MAX_RETRY_DELAY_MS = 5000;
|
|
3510
|
+
function retryDelayMs(attempt) {
|
|
3511
|
+
const jitter = Math.random() * 500;
|
|
3512
|
+
return Math.min(BASE_RETRY_DELAY_MS * 2 ** attempt + jitter, MAX_RETRY_DELAY_MS);
|
|
3513
|
+
}
|
|
3514
|
+
async function withRetry(fn, maxRetries) {
|
|
3515
|
+
let lastError = new Error("unknown");
|
|
3516
|
+
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
3517
|
+
try {
|
|
3518
|
+
return await fn();
|
|
3519
|
+
} catch (err) {
|
|
3520
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
3521
|
+
if (lastError.message === "compression_timeout")
|
|
3522
|
+
throw lastError;
|
|
3523
|
+
if (attempt < maxRetries - 1) {
|
|
3524
|
+
await new Promise((r) => setTimeout(r, retryDelayMs(attempt)));
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
throw lastError;
|
|
3529
|
+
}
|
|
3530
|
+
var SUMMARY_PREFIX = "[Summary of previous conversation]";
|
|
3531
|
+
function messageContentToString2(content) {
|
|
3532
|
+
if (typeof content === "string")
|
|
3533
|
+
return content;
|
|
3534
|
+
if (Array.isArray(content)) {
|
|
3535
|
+
return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
|
|
3536
|
+
`);
|
|
3537
|
+
}
|
|
3538
|
+
return JSON.stringify(content);
|
|
3539
|
+
}
|
|
3540
|
+
function estimateMessageTokens(msg) {
|
|
3541
|
+
const MESSAGE_OVERHEAD2 = 4;
|
|
3542
|
+
return MESSAGE_OVERHEAD2 + estimateTokens(messageContentToString2(msg.content));
|
|
3543
|
+
}
|
|
3388
3544
|
function truncateToFit(messages, targetTokens) {
|
|
3389
3545
|
const result = [];
|
|
3390
3546
|
let usedTokens = 0;
|
|
@@ -3416,6 +3572,21 @@ function createCompressionWorker(config) {
|
|
|
3416
3572
|
const thresholdTokens = config.threshold * config.contextWindow;
|
|
3417
3573
|
return session.tokenCount >= thresholdTokens && session.compressionState === "idle";
|
|
3418
3574
|
}
|
|
3575
|
+
async function summarizeWithQualityGuard(messages, targetTokens, previousSummary, makeApiCall) {
|
|
3576
|
+
const initialPrompt = buildCompressionPrompt(messages, targetTokens, previousSummary);
|
|
3577
|
+
let summaryText = await makeApiCall(config.compressionModel, initialPrompt);
|
|
3578
|
+
for (let attempt = 0;attempt < MAX_QUALITY_RETRIES - 1; attempt++) {
|
|
3579
|
+
const { valid, missingSections } = validateSummary(summaryText);
|
|
3580
|
+
if (valid)
|
|
3581
|
+
break;
|
|
3582
|
+
const feedbackMessages = [
|
|
3583
|
+
...initialPrompt,
|
|
3584
|
+
...buildQualityFeedbackPrompt(summaryText, missingSections)
|
|
3585
|
+
];
|
|
3586
|
+
summaryText = await makeApiCall(config.compressionModel, feedbackMessages);
|
|
3587
|
+
}
|
|
3588
|
+
return summaryText;
|
|
3589
|
+
}
|
|
3419
3590
|
function triggerCompression(session, sessionStore, makeApiCall) {
|
|
3420
3591
|
if (activeJobs >= config.maxConcurrent)
|
|
3421
3592
|
return;
|
|
@@ -3427,11 +3598,11 @@ function createCompressionWorker(config) {
|
|
|
3427
3598
|
});
|
|
3428
3599
|
activeJobs++;
|
|
3429
3600
|
const targetTokens = config.targetRatio * config.contextWindow;
|
|
3430
|
-
const promptMessages = buildCompressionPrompt(session.messages, targetTokens);
|
|
3431
3601
|
const sessionId = session.id;
|
|
3432
3602
|
const originalMessages = [...session.messages];
|
|
3603
|
+
const previousSummary = session.compressedSummary;
|
|
3433
3604
|
const jobPromise = Promise.race([
|
|
3434
|
-
|
|
3605
|
+
withRetry(() => summarizeWithQualityGuard(originalMessages, targetTokens, previousSummary, makeApiCall), MAX_RETRIES),
|
|
3435
3606
|
new Promise((_resolve, reject) => {
|
|
3436
3607
|
setTimeout(() => reject(new Error("compression_timeout")), config.timeoutMs);
|
|
3437
3608
|
})
|
|
@@ -3568,7 +3739,7 @@ function createCompressionMiddleware(config) {
|
|
|
3568
3739
|
compressionModel: config.compressionModel,
|
|
3569
3740
|
contextWindow,
|
|
3570
3741
|
maxConcurrent: 2,
|
|
3571
|
-
timeoutMs:
|
|
3742
|
+
timeoutMs: 900000
|
|
3572
3743
|
});
|
|
3573
3744
|
function beforeForward(parsed, adapter) {
|
|
3574
3745
|
const messages = parsed.messages;
|