@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/cli.js CHANGED
@@ -2441,12 +2441,13 @@ var DEFAULT_COMPRESSION_CONFIG = {
2441
2441
  // src/compression/layers/deduplication.ts
2442
2442
  import crypto2 from "crypto";
2443
2443
  function hashMessage(message) {
2444
- const parts = [
2445
- message.role,
2446
- message.content || "",
2447
- message.tool_call_id || "",
2448
- message.name || ""
2449
- ];
2444
+ let contentStr = "";
2445
+ if (typeof message.content === "string") {
2446
+ contentStr = message.content;
2447
+ } else if (Array.isArray(message.content)) {
2448
+ contentStr = JSON.stringify(message.content);
2449
+ }
2450
+ const parts = [message.role, contentStr, message.tool_call_id || "", message.name || ""];
2450
2451
  if (message.tool_calls) {
2451
2452
  parts.push(
2452
2453
  JSON.stringify(
@@ -2509,13 +2510,13 @@ function deduplicateMessages(messages) {
2509
2510
 
2510
2511
  // src/compression/layers/whitespace.ts
2511
2512
  function normalizeWhitespace(content) {
2512
- if (!content) return content;
2513
+ if (!content || typeof content !== "string") return content;
2513
2514
  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();
2514
2515
  }
2515
2516
  function normalizeMessagesWhitespace(messages) {
2516
2517
  let charsSaved = 0;
2517
2518
  const result = messages.map((message) => {
2518
- if (!message.content) return message;
2519
+ if (!message.content || typeof message.content !== "string") return message;
2519
2520
  const originalLength = message.content.length;
2520
2521
  const normalizedContent = normalizeWhitespace(message.content);
2521
2522
  charsSaved += originalLength - normalizedContent.length;
@@ -2619,6 +2620,9 @@ function generateCodebookHeader(usedCodes, pathMap = {}) {
2619
2620
 
2620
2621
  // src/compression/layers/dictionary.ts
2621
2622
  function encodeContent(content, inverseCodebook) {
2623
+ if (!content || typeof content !== "string") {
2624
+ return { encoded: content, substitutions: 0, codes: /* @__PURE__ */ new Set(), charsSaved: 0 };
2625
+ }
2622
2626
  let encoded = content;
2623
2627
  let substitutions = 0;
2624
2628
  let charsSaved = 0;
@@ -2646,7 +2650,7 @@ function encodeMessages(messages) {
2646
2650
  let totalCharsSaved = 0;
2647
2651
  const allUsedCodes = /* @__PURE__ */ new Set();
2648
2652
  const result = messages.map((message) => {
2649
- if (!message.content) return message;
2653
+ if (!message.content || typeof message.content !== "string") return message;
2650
2654
  const { encoded, substitutions, codes, charsSaved } = encodeContent(
2651
2655
  message.content,
2652
2656
  inverseCodebook
@@ -2672,7 +2676,7 @@ var PATH_REGEX = /(?:\/[\w.-]+){3,}/g;
2672
2676
  function extractPaths(messages) {
2673
2677
  const paths = [];
2674
2678
  for (const message of messages) {
2675
- if (!message.content) continue;
2679
+ if (!message.content || typeof message.content !== "string") continue;
2676
2680
  const matches = message.content.match(PATH_REGEX);
2677
2681
  if (matches) {
2678
2682
  paths.push(...matches);
@@ -2714,7 +2718,7 @@ function shortenPaths(messages) {
2714
2718
  });
2715
2719
  let charsSaved = 0;
2716
2720
  const result = messages.map((message) => {
2717
- if (!message.content) return message;
2721
+ if (!message.content || typeof message.content !== "string") return message;
2718
2722
  let content = message.content;
2719
2723
  const originalLength = content.length;
2720
2724
  for (const [code, prefix] of Object.entries(pathMap)) {
@@ -2765,7 +2769,7 @@ function compactMessagesJson(messages) {
2765
2769
  const newLength = JSON.stringify(newMessage.tool_calls).length;
2766
2770
  charsSaved += originalLength - newLength;
2767
2771
  }
2768
- if (message.role === "tool" && message.content && looksLikeJson(message.content)) {
2772
+ if (message.role === "tool" && message.content && typeof message.content === "string" && looksLikeJson(message.content)) {
2769
2773
  const originalLength = message.content.length;
2770
2774
  const compacted = compactJson(message.content);
2771
2775
  charsSaved += originalLength - compacted.length;
@@ -2830,7 +2834,7 @@ function deduplicateLargeBlocks(messages) {
2830
2834
  const blockHashes = /* @__PURE__ */ new Map();
2831
2835
  let charsSaved = 0;
2832
2836
  const result = messages.map((msg, idx) => {
2833
- if (!msg.content || msg.content.length < 500) {
2837
+ if (!msg.content || typeof msg.content !== "string" || msg.content.length < 500) {
2834
2838
  return msg;
2835
2839
  }
2836
2840
  const blockKey = msg.content.slice(0, 200);
@@ -2850,7 +2854,7 @@ function compressObservations(messages) {
2850
2854
  let charsSaved = 0;
2851
2855
  let observationsCompressed = 0;
2852
2856
  let result = messages.map((msg) => {
2853
- if (msg.role !== "tool" || !msg.content) {
2857
+ if (msg.role !== "tool" || !msg.content || typeof msg.content !== "string") {
2854
2858
  return msg;
2855
2859
  }
2856
2860
  const original = msg.content;
@@ -2903,7 +2907,7 @@ function findRepeatedPhrases(allContent) {
2903
2907
  function buildDynamicCodebook(messages) {
2904
2908
  let allContent = "";
2905
2909
  for (const msg of messages) {
2906
- if (msg.content) {
2910
+ if (msg.content && typeof msg.content === "string") {
2907
2911
  allContent += msg.content + "\n";
2908
2912
  }
2909
2913
  }
@@ -2928,6 +2932,7 @@ function buildDynamicCodebook(messages) {
2928
2932
  return codebook;
2929
2933
  }
2930
2934
  function escapeRegex2(str) {
2935
+ if (!str || typeof str !== "string") return "";
2931
2936
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2932
2937
  }
2933
2938
  function applyDynamicCodebook(messages) {
@@ -2948,7 +2953,7 @@ function applyDynamicCodebook(messages) {
2948
2953
  let charsSaved = 0;
2949
2954
  let substitutions = 0;
2950
2955
  const result = messages.map((msg) => {
2951
- if (!msg.content) return msg;
2956
+ if (!msg.content || typeof msg.content !== "string") return msg;
2952
2957
  let content = msg.content;
2953
2958
  for (const phrase of sortedPhrases) {
2954
2959
  const code = phraseToCode[phrase];
@@ -2981,7 +2986,12 @@ function generateDynamicCodebookHeader(codebook) {
2981
2986
  // src/compression/index.ts
2982
2987
  function calculateTotalChars(messages) {
2983
2988
  return messages.reduce((total, msg) => {
2984
- let chars = msg.content?.length || 0;
2989
+ let chars = 0;
2990
+ if (typeof msg.content === "string") {
2991
+ chars = msg.content.length;
2992
+ } else if (Array.isArray(msg.content)) {
2993
+ chars = JSON.stringify(msg.content).length;
2994
+ }
2985
2995
  if (msg.tool_calls) {
2986
2996
  chars += JSON.stringify(msg.tool_calls).length;
2987
2997
  }
@@ -3000,12 +3010,14 @@ function prependCodebookHeader(messages, usedCodes, pathMap) {
3000
3010
  }
3001
3011
  return messages.map((msg, i) => {
3002
3012
  if (i === userIndex) {
3003
- return {
3004
- ...msg,
3005
- content: `${header}
3013
+ if (typeof msg.content === "string") {
3014
+ return {
3015
+ ...msg,
3016
+ content: `${header}
3006
3017
 
3007
- ${msg.content || ""}`
3008
- };
3018
+ ${msg.content}`
3019
+ };
3020
+ }
3009
3021
  }
3010
3022
  return msg;
3011
3023
  });
@@ -3110,11 +3122,11 @@ async function compressContext(messages, config = {}) {
3110
3122
  const dynHeader = generateDynamicCodebookHeader(dynamicCodes);
3111
3123
  if (dynHeader) {
3112
3124
  const systemIndex = result.findIndex((m) => m.role === "system");
3113
- if (systemIndex >= 0) {
3125
+ if (systemIndex >= 0 && typeof result[systemIndex].content === "string") {
3114
3126
  result[systemIndex] = {
3115
3127
  ...result[systemIndex],
3116
3128
  content: `${dynHeader}
3117
- ${result[systemIndex].content || ""}`
3129
+ ${result[systemIndex].content}`
3118
3130
  };
3119
3131
  }
3120
3132
  }
@@ -3322,6 +3334,174 @@ var PROXY_PORT = (() => {
3322
3334
  return DEFAULT_PORT;
3323
3335
  })();
3324
3336
 
3337
+ // src/journal.ts
3338
+ var DEFAULT_CONFIG2 = {
3339
+ maxEntries: 100,
3340
+ maxAgeMs: 24 * 60 * 60 * 1e3,
3341
+ // 24 hours
3342
+ maxEventsPerResponse: 5
3343
+ };
3344
+ var SessionJournal = class {
3345
+ journals = /* @__PURE__ */ new Map();
3346
+ config;
3347
+ constructor(config) {
3348
+ this.config = { ...DEFAULT_CONFIG2, ...config };
3349
+ }
3350
+ /**
3351
+ * Extract key events from assistant response content.
3352
+ * Looks for patterns like "I created...", "I fixed...", "Successfully..."
3353
+ */
3354
+ extractEvents(content) {
3355
+ if (!content || typeof content !== "string") {
3356
+ return [];
3357
+ }
3358
+ const events = [];
3359
+ const seen = /* @__PURE__ */ new Set();
3360
+ const patterns = [
3361
+ // Creation patterns
3362
+ /I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
3363
+ // Fix patterns
3364
+ /I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
3365
+ // Completion patterns
3366
+ /I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
3367
+ // Update patterns
3368
+ /I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
3369
+ // Success patterns
3370
+ /Successfully ([^.!?\n]{10,150})/gi,
3371
+ // Tool usage patterns (when agent uses tools)
3372
+ /I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
3373
+ ];
3374
+ for (const pattern of patterns) {
3375
+ pattern.lastIndex = 0;
3376
+ let match;
3377
+ while ((match = pattern.exec(content)) !== null) {
3378
+ const action = match[0].trim();
3379
+ const normalized = action.toLowerCase();
3380
+ if (seen.has(normalized)) {
3381
+ continue;
3382
+ }
3383
+ if (action.length >= 15 && action.length <= 200) {
3384
+ events.push(action);
3385
+ seen.add(normalized);
3386
+ }
3387
+ if (events.length >= this.config.maxEventsPerResponse) {
3388
+ break;
3389
+ }
3390
+ }
3391
+ if (events.length >= this.config.maxEventsPerResponse) {
3392
+ break;
3393
+ }
3394
+ }
3395
+ return events;
3396
+ }
3397
+ /**
3398
+ * Record events to the session journal.
3399
+ */
3400
+ record(sessionId, events, model) {
3401
+ if (!sessionId || !events.length) {
3402
+ return;
3403
+ }
3404
+ const journal = this.journals.get(sessionId) || [];
3405
+ const now = Date.now();
3406
+ for (const action of events) {
3407
+ journal.push({
3408
+ timestamp: now,
3409
+ action,
3410
+ model
3411
+ });
3412
+ }
3413
+ const cutoff = now - this.config.maxAgeMs;
3414
+ const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
3415
+ this.journals.set(sessionId, trimmed);
3416
+ }
3417
+ /**
3418
+ * Check if the user message indicates a need for historical context.
3419
+ */
3420
+ needsContext(lastUserMessage) {
3421
+ if (!lastUserMessage || typeof lastUserMessage !== "string") {
3422
+ return false;
3423
+ }
3424
+ const lower = lastUserMessage.toLowerCase();
3425
+ const triggers = [
3426
+ // Direct questions about past work
3427
+ "what did you do",
3428
+ "what have you done",
3429
+ "what did we do",
3430
+ "what have we done",
3431
+ // Temporal references
3432
+ "earlier",
3433
+ "before",
3434
+ "previously",
3435
+ "this session",
3436
+ "today",
3437
+ "so far",
3438
+ // Summary requests
3439
+ "remind me",
3440
+ "summarize",
3441
+ "summary of",
3442
+ "recap",
3443
+ // Progress inquiries
3444
+ "your work",
3445
+ "your progress",
3446
+ "accomplished",
3447
+ "achievements",
3448
+ "completed tasks"
3449
+ ];
3450
+ return triggers.some((t) => lower.includes(t));
3451
+ }
3452
+ /**
3453
+ * Format the journal for injection into system message.
3454
+ * Returns null if journal is empty.
3455
+ */
3456
+ format(sessionId) {
3457
+ const journal = this.journals.get(sessionId);
3458
+ if (!journal?.length) {
3459
+ return null;
3460
+ }
3461
+ const lines = journal.map((e) => {
3462
+ const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
3463
+ hour: "2-digit",
3464
+ minute: "2-digit",
3465
+ hour12: true
3466
+ });
3467
+ return `- ${time}: ${e.action}`;
3468
+ });
3469
+ return `[Session Memory - Key Actions]
3470
+ ${lines.join("\n")}`;
3471
+ }
3472
+ /**
3473
+ * Get the raw journal entries for a session (for debugging/testing).
3474
+ */
3475
+ getEntries(sessionId) {
3476
+ return this.journals.get(sessionId) || [];
3477
+ }
3478
+ /**
3479
+ * Clear journal for a specific session.
3480
+ */
3481
+ clear(sessionId) {
3482
+ this.journals.delete(sessionId);
3483
+ }
3484
+ /**
3485
+ * Clear all journals.
3486
+ */
3487
+ clearAll() {
3488
+ this.journals.clear();
3489
+ }
3490
+ /**
3491
+ * Get stats about the journal.
3492
+ */
3493
+ getStats() {
3494
+ let totalEntries = 0;
3495
+ for (const entries of this.journals.values()) {
3496
+ totalEntries += entries.length;
3497
+ }
3498
+ return {
3499
+ sessions: this.journals.size,
3500
+ totalEntries
3501
+ };
3502
+ }
3503
+ };
3504
+
3325
3505
  // src/proxy.ts
3326
3506
  var BLOCKRUN_API = "https://blockrun.ai/api";
3327
3507
  var AUTO_MODEL = "blockrun/auto";
@@ -3731,6 +3911,7 @@ async function startProxy(options) {
3731
3911
  const deduplicator = new RequestDeduplicator();
3732
3912
  const responseCache = new ResponseCache(options.cacheConfig);
3733
3913
  const sessionStore = new SessionStore(options.sessionConfig);
3914
+ const sessionJournal = new SessionJournal();
3734
3915
  const connections = /* @__PURE__ */ new Set();
3735
3916
  const server = createServer(async (req, res) => {
3736
3917
  req.on("error", (err) => {
@@ -3826,7 +4007,8 @@ async function startProxy(options) {
3826
4007
  deduplicator,
3827
4008
  balanceMonitor,
3828
4009
  sessionStore,
3829
- responseCache
4010
+ responseCache,
4011
+ sessionJournal
3830
4012
  );
3831
4013
  } catch (err) {
3832
4014
  const error = err instanceof Error ? err : new Error(String(err));
@@ -4027,7 +4209,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
4027
4209
  };
4028
4210
  }
4029
4211
  }
4030
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
4212
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
4031
4213
  const startTime = Date.now();
4032
4214
  const upstreamUrl = `${apiBase}${req.url}`;
4033
4215
  const bodyChunks = [];
@@ -4040,13 +4222,38 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4040
4222
  let modelId = "";
4041
4223
  let maxTokens = 4096;
4042
4224
  let routingProfile = null;
4225
+ let accumulatedContent = "";
4043
4226
  const isChatCompletion = req.url?.includes("/chat/completions");
4227
+ const sessionId = getSessionId(req.headers);
4044
4228
  if (isChatCompletion && body.length > 0) {
4045
4229
  try {
4046
4230
  const parsed = JSON.parse(body.toString());
4047
4231
  isStreaming = parsed.stream === true;
4048
4232
  modelId = parsed.model || "";
4049
4233
  maxTokens = parsed.max_tokens || 4096;
4234
+ if (sessionId && Array.isArray(parsed.messages)) {
4235
+ const messages = parsed.messages;
4236
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
4237
+ const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
4238
+ if (sessionJournal.needsContext(lastContent)) {
4239
+ const journalText = sessionJournal.format(sessionId);
4240
+ if (journalText) {
4241
+ const sysIdx = messages.findIndex((m) => m.role === "system");
4242
+ if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
4243
+ messages[sysIdx] = {
4244
+ ...messages[sysIdx],
4245
+ content: journalText + "\n\n" + messages[sysIdx].content
4246
+ };
4247
+ } else {
4248
+ messages.unshift({ role: "system", content: journalText });
4249
+ }
4250
+ parsed.messages = messages;
4251
+ console.log(
4252
+ `[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
4253
+ );
4254
+ }
4255
+ }
4256
+ }
4050
4257
  let bodyModified = false;
4051
4258
  if (parsed.stream === true) {
4052
4259
  parsed.stream = false;
@@ -4086,18 +4293,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4086
4293
  latencyMs: 0
4087
4294
  });
4088
4295
  } else {
4089
- const sessionId = getSessionId(
4296
+ const sessionId2 = getSessionId(
4090
4297
  req.headers
4091
4298
  );
4092
- const existingSession = sessionId ? sessionStore.getSession(sessionId) : void 0;
4299
+ const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
4093
4300
  if (existingSession) {
4094
4301
  console.log(
4095
- `[ClawRouter] Session ${sessionId?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4302
+ `[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4096
4303
  );
4097
4304
  parsed.model = existingSession.model;
4098
4305
  modelId = existingSession.model;
4099
4306
  bodyModified = true;
4100
- sessionStore.touchSession(sessionId);
4307
+ sessionStore.touchSession(sessionId2);
4101
4308
  } else {
4102
4309
  const messages = parsed.messages;
4103
4310
  let lastUserMsg;
@@ -4126,10 +4333,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4126
4333
  parsed.model = routingDecision.model;
4127
4334
  modelId = routingDecision.model;
4128
4335
  bodyModified = true;
4129
- if (sessionId) {
4130
- sessionStore.setSession(sessionId, routingDecision.model, routingDecision.tier);
4336
+ if (sessionId2) {
4337
+ sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
4131
4338
  console.log(
4132
- `[ClawRouter] Session ${sessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4339
+ `[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4133
4340
  );
4134
4341
  }
4135
4342
  options.onRouted?.(routingDecision);
@@ -4464,6 +4671,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4464
4671
  const content = stripThinkingTokens(rawContent);
4465
4672
  const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
4466
4673
  const index = choice.index ?? 0;
4674
+ if (content) {
4675
+ accumulatedContent += content;
4676
+ }
4467
4677
  const roleChunk = {
4468
4678
  ...baseChunk,
4469
4679
  choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
@@ -4577,6 +4787,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4577
4787
  });
4578
4788
  console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
4579
4789
  }
4790
+ try {
4791
+ const rspJson = JSON.parse(responseBody.toString());
4792
+ if (rspJson.choices?.[0]?.message?.content) {
4793
+ accumulatedContent = rspJson.choices[0].message.content;
4794
+ }
4795
+ } catch {
4796
+ }
4797
+ }
4798
+ if (sessionId && accumulatedContent) {
4799
+ const events = sessionJournal.extractEvents(accumulatedContent);
4800
+ if (events.length > 0) {
4801
+ sessionJournal.record(sessionId, events, actualModelUsed);
4802
+ console.log(
4803
+ `[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
4804
+ );
4805
+ }
4580
4806
  }
4581
4807
  if (estimatedCostMicros !== void 0) {
4582
4808
  balanceMonitor.deductEstimated(estimatedCostMicros);