@blockrun/clawrouter 0.9.12 → 0.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2581,12 +2581,13 @@ var DEFAULT_COMPRESSION_CONFIG = {
2581
2581
  // src/compression/layers/deduplication.ts
2582
2582
  import crypto2 from "crypto";
2583
2583
  function hashMessage(message) {
2584
- const parts = [
2585
- message.role,
2586
- message.content || "",
2587
- message.tool_call_id || "",
2588
- message.name || ""
2589
- ];
2584
+ let contentStr = "";
2585
+ if (typeof message.content === "string") {
2586
+ contentStr = message.content;
2587
+ } else if (Array.isArray(message.content)) {
2588
+ contentStr = JSON.stringify(message.content);
2589
+ }
2590
+ const parts = [message.role, contentStr, message.tool_call_id || "", message.name || ""];
2590
2591
  if (message.tool_calls) {
2591
2592
  parts.push(
2592
2593
  JSON.stringify(
@@ -2649,13 +2650,13 @@ function deduplicateMessages(messages) {
2649
2650
 
2650
2651
  // src/compression/layers/whitespace.ts
2651
2652
  function normalizeWhitespace(content) {
2652
- if (!content) return content;
2653
+ if (!content || typeof content !== "string") return content;
2653
2654
  return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]+$/gm, "").replace(/([^\n]) {2,}/g, "$1 ").replace(/^[ ]{8,}/gm, (match) => " ".repeat(Math.ceil(match.length / 4))).replace(/\t/g, " ").trim();
2654
2655
  }
2655
2656
  function normalizeMessagesWhitespace(messages) {
2656
2657
  let charsSaved = 0;
2657
2658
  const result = messages.map((message) => {
2658
- if (!message.content) return message;
2659
+ if (!message.content || typeof message.content !== "string") return message;
2659
2660
  const originalLength = message.content.length;
2660
2661
  const normalizedContent = normalizeWhitespace(message.content);
2661
2662
  charsSaved += originalLength - normalizedContent.length;
@@ -2759,6 +2760,9 @@ function generateCodebookHeader(usedCodes, pathMap = {}) {
2759
2760
 
2760
2761
  // src/compression/layers/dictionary.ts
2761
2762
  function encodeContent(content, inverseCodebook) {
2763
+ if (!content || typeof content !== "string") {
2764
+ return { encoded: content, substitutions: 0, codes: /* @__PURE__ */ new Set(), charsSaved: 0 };
2765
+ }
2762
2766
  let encoded = content;
2763
2767
  let substitutions = 0;
2764
2768
  let charsSaved = 0;
@@ -2786,7 +2790,7 @@ function encodeMessages(messages) {
2786
2790
  let totalCharsSaved = 0;
2787
2791
  const allUsedCodes = /* @__PURE__ */ new Set();
2788
2792
  const result = messages.map((message) => {
2789
- if (!message.content) return message;
2793
+ if (!message.content || typeof message.content !== "string") return message;
2790
2794
  const { encoded, substitutions, codes, charsSaved } = encodeContent(
2791
2795
  message.content,
2792
2796
  inverseCodebook
@@ -2812,7 +2816,7 @@ var PATH_REGEX = /(?:\/[\w.-]+){3,}/g;
2812
2816
  function extractPaths(messages) {
2813
2817
  const paths = [];
2814
2818
  for (const message of messages) {
2815
- if (!message.content) continue;
2819
+ if (!message.content || typeof message.content !== "string") continue;
2816
2820
  const matches = message.content.match(PATH_REGEX);
2817
2821
  if (matches) {
2818
2822
  paths.push(...matches);
@@ -2854,7 +2858,7 @@ function shortenPaths(messages) {
2854
2858
  });
2855
2859
  let charsSaved = 0;
2856
2860
  const result = messages.map((message) => {
2857
- if (!message.content) return message;
2861
+ if (!message.content || typeof message.content !== "string") return message;
2858
2862
  let content = message.content;
2859
2863
  const originalLength = content.length;
2860
2864
  for (const [code, prefix] of Object.entries(pathMap)) {
@@ -2905,7 +2909,7 @@ function compactMessagesJson(messages) {
2905
2909
  const newLength = JSON.stringify(newMessage.tool_calls).length;
2906
2910
  charsSaved += originalLength - newLength;
2907
2911
  }
2908
- if (message.role === "tool" && message.content && looksLikeJson(message.content)) {
2912
+ if (message.role === "tool" && message.content && typeof message.content === "string" && looksLikeJson(message.content)) {
2909
2913
  const originalLength = message.content.length;
2910
2914
  const compacted = compactJson(message.content);
2911
2915
  charsSaved += originalLength - compacted.length;
@@ -2970,7 +2974,7 @@ function deduplicateLargeBlocks(messages) {
2970
2974
  const blockHashes = /* @__PURE__ */ new Map();
2971
2975
  let charsSaved = 0;
2972
2976
  const result = messages.map((msg, idx) => {
2973
- if (!msg.content || msg.content.length < 500) {
2977
+ if (!msg.content || typeof msg.content !== "string" || msg.content.length < 500) {
2974
2978
  return msg;
2975
2979
  }
2976
2980
  const blockKey = msg.content.slice(0, 200);
@@ -2990,7 +2994,7 @@ function compressObservations(messages) {
2990
2994
  let charsSaved = 0;
2991
2995
  let observationsCompressed = 0;
2992
2996
  let result = messages.map((msg) => {
2993
- if (msg.role !== "tool" || !msg.content) {
2997
+ if (msg.role !== "tool" || !msg.content || typeof msg.content !== "string") {
2994
2998
  return msg;
2995
2999
  }
2996
3000
  const original = msg.content;
@@ -3043,7 +3047,7 @@ function findRepeatedPhrases(allContent) {
3043
3047
  function buildDynamicCodebook(messages) {
3044
3048
  let allContent = "";
3045
3049
  for (const msg of messages) {
3046
- if (msg.content) {
3050
+ if (msg.content && typeof msg.content === "string") {
3047
3051
  allContent += msg.content + "\n";
3048
3052
  }
3049
3053
  }
@@ -3068,6 +3072,7 @@ function buildDynamicCodebook(messages) {
3068
3072
  return codebook;
3069
3073
  }
3070
3074
  function escapeRegex2(str) {
3075
+ if (!str || typeof str !== "string") return "";
3071
3076
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3072
3077
  }
3073
3078
  function applyDynamicCodebook(messages) {
@@ -3088,7 +3093,7 @@ function applyDynamicCodebook(messages) {
3088
3093
  let charsSaved = 0;
3089
3094
  let substitutions = 0;
3090
3095
  const result = messages.map((msg) => {
3091
- if (!msg.content) return msg;
3096
+ if (!msg.content || typeof msg.content !== "string") return msg;
3092
3097
  let content = msg.content;
3093
3098
  for (const phrase of sortedPhrases) {
3094
3099
  const code = phraseToCode[phrase];
@@ -3121,7 +3126,12 @@ function generateDynamicCodebookHeader(codebook) {
3121
3126
  // src/compression/index.ts
3122
3127
  function calculateTotalChars(messages) {
3123
3128
  return messages.reduce((total, msg) => {
3124
- let chars = msg.content?.length || 0;
3129
+ let chars = 0;
3130
+ if (typeof msg.content === "string") {
3131
+ chars = msg.content.length;
3132
+ } else if (Array.isArray(msg.content)) {
3133
+ chars = JSON.stringify(msg.content).length;
3134
+ }
3125
3135
  if (msg.tool_calls) {
3126
3136
  chars += JSON.stringify(msg.tool_calls).length;
3127
3137
  }
@@ -3140,12 +3150,14 @@ function prependCodebookHeader(messages, usedCodes, pathMap) {
3140
3150
  }
3141
3151
  return messages.map((msg, i) => {
3142
3152
  if (i === userIndex) {
3143
- return {
3144
- ...msg,
3145
- content: `${header}
3153
+ if (typeof msg.content === "string") {
3154
+ return {
3155
+ ...msg,
3156
+ content: `${header}
3146
3157
 
3147
- ${msg.content || ""}`
3148
- };
3158
+ ${msg.content}`
3159
+ };
3160
+ }
3149
3161
  }
3150
3162
  return msg;
3151
3163
  });
@@ -3250,11 +3262,11 @@ async function compressContext(messages, config = {}) {
3250
3262
  const dynHeader = generateDynamicCodebookHeader(dynamicCodes);
3251
3263
  if (dynHeader) {
3252
3264
  const systemIndex = result.findIndex((m) => m.role === "system");
3253
- if (systemIndex >= 0) {
3265
+ if (systemIndex >= 0 && typeof result[systemIndex].content === "string") {
3254
3266
  result[systemIndex] = {
3255
3267
  ...result[systemIndex],
3256
3268
  content: `${dynHeader}
3257
- ${result[systemIndex].content || ""}`
3269
+ ${result[systemIndex].content}`
3258
3270
  };
3259
3271
  }
3260
3272
  }
@@ -3462,6 +3474,174 @@ var PROXY_PORT = (() => {
3462
3474
  return DEFAULT_PORT;
3463
3475
  })();
3464
3476
 
3477
+ // src/journal.ts
3478
+ var DEFAULT_CONFIG2 = {
3479
+ maxEntries: 100,
3480
+ maxAgeMs: 24 * 60 * 60 * 1e3,
3481
+ // 24 hours
3482
+ maxEventsPerResponse: 5
3483
+ };
3484
+ var SessionJournal = class {
3485
+ journals = /* @__PURE__ */ new Map();
3486
+ config;
3487
+ constructor(config) {
3488
+ this.config = { ...DEFAULT_CONFIG2, ...config };
3489
+ }
3490
+ /**
3491
+ * Extract key events from assistant response content.
3492
+ * Looks for patterns like "I created...", "I fixed...", "Successfully..."
3493
+ */
3494
+ extractEvents(content) {
3495
+ if (!content || typeof content !== "string") {
3496
+ return [];
3497
+ }
3498
+ const events = [];
3499
+ const seen = /* @__PURE__ */ new Set();
3500
+ const patterns = [
3501
+ // Creation patterns
3502
+ /I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
3503
+ // Fix patterns
3504
+ /I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
3505
+ // Completion patterns
3506
+ /I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
3507
+ // Update patterns
3508
+ /I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
3509
+ // Success patterns
3510
+ /Successfully ([^.!?\n]{10,150})/gi,
3511
+ // Tool usage patterns (when agent uses tools)
3512
+ /I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
3513
+ ];
3514
+ for (const pattern of patterns) {
3515
+ pattern.lastIndex = 0;
3516
+ let match;
3517
+ while ((match = pattern.exec(content)) !== null) {
3518
+ const action = match[0].trim();
3519
+ const normalized = action.toLowerCase();
3520
+ if (seen.has(normalized)) {
3521
+ continue;
3522
+ }
3523
+ if (action.length >= 15 && action.length <= 200) {
3524
+ events.push(action);
3525
+ seen.add(normalized);
3526
+ }
3527
+ if (events.length >= this.config.maxEventsPerResponse) {
3528
+ break;
3529
+ }
3530
+ }
3531
+ if (events.length >= this.config.maxEventsPerResponse) {
3532
+ break;
3533
+ }
3534
+ }
3535
+ return events;
3536
+ }
3537
+ /**
3538
+ * Record events to the session journal.
3539
+ */
3540
+ record(sessionId, events, model) {
3541
+ if (!sessionId || !events.length) {
3542
+ return;
3543
+ }
3544
+ const journal = this.journals.get(sessionId) || [];
3545
+ const now = Date.now();
3546
+ for (const action of events) {
3547
+ journal.push({
3548
+ timestamp: now,
3549
+ action,
3550
+ model
3551
+ });
3552
+ }
3553
+ const cutoff = now - this.config.maxAgeMs;
3554
+ const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
3555
+ this.journals.set(sessionId, trimmed);
3556
+ }
3557
+ /**
3558
+ * Check if the user message indicates a need for historical context.
3559
+ */
3560
+ needsContext(lastUserMessage) {
3561
+ if (!lastUserMessage || typeof lastUserMessage !== "string") {
3562
+ return false;
3563
+ }
3564
+ const lower = lastUserMessage.toLowerCase();
3565
+ const triggers = [
3566
+ // Direct questions about past work
3567
+ "what did you do",
3568
+ "what have you done",
3569
+ "what did we do",
3570
+ "what have we done",
3571
+ // Temporal references
3572
+ "earlier",
3573
+ "before",
3574
+ "previously",
3575
+ "this session",
3576
+ "today",
3577
+ "so far",
3578
+ // Summary requests
3579
+ "remind me",
3580
+ "summarize",
3581
+ "summary of",
3582
+ "recap",
3583
+ // Progress inquiries
3584
+ "your work",
3585
+ "your progress",
3586
+ "accomplished",
3587
+ "achievements",
3588
+ "completed tasks"
3589
+ ];
3590
+ return triggers.some((t) => lower.includes(t));
3591
+ }
3592
+ /**
3593
+ * Format the journal for injection into system message.
3594
+ * Returns null if journal is empty.
3595
+ */
3596
+ format(sessionId) {
3597
+ const journal = this.journals.get(sessionId);
3598
+ if (!journal?.length) {
3599
+ return null;
3600
+ }
3601
+ const lines = journal.map((e) => {
3602
+ const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
3603
+ hour: "2-digit",
3604
+ minute: "2-digit",
3605
+ hour12: true
3606
+ });
3607
+ return `- ${time}: ${e.action}`;
3608
+ });
3609
+ return `[Session Memory - Key Actions]
3610
+ ${lines.join("\n")}`;
3611
+ }
3612
+ /**
3613
+ * Get the raw journal entries for a session (for debugging/testing).
3614
+ */
3615
+ getEntries(sessionId) {
3616
+ return this.journals.get(sessionId) || [];
3617
+ }
3618
+ /**
3619
+ * Clear journal for a specific session.
3620
+ */
3621
+ clear(sessionId) {
3622
+ this.journals.delete(sessionId);
3623
+ }
3624
+ /**
3625
+ * Clear all journals.
3626
+ */
3627
+ clearAll() {
3628
+ this.journals.clear();
3629
+ }
3630
+ /**
3631
+ * Get stats about the journal.
3632
+ */
3633
+ getStats() {
3634
+ let totalEntries = 0;
3635
+ for (const entries of this.journals.values()) {
3636
+ totalEntries += entries.length;
3637
+ }
3638
+ return {
3639
+ sessions: this.journals.size,
3640
+ totalEntries
3641
+ };
3642
+ }
3643
+ };
3644
+
3465
3645
  // src/proxy.ts
3466
3646
  var BLOCKRUN_API = "https://blockrun.ai/api";
3467
3647
  var AUTO_MODEL = "blockrun/auto";
@@ -3871,6 +4051,7 @@ async function startProxy(options) {
3871
4051
  const deduplicator = new RequestDeduplicator();
3872
4052
  const responseCache = new ResponseCache(options.cacheConfig);
3873
4053
  const sessionStore = new SessionStore(options.sessionConfig);
4054
+ const sessionJournal = new SessionJournal();
3874
4055
  const connections = /* @__PURE__ */ new Set();
3875
4056
  const server = createServer(async (req, res) => {
3876
4057
  req.on("error", (err) => {
@@ -3966,7 +4147,8 @@ async function startProxy(options) {
3966
4147
  deduplicator,
3967
4148
  balanceMonitor,
3968
4149
  sessionStore,
3969
- responseCache
4150
+ responseCache,
4151
+ sessionJournal
3970
4152
  );
3971
4153
  } catch (err) {
3972
4154
  const error = err instanceof Error ? err : new Error(String(err));
@@ -4167,7 +4349,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
4167
4349
  };
4168
4350
  }
4169
4351
  }
4170
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
4352
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
4171
4353
  const startTime = Date.now();
4172
4354
  const upstreamUrl = `${apiBase}${req.url}`;
4173
4355
  const bodyChunks = [];
@@ -4180,13 +4362,38 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4180
4362
  let modelId = "";
4181
4363
  let maxTokens = 4096;
4182
4364
  let routingProfile = null;
4365
+ let accumulatedContent = "";
4183
4366
  const isChatCompletion = req.url?.includes("/chat/completions");
4367
+ const sessionId = getSessionId(req.headers);
4184
4368
  if (isChatCompletion && body.length > 0) {
4185
4369
  try {
4186
4370
  const parsed = JSON.parse(body.toString());
4187
4371
  isStreaming = parsed.stream === true;
4188
4372
  modelId = parsed.model || "";
4189
4373
  maxTokens = parsed.max_tokens || 4096;
4374
+ if (sessionId && Array.isArray(parsed.messages)) {
4375
+ const messages = parsed.messages;
4376
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
4377
+ const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
4378
+ if (sessionJournal.needsContext(lastContent)) {
4379
+ const journalText = sessionJournal.format(sessionId);
4380
+ if (journalText) {
4381
+ const sysIdx = messages.findIndex((m) => m.role === "system");
4382
+ if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
4383
+ messages[sysIdx] = {
4384
+ ...messages[sysIdx],
4385
+ content: journalText + "\n\n" + messages[sysIdx].content
4386
+ };
4387
+ } else {
4388
+ messages.unshift({ role: "system", content: journalText });
4389
+ }
4390
+ parsed.messages = messages;
4391
+ console.log(
4392
+ `[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
4393
+ );
4394
+ }
4395
+ }
4396
+ }
4190
4397
  let bodyModified = false;
4191
4398
  if (parsed.stream === true) {
4192
4399
  parsed.stream = false;
@@ -4226,18 +4433,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4226
4433
  latencyMs: 0
4227
4434
  });
4228
4435
  } else {
4229
- const sessionId = getSessionId(
4436
+ const sessionId2 = getSessionId(
4230
4437
  req.headers
4231
4438
  );
4232
- const existingSession = sessionId ? sessionStore.getSession(sessionId) : void 0;
4439
+ const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
4233
4440
  if (existingSession) {
4234
4441
  console.log(
4235
- `[ClawRouter] Session ${sessionId?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4442
+ `[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4236
4443
  );
4237
4444
  parsed.model = existingSession.model;
4238
4445
  modelId = existingSession.model;
4239
4446
  bodyModified = true;
4240
- sessionStore.touchSession(sessionId);
4447
+ sessionStore.touchSession(sessionId2);
4241
4448
  } else {
4242
4449
  const messages = parsed.messages;
4243
4450
  let lastUserMsg;
@@ -4266,10 +4473,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4266
4473
  parsed.model = routingDecision.model;
4267
4474
  modelId = routingDecision.model;
4268
4475
  bodyModified = true;
4269
- if (sessionId) {
4270
- sessionStore.setSession(sessionId, routingDecision.model, routingDecision.tier);
4476
+ if (sessionId2) {
4477
+ sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
4271
4478
  console.log(
4272
- `[ClawRouter] Session ${sessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4479
+ `[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4273
4480
  );
4274
4481
  }
4275
4482
  options.onRouted?.(routingDecision);
@@ -4604,6 +4811,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4604
4811
  const content = stripThinkingTokens(rawContent);
4605
4812
  const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
4606
4813
  const index = choice.index ?? 0;
4814
+ if (content) {
4815
+ accumulatedContent += content;
4816
+ }
4607
4817
  const roleChunk = {
4608
4818
  ...baseChunk,
4609
4819
  choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
@@ -4717,6 +4927,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4717
4927
  });
4718
4928
  console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
4719
4929
  }
4930
+ try {
4931
+ const rspJson = JSON.parse(responseBody.toString());
4932
+ if (rspJson.choices?.[0]?.message?.content) {
4933
+ accumulatedContent = rspJson.choices[0].message.content;
4934
+ }
4935
+ } catch {
4936
+ }
4937
+ }
4938
+ if (sessionId && accumulatedContent) {
4939
+ const events = sessionJournal.extractEvents(accumulatedContent);
4940
+ if (events.length > 0) {
4941
+ sessionJournal.record(sessionId, events, actualModelUsed);
4942
+ console.log(
4943
+ `[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
4944
+ );
4945
+ }
4720
4946
  }
4721
4947
  if (estimatedCostMicros !== void 0) {
4722
4948
  balanceMonitor.deductEstimated(estimatedCostMicros);