@blockrun/franklin 3.15.41 → 3.15.43

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.
@@ -1236,9 +1236,16 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1236
1236
  savings: routingSavings,
1237
1237
  contextPct: Math.round(contextUsagePct),
1238
1238
  });
1239
- // Record usage for stats tracking (franklin stats command)
1239
+ // Record usage for stats tracking (franklin stats command).
1240
+ // Pass the fallback flag so franklin-stats.json's totalFallbacks +
1241
+ // per-model fallbackCount stay in sync with the audit log a few
1242
+ // lines below — same `turnFailedModels.size > 0` predicate, same
1243
+ // turn. Without this, stats showed 0 fallbacks across 5150 real
1244
+ // requests on a machine that visibly hit fallback paths in
1245
+ // franklin-debug.log; `franklin insights` was therefore useless
1246
+ // for spotting a hot routing chain.
1240
1247
  const costEstimate = estimateCost(resolvedModel, inputTokens, usage.outputTokens, 1);
1241
- recordUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, 0);
1248
+ recordUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, 0, turnFailedModels.size > 0);
1242
1249
  // ── Circuit breakers: prevent infinite-loop wallet drain ──
1243
1250
  // Per-turn $-cap was removed in v3.11.0 — runaway loops are caught by
1244
1251
  // MAX_TOOL_CALLS_PER_TURN (25) and MAX_TINY_RESPONSES (2) above; the
@@ -3,8 +3,24 @@
3
3
  * meta.json — single TaskRecord, atomically rewritten
4
4
  * events.jsonl — append-only event log
5
5
  * log.txt — child process stdout/stderr
6
+ *
7
+ * Storage location: defaults to BLOCKRUN_DIR (~/.blockrun), matching
8
+ * every other persistent state in the codebase (sessions, audit, stats,
9
+ * brain, etc.). Earlier releases used ~/.franklin instead, so we
10
+ * lazily fall back to that legacy directory on reads when a task isn't
11
+ * found in the primary location. New tasks always write to the primary.
12
+ *
13
+ * Why a lazy fallback instead of a startup migration: a long-running
14
+ * task runner (`franklin _task-runner <runId>`) captures its task dir
15
+ * path in memory at spawn and continues writing there for the duration
16
+ * of the run. Verified 2026-05-04: an in-flight ETL task at PID 59095
17
+ * had been writing to ~/.franklin/tasks/ for 4 minutes, with ~10 hours
18
+ * of progress still ahead. Moving the directory mid-flight would
19
+ * orphan its writes; the fallback path lets new CLI commands keep
20
+ * reading legacy task state without disturbing an active runner.
6
21
  */
7
22
  export declare function getTasksDir(): string;
23
+ export declare function getLegacyTasksDir(): string;
8
24
  export declare function getTaskDir(runId: string): string;
9
25
  export declare function ensureTaskDir(runId: string): string;
10
26
  export declare function taskMetaPath(runId: string): string;
@@ -3,18 +3,51 @@
3
3
  * meta.json — single TaskRecord, atomically rewritten
4
4
  * events.jsonl — append-only event log
5
5
  * log.txt — child process stdout/stderr
6
+ *
7
+ * Storage location: defaults to BLOCKRUN_DIR (~/.blockrun), matching
8
+ * every other persistent state in the codebase (sessions, audit, stats,
9
+ * brain, etc.). Earlier releases used ~/.franklin instead, so we
10
+ * lazily fall back to that legacy directory on reads when a task isn't
11
+ * found in the primary location. New tasks always write to the primary.
12
+ *
13
+ * Why a lazy fallback instead of a startup migration: a long-running
14
+ * task runner (`franklin _task-runner <runId>`) captures its task dir
15
+ * path in memory at spawn and continues writing there for the duration
16
+ * of the run. Verified 2026-05-04: an in-flight ETL task at PID 59095
17
+ * had been writing to ~/.franklin/tasks/ for 4 minutes, with ~10 hours
18
+ * of progress still ahead. Moving the directory mid-flight would
19
+ * orphan its writes; the fallback path lets new CLI commands keep
20
+ * reading legacy task state without disturbing an active runner.
6
21
  */
7
22
  import fs from 'node:fs';
8
23
  import os from 'node:os';
9
24
  import path from 'node:path';
25
+ import { BLOCKRUN_DIR } from '../config.js';
26
+ const LEGACY_FRANKLIN_HOME = path.join(os.homedir(), '.franklin');
10
27
  function franklinHome() {
11
- return process.env.FRANKLIN_HOME || path.join(os.homedir(), '.franklin');
28
+ return process.env.FRANKLIN_HOME || BLOCKRUN_DIR;
12
29
  }
13
30
  export function getTasksDir() {
14
31
  return path.join(franklinHome(), 'tasks');
15
32
  }
33
+ export function getLegacyTasksDir() {
34
+ return path.join(LEGACY_FRANKLIN_HOME, 'tasks');
35
+ }
16
36
  export function getTaskDir(runId) {
17
- return path.join(getTasksDir(), runId);
37
+ // Prefer the primary location. If a task already exists in the
38
+ // legacy ~/.franklin/tasks/ — either created by an older release or
39
+ // by a runner subprocess started before this version was installed —
40
+ // continue to read/write there until it completes, so we don't strand
41
+ // its in-flight events.jsonl + meta.json writes.
42
+ const primary = path.join(getTasksDir(), runId);
43
+ if (fs.existsSync(primary))
44
+ return primary;
45
+ if (process.env.FRANKLIN_HOME === undefined) {
46
+ const legacy = path.join(getLegacyTasksDir(), runId);
47
+ if (fs.existsSync(legacy))
48
+ return legacy;
49
+ }
50
+ return primary;
18
51
  }
19
52
  export function ensureTaskDir(runId) {
20
53
  const dir = getTaskDir(runId);
@@ -16,7 +16,7 @@
16
16
  * tolerant of a torn last line.
17
17
  */
18
18
  import fs from 'node:fs';
19
- import { ensureTaskDir, taskMetaPath, taskEventsPath, getTasksDir, } from './paths.js';
19
+ import { ensureTaskDir, taskMetaPath, taskEventsPath, getTasksDir, getLegacyTasksDir, } from './paths.js';
20
20
  export function writeTaskMeta(record) {
21
21
  ensureTaskDir(record.runId);
22
22
  const target = taskMetaPath(record.runId);
@@ -103,21 +103,35 @@ export function applyEvent(runId, event) {
103
103
  return next;
104
104
  }
105
105
  export function listTasks() {
106
- let entries;
107
- try {
108
- entries = fs.readdirSync(getTasksDir(), { withFileTypes: true });
109
- }
110
- catch {
111
- return [];
112
- }
106
+ // Walk both the primary tasks dir and the legacy ~/.franklin/tasks/
107
+ // location so `franklin task list` keeps showing legacy tasks until
108
+ // their dirs are cleaned up. Dedupe by runId (first-wins, primary
109
+ // ordered first) — protects against the unlikely case of the same
110
+ // runId existing in both locations.
111
+ const dirs = [getTasksDir()];
112
+ if (process.env.FRANKLIN_HOME === undefined)
113
+ dirs.push(getLegacyTasksDir());
114
+ const seen = new Set();
113
115
  const out = [];
114
- for (const ent of entries) {
115
- // Skip junk like .DS_Store — only real per-task subdirectories are valid.
116
- if (!ent.isDirectory())
116
+ for (const dir of dirs) {
117
+ let entries;
118
+ try {
119
+ entries = fs.readdirSync(dir, { withFileTypes: true });
120
+ }
121
+ catch {
117
122
  continue;
118
- const meta = readTaskMeta(ent.name);
119
- if (meta)
120
- out.push(meta);
123
+ }
124
+ for (const ent of entries) {
125
+ // Skip junk like .DS_Store — only real per-task subdirectories are valid.
126
+ if (!ent.isDirectory())
127
+ continue;
128
+ if (seen.has(ent.name))
129
+ continue;
130
+ seen.add(ent.name);
131
+ const meta = readTaskMeta(ent.name);
132
+ if (meta)
133
+ out.push(meta);
134
+ }
121
135
  }
122
136
  out.sort((a, b) => b.createdAt - a.createdAt);
123
137
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.41",
3
+ "version": "3.15.43",
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": {