@blockrun/franklin 3.15.64 → 3.15.66
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/context.js +1 -1
- package/dist/commands/task.js +11 -1
- package/dist/storage/hygiene.d.ts +1 -0
- package/dist/storage/hygiene.js +77 -0
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -192,7 +192,7 @@ You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun
|
|
|
192
192
|
- \`GET /.well-known/x402\` — x402 resource list with prices
|
|
193
193
|
|
|
194
194
|
**LLM (POST, x402-paid)**
|
|
195
|
-
- \`POST /v1/chat/completions\` — OpenAI-compatible. Body: \`{ model, messages, stream?, tools?, max_tokens?, temperature? }\`. \`model\` MUST come from \`GET /v1/models\` (
|
|
195
|
+
- \`POST /v1/chat/completions\` — OpenAI-compatible. Body: \`{ model, messages, stream?, tools?, max_tokens?, temperature? }\`. \`model\` MUST come from \`GET /v1/models\` (real frontier examples on the gateway as of 2026-05: \`anthropic/claude-sonnet-4.6\`, \`anthropic/claude-opus-4.7\`, \`deepseek/deepseek-v4-pro\`, \`zai/glm-5.1\`, \`nvidia/qwen3-coder-480b\`, \`openai/gpt-5-nano\`). Do NOT invent versions like \`openai/gpt-5.1\` or \`xai/grok-5\` — those don't exist; the gateway 400s with the valid list in the error body, so when in doubt fetch \`GET /v1/models\` first.
|
|
196
196
|
- \`POST /v1/messages\` — Anthropic-compatible. Body: \`{ model, messages, max_tokens, system?, tools? }\`.
|
|
197
197
|
|
|
198
198
|
**Media (POST, x402-paid; GET to poll async jobs)**
|
package/dist/commands/task.js
CHANGED
|
@@ -22,13 +22,19 @@ 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.');
|
|
@@ -56,6 +62,7 @@ export function buildTaskCommand() {
|
|
|
56
62
|
.description('Print log + current status for a task')
|
|
57
63
|
.option('-f, --follow', 'Poll until task reaches terminal state')
|
|
58
64
|
.action(async (runId, opts) => {
|
|
65
|
+
reconcileBestEffort();
|
|
59
66
|
const meta0 = readTaskMeta(runId);
|
|
60
67
|
if (!meta0) {
|
|
61
68
|
console.error(`No task: ${runId}`);
|
|
@@ -78,6 +85,7 @@ export function buildTaskCommand() {
|
|
|
78
85
|
if (opts.follow) {
|
|
79
86
|
while (true) {
|
|
80
87
|
await new Promise((r) => setTimeout(r, 1000));
|
|
88
|
+
reconcileBestEffort();
|
|
81
89
|
printNew();
|
|
82
90
|
const meta = readTaskMeta(runId);
|
|
83
91
|
if (meta && isTerminalTaskStatus(meta.status))
|
|
@@ -108,6 +116,7 @@ export function buildTaskCommand() {
|
|
|
108
116
|
const cap = parseInt(opts.timeout, 10);
|
|
109
117
|
const t0 = Date.now();
|
|
110
118
|
while (true) {
|
|
119
|
+
reconcileBestEffort();
|
|
111
120
|
const meta = readTaskMeta(runId);
|
|
112
121
|
if (!meta) {
|
|
113
122
|
console.error(`No task: ${runId}`);
|
|
@@ -128,6 +137,7 @@ export function buildTaskCommand() {
|
|
|
128
137
|
.command('cancel <runId>')
|
|
129
138
|
.description('Cancel a running task (SIGTERM to runner)')
|
|
130
139
|
.action((runId) => {
|
|
140
|
+
reconcileBestEffort();
|
|
131
141
|
const meta = readTaskMeta(runId);
|
|
132
142
|
if (!meta) {
|
|
133
143
|
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