@blockrun/franklin 3.15.65 → 3.15.67
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 +4 -2
- package/dist/commands/task.js +21 -2
- package/dist/storage/hygiene.d.ts +1 -0
- package/dist/storage/hygiene.js +77 -0
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -562,9 +562,11 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
562
562
|
const totalCleaned = hygieneReport.legacyFilesRemoved +
|
|
563
563
|
hygieneReport.dataFilesTrimmed +
|
|
564
564
|
hygieneReport.costLogRowsTrimmed +
|
|
565
|
-
hygieneReport.orphanToolResultsRemoved
|
|
565
|
+
hygieneReport.orphanToolResultsRemoved +
|
|
566
|
+
hygieneReport.brainJunkEntitiesRemoved +
|
|
567
|
+
hygieneReport.oldTasksRemoved;
|
|
566
568
|
if (totalCleaned > 0) {
|
|
567
|
-
logger.info(`[franklin] Data hygiene: ${hygieneReport.legacyFilesRemoved} legacy, ${hygieneReport.dataFilesTrimmed} data files, ${hygieneReport.costLogRowsTrimmed} cost_log rows, ${hygieneReport.orphanToolResultsRemoved} orphan tool-results dirs cleaned`);
|
|
569
|
+
logger.info(`[franklin] Data hygiene: ${hygieneReport.legacyFilesRemoved} legacy, ${hygieneReport.dataFilesTrimmed} data files, ${hygieneReport.costLogRowsTrimmed} cost_log rows, ${hygieneReport.orphanToolResultsRemoved} orphan tool-results dirs, ${hygieneReport.brainJunkEntitiesRemoved} junk brain entities, ${hygieneReport.oldTasksRemoved} expired tasks cleaned`);
|
|
568
570
|
}
|
|
569
571
|
persistSessionMeta();
|
|
570
572
|
// Flush session meta on SIGINT/SIGTERM so mid-stream Ctrl+C doesn't
|
package/dist/commands/task.js
CHANGED
|
@@ -22,18 +22,33 @@ function fmtAge(ms) {
|
|
|
22
22
|
return `${m}m`;
|
|
23
23
|
return `${Math.floor(m / 60)}h${m % 60}m`;
|
|
24
24
|
}
|
|
25
|
+
function reconcileBestEffort() {
|
|
26
|
+
try {
|
|
27
|
+
reconcileLostTasks();
|
|
28
|
+
}
|
|
29
|
+
catch { /* best-effort */ }
|
|
30
|
+
}
|
|
25
31
|
export function buildTaskCommand() {
|
|
26
32
|
const cmd = new Command('task').description('Manage long-running detached tasks');
|
|
27
33
|
cmd
|
|
28
34
|
.command('list')
|
|
29
35
|
.description('List recent tasks (newest first)')
|
|
30
36
|
.action(() => {
|
|
31
|
-
|
|
37
|
+
reconcileBestEffort();
|
|
32
38
|
const tasks = listTasks();
|
|
33
39
|
if (tasks.length === 0) {
|
|
34
40
|
console.log('No tasks. Start one via the Task agent tool.');
|
|
35
41
|
return;
|
|
36
42
|
}
|
|
43
|
+
// Header row matches `franklin content list` shape — verified
|
|
44
|
+
// 2026-05-05 that task list was emitting bare data rows with no
|
|
45
|
+
// column labels, leaving users to guess at the column meaning
|
|
46
|
+
// (`14h19m` could be elapsed-since-start or since-end). The
|
|
47
|
+
// running-vs-terminal age semantics are documented in 3.15.46;
|
|
48
|
+
// the header makes the column itself self-explanatory.
|
|
49
|
+
const idHeader = 'runId';
|
|
50
|
+
const idWidth = Math.max(idHeader.length, ...tasks.map(t => t.runId.length));
|
|
51
|
+
console.log([idHeader.padEnd(idWidth), 'status'.padEnd(10), ' age', 'label'].join(' '));
|
|
37
52
|
const now = Date.now();
|
|
38
53
|
for (const t of tasks) {
|
|
39
54
|
// For a running task, "age" should mean "how long has this been
|
|
@@ -48,7 +63,7 @@ export function buildTaskCommand() {
|
|
|
48
63
|
? (t.endedAt ?? t.lastEventAt ?? t.createdAt)
|
|
49
64
|
: (t.startedAt ?? t.createdAt);
|
|
50
65
|
const age = fmtAge(now - ageRefMs);
|
|
51
|
-
console.log(`${t.runId} ${t.status.padEnd(10)} ${age.padStart(5)} ${t.label}`);
|
|
66
|
+
console.log(`${t.runId.padEnd(idWidth)} ${t.status.padEnd(10)} ${age.padStart(5)} ${t.label}`);
|
|
52
67
|
}
|
|
53
68
|
});
|
|
54
69
|
cmd
|
|
@@ -56,6 +71,7 @@ export function buildTaskCommand() {
|
|
|
56
71
|
.description('Print log + current status for a task')
|
|
57
72
|
.option('-f, --follow', 'Poll until task reaches terminal state')
|
|
58
73
|
.action(async (runId, opts) => {
|
|
74
|
+
reconcileBestEffort();
|
|
59
75
|
const meta0 = readTaskMeta(runId);
|
|
60
76
|
if (!meta0) {
|
|
61
77
|
console.error(`No task: ${runId}`);
|
|
@@ -78,6 +94,7 @@ export function buildTaskCommand() {
|
|
|
78
94
|
if (opts.follow) {
|
|
79
95
|
while (true) {
|
|
80
96
|
await new Promise((r) => setTimeout(r, 1000));
|
|
97
|
+
reconcileBestEffort();
|
|
81
98
|
printNew();
|
|
82
99
|
const meta = readTaskMeta(runId);
|
|
83
100
|
if (meta && isTerminalTaskStatus(meta.status))
|
|
@@ -108,6 +125,7 @@ export function buildTaskCommand() {
|
|
|
108
125
|
const cap = parseInt(opts.timeout, 10);
|
|
109
126
|
const t0 = Date.now();
|
|
110
127
|
while (true) {
|
|
128
|
+
reconcileBestEffort();
|
|
111
129
|
const meta = readTaskMeta(runId);
|
|
112
130
|
if (!meta) {
|
|
113
131
|
console.error(`No task: ${runId}`);
|
|
@@ -128,6 +146,7 @@ export function buildTaskCommand() {
|
|
|
128
146
|
.command('cancel <runId>')
|
|
129
147
|
.description('Cancel a running task (SIGTERM to runner)')
|
|
130
148
|
.action((runId) => {
|
|
149
|
+
reconcileBestEffort();
|
|
131
150
|
const meta = readTaskMeta(runId);
|
|
132
151
|
if (!meta) {
|
|
133
152
|
console.error(`No task: ${runId}`);
|
package/dist/storage/hygiene.js
CHANGED
|
@@ -25,12 +25,22 @@ import fs from 'node:fs';
|
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import { BLOCKRUN_DIR } from '../config.js';
|
|
27
27
|
import { pruneJunkBrainEntries } from '../brain/store.js';
|
|
28
|
+
import { getTasksDir, getLegacyTasksDir } from '../tasks/paths.js';
|
|
29
|
+
import { isTerminalTaskStatus } from '../tasks/types.js';
|
|
28
30
|
// Retention knobs. Tuned conservatively — a power user with 50+ calls/day
|
|
29
31
|
// for 30 days still fits in DATA_DIR_MAX_FILES, and 5000 cost-log entries
|
|
30
32
|
// covers months of normal use without truncating the running totals.
|
|
31
33
|
const DATA_DIR_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
32
34
|
const DATA_DIR_MAX_FILES = 2000;
|
|
33
35
|
const COST_LOG_MAX_ENTRIES = 5000;
|
|
36
|
+
// Task records (meta + events + log per task dir). Verified 2026-05-05:
|
|
37
|
+
// 10 tasks across ~/.franklin/tasks/, oldest "lost" status from 53 hours
|
|
38
|
+
// ago, none ever cleaned up. Each task's log.txt can run 1+ MB for ETL
|
|
39
|
+
// jobs. Without retention, disk fills slowly. 7 days lets a user inspect
|
|
40
|
+
// the previous week's runs but archives anything older. Running tasks
|
|
41
|
+
// are NEVER touched (status check + heartbeat freshness).
|
|
42
|
+
const TASK_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
43
|
+
const TASK_MIN_RETAIN = 5; // always keep the 5 most-recent records regardless of age
|
|
34
44
|
// Cost log entries are tiny (~60 bytes — ts, endpoint, cost only). 40 bytes
|
|
35
45
|
// per entry keeps the probe under the real average so a slightly-overlong
|
|
36
46
|
// file always triggers the rescan rather than silently growing past cap.
|
|
@@ -51,6 +61,7 @@ const ZERO_REPORT = {
|
|
|
51
61
|
costLogRowsTrimmed: 0,
|
|
52
62
|
orphanToolResultsRemoved: 0,
|
|
53
63
|
brainJunkEntitiesRemoved: 0,
|
|
64
|
+
oldTasksRemoved: 0,
|
|
54
65
|
};
|
|
55
66
|
/**
|
|
56
67
|
* Top-level entry. Call once at agent session start. Catches its own
|
|
@@ -81,8 +92,74 @@ export function runDataHygiene() {
|
|
|
81
92
|
report.brainJunkEntitiesRemoved = pruneJunkBrainEntries().entitiesRemoved;
|
|
82
93
|
}
|
|
83
94
|
catch { /* best effort */ }
|
|
95
|
+
try {
|
|
96
|
+
report.oldTasksRemoved = pruneOldTaskRecords();
|
|
97
|
+
}
|
|
98
|
+
catch { /* best effort */ }
|
|
84
99
|
return report;
|
|
85
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Remove terminal-state task directories older than TASK_MAX_AGE_MS.
|
|
103
|
+
* Scans both the canonical (~/.blockrun/tasks/) and legacy
|
|
104
|
+
* (~/.franklin/tasks/) locations, since 3.15.42 leaves both readable.
|
|
105
|
+
*
|
|
106
|
+
* Safety:
|
|
107
|
+
* - Running / queued tasks are NEVER removed (status check).
|
|
108
|
+
* - Always keep the most-recent TASK_MIN_RETAIN records regardless of
|
|
109
|
+
* age, so users can see recent history after a long pause.
|
|
110
|
+
* - Best-effort: corrupt meta or unreadable dirs are skipped silently.
|
|
111
|
+
*/
|
|
112
|
+
function pruneOldTaskRecords() {
|
|
113
|
+
const cutoff = Date.now() - TASK_MAX_AGE_MS;
|
|
114
|
+
let removed = 0;
|
|
115
|
+
const dirs = [getTasksDir()];
|
|
116
|
+
if (process.env.FRANKLIN_HOME === undefined)
|
|
117
|
+
dirs.push(getLegacyTasksDir());
|
|
118
|
+
for (const dir of dirs) {
|
|
119
|
+
let entries;
|
|
120
|
+
try {
|
|
121
|
+
entries = fs.readdirSync(dir);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const cands = [];
|
|
127
|
+
for (const name of entries) {
|
|
128
|
+
const taskDir = path.join(dir, name);
|
|
129
|
+
const metaPath = path.join(taskDir, 'meta.json');
|
|
130
|
+
try {
|
|
131
|
+
const stat = fs.statSync(taskDir);
|
|
132
|
+
if (!stat.isDirectory())
|
|
133
|
+
continue;
|
|
134
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
135
|
+
const terminal = typeof meta.status === 'string' && isTerminalTaskStatus(meta.status);
|
|
136
|
+
cands.push({ runId: name, mtime: stat.mtimeMs, terminal, metaPath });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Unreadable meta or stat — skip silently. We never delete a dir
|
|
140
|
+
// we can't confirm is terminal, to avoid killing a running task
|
|
141
|
+
// whose meta we just couldn't read.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Sort newest-first so the slice for retention is at index 0..N-1.
|
|
145
|
+
cands.sort((a, b) => b.mtime - a.mtime);
|
|
146
|
+
const protectedIds = new Set(cands.slice(0, TASK_MIN_RETAIN).map(c => c.runId));
|
|
147
|
+
for (const c of cands) {
|
|
148
|
+
if (protectedIds.has(c.runId))
|
|
149
|
+
continue;
|
|
150
|
+
if (!c.terminal)
|
|
151
|
+
continue; // never touch running/queued
|
|
152
|
+
if (c.mtime >= cutoff)
|
|
153
|
+
continue; // young enough to keep
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(path.join(dir, c.runId), { recursive: true, force: true });
|
|
156
|
+
removed++;
|
|
157
|
+
}
|
|
158
|
+
catch { /* ok */ }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return removed;
|
|
162
|
+
}
|
|
86
163
|
function trimDataDir() {
|
|
87
164
|
const dir = path.join(BLOCKRUN_DIR, 'data');
|
|
88
165
|
if (!fs.existsSync(dir))
|
package/package.json
CHANGED