clawmux 0.3.4 → 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 +232 -37
- package/dist/index.cjs +203 -29
- 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;
|
|
@@ -3778,7 +3949,10 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
|
|
|
3778
3949
|
const message = err instanceof Error ? err.message : String(err);
|
|
3779
3950
|
return jsonErrorResponse(`Upstream request failed: ${message}`, 502);
|
|
3780
3951
|
}
|
|
3781
|
-
|
|
3952
|
+
const lastUserMsg = [...parsed.messages].reverse().find((m) => m.role === "user");
|
|
3953
|
+
const msgText = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : Array.isArray(lastUserMsg?.content) ? lastUserMsg.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
|
|
3954
|
+
const preview = msgText.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3955
|
+
console.log(`[clawmux] [llm] ${decision.tier} → ${decision.model} | conf=${classification.confidence.toFixed(2)}${classification.reasoning ? ` | ${classification.reasoning}` : ""}${preview ? ` | "${preview}${msgText.length > 100 ? "…" : ""}"` : ""}`);
|
|
3782
3956
|
if (compressionMiddleware && upstreamResponse.ok) {
|
|
3783
3957
|
compressionMiddleware.afterResponse(parsed, adapter, baseUrl, authInfo);
|
|
3784
3958
|
}
|
|
@@ -3942,7 +4116,7 @@ function getLogDir() {
|
|
|
3942
4116
|
}
|
|
3943
4117
|
|
|
3944
4118
|
// src/cli.ts
|
|
3945
|
-
var VERSION2 = process.env.npm_package_version ?? "0.3.
|
|
4119
|
+
var VERSION2 = process.env.npm_package_version ?? "0.3.6";
|
|
3946
4120
|
var SERVICE_NAME = "clawmux";
|
|
3947
4121
|
var HELP = `Usage: clawmux <command>
|
|
3948
4122
|
|
|
@@ -3983,23 +4157,30 @@ async function fileExistsLocal(path) {
|
|
|
3983
4157
|
return false;
|
|
3984
4158
|
}
|
|
3985
4159
|
}
|
|
3986
|
-
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 {}
|
|
3987
4168
|
try {
|
|
3988
4169
|
import_node_child_process.execSync("which bun", { stdio: "pipe" });
|
|
3989
|
-
return "
|
|
4170
|
+
return "bun";
|
|
3990
4171
|
} catch {
|
|
3991
|
-
return "
|
|
4172
|
+
return "npm";
|
|
3992
4173
|
}
|
|
3993
4174
|
}
|
|
3994
4175
|
function resolveClawmuxBin() {
|
|
3995
4176
|
try {
|
|
3996
4177
|
const bin = import_node_child_process.execSync("which clawmux", { encoding: "utf-8" }).trim();
|
|
3997
4178
|
if (bin.includes("/tmp/") || bin.includes("bunx-") || bin.includes("npx-")) {
|
|
3998
|
-
return
|
|
4179
|
+
return detectInstallMethod() === "bun" ? "bunx clawmux" : "npx clawmux";
|
|
3999
4180
|
}
|
|
4000
4181
|
return bin;
|
|
4001
4182
|
} catch {
|
|
4002
|
-
return
|
|
4183
|
+
return detectInstallMethod() === "bun" ? "bunx clawmux" : "npx clawmux";
|
|
4003
4184
|
}
|
|
4004
4185
|
}
|
|
4005
4186
|
function getHomeDir3() {
|
|
@@ -4182,7 +4363,7 @@ async function checkForUpdate() {
|
|
|
4182
4363
|
} catch (_) {}
|
|
4183
4364
|
}
|
|
4184
4365
|
async function update() {
|
|
4185
|
-
const pm =
|
|
4366
|
+
const pm = detectInstallMethod();
|
|
4186
4367
|
console.log(`[clawmux] Checking for updates...`);
|
|
4187
4368
|
try {
|
|
4188
4369
|
const res = await fetch("https://registry.npmjs.org/clawmux/latest", {
|
|
@@ -4203,7 +4384,7 @@ async function update() {
|
|
|
4203
4384
|
return;
|
|
4204
4385
|
}
|
|
4205
4386
|
console.log(`[clawmux] Updating ${VERSION2} → ${latest}...`);
|
|
4206
|
-
if (pm === "
|
|
4387
|
+
if (pm === "bun") {
|
|
4207
4388
|
import_node_child_process.execSync("bun pm cache rm clawmux 2>/dev/null; bunx clawmux@latest version", { stdio: "inherit" });
|
|
4208
4389
|
} else {
|
|
4209
4390
|
import_node_child_process.execSync("npx clawmux@latest version", { stdio: "inherit" });
|
|
@@ -4359,7 +4540,21 @@ async function uninstall() {
|
|
|
4359
4540
|
console.log(`[info] Removed ${removed} ClawMux provider(s) from openclaw.json`);
|
|
4360
4541
|
}
|
|
4361
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
|
+
}
|
|
4362
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
|
+
}
|
|
4363
4558
|
}
|
|
4364
4559
|
var command = process.argv[2];
|
|
4365
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;
|
|
@@ -3801,7 +3972,10 @@ async function handleApiRequest(req, body, apiType, config, openclawConfig, auth
|
|
|
3801
3972
|
const message = err instanceof Error ? err.message : String(err);
|
|
3802
3973
|
return jsonErrorResponse(`Upstream request failed: ${message}`, 502);
|
|
3803
3974
|
}
|
|
3804
|
-
|
|
3975
|
+
const lastUserMsg = [...parsed.messages].reverse().find((m) => m.role === "user");
|
|
3976
|
+
const msgText = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : Array.isArray(lastUserMsg?.content) ? lastUserMsg.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
|
|
3977
|
+
const preview = msgText.replace(/\s+/g, " ").trim().slice(0, 100);
|
|
3978
|
+
console.log(`[clawmux] [llm] ${decision.tier} → ${decision.model} | conf=${classification.confidence.toFixed(2)}${classification.reasoning ? ` | ${classification.reasoning}` : ""}${preview ? ` | "${preview}${msgText.length > 100 ? "…" : ""}"` : ""}`);
|
|
3805
3979
|
if (compressionMiddleware && upstreamResponse.ok) {
|
|
3806
3980
|
compressionMiddleware.afterResponse(parsed, adapter, baseUrl, authInfo);
|
|
3807
3981
|
}
|