@blockrun/franklin 3.15.21 → 3.15.23

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 ───────────────────────────────────────────────────────────────
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
7
7
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
8
+ import { logger } from '../logger.js';
8
9
  // ─── Connection Management ────────────────────────────────────────────────
9
10
  const connections = new Map();
10
11
  /**
@@ -168,13 +169,9 @@ export async function connectMcpServers(config, debug) {
168
169
  if (serverConfig.disabled)
169
170
  continue;
170
171
  try {
171
- if (debug) {
172
- console.error(`[franklin] Connecting to MCP server: ${name}...`);
173
- }
172
+ logger.debug(`[franklin] Connecting to MCP server: ${name}...`);
174
173
  if (serverConfig.transport !== 'stdio') {
175
- if (debug) {
176
- console.error(`[franklin] MCP HTTP transport not yet supported for ${name}`);
177
- }
174
+ logger.debug(`[franklin] MCP HTTP transport not yet supported for ${name}`);
178
175
  continue;
179
176
  }
180
177
  // Timeout: don't let a slow server block startup
@@ -182,14 +179,18 @@ export async function connectMcpServers(config, debug) {
182
179
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('connection timeout (5s)')), MCP_CONNECT_TIMEOUT));
183
180
  const connected = await Promise.race([connectPromise, timeoutPromise]);
184
181
  allTools.push(...connected.tools);
185
- if (debug) {
186
- console.error(`[franklin] MCP ${name}: ${connected.tools.length} tools discovered`);
187
- }
182
+ logger.info(`[franklin] MCP ${name}: ${connected.tools.length} tools discovered`);
188
183
  }
189
184
  catch (err) {
190
185
  // Graceful degradation — one-line warning, continue without this server.
191
- // Always visible (not debug-only) so the user knows why tools are missing.
186
+ // Always written to franklin-debug.log so the user can post-mortem
187
+ // why tools went missing; ALSO printed to stderr at session boot
188
+ // so the user sees it in real time before the agent eats the
189
+ // terminal. Two separate writes (logger + stderr) is fine here —
190
+ // the user-visible "tool missing" notice has different timing
191
+ // requirements than the persistent log entry.
192
192
  const shortMsg = err.message?.split('\n')[0]?.slice(0, 100) || 'unknown error';
193
+ logger.warn(`[franklin] MCP ${name}: ${shortMsg}`);
193
194
  console.error(` ${name}: ${shortMsg} ${debug ? '' : '(--debug for details)'}`);
194
195
  }
195
196
  }
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
19
19
  import { loadChain, API_URLS, VERSION } from '../config.js';
20
+ import { logger } from '../logger.js';
20
21
  const TIMEOUT_MS = 30_000;
21
22
  // ─── Shared GET-with-x402 flow ────────────────────────────────────────────
22
23
  async function getWithPayment(path, ctx) {
@@ -90,7 +91,7 @@ async function signPayment(response, chain, endpoint) {
90
91
  return { 'PAYMENT-SIGNATURE': payload };
91
92
  }
92
93
  catch (err) {
93
- console.error(`[franklin] DefiLlama payment error: ${err.message}`);
94
+ logger.warn(`[franklin] DefiLlama payment error: ${err.message}`);
94
95
  return null;
95
96
  }
96
97
  }
package/dist/tools/exa.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
21
21
  import { loadChain, API_URLS, VERSION } from '../config.js';
22
+ import { logger } from '../logger.js';
22
23
  const GEN_TIMEOUT_MS = 30_000;
23
24
  // ─── Shared payment flow ─────────────────────────────────────────────
24
25
  async function postWithPayment(path, body, ctx) {
@@ -95,7 +96,7 @@ async function signPayment(response, chain, endpoint) {
95
96
  return { 'PAYMENT-SIGNATURE': payload };
96
97
  }
97
98
  catch (err) {
98
- console.error(`[franklin] Exa payment error: ${err.message}`);
99
+ logger.warn(`[franklin] Exa payment error: ${err.message}`);
99
100
  return null;
100
101
  }
101
102
  }
@@ -11,6 +11,7 @@ import { ModelClient } from '../agent/llm.js';
11
11
  import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
12
12
  import { recordUsage } from '../stats/tracker.js';
13
13
  import { findModel, estimateCostUsd } from '../gateway-models.js';
14
+ import { logger } from '../logger.js';
14
15
  /**
15
16
  * Models that accept a reference image via /v1/images/image2image. Currently
16
17
  * limited to OpenAI's edit endpoint — Gemini Nano Banana Pro and Grok Imagine
@@ -391,7 +392,7 @@ async function signPayment(response, chain, endpoint) {
391
392
  }
392
393
  }
393
394
  catch (err) {
394
- console.error(`[franklin] Image payment error: ${err.message}`);
395
+ logger.warn(`[franklin] Image payment error: ${err.message}`);
395
396
  return null;
396
397
  }
397
398
  }
@@ -29,6 +29,7 @@ import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, creat
29
29
  import { loadChain, API_URLS, VERSION } from '../config.js';
30
30
  import { walletReservation } from '../wallet/reservation.js';
31
31
  import { recordUsage } from '../stats/tracker.js';
32
+ import { logger } from '../logger.js';
32
33
  // ─── Pricing table (probed from /.well-known/x402 + 402 responses) ─────────
33
34
  const CREATE_PRICE_USD = {
34
35
  cpu: 0.01,
@@ -95,7 +96,7 @@ async function signPayment(response, chain, endpoint, resourceDescription) {
95
96
  }
96
97
  }
97
98
  catch (err) {
98
- console.error(`[franklin] Modal payment error: ${err.message}`);
99
+ logger.warn(`[franklin] Modal payment error: ${err.message}`);
99
100
  return null;
100
101
  }
101
102
  }
@@ -17,6 +17,7 @@ import fs from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
19
19
  import { loadChain, API_URLS, VERSION } from '../config.js';
20
+ import { logger } from '../logger.js';
20
21
  const DEFAULT_MODEL = 'minimax/music-2.5+';
21
22
  const PRICE_USD = 0.1575;
22
23
  // MiniMax generation is 1-3 minutes + small buffer for payment + download.
@@ -199,7 +200,7 @@ async function signPayment(response, chain, endpoint) {
199
200
  return { 'PAYMENT-SIGNATURE': payload };
200
201
  }
201
202
  catch (err) {
202
- console.error(`[franklin] Music payment error: ${err.message}`);
203
+ logger.warn(`[franklin] Music payment error: ${err.message}`);
203
204
  return null;
204
205
  }
205
206
  }
@@ -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);
@@ -22,6 +22,7 @@ import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
24
24
  import { loadChain, API_URLS, VERSION } from '../config.js';
25
+ import { logger } from '../logger.js';
25
26
  import { ModelClient } from '../agent/llm.js';
26
27
  import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
27
28
  import { recordUsage } from '../stats/tracker.js';
@@ -385,7 +386,7 @@ async function signPayment(response, chain, endpoint) {
385
386
  return { 'PAYMENT-SIGNATURE': payload };
386
387
  }
387
388
  catch (err) {
388
- console.error(`[franklin] Video payment error: ${err.message}`);
389
+ logger.warn(`[franklin] Video payment error: ${err.message}`);
389
390
  return null;
390
391
  }
391
392
  }
@@ -24,6 +24,7 @@ import { createWalletClient, http, publicActions } from 'viem';
24
24
  import { getOrCreateWallet } from '@blockrun/llm';
25
25
  import { loadConfig } from '../commands/config.js';
26
26
  import { loadChain, API_URLS, VERSION } from '../config.js';
27
+ import { logger } from '../logger.js';
27
28
  // ─── Constants ────────────────────────────────────────────────────────────
28
29
  const ZEROX_GATEWAY_PATH = '/v1/zerox/gasless';
29
30
  const QUOTE_TIMEOUT_MS = 30_000;
@@ -235,7 +236,7 @@ async function pollUntilDone(tradeHash, ctx) {
235
236
  }
236
237
  catch (err) {
237
238
  // Surface a transient failure but keep polling — relayer might be backlogged.
238
- console.error(`[franklin] gasless status poll error: ${err.message}`);
239
+ logger.warn(`[franklin] gasless status poll error: ${err.message}`);
239
240
  }
240
241
  if (last.status === 'confirmed' ||
241
242
  last.status === 'succeeded' ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.21",
3
+ "version": "3.15.23",
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": {