@blockrun/clawrouter 0.9.14 → 0.9.16

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
@@ -3479,6 +3479,174 @@ var PROXY_PORT = (() => {
3479
3479
  return DEFAULT_PORT;
3480
3480
  })();
3481
3481
 
3482
+ // src/journal.ts
3483
+ var DEFAULT_CONFIG2 = {
3484
+ maxEntries: 100,
3485
+ maxAgeMs: 24 * 60 * 60 * 1e3,
3486
+ // 24 hours
3487
+ maxEventsPerResponse: 5
3488
+ };
3489
+ var SessionJournal = class {
3490
+ journals = /* @__PURE__ */ new Map();
3491
+ config;
3492
+ constructor(config) {
3493
+ this.config = { ...DEFAULT_CONFIG2, ...config };
3494
+ }
3495
+ /**
3496
+ * Extract key events from assistant response content.
3497
+ * Looks for patterns like "I created...", "I fixed...", "Successfully..."
3498
+ */
3499
+ extractEvents(content) {
3500
+ if (!content || typeof content !== "string") {
3501
+ return [];
3502
+ }
3503
+ const events = [];
3504
+ const seen = /* @__PURE__ */ new Set();
3505
+ const patterns = [
3506
+ // Creation patterns
3507
+ /I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
3508
+ // Fix patterns
3509
+ /I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
3510
+ // Completion patterns
3511
+ /I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
3512
+ // Update patterns
3513
+ /I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
3514
+ // Success patterns
3515
+ /Successfully ([^.!?\n]{10,150})/gi,
3516
+ // Tool usage patterns (when agent uses tools)
3517
+ /I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
3518
+ ];
3519
+ for (const pattern of patterns) {
3520
+ pattern.lastIndex = 0;
3521
+ let match;
3522
+ while ((match = pattern.exec(content)) !== null) {
3523
+ const action = match[0].trim();
3524
+ const normalized = action.toLowerCase();
3525
+ if (seen.has(normalized)) {
3526
+ continue;
3527
+ }
3528
+ if (action.length >= 15 && action.length <= 200) {
3529
+ events.push(action);
3530
+ seen.add(normalized);
3531
+ }
3532
+ if (events.length >= this.config.maxEventsPerResponse) {
3533
+ break;
3534
+ }
3535
+ }
3536
+ if (events.length >= this.config.maxEventsPerResponse) {
3537
+ break;
3538
+ }
3539
+ }
3540
+ return events;
3541
+ }
3542
+ /**
3543
+ * Record events to the session journal.
3544
+ */
3545
+ record(sessionId, events, model) {
3546
+ if (!sessionId || !events.length) {
3547
+ return;
3548
+ }
3549
+ const journal = this.journals.get(sessionId) || [];
3550
+ const now = Date.now();
3551
+ for (const action of events) {
3552
+ journal.push({
3553
+ timestamp: now,
3554
+ action,
3555
+ model
3556
+ });
3557
+ }
3558
+ const cutoff = now - this.config.maxAgeMs;
3559
+ const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
3560
+ this.journals.set(sessionId, trimmed);
3561
+ }
3562
+ /**
3563
+ * Check if the user message indicates a need for historical context.
3564
+ */
3565
+ needsContext(lastUserMessage) {
3566
+ if (!lastUserMessage || typeof lastUserMessage !== "string") {
3567
+ return false;
3568
+ }
3569
+ const lower = lastUserMessage.toLowerCase();
3570
+ const triggers = [
3571
+ // Direct questions about past work
3572
+ "what did you do",
3573
+ "what have you done",
3574
+ "what did we do",
3575
+ "what have we done",
3576
+ // Temporal references
3577
+ "earlier",
3578
+ "before",
3579
+ "previously",
3580
+ "this session",
3581
+ "today",
3582
+ "so far",
3583
+ // Summary requests
3584
+ "remind me",
3585
+ "summarize",
3586
+ "summary of",
3587
+ "recap",
3588
+ // Progress inquiries
3589
+ "your work",
3590
+ "your progress",
3591
+ "accomplished",
3592
+ "achievements",
3593
+ "completed tasks"
3594
+ ];
3595
+ return triggers.some((t) => lower.includes(t));
3596
+ }
3597
+ /**
3598
+ * Format the journal for injection into system message.
3599
+ * Returns null if journal is empty.
3600
+ */
3601
+ format(sessionId) {
3602
+ const journal = this.journals.get(sessionId);
3603
+ if (!journal?.length) {
3604
+ return null;
3605
+ }
3606
+ const lines = journal.map((e) => {
3607
+ const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
3608
+ hour: "2-digit",
3609
+ minute: "2-digit",
3610
+ hour12: true
3611
+ });
3612
+ return `- ${time}: ${e.action}`;
3613
+ });
3614
+ return `[Session Memory - Key Actions]
3615
+ ${lines.join("\n")}`;
3616
+ }
3617
+ /**
3618
+ * Get the raw journal entries for a session (for debugging/testing).
3619
+ */
3620
+ getEntries(sessionId) {
3621
+ return this.journals.get(sessionId) || [];
3622
+ }
3623
+ /**
3624
+ * Clear journal for a specific session.
3625
+ */
3626
+ clear(sessionId) {
3627
+ this.journals.delete(sessionId);
3628
+ }
3629
+ /**
3630
+ * Clear all journals.
3631
+ */
3632
+ clearAll() {
3633
+ this.journals.clear();
3634
+ }
3635
+ /**
3636
+ * Get stats about the journal.
3637
+ */
3638
+ getStats() {
3639
+ let totalEntries = 0;
3640
+ for (const entries of this.journals.values()) {
3641
+ totalEntries += entries.length;
3642
+ }
3643
+ return {
3644
+ sessions: this.journals.size,
3645
+ totalEntries
3646
+ };
3647
+ }
3648
+ };
3649
+
3482
3650
  // src/proxy.ts
3483
3651
  var BLOCKRUN_API = "https://blockrun.ai/api";
3484
3652
  var AUTO_MODEL = "blockrun/auto";
@@ -3888,6 +4056,7 @@ async function startProxy(options) {
3888
4056
  const deduplicator = new RequestDeduplicator();
3889
4057
  const responseCache = new ResponseCache(options.cacheConfig);
3890
4058
  const sessionStore = new SessionStore(options.sessionConfig);
4059
+ const sessionJournal = new SessionJournal();
3891
4060
  const connections = /* @__PURE__ */ new Set();
3892
4061
  const server = createServer(async (req, res) => {
3893
4062
  req.on("error", (err) => {
@@ -3983,7 +4152,8 @@ async function startProxy(options) {
3983
4152
  deduplicator,
3984
4153
  balanceMonitor,
3985
4154
  sessionStore,
3986
- responseCache
4155
+ responseCache,
4156
+ sessionJournal
3987
4157
  );
3988
4158
  } catch (err) {
3989
4159
  const error = err instanceof Error ? err : new Error(String(err));
@@ -4184,7 +4354,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
4184
4354
  };
4185
4355
  }
4186
4356
  }
4187
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
4357
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
4188
4358
  const startTime = Date.now();
4189
4359
  const upstreamUrl = `${apiBase}${req.url}`;
4190
4360
  const bodyChunks = [];
@@ -4197,7 +4367,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4197
4367
  let modelId = "";
4198
4368
  let maxTokens = 4096;
4199
4369
  let routingProfile = null;
4370
+ let accumulatedContent = "";
4200
4371
  const isChatCompletion = req.url?.includes("/chat/completions");
4372
+ const sessionId = getSessionId(req.headers);
4201
4373
  if (isChatCompletion && body.length > 0) {
4202
4374
  try {
4203
4375
  const parsed = JSON.parse(body.toString());
@@ -4205,6 +4377,30 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4205
4377
  modelId = parsed.model || "";
4206
4378
  maxTokens = parsed.max_tokens || 4096;
4207
4379
  let bodyModified = false;
4380
+ if (sessionId && Array.isArray(parsed.messages)) {
4381
+ const messages = parsed.messages;
4382
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
4383
+ const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
4384
+ if (sessionJournal.needsContext(lastContent)) {
4385
+ const journalText = sessionJournal.format(sessionId);
4386
+ if (journalText) {
4387
+ const sysIdx = messages.findIndex((m) => m.role === "system");
4388
+ if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
4389
+ messages[sysIdx] = {
4390
+ ...messages[sysIdx],
4391
+ content: journalText + "\n\n" + messages[sysIdx].content
4392
+ };
4393
+ } else {
4394
+ messages.unshift({ role: "system", content: journalText });
4395
+ }
4396
+ parsed.messages = messages;
4397
+ bodyModified = true;
4398
+ console.log(
4399
+ `[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
4400
+ );
4401
+ }
4402
+ }
4403
+ }
4208
4404
  if (parsed.stream === true) {
4209
4405
  parsed.stream = false;
4210
4406
  bodyModified = true;
@@ -4243,18 +4439,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4243
4439
  latencyMs: 0
4244
4440
  });
4245
4441
  } else {
4246
- const sessionId = getSessionId(
4442
+ const sessionId2 = getSessionId(
4247
4443
  req.headers
4248
4444
  );
4249
- const existingSession = sessionId ? sessionStore.getSession(sessionId) : void 0;
4445
+ const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
4250
4446
  if (existingSession) {
4251
4447
  console.log(
4252
- `[ClawRouter] Session ${sessionId?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4448
+ `[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4253
4449
  );
4254
4450
  parsed.model = existingSession.model;
4255
4451
  modelId = existingSession.model;
4256
4452
  bodyModified = true;
4257
- sessionStore.touchSession(sessionId);
4453
+ sessionStore.touchSession(sessionId2);
4258
4454
  } else {
4259
4455
  const messages = parsed.messages;
4260
4456
  let lastUserMsg;
@@ -4283,10 +4479,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4283
4479
  parsed.model = routingDecision.model;
4284
4480
  modelId = routingDecision.model;
4285
4481
  bodyModified = true;
4286
- if (sessionId) {
4287
- sessionStore.setSession(sessionId, routingDecision.model, routingDecision.tier);
4482
+ if (sessionId2) {
4483
+ sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
4288
4484
  console.log(
4289
- `[ClawRouter] Session ${sessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4485
+ `[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4290
4486
  );
4291
4487
  }
4292
4488
  options.onRouted?.(routingDecision);
@@ -4621,6 +4817,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4621
4817
  const content = stripThinkingTokens(rawContent);
4622
4818
  const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
4623
4819
  const index = choice.index ?? 0;
4820
+ if (content) {
4821
+ accumulatedContent += content;
4822
+ }
4624
4823
  const roleChunk = {
4625
4824
  ...baseChunk,
4626
4825
  choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
@@ -4734,6 +4933,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4734
4933
  });
4735
4934
  console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
4736
4935
  }
4936
+ try {
4937
+ const rspJson = JSON.parse(responseBody.toString());
4938
+ if (rspJson.choices?.[0]?.message?.content) {
4939
+ accumulatedContent = rspJson.choices[0].message.content;
4940
+ }
4941
+ } catch {
4942
+ }
4943
+ }
4944
+ if (sessionId && accumulatedContent) {
4945
+ const events = sessionJournal.extractEvents(accumulatedContent);
4946
+ if (events.length > 0) {
4947
+ sessionJournal.record(sessionId, events, actualModelUsed);
4948
+ console.log(
4949
+ `[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
4950
+ );
4951
+ }
4737
4952
  }
4738
4953
  if (estimatedCostMicros !== void 0) {
4739
4954
  balanceMonitor.deductEstimated(estimatedCostMicros);
@@ -5044,7 +5259,7 @@ function injectModelsConfig(logger) {
5044
5259
  { id: "opus", alias: "br-opus" },
5045
5260
  { id: "haiku", alias: "br-haiku" },
5046
5261
  { id: "gpt5", alias: "gpt5" },
5047
- { id: "mini", alias: "mini" },
5262
+ { id: "codex", alias: "codex" },
5048
5263
  { id: "grok-fast", alias: "grok-fast" },
5049
5264
  { id: "grok-code", alias: "grok-code" },
5050
5265
  { id: "deepseek", alias: "deepseek" },
@@ -5053,7 +5268,7 @@ function injectModelsConfig(logger) {
5053
5268
  { id: "gemini", alias: "gemini" },
5054
5269
  { id: "flash", alias: "flash" }
5055
5270
  ];
5056
- const DEPRECATED_ALIASES = ["blockrun/nvidia", "blockrun/gpt", "blockrun/o3", "blockrun/grok"];
5271
+ const DEPRECATED_ALIASES = ["blockrun/nvidia", "blockrun/gpt", "blockrun/o3", "blockrun/grok", "blockrun/mini"];
5057
5272
  if (!defaults.models) {
5058
5273
  defaults.models = {};
5059
5274
  needsWrite = true;