@blockrun/franklin 3.15.16 → 3.15.18

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.
@@ -22,6 +22,8 @@ import { recordSessionUsage } from '../stats/session-tracker.js';
22
22
  import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
23
23
  import { logger, setDebugMode } from '../logger.js';
24
24
  import { runDataHygiene } from '../storage/hygiene.js';
25
+ import { isTestFixtureModel } from '../stats/test-fixture.js';
26
+ import { setSessionPersistenceDisabled } from '../session/storage.js';
25
27
  import { estimateCost, OPUS_PRICING } from '../pricing.js';
26
28
  import { maybeMidSessionExtract } from '../learnings/extractor.js';
27
29
  import { extractMentions, buildEntityContext, loadEntities } from '../brain/store.js';
@@ -330,6 +332,14 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
330
332
  // Wire stderr-mirroring of log lines to the same flag the agent already
331
333
  // uses to gate verbose console output. File writes happen regardless.
332
334
  setDebugMode(!!config.debug);
335
+ // In-process tests run interactiveSession() with model="local/test*"
336
+ // and were creating real session files on the user's machine —
337
+ // verified 19 of 33 metas (57.6%) were polluted on a real install.
338
+ // Gate session persistence at the entry point so the rest of the
339
+ // loop doesn't have to thread the flag through. Tests that genuinely
340
+ // exercise the persistence path use a non-fixture model name like
341
+ // `zai/glm-5.1` (mock-server-backed) so they keep writing.
342
+ setSessionPersistenceDisabled(isTestFixtureModel(config.model));
333
343
  const client = new ModelClient({
334
344
  apiUrl: config.apiUrl,
335
345
  chain: config.chain,
@@ -8,6 +8,7 @@
8
8
  import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import { BLOCKRUN_DIR } from '../config.js';
11
+ import { isTestFixtureModel } from '../stats/test-fixture.js';
11
12
  const HISTORY_FILE = path.join(BLOCKRUN_DIR, 'router-history.jsonl');
12
13
  const MAX_RECORDS = 2000;
13
14
  const K_FACTOR = 32; // Elo K-factor — how much each outcome shifts the rating
@@ -15,6 +16,13 @@ const K_FACTOR = 32; // Elo K-factor — how much each outcome shifts the rating
15
16
  * Record a model outcome for local learning.
16
17
  */
17
18
  export function recordOutcome(category, model, outcome, toolCalls) {
19
+ // Defensive: same fixture-model gate as appendAudit / recordUsage.
20
+ // router-history.jsonl is currently clean (test runs typically have
21
+ // an empty `lastRoutedCategory` and the agent loop already guards
22
+ // against that), but a future change to category detection would
23
+ // immediately leak. Belt-and-braces.
24
+ if (isTestFixtureModel(model))
25
+ return;
18
26
  try {
19
27
  fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
20
28
  const record = { ts: Date.now(), category, model, outcome, toolCalls };
@@ -3,6 +3,8 @@
3
3
  * Saves conversation history as JSONL for resume capability.
4
4
  */
5
5
  import type { Dialogue } from '../agent/types.js';
6
+ export declare function setSessionPersistenceDisabled(disabled: boolean): void;
7
+ export declare function isSessionPersistenceDisabled(): boolean;
6
8
  export interface SessionMeta {
7
9
  id: string;
8
10
  model: string;
@@ -9,6 +9,20 @@ import { randomUUID } from 'node:crypto';
9
9
  import { BLOCKRUN_DIR } from '../config.js';
10
10
  const MAX_SESSIONS = 20; // Keep last 20 sessions
11
11
  let resolvedSessionsDir = null;
12
+ // When in-process tests run interactiveSession() with model="local/test*",
13
+ // session writes were creating real .jsonl + .meta.json files in the
14
+ // user's ~/.blockrun/sessions/ — verified 19 of 33 metas (57.6%) on a
15
+ // real machine. Toggled at session start by the agent loop based on the
16
+ // model name; defaults to enabled so production never accidentally goes
17
+ // silent. No-op writes when disabled — reads still work so resume tests
18
+ // can pre-seed state with their own writes if they want to.
19
+ let persistenceDisabled = false;
20
+ export function setSessionPersistenceDisabled(disabled) {
21
+ persistenceDisabled = disabled;
22
+ }
23
+ export function isSessionPersistenceDisabled() {
24
+ return persistenceDisabled;
25
+ }
12
26
  function getSessionsDir() {
13
27
  if (resolvedSessionsDir)
14
28
  return resolvedSessionsDir;
@@ -69,6 +83,8 @@ export function createSessionId() {
69
83
  * Save a message to the session transcript (append-only JSONL).
70
84
  */
71
85
  export function appendToSession(sessionId, message) {
86
+ if (persistenceDisabled)
87
+ return;
72
88
  const line = JSON.stringify(message) + '\n';
73
89
  withWritableSessionDir(() => {
74
90
  fs.appendFileSync(sessionPath(sessionId), line);
@@ -78,6 +94,8 @@ export function appendToSession(sessionId, message) {
78
94
  * Update session metadata.
79
95
  */
80
96
  export function updateSessionMeta(sessionId, meta) {
97
+ if (persistenceDisabled)
98
+ return;
81
99
  withWritableSessionDir(() => {
82
100
  const existing = loadSessionMeta(sessionId);
83
101
  const updated = {
@@ -61,6 +61,10 @@ export function runDataHygiene() {
61
61
  removeLegacyFiles();
62
62
  }
63
63
  catch { /* best effort */ }
64
+ try {
65
+ sweepOrphanToolResults();
66
+ }
67
+ catch { /* best effort */ }
64
68
  }
65
69
  function trimDataDir() {
66
70
  const dir = path.join(BLOCKRUN_DIR, 'data');
@@ -132,3 +136,57 @@ function removeLegacyFiles() {
132
136
  catch { /* ok */ }
133
137
  }
134
138
  }
139
+ /**
140
+ * `streaming-executor` writes large tool outputs to
141
+ * `~/.blockrun/tool-results/<sessionId>/<toolUseId>.txt`. When a session is
142
+ * pruned by `pruneOldSessions`, the meta + jsonl are deleted but the
143
+ * tool-results dir is left dangling. Verified on a real machine: 5 dirs,
144
+ * oldest from 4/14 (3 weeks past MAX_SESSIONS=20 retention).
145
+ *
146
+ * A tool-results dir is considered orphan when its name (the session id)
147
+ * has no `<sessionId>.meta.json` partner in the sessions/ dir. The active
148
+ * session is implicitly protected because its meta exists.
149
+ */
150
+ function sweepOrphanToolResults() {
151
+ const toolResultsDir = path.join(BLOCKRUN_DIR, 'tool-results');
152
+ const sessionsDir = path.join(BLOCKRUN_DIR, 'sessions');
153
+ if (!fs.existsSync(toolResultsDir))
154
+ return;
155
+ const knownSessionIds = new Set();
156
+ if (fs.existsSync(sessionsDir)) {
157
+ try {
158
+ for (const f of fs.readdirSync(sessionsDir)) {
159
+ if (f.endsWith('.meta.json')) {
160
+ knownSessionIds.add(f.slice(0, -'.meta.json'.length));
161
+ }
162
+ }
163
+ }
164
+ catch {
165
+ // Best-effort — if we can't read sessions/, skip the sweep so
166
+ // we never delete tool-results that might still belong to a
167
+ // live session.
168
+ return;
169
+ }
170
+ }
171
+ let entries;
172
+ try {
173
+ entries = fs.readdirSync(toolResultsDir);
174
+ }
175
+ catch {
176
+ return;
177
+ }
178
+ for (const name of entries) {
179
+ if (knownSessionIds.has(name))
180
+ continue;
181
+ const dir = path.join(toolResultsDir, name);
182
+ try {
183
+ const stat = fs.statSync(dir);
184
+ if (!stat.isDirectory())
185
+ continue;
186
+ fs.rmSync(dir, { recursive: true, force: true });
187
+ }
188
+ catch {
189
+ // Skip — best-effort cleanup.
190
+ }
191
+ }
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.16",
3
+ "version": "3.15.18",
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": {