@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/cli.js CHANGED
@@ -3339,6 +3339,174 @@ var PROXY_PORT = (() => {
3339
3339
  return DEFAULT_PORT;
3340
3340
  })();
3341
3341
 
3342
+ // src/journal.ts
3343
+ var DEFAULT_CONFIG2 = {
3344
+ maxEntries: 100,
3345
+ maxAgeMs: 24 * 60 * 60 * 1e3,
3346
+ // 24 hours
3347
+ maxEventsPerResponse: 5
3348
+ };
3349
+ var SessionJournal = class {
3350
+ journals = /* @__PURE__ */ new Map();
3351
+ config;
3352
+ constructor(config) {
3353
+ this.config = { ...DEFAULT_CONFIG2, ...config };
3354
+ }
3355
+ /**
3356
+ * Extract key events from assistant response content.
3357
+ * Looks for patterns like "I created...", "I fixed...", "Successfully..."
3358
+ */
3359
+ extractEvents(content) {
3360
+ if (!content || typeof content !== "string") {
3361
+ return [];
3362
+ }
3363
+ const events = [];
3364
+ const seen = /* @__PURE__ */ new Set();
3365
+ const patterns = [
3366
+ // Creation patterns
3367
+ /I (?:also |then |have |)?(?:created|implemented|added|wrote|built|generated|set up|initialized) ([^.!?\n]{10,150})/gi,
3368
+ // Fix patterns
3369
+ /I (?:also |then |have |)?(?:fixed|resolved|solved|patched|corrected|addressed|debugged) ([^.!?\n]{10,150})/gi,
3370
+ // Completion patterns
3371
+ /I (?:also |then |have |)?(?:completed|finished|done with|wrapped up) ([^.!?\n]{10,150})/gi,
3372
+ // Update patterns
3373
+ /I (?:also |then |have |)?(?:updated|modified|changed|refactored|improved|enhanced|optimized) ([^.!?\n]{10,150})/gi,
3374
+ // Success patterns
3375
+ /Successfully ([^.!?\n]{10,150})/gi,
3376
+ // Tool usage patterns (when agent uses tools)
3377
+ /I (?:also |then |have |)?(?:ran|executed|called|invoked) ([^.!?\n]{10,100})/gi
3378
+ ];
3379
+ for (const pattern of patterns) {
3380
+ pattern.lastIndex = 0;
3381
+ let match;
3382
+ while ((match = pattern.exec(content)) !== null) {
3383
+ const action = match[0].trim();
3384
+ const normalized = action.toLowerCase();
3385
+ if (seen.has(normalized)) {
3386
+ continue;
3387
+ }
3388
+ if (action.length >= 15 && action.length <= 200) {
3389
+ events.push(action);
3390
+ seen.add(normalized);
3391
+ }
3392
+ if (events.length >= this.config.maxEventsPerResponse) {
3393
+ break;
3394
+ }
3395
+ }
3396
+ if (events.length >= this.config.maxEventsPerResponse) {
3397
+ break;
3398
+ }
3399
+ }
3400
+ return events;
3401
+ }
3402
+ /**
3403
+ * Record events to the session journal.
3404
+ */
3405
+ record(sessionId, events, model) {
3406
+ if (!sessionId || !events.length) {
3407
+ return;
3408
+ }
3409
+ const journal = this.journals.get(sessionId) || [];
3410
+ const now = Date.now();
3411
+ for (const action of events) {
3412
+ journal.push({
3413
+ timestamp: now,
3414
+ action,
3415
+ model
3416
+ });
3417
+ }
3418
+ const cutoff = now - this.config.maxAgeMs;
3419
+ const trimmed = journal.filter((e) => e.timestamp > cutoff).slice(-this.config.maxEntries);
3420
+ this.journals.set(sessionId, trimmed);
3421
+ }
3422
+ /**
3423
+ * Check if the user message indicates a need for historical context.
3424
+ */
3425
+ needsContext(lastUserMessage) {
3426
+ if (!lastUserMessage || typeof lastUserMessage !== "string") {
3427
+ return false;
3428
+ }
3429
+ const lower = lastUserMessage.toLowerCase();
3430
+ const triggers = [
3431
+ // Direct questions about past work
3432
+ "what did you do",
3433
+ "what have you done",
3434
+ "what did we do",
3435
+ "what have we done",
3436
+ // Temporal references
3437
+ "earlier",
3438
+ "before",
3439
+ "previously",
3440
+ "this session",
3441
+ "today",
3442
+ "so far",
3443
+ // Summary requests
3444
+ "remind me",
3445
+ "summarize",
3446
+ "summary of",
3447
+ "recap",
3448
+ // Progress inquiries
3449
+ "your work",
3450
+ "your progress",
3451
+ "accomplished",
3452
+ "achievements",
3453
+ "completed tasks"
3454
+ ];
3455
+ return triggers.some((t) => lower.includes(t));
3456
+ }
3457
+ /**
3458
+ * Format the journal for injection into system message.
3459
+ * Returns null if journal is empty.
3460
+ */
3461
+ format(sessionId) {
3462
+ const journal = this.journals.get(sessionId);
3463
+ if (!journal?.length) {
3464
+ return null;
3465
+ }
3466
+ const lines = journal.map((e) => {
3467
+ const time = new Date(e.timestamp).toLocaleTimeString("en-US", {
3468
+ hour: "2-digit",
3469
+ minute: "2-digit",
3470
+ hour12: true
3471
+ });
3472
+ return `- ${time}: ${e.action}`;
3473
+ });
3474
+ return `[Session Memory - Key Actions]
3475
+ ${lines.join("\n")}`;
3476
+ }
3477
+ /**
3478
+ * Get the raw journal entries for a session (for debugging/testing).
3479
+ */
3480
+ getEntries(sessionId) {
3481
+ return this.journals.get(sessionId) || [];
3482
+ }
3483
+ /**
3484
+ * Clear journal for a specific session.
3485
+ */
3486
+ clear(sessionId) {
3487
+ this.journals.delete(sessionId);
3488
+ }
3489
+ /**
3490
+ * Clear all journals.
3491
+ */
3492
+ clearAll() {
3493
+ this.journals.clear();
3494
+ }
3495
+ /**
3496
+ * Get stats about the journal.
3497
+ */
3498
+ getStats() {
3499
+ let totalEntries = 0;
3500
+ for (const entries of this.journals.values()) {
3501
+ totalEntries += entries.length;
3502
+ }
3503
+ return {
3504
+ sessions: this.journals.size,
3505
+ totalEntries
3506
+ };
3507
+ }
3508
+ };
3509
+
3342
3510
  // src/proxy.ts
3343
3511
  var BLOCKRUN_API = "https://blockrun.ai/api";
3344
3512
  var AUTO_MODEL = "blockrun/auto";
@@ -3748,6 +3916,7 @@ async function startProxy(options) {
3748
3916
  const deduplicator = new RequestDeduplicator();
3749
3917
  const responseCache = new ResponseCache(options.cacheConfig);
3750
3918
  const sessionStore = new SessionStore(options.sessionConfig);
3919
+ const sessionJournal = new SessionJournal();
3751
3920
  const connections = /* @__PURE__ */ new Set();
3752
3921
  const server = createServer(async (req, res) => {
3753
3922
  req.on("error", (err) => {
@@ -3843,7 +4012,8 @@ async function startProxy(options) {
3843
4012
  deduplicator,
3844
4013
  balanceMonitor,
3845
4014
  sessionStore,
3846
- responseCache
4015
+ responseCache,
4016
+ sessionJournal
3847
4017
  );
3848
4018
  } catch (err) {
3849
4019
  const error = err instanceof Error ? err : new Error(String(err));
@@ -4044,7 +4214,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
4044
4214
  };
4045
4215
  }
4046
4216
  }
4047
- async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
4217
+ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache, sessionJournal) {
4048
4218
  const startTime = Date.now();
4049
4219
  const upstreamUrl = `${apiBase}${req.url}`;
4050
4220
  const bodyChunks = [];
@@ -4057,7 +4227,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4057
4227
  let modelId = "";
4058
4228
  let maxTokens = 4096;
4059
4229
  let routingProfile = null;
4230
+ let accumulatedContent = "";
4060
4231
  const isChatCompletion = req.url?.includes("/chat/completions");
4232
+ const sessionId = getSessionId(req.headers);
4061
4233
  if (isChatCompletion && body.length > 0) {
4062
4234
  try {
4063
4235
  const parsed = JSON.parse(body.toString());
@@ -4065,6 +4237,30 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4065
4237
  modelId = parsed.model || "";
4066
4238
  maxTokens = parsed.max_tokens || 4096;
4067
4239
  let bodyModified = false;
4240
+ if (sessionId && Array.isArray(parsed.messages)) {
4241
+ const messages = parsed.messages;
4242
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
4243
+ const lastContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
4244
+ if (sessionJournal.needsContext(lastContent)) {
4245
+ const journalText = sessionJournal.format(sessionId);
4246
+ if (journalText) {
4247
+ const sysIdx = messages.findIndex((m) => m.role === "system");
4248
+ if (sysIdx >= 0 && typeof messages[sysIdx].content === "string") {
4249
+ messages[sysIdx] = {
4250
+ ...messages[sysIdx],
4251
+ content: journalText + "\n\n" + messages[sysIdx].content
4252
+ };
4253
+ } else {
4254
+ messages.unshift({ role: "system", content: journalText });
4255
+ }
4256
+ parsed.messages = messages;
4257
+ bodyModified = true;
4258
+ console.log(
4259
+ `[ClawRouter] Injected session journal (${journalText.length} chars) for session ${sessionId.slice(0, 8)}...`
4260
+ );
4261
+ }
4262
+ }
4263
+ }
4068
4264
  if (parsed.stream === true) {
4069
4265
  parsed.stream = false;
4070
4266
  bodyModified = true;
@@ -4103,18 +4299,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4103
4299
  latencyMs: 0
4104
4300
  });
4105
4301
  } else {
4106
- const sessionId = getSessionId(
4302
+ const sessionId2 = getSessionId(
4107
4303
  req.headers
4108
4304
  );
4109
- const existingSession = sessionId ? sessionStore.getSession(sessionId) : void 0;
4305
+ const existingSession = sessionId2 ? sessionStore.getSession(sessionId2) : void 0;
4110
4306
  if (existingSession) {
4111
4307
  console.log(
4112
- `[ClawRouter] Session ${sessionId?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4308
+ `[ClawRouter] Session ${sessionId2?.slice(0, 8)}... using pinned model: ${existingSession.model}`
4113
4309
  );
4114
4310
  parsed.model = existingSession.model;
4115
4311
  modelId = existingSession.model;
4116
4312
  bodyModified = true;
4117
- sessionStore.touchSession(sessionId);
4313
+ sessionStore.touchSession(sessionId2);
4118
4314
  } else {
4119
4315
  const messages = parsed.messages;
4120
4316
  let lastUserMsg;
@@ -4143,10 +4339,10 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4143
4339
  parsed.model = routingDecision.model;
4144
4340
  modelId = routingDecision.model;
4145
4341
  bodyModified = true;
4146
- if (sessionId) {
4147
- sessionStore.setSession(sessionId, routingDecision.model, routingDecision.tier);
4342
+ if (sessionId2) {
4343
+ sessionStore.setSession(sessionId2, routingDecision.model, routingDecision.tier);
4148
4344
  console.log(
4149
- `[ClawRouter] Session ${sessionId.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4345
+ `[ClawRouter] Session ${sessionId2.slice(0, 8)}... pinned to model: ${routingDecision.model}`
4150
4346
  );
4151
4347
  }
4152
4348
  options.onRouted?.(routingDecision);
@@ -4481,6 +4677,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4481
4677
  const content = stripThinkingTokens(rawContent);
4482
4678
  const role = choice.message?.role ?? choice.delta?.role ?? "assistant";
4483
4679
  const index = choice.index ?? 0;
4680
+ if (content) {
4681
+ accumulatedContent += content;
4682
+ }
4484
4683
  const roleChunk = {
4485
4684
  ...baseChunk,
4486
4685
  choices: [{ index, delta: { role }, logprobs: null, finish_reason: null }]
@@ -4594,6 +4793,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
4594
4793
  });
4595
4794
  console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
4596
4795
  }
4796
+ try {
4797
+ const rspJson = JSON.parse(responseBody.toString());
4798
+ if (rspJson.choices?.[0]?.message?.content) {
4799
+ accumulatedContent = rspJson.choices[0].message.content;
4800
+ }
4801
+ } catch {
4802
+ }
4803
+ }
4804
+ if (sessionId && accumulatedContent) {
4805
+ const events = sessionJournal.extractEvents(accumulatedContent);
4806
+ if (events.length > 0) {
4807
+ sessionJournal.record(sessionId, events, actualModelUsed);
4808
+ console.log(
4809
+ `[ClawRouter] Recorded ${events.length} events to session journal for session ${sessionId.slice(0, 8)}...`
4810
+ );
4811
+ }
4597
4812
  }
4598
4813
  if (estimatedCostMicros !== void 0) {
4599
4814
  balanceMonitor.deductEstimated(estimatedCostMicros);