@blockrun/franklin 3.15.30 → 3.15.32
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/agent/loop.js +12 -1
- package/dist/agent/streaming-executor.js +24 -2
- package/dist/storage/hygiene.d.ts +17 -2
- package/dist/storage/hygiene.js +36 -13
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -448,7 +448,18 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
448
448
|
persistSessionMeta();
|
|
449
449
|
};
|
|
450
450
|
pruneOldSessions(sessionId); // Cleanup old sessions on start, protect current
|
|
451
|
-
|
|
451
|
+
// Trim ~/.blockrun/data + cost_log + remove legacy files + sweep
|
|
452
|
+
// orphan tool-results dirs. Logs a summary if anything was actually
|
|
453
|
+
// touched — pre-3.15.31 hygiene was completely silent and the only
|
|
454
|
+
// way to verify it was running was poking disk yourself.
|
|
455
|
+
const hygieneReport = runDataHygiene();
|
|
456
|
+
const totalCleaned = hygieneReport.legacyFilesRemoved +
|
|
457
|
+
hygieneReport.dataFilesTrimmed +
|
|
458
|
+
hygieneReport.costLogRowsTrimmed +
|
|
459
|
+
hygieneReport.orphanToolResultsRemoved;
|
|
460
|
+
if (totalCleaned > 0) {
|
|
461
|
+
logger.info(`[franklin] Data hygiene: ${hygieneReport.legacyFilesRemoved} legacy, ${hygieneReport.dataFilesTrimmed} data files, ${hygieneReport.costLogRowsTrimmed} cost_log rows, ${hygieneReport.orphanToolResultsRemoved} orphan tool-results dirs cleaned`);
|
|
462
|
+
}
|
|
452
463
|
persistSessionMeta();
|
|
453
464
|
// Flush session meta on SIGINT/SIGTERM so mid-stream Ctrl+C doesn't
|
|
454
465
|
// leave a stale .meta.json (wrong turnCount/messageCount/cost).
|
|
@@ -7,6 +7,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { recordFailure } from '../stats/failures.js';
|
|
9
9
|
import { BLOCKRUN_DIR } from '../config.js';
|
|
10
|
+
import { logger } from '../logger.js';
|
|
10
11
|
/** Persist a large tool result to disk and return a preview string. */
|
|
11
12
|
const PERSIST_THRESHOLD = 50_000;
|
|
12
13
|
const PREVIEW_SIZE = 2_000;
|
|
@@ -205,8 +206,22 @@ export class StreamingExecutor {
|
|
|
205
206
|
}
|
|
206
207
|
}
|
|
207
208
|
}
|
|
209
|
+
// Track elapsed for slow-tool forensics. Verified 2026-05-04
|
|
210
|
+
// from a real session: a 337.6s Bash error left no trace in
|
|
211
|
+
// franklin-debug.log — the user could see the ✗ in the UI but
|
|
212
|
+
// had no post-hoc way to ask "which Bash took 5+ minutes
|
|
213
|
+
// yesterday and what did it run". 30s threshold is conservative
|
|
214
|
+
// (Read/Glob/Grep finish in <1s; only network or shell work
|
|
215
|
+
// crosses).
|
|
216
|
+
const execStart = Date.now();
|
|
208
217
|
let result = await handler.execute(invocation.input, progressScope);
|
|
209
218
|
this.guard?.afterExecute(invocation, result);
|
|
219
|
+
const execElapsed = Date.now() - execStart;
|
|
220
|
+
if (execElapsed >= 30_000) {
|
|
221
|
+
const status = result.isError ? 'error' : 'ok';
|
|
222
|
+
const preview = this.inputPreview(invocation) || '';
|
|
223
|
+
logger.info(`[franklin] Slow tool: ${invocation.name} ${status} after ${(execElapsed / 1000).toFixed(1)}s${preview ? ` — ${preview.slice(0, 80)}` : ''}`);
|
|
224
|
+
}
|
|
210
225
|
// Persist large results to disk with preview.
|
|
211
226
|
// Instead of just truncating, save the full result to disk so it can be re-read later.
|
|
212
227
|
if (result.output.length > PERSIST_THRESHOLD) {
|
|
@@ -219,15 +234,22 @@ export class StreamingExecutor {
|
|
|
219
234
|
}
|
|
220
235
|
catch (err) {
|
|
221
236
|
this.guard?.cancelInvocation(invocation.id);
|
|
237
|
+
const errMsg = err.message;
|
|
222
238
|
recordFailure({
|
|
223
239
|
timestamp: Date.now(),
|
|
224
240
|
model: '', // not available at tool level
|
|
225
241
|
failureType: 'tool_error',
|
|
226
242
|
toolName: invocation.name,
|
|
227
|
-
errorMessage:
|
|
243
|
+
errorMessage: errMsg,
|
|
228
244
|
});
|
|
245
|
+
// Always log thrown tool errors. Pre-3.15.32 these went only to
|
|
246
|
+
// failures.jsonl (which the user rarely opens) and were absent
|
|
247
|
+
// from franklin-debug.log entirely. Now `franklin logs` shows
|
|
248
|
+
// them alongside everything else.
|
|
249
|
+
const preview = this.inputPreview(invocation) || '';
|
|
250
|
+
logger.warn(`[franklin] Tool error: ${invocation.name} threw "${errMsg.slice(0, 120)}"${preview ? ` — ${preview.slice(0, 80)}` : ''}`);
|
|
229
251
|
return {
|
|
230
|
-
output: `Error executing ${invocation.name}: ${
|
|
252
|
+
output: `Error executing ${invocation.name}: ${errMsg}`,
|
|
231
253
|
isError: true,
|
|
232
254
|
};
|
|
233
255
|
}
|
|
@@ -21,8 +21,23 @@
|
|
|
21
21
|
* unlinkSync). Best-effort: every operation is wrapped so a single failure
|
|
22
22
|
* never breaks agent boot.
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* Summary of what hygiene removed/trimmed in one pass. Returned so the
|
|
26
|
+
* caller (agent loop) can log it — silent hygiene is hard to verify
|
|
27
|
+
* without poking at disk yourself, which is exactly the kind of thing
|
|
28
|
+
* users shouldn't have to do.
|
|
29
|
+
*/
|
|
30
|
+
export interface HygieneReport {
|
|
31
|
+
legacyFilesRemoved: number;
|
|
32
|
+
dataFilesTrimmed: number;
|
|
33
|
+
costLogRowsTrimmed: number;
|
|
34
|
+
orphanToolResultsRemoved: number;
|
|
35
|
+
}
|
|
24
36
|
/**
|
|
25
37
|
* Top-level entry. Call once at agent session start. Catches its own
|
|
26
|
-
* errors so a bad disk never blocks startup.
|
|
38
|
+
* errors so a bad disk never blocks startup. Returns counts so callers
|
|
39
|
+
* can log a one-line summary — verified 2026-05-04 from a real session
|
|
40
|
+
* where hygiene was running silently for hours and there was no way to
|
|
41
|
+
* tell from the log whether anything was being cleaned.
|
|
27
42
|
*/
|
|
28
|
-
export declare function runDataHygiene():
|
|
43
|
+
export declare function runDataHygiene(): HygieneReport;
|
package/dist/storage/hygiene.js
CHANGED
|
@@ -44,35 +44,46 @@ const LEGACY_FILENAMES = [
|
|
|
44
44
|
'0xcode-stats.json',
|
|
45
45
|
'runcode-debug.log',
|
|
46
46
|
];
|
|
47
|
+
const ZERO_REPORT = {
|
|
48
|
+
legacyFilesRemoved: 0,
|
|
49
|
+
dataFilesTrimmed: 0,
|
|
50
|
+
costLogRowsTrimmed: 0,
|
|
51
|
+
orphanToolResultsRemoved: 0,
|
|
52
|
+
};
|
|
47
53
|
/**
|
|
48
54
|
* Top-level entry. Call once at agent session start. Catches its own
|
|
49
|
-
* errors so a bad disk never blocks startup.
|
|
55
|
+
* errors so a bad disk never blocks startup. Returns counts so callers
|
|
56
|
+
* can log a one-line summary — verified 2026-05-04 from a real session
|
|
57
|
+
* where hygiene was running silently for hours and there was no way to
|
|
58
|
+
* tell from the log whether anything was being cleaned.
|
|
50
59
|
*/
|
|
51
60
|
export function runDataHygiene() {
|
|
61
|
+
const report = { ...ZERO_REPORT };
|
|
52
62
|
try {
|
|
53
|
-
trimDataDir();
|
|
63
|
+
report.dataFilesTrimmed = trimDataDir();
|
|
54
64
|
}
|
|
55
65
|
catch { /* best effort */ }
|
|
56
66
|
try {
|
|
57
|
-
trimCostLog();
|
|
67
|
+
report.costLogRowsTrimmed = trimCostLog();
|
|
58
68
|
}
|
|
59
69
|
catch { /* best effort */ }
|
|
60
70
|
try {
|
|
61
|
-
removeLegacyFiles();
|
|
71
|
+
report.legacyFilesRemoved = removeLegacyFiles();
|
|
62
72
|
}
|
|
63
73
|
catch { /* best effort */ }
|
|
64
74
|
try {
|
|
65
|
-
sweepOrphanToolResults();
|
|
75
|
+
report.orphanToolResultsRemoved = sweepOrphanToolResults();
|
|
66
76
|
}
|
|
67
77
|
catch { /* best effort */ }
|
|
78
|
+
return report;
|
|
68
79
|
}
|
|
69
80
|
function trimDataDir() {
|
|
70
81
|
const dir = path.join(BLOCKRUN_DIR, 'data');
|
|
71
82
|
if (!fs.existsSync(dir))
|
|
72
|
-
return;
|
|
83
|
+
return 0;
|
|
73
84
|
const entries = fs.readdirSync(dir);
|
|
74
85
|
if (entries.length === 0)
|
|
75
|
-
return;
|
|
86
|
+
return 0;
|
|
76
87
|
const cutoff = Date.now() - DATA_DIR_MAX_AGE_MS;
|
|
77
88
|
const stats = [];
|
|
78
89
|
for (const name of entries) {
|
|
@@ -86,11 +97,13 @@ function trimDataDir() {
|
|
|
86
97
|
// Best effort — skip unreadable entries.
|
|
87
98
|
}
|
|
88
99
|
}
|
|
100
|
+
let removed = 0;
|
|
89
101
|
// Pass 1: age-based delete.
|
|
90
102
|
for (const e of stats) {
|
|
91
103
|
if (e.mtime < cutoff) {
|
|
92
104
|
try {
|
|
93
105
|
fs.unlinkSync(path.join(dir, e.name));
|
|
106
|
+
removed++;
|
|
94
107
|
}
|
|
95
108
|
catch { /* ok */ }
|
|
96
109
|
}
|
|
@@ -106,35 +119,42 @@ function trimDataDir() {
|
|
|
106
119
|
for (let i = 0; i < excess; i++) {
|
|
107
120
|
try {
|
|
108
121
|
fs.unlinkSync(path.join(dir, survivors[i].name));
|
|
122
|
+
removed++;
|
|
109
123
|
}
|
|
110
124
|
catch { /* ok */ }
|
|
111
125
|
}
|
|
112
126
|
}
|
|
127
|
+
return removed;
|
|
113
128
|
}
|
|
114
129
|
function trimCostLog() {
|
|
115
130
|
const file = path.join(BLOCKRUN_DIR, 'cost_log.jsonl');
|
|
116
131
|
if (!fs.existsSync(file))
|
|
117
|
-
return;
|
|
132
|
+
return 0;
|
|
118
133
|
// Cheap probe — skip the full read+rewrite when the file is small.
|
|
119
134
|
const stat = fs.statSync(file);
|
|
120
135
|
if (stat.size < COST_LOG_PROBE_BYTES)
|
|
121
|
-
return;
|
|
136
|
+
return 0;
|
|
122
137
|
const lines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean);
|
|
123
138
|
if (lines.length <= COST_LOG_MAX_ENTRIES)
|
|
124
|
-
return;
|
|
139
|
+
return 0;
|
|
140
|
+
const dropped = lines.length - COST_LOG_MAX_ENTRIES;
|
|
125
141
|
const kept = lines.slice(lines.length - COST_LOG_MAX_ENTRIES);
|
|
126
142
|
fs.writeFileSync(file, kept.join('\n') + '\n');
|
|
143
|
+
return dropped;
|
|
127
144
|
}
|
|
128
145
|
function removeLegacyFiles() {
|
|
146
|
+
let removed = 0;
|
|
129
147
|
for (const name of LEGACY_FILENAMES) {
|
|
130
148
|
const p = path.join(BLOCKRUN_DIR, name);
|
|
131
149
|
if (!fs.existsSync(p))
|
|
132
150
|
continue;
|
|
133
151
|
try {
|
|
134
152
|
fs.unlinkSync(p);
|
|
153
|
+
removed++;
|
|
135
154
|
}
|
|
136
155
|
catch { /* ok */ }
|
|
137
156
|
}
|
|
157
|
+
return removed;
|
|
138
158
|
}
|
|
139
159
|
/**
|
|
140
160
|
* `streaming-executor` writes large tool outputs to
|
|
@@ -151,7 +171,7 @@ function sweepOrphanToolResults() {
|
|
|
151
171
|
const toolResultsDir = path.join(BLOCKRUN_DIR, 'tool-results');
|
|
152
172
|
const sessionsDir = path.join(BLOCKRUN_DIR, 'sessions');
|
|
153
173
|
if (!fs.existsSync(toolResultsDir))
|
|
154
|
-
return;
|
|
174
|
+
return 0;
|
|
155
175
|
const knownSessionIds = new Set();
|
|
156
176
|
if (fs.existsSync(sessionsDir)) {
|
|
157
177
|
try {
|
|
@@ -165,7 +185,7 @@ function sweepOrphanToolResults() {
|
|
|
165
185
|
// Best-effort — if we can't read sessions/, skip the sweep so
|
|
166
186
|
// we never delete tool-results that might still belong to a
|
|
167
187
|
// live session.
|
|
168
|
-
return;
|
|
188
|
+
return 0;
|
|
169
189
|
}
|
|
170
190
|
}
|
|
171
191
|
let entries;
|
|
@@ -173,8 +193,9 @@ function sweepOrphanToolResults() {
|
|
|
173
193
|
entries = fs.readdirSync(toolResultsDir);
|
|
174
194
|
}
|
|
175
195
|
catch {
|
|
176
|
-
return;
|
|
196
|
+
return 0;
|
|
177
197
|
}
|
|
198
|
+
let removed = 0;
|
|
178
199
|
for (const name of entries) {
|
|
179
200
|
if (knownSessionIds.has(name))
|
|
180
201
|
continue;
|
|
@@ -184,9 +205,11 @@ function sweepOrphanToolResults() {
|
|
|
184
205
|
if (!stat.isDirectory())
|
|
185
206
|
continue;
|
|
186
207
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
208
|
+
removed++;
|
|
187
209
|
}
|
|
188
210
|
catch {
|
|
189
211
|
// Skip — best-effort cleanup.
|
|
190
212
|
}
|
|
191
213
|
}
|
|
214
|
+
return removed;
|
|
192
215
|
}
|
package/package.json
CHANGED