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 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
- clawmux uninstall
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/worker.ts
3317
- var SUMMARY_PREFIX = "[Summary of previous conversation]";
3318
- function messageContentToString2(content) {
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((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
3323
- `);
3402
+ return content.filter((sub) => sub.type === "text" && sub.text !== undefined).map((sub) => sub.text).join(" ");
3324
3403
  }
3325
- return JSON.stringify(content);
3404
+ return "";
3326
3405
  }
3327
- function estimateMessageTokens(msg) {
3328
- const MESSAGE_OVERHEAD2 = 4;
3329
- return MESSAGE_OVERHEAD2 + estimateTokens(messageContentToString2(msg.content));
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.map((m) => `${m.role}: ${messageContentToString2(m.content)}`).join(`
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
- role: "system",
3338
- content: [
3339
- "You are a conversation summarizer. Produce a concise summary of the conversation below.",
3340
- `Target length: approximately ${targetTokens} tokens.`,
3341
- "Preserve: key decisions, code snippets, technical details, and action items.",
3342
- "Format: plain text paragraphs. Start with the most important context."
3343
- ].join(`
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: conversationText
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: `${SUMMARY_PREFIX}
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
- makeApiCall(config.compressionModel, promptMessages),
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: 60000
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
- console.log(`[clawmux] [llm] ${decision.tier} → ${decision.model} | conf=${classification.confidence.toFixed(2)}${classification.reasoning ? ` | ${classification.reasoning}` : ""}`);
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.4";
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 detectPackageManager() {
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 "bunx";
4170
+ return "bun";
3990
4171
  } catch {
3991
- return "npx";
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 detectPackageManager() === "bunx" ? "bunx clawmux" : "npx clawmux";
4179
+ return detectInstallMethod() === "bun" ? "bunx clawmux" : "npx clawmux";
3999
4180
  }
4000
4181
  return bin;
4001
4182
  } catch {
4002
- return detectPackageManager() === "bunx" ? "bunx clawmux" : "npx clawmux";
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 = detectPackageManager();
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 === "bunx") {
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/worker.ts
3340
- var SUMMARY_PREFIX = "[Summary of previous conversation]";
3341
- function messageContentToString2(content) {
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((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(`
3346
- `);
3425
+ return content.filter((sub) => sub.type === "text" && sub.text !== undefined).map((sub) => sub.text).join(" ");
3347
3426
  }
3348
- return JSON.stringify(content);
3427
+ return "";
3349
3428
  }
3350
- function estimateMessageTokens(msg) {
3351
- const MESSAGE_OVERHEAD2 = 4;
3352
- return MESSAGE_OVERHEAD2 + estimateTokens(messageContentToString2(msg.content));
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.map((m) => `${m.role}: ${messageContentToString2(m.content)}`).join(`
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
- role: "system",
3361
- content: [
3362
- "You are a conversation summarizer. Produce a concise summary of the conversation below.",
3363
- `Target length: approximately ${targetTokens} tokens.`,
3364
- "Preserve: key decisions, code snippets, technical details, and action items.",
3365
- "Format: plain text paragraphs. Start with the most important context."
3366
- ].join(`
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: conversationText
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: `${SUMMARY_PREFIX}
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
- makeApiCall(config.compressionModel, promptMessages),
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: 60000
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
- console.log(`[clawmux] [llm] ${decision.tier} → ${decision.model} | conf=${classification.confidence.toFixed(2)}${classification.reasoning ? ` | ${classification.reasoning}` : ""}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmux",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Smart model routing + context compression proxy for OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {