@blockrun/franklin 3.15.20 → 3.15.22

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.
@@ -11,6 +11,16 @@ const ENTITIES_FILE = path.join(BRAIN_DIR, 'entities.jsonl');
11
11
  const OBSERVATIONS_FILE = path.join(BRAIN_DIR, 'observations.jsonl');
12
12
  const RELATIONS_FILE = path.join(BRAIN_DIR, 'relations.jsonl');
13
13
  const MAX_ENTITIES = 200;
14
+ // Observations and relations were previously unbounded — `extract.ts`
15
+ // runs at every session end (commands/start.ts:515) so they grew
16
+ // linearly forever. Caps below give comfortable headroom for a year+
17
+ // of normal use without making per-entity scans pathological:
18
+ // - 2000 obs / 200 entities = ~10 observations per entity on average
19
+ // - 500 relations covers heavy cross-references between the entity set
20
+ // On cap breach we drop the oldest entries — younger observations are
21
+ // usually more relevant and more confident than an aging one.
22
+ const MAX_OBSERVATIONS = 2000;
23
+ const MAX_RELATIONS = 500;
14
24
  function uid() { return crypto.randomBytes(8).toString('hex'); }
15
25
  function ensureDir() {
16
26
  fs.mkdirSync(BRAIN_DIR, { recursive: true });
@@ -110,7 +120,7 @@ export function addObservation(entityId, content, source, confidence = 0.8, tags
110
120
  if (existing.some(o => o.entity_id === entityId && o.content.toLowerCase().trim() === contentLower)) {
111
121
  return;
112
122
  }
113
- appendJsonl(OBSERVATIONS_FILE, {
123
+ const next = {
114
124
  id: uid(),
115
125
  entity_id: entityId,
116
126
  content,
@@ -118,7 +128,18 @@ export function addObservation(entityId, content, source, confidence = 0.8, tags
118
128
  confidence,
119
129
  tags,
120
130
  created_at: Date.now(),
121
- });
131
+ };
132
+ // Cap reached — rewrite the file with the trimmed set instead of
133
+ // appending. saveJsonl is atomic (tmp + rename) so a crash mid-write
134
+ // can't corrupt observations.
135
+ if (existing.length >= MAX_OBSERVATIONS) {
136
+ existing.sort((a, b) => b.created_at - a.created_at);
137
+ existing.length = MAX_OBSERVATIONS - 1;
138
+ existing.unshift(next);
139
+ saveJsonl(OBSERVATIONS_FILE, existing);
140
+ return;
141
+ }
142
+ appendJsonl(OBSERVATIONS_FILE, next);
122
143
  }
123
144
  // ─── Relations ────────────────────────────────────────────────────────────
124
145
  export function loadRelations() {
@@ -140,7 +161,7 @@ export function upsertRelation(fromId, toId, type, confidence = 0.8) {
140
161
  saveJsonl(RELATIONS_FILE, relations);
141
162
  }
142
163
  else {
143
- appendJsonl(RELATIONS_FILE, {
164
+ const next = {
144
165
  id: uid(),
145
166
  from_id: fromId,
146
167
  to_id: toId,
@@ -148,7 +169,25 @@ export function upsertRelation(fromId, toId, type, confidence = 0.8) {
148
169
  confidence,
149
170
  count: 1,
150
171
  last_seen: Date.now(),
151
- });
172
+ };
173
+ // Same cap pattern as observations. When the relation set is at
174
+ // its ceiling we drop the lowest-count, oldest-seen entries to
175
+ // make room — count + recency together approximate "this
176
+ // relation is still being seen", so eviction targets entries the
177
+ // brain extractor hasn't reinforced in a while.
178
+ if (relations.length >= MAX_RELATIONS) {
179
+ relations.sort((a, b) => {
180
+ if (b.count !== a.count)
181
+ return b.count - a.count;
182
+ return b.last_seen - a.last_seen;
183
+ });
184
+ relations.length = MAX_RELATIONS - 1;
185
+ relations.unshift(next);
186
+ saveJsonl(RELATIONS_FILE, relations);
187
+ }
188
+ else {
189
+ appendJsonl(RELATIONS_FILE, next);
190
+ }
152
191
  }
153
192
  }
154
193
  // ─── Search ───────────────────────────────────────────────────────────────
@@ -2,19 +2,7 @@
2
2
  * Fallback chain for Franklin
3
3
  * Automatically switches to backup models when primary fails (429, 5xx, etc.)
4
4
  */
5
- import fs from 'node:fs';
6
- import os from 'node:os';
7
- import path from 'node:path';
8
- const LOG_FILE = path.join(os.homedir(), '.blockrun', 'franklin-debug.log');
9
- // eslint-disable-next-line no-control-regex
10
- const ANSI_RE = /\x1B\[[0-9;]*[A-Za-z]|\x1B\][^\x07]*\x07|\x1B[()][A-B]|\r/g;
11
- function appendLog(msg) {
12
- try {
13
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
14
- fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg.replace(ANSI_RE, '')}\n`);
15
- }
16
- catch { /* ignore */ }
17
- }
5
+ import { logger } from '../logger.js';
18
6
  export const DEFAULT_FALLBACK_CONFIG = {
19
7
  chain: [
20
8
  'deepseek/deepseek-chat', // Direct fallback — cheap & reliable
@@ -93,7 +81,7 @@ export async function fetchWithFallback(url, init, originalBody, config = DEFAUL
93
81
  if (nextModel && onFallback) {
94
82
  const errMsg = err instanceof Error ? err.message : 'Network error';
95
83
  onFallback(model, 0, nextModel);
96
- appendLog(`[franklin] [fallback] ${model} network error: ${errMsg}`);
84
+ logger.warn(`[franklin] [fallback] ${model} network error: ${errMsg}`);
97
85
  }
98
86
  if (i < config.chain.length - 1) {
99
87
  await sleep(config.retryDelayMs);
@@ -1,7 +1,4 @@
1
1
  import http from 'node:http';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import os from 'node:os';
5
2
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
6
3
  import { recordUsage } from '../stats/tracker.js';
7
4
  import { appendAudit } from '../stats/audit.js';
@@ -12,34 +9,13 @@ import { VERSION } from '../config.js';
12
9
  // User-Agent for backend requests
13
10
  const USER_AGENT = `franklin/${VERSION}`;
14
11
  const X_FRANKLIN_VERSION = VERSION;
15
- const LOG_FILE = path.join(os.homedir(), '.blockrun', 'franklin-debug.log');
16
- // Strip ANSI escape codes so log file doesn't distort terminal on replay
17
- function stripAnsi(str) {
18
- // eslint-disable-next-line no-control-regex
19
- return str.replace(/\x1B\[[0-9;]*[A-Za-z]|\x1B\][^\x07]*\x07|\x1B[()][A-B]|\r/g, '');
20
- }
21
- function debug(options, ...args) {
22
- if (!options.debug)
23
- return;
24
- const msg = `[${new Date().toISOString()}] ${stripAnsi(args.map(String).join(' '))}\n`;
25
- try {
26
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
27
- fs.appendFileSync(LOG_FILE, msg);
28
- }
29
- catch {
30
- /* ignore */
31
- }
32
- }
33
- function log(...args) {
34
- const msg = `[franklin] ${args.map(String).join(' ')}`;
35
- // Do NOT print to stdout — the terminal is owned by the parent process (stdio: inherit).
36
- // Use `franklin logs` to read runtime messages.
37
- try {
38
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
39
- fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${stripAnsi(msg)}\n`);
40
- }
41
- catch { /* ignore */ }
42
- }
12
+ // Logging here goes through the unified logger introduced in 3.15.11
13
+ // (timestamp + [LEVEL] tag, self-rotating at 10 MB, optional stderr
14
+ // mirror in debug mode). The previous per-module debug()/log() helpers
15
+ // duplicated the file path, ANSI strip regex (with a slightly different
16
+ // pattern!), and timestamp format — bug fixes never propagated. They
17
+ // were the last holdouts after the agent loop was migrated.
18
+ import { logger, setDebugMode } from '../logger.js';
43
19
  const DEFAULT_MAX_TOKENS = 4096;
44
20
  // 180s budget for *time-to-headers* — reasoning-class models (zai/glm-*,
45
21
  // nemotron *-reasoning, deepseek-r*, gpt-5-codex, anthropic extended-thinking)
@@ -249,6 +225,10 @@ function withinRateLimit() {
249
225
  return true;
250
226
  }
251
227
  export function createProxy(options) {
228
+ // Wire stderr-mirroring of unified logger output to the proxy's debug
229
+ // flag — same pattern as interactiveSession in agent/loop. File writes
230
+ // happen regardless; only the live stderr mirror is gated.
231
+ setDebugMode(!!options.debug);
252
232
  const chain = options.chain || 'base';
253
233
  let currentModel = options.modelOverride || DEFAULT_MODEL;
254
234
  const fallbackEnabled = options.fallbackEnabled !== false; // Default true
@@ -307,19 +287,22 @@ export function createProxy(options) {
307
287
  let requestModel = currentModel || options.modelOverride || 'unknown';
308
288
  let usedFallback = false;
309
289
  try {
310
- debug(options, `request: ${req.method} ${req.url} currentModel=${currentModel || 'none'}`);
290
+ if (options.debug)
291
+ logger.debug(`[franklin] request: ${req.method} ${req.url} currentModel=${currentModel || 'none'}`);
311
292
  if (body) {
312
293
  try {
313
294
  const parsed = JSON.parse(body);
314
295
  // Intercept "use <model>" commands for in-session model switching
315
296
  if (parsed.messages) {
316
297
  const last = parsed.messages[parsed.messages.length - 1];
317
- debug(options, `last msg role=${last?.role} content-type=${typeof last?.content} content=${JSON.stringify(last?.content).slice(0, 200)}`);
298
+ if (options.debug)
299
+ logger.debug(`[franklin] last msg role=${last?.role} content-type=${typeof last?.content} content=${JSON.stringify(last?.content).slice(0, 200)}`);
318
300
  }
319
301
  const switchCmd = detectModelSwitch(parsed);
320
302
  if (switchCmd) {
321
303
  currentModel = switchCmd;
322
- debug(options, `model switched to: ${currentModel}`);
304
+ if (options.debug)
305
+ logger.debug(`[franklin] model switched to: ${currentModel}`);
323
306
  const fakeResponse = {
324
307
  id: `msg_franklin_${Date.now()}`,
325
308
  type: 'message',
@@ -374,7 +357,7 @@ export function createProxy(options) {
374
357
  const routing = routeRequest(promptText, routingProfile);
375
358
  parsed.model = routing.model;
376
359
  requestModel = routing.model;
377
- log(`🧠 Smart routing: ${routingProfile} → ${routing.tier} → ${routing.model} ` +
360
+ logger.info(`[franklin] 🧠 Smart routing: ${routingProfile} → ${routing.tier} → ${routing.model} ` +
378
361
  `(${(routing.savings * 100).toFixed(0)}% savings) [${routing.signals.join(', ')}]`);
379
362
  }
380
363
  {
@@ -392,8 +375,8 @@ export function createProxy(options) {
392
375
  ? Math.max(lastOut * 2, DEFAULT_MAX_TOKENS)
393
376
  : DEFAULT_MAX_TOKENS;
394
377
  parsed.max_tokens = Math.min(adaptive, modelCap);
395
- if (original !== parsed.max_tokens) {
396
- debug(options, `max_tokens: ${original || 'unset'} → ${parsed.max_tokens} (last output: ${lastOut || 'none'})`);
378
+ if (original !== parsed.max_tokens && options.debug) {
379
+ logger.debug(`[franklin] max_tokens: ${original || 'unset'} → ${parsed.max_tokens} (last output: ${lastOut || 'none'})`);
397
380
  }
398
381
  }
399
382
  body = JSON.stringify(parsed);
@@ -430,7 +413,7 @@ export function createProxy(options) {
430
413
  body = JSON.stringify(parsed);
431
414
  }
432
415
  catch { /* body not JSON, skip */ }
433
- log(`⚠️ Safety net: resolved unrouted ${virtualName} → ${requestModel}`);
416
+ logger.warn(`[franklin] ⚠️ Safety net: resolved unrouted ${virtualName} → ${requestModel}`);
434
417
  }
435
418
  }
436
419
  // Build request init
@@ -456,7 +439,7 @@ export function createProxy(options) {
456
439
  solanaWallet,
457
440
  timeoutMs: requestTimeoutMs,
458
441
  }, (failedModel, status, nextModel) => {
459
- log(`⚠️ ${failedModel} returned ${status}, falling back to ${nextModel}`);
442
+ logger.warn(`[franklin] ⚠️ ${failedModel} returned ${status}, falling back to ${nextModel}`);
460
443
  });
461
444
  response = result.response;
462
445
  finalModel = result.modelUsed;
@@ -464,7 +447,7 @@ export function createProxy(options) {
464
447
  body = result.bodyUsed;
465
448
  usedFallback = result.fallbackUsed;
466
449
  if (usedFallback) {
467
- log(`↺ Fallback successful: using ${finalModel}`);
450
+ logger.info(`[franklin] Fallback successful: using ${finalModel}`);
468
451
  }
469
452
  }
470
453
  else {
@@ -516,7 +499,7 @@ export function createProxy(options) {
516
499
  }
517
500
  res.writeHead(response.status, { 'Content-Type': 'application/json' });
518
501
  res.end(errorBody);
519
- log(`⚠️ ${response.status} from backend for ${finalModel}`);
502
+ logger.warn(`[franklin] ⚠️ ${response.status} from backend for ${finalModel}`);
520
503
  return;
521
504
  }
522
505
  res.writeHead(response.status, responseHeaders);
@@ -531,7 +514,7 @@ export function createProxy(options) {
531
514
  const pump = async () => {
532
515
  while (true) {
533
516
  if (Date.now() > streamDeadline) {
534
- log('⚠️ Stream timeout after 5 minutes');
517
+ logger.warn('[franklin] ⚠️ Stream timeout after 5 minutes');
535
518
  try {
536
519
  reader.cancel();
537
520
  }
@@ -576,7 +559,8 @@ export function createProxy(options) {
576
559
  fallback: usedFallback,
577
560
  source: 'proxy',
578
561
  });
579
- debug(options, `recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
562
+ if (options.debug)
563
+ logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
580
564
  }
581
565
  }
582
566
  res.end();
@@ -590,7 +574,7 @@ export function createProxy(options) {
590
574
  }
591
575
  };
592
576
  pump().catch((err) => {
593
- log(`❌ Stream error: ${err instanceof Error ? err.message : String(err)}`);
577
+ logger.error(`[franklin] Stream error: ${err instanceof Error ? err.message : String(err)}`);
594
578
  res.end();
595
579
  });
596
580
  }
@@ -615,7 +599,8 @@ export function createProxy(options) {
615
599
  fallback: usedFallback,
616
600
  source: 'proxy',
617
601
  });
618
- debug(options, `recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
602
+ if (options.debug)
603
+ logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
619
604
  }
620
605
  }
621
606
  catch {
@@ -626,7 +611,7 @@ export function createProxy(options) {
626
611
  }
627
612
  catch (error) {
628
613
  const msg = error instanceof Error ? error.message : 'Proxy error';
629
- log(`❌ Error: ${msg}`);
614
+ logger.error(`[franklin] Error: ${msg}`);
630
615
  res.writeHead(502, { 'Content-Type': 'application/json' });
631
616
  res.end(JSON.stringify({
632
617
  type: 'error',
@@ -693,7 +678,7 @@ async function fetchWithPaymentFallback(url, init, originalBody, config, payment
693
678
  if (nextModel && onFallback) {
694
679
  onFallback(model, 0, nextModel);
695
680
  }
696
- log(`[fallback] ${model} request error: ${err instanceof Error ? err.message : String(err)}`);
681
+ logger.warn(`[franklin] [fallback] ${model} request error: ${err instanceof Error ? err.message : String(err)}`);
697
682
  if (i < config.chain.length - 1) {
698
683
  await sleep(config.retryDelayMs);
699
684
  }
@@ -163,7 +163,7 @@ async function execute(input, ctx) {
163
163
  try {
164
164
  switch (action) {
165
165
  case 'searchPolymarket': {
166
- const raw = await getWithPayment('/api/v1/pm/polymarket/markets', {
166
+ const raw = await getWithPayment('/v1/pm/polymarket/markets', {
167
167
  search,
168
168
  status: status ?? 'active',
169
169
  sort: sort ?? 'volume',
@@ -193,7 +193,7 @@ async function execute(input, ctx) {
193
193
  return { output: lines.join('\n') };
194
194
  }
195
195
  case 'searchKalshi': {
196
- const raw = await getWithPayment('/api/v1/pm/kalshi/markets', {
196
+ const raw = await getWithPayment('/v1/pm/kalshi/markets', {
197
197
  search,
198
198
  status: status ?? 'open',
199
199
  sort: sort ?? 'volume',
@@ -223,7 +223,7 @@ async function execute(input, ctx) {
223
223
  return { output: lines.join('\n') };
224
224
  }
225
225
  case 'crossPlatform': {
226
- const raw = await getWithPayment('/api/v1/pm/matching-markets/pairs', {
226
+ const raw = await getWithPayment('/v1/pm/matching-markets/pairs', {
227
227
  limit: cappedLimit,
228
228
  }, ctx);
229
229
  const pairs = unwrapList(raw);
@@ -249,7 +249,7 @@ async function execute(input, ctx) {
249
249
  if (!conditionId) {
250
250
  return { output: 'Error: conditionId is required for smartMoney (Polymarket condition_id from a prior searchPolymarket call)', isError: true };
251
251
  }
252
- const path = `/api/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
252
+ const path = `/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
253
253
  const data = await getWithPayment(path, {}, ctx);
254
254
  const buyers = (data.buyers ?? []).slice(0, 5);
255
255
  const sellers = (data.sellers ?? []).slice(0, 5);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.20",
3
+ "version": "3.15.22",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {