From 95815c07babfc145bc09803f59d351ede178d622 Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Fri, 7 Aug 2026 21:09:19 +0200 Subject: [PATCH 1/3] feat: implement src/storage/file-run-history.js Signed-off-by: laurentketterle-hub --- src/storage/file-run-history.js | 160 ++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/storage/file-run-history.js diff --git a/src/storage/file-run-history.js b/src/storage/file-run-history.js new file mode 100644 index 0000000..9d6dd41 --- /dev/null +++ b/src/storage/file-run-history.js @@ -0,0 +1,160 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import crypto from 'node:crypto' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +function createRunId() { + return `run_${Date.now()}_${crypto.randomBytes(4).toString('hex')}` +} + +function normalizeLimit(limit, fallback = 20, max = 200) { + const parsed = Number.parseInt(limit, 10) + if (!Number.isFinite(parsed) || parsed <= 0) return fallback + return Math.min(parsed, max) +} + +function toAuditEvent(event) { + return { + type: event.type || 'unknown', + timestamp: event.timestamp || new Date().toISOString(), + agentId: event.agentId || null, + agent: event.agent || null, + cost: event.cost || null, + paidVia: event.paidVia || null, + txHash: event.txHash || null, + explorerUrl: event.explorerUrl || null, + status: event.status || null, + totalSpent: event.totalSpent || null, + reason: event.reason || null, + } +} + +/** + * FileRunHistoryStore — durable run history persisted to a JSON file. + * + * Survives process restarts. Uses atomic file writes (write tmp → rename) + * to prevent corruption from concurrent or interrupted writes. + */ +export class FileRunHistoryStore { + constructor({ filePath, maxRuns = 200 }) { + this.filePath = filePath || path.join(__dirname, '..', '..', 'data', 'run-history.json') + this.maxRuns = Math.max(10, maxRuns) + this.runs = [] + } + + async init() { + try { + const dir = path.dirname(this.filePath) + await fs.mkdir(dir, { recursive: true }) + const raw = await fs.readFile(this.filePath, 'utf-8') + const parsed = JSON.parse(raw) + this.runs = Array.isArray(parsed) ? parsed : [] + console.log(`[run-history] Loaded ${this.runs.length} runs from ${this.filePath}`) + } catch (err) { + if (err.code === 'ENOENT') { + this.runs = [] + console.log(`[run-history] No existing history file, starting fresh`) + } else { + console.error(`[run-history] Failed to load: ${err.message}`) + this.runs = [] + } + } + } + + async _persist() { + const dir = path.dirname(this.filePath) + await fs.mkdir(dir, { recursive: true }) + const tmp = `${this.filePath}.tmp` + await fs.writeFile(tmp, JSON.stringify(this.runs, null, 2), 'utf-8') + await fs.rename(tmp, this.filePath) + } + + async createRun({ task, budget, source }) { + const now = new Date().toISOString() + const run = { + id: createRunId(), + task, + budget, + source: source || 'api', + status: 'running', + createdAt: now, + updatedAt: now, + completedAt: null, + summary: null, + events: [], + txProofs: [], + } + this.runs.unshift(run) + this.runs = this.runs.slice(0, this.maxRuns) + await this._persist() + return run + } + + async appendEvent(runId, event) { + const run = this.runs.find((e) => e.id === runId) + if (!run) return + run.events.push(toAuditEvent(event)) + run.updatedAt = new Date().toISOString() + await this._persist() + } + + async completeRun(runId, result) { + const run = this.runs.find((e) => e.id === runId) + if (!run) return + const txProofs = (result.payments || []) + .filter((p) => p.paymentSuccess) + .map((p) => ({ + method: p.paidVia || p.paymentMethod || 'unknown', + txHash: p.txHash || null, + explorerUrl: p.explorerUrl || null, + })) + run.status = 'completed' + run.completedAt = new Date().toISOString() + run.updatedAt = run.completedAt + run.summary = { + totalSpent: result.totalSpent, + budget: result.budget, + budgetExhausted: result.budgetExhausted, + paymentProtocol: result.paymentProtocol, + txCount: result.txCount, + x402PaymentCount: result.x402PaymentCount, + xlmFallbackCount: result.xlmFallbackCount, + unpaidCount: result.unpaidCount, + elapsed: result.elapsed, + } + run.txProofs = txProofs + await this._persist() + } + + async failRun(runId, err) { + const run = this.runs.find((e) => e.id === runId) + if (!run) return + run.status = 'failed' + run.completedAt = new Date().toISOString() + run.updatedAt = run.completedAt + run.summary = { error: err?.message || 'unknown error' } + await this._persist() + } + + async listRecent(limit = 20) { + return this.runs.slice(0, normalizeLimit(limit, 20, this.maxRuns)) + } + + async getRun(id) { + return this.runs.find((e) => e.id === id) || null + } +} + +/** + * Factory that creates a FileRunHistoryStore from the standard config shape. + */ +export async function createFileRunHistoryStore(cfg) { + const store = new FileRunHistoryStore({ + filePath: cfg.runHistoryFile, + maxRuns: cfg.runHistoryMaxRuns, + }) + await store.init() + return store +} From bccea2442bad8cb5dba4d212dd4ea45f424a967e Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Fri, 7 Aug 2026 21:16:04 +0200 Subject: [PATCH 2/3] fix: prettier formatting compliance Signed-off-by: laurentketterle-hub --- src/storage/file-run-history.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/storage/file-run-history.js b/src/storage/file-run-history.js index 9d6dd41..c3f8a8d 100644 --- a/src/storage/file-run-history.js +++ b/src/storage/file-run-history.js @@ -34,12 +34,14 @@ function toAuditEvent(event) { /** * FileRunHistoryStore — durable run history persisted to a JSON file. * - * Survives process restarts. Uses atomic file writes (write tmp → rename) + * Survives process restarts. Uses atomic file writes (write tmp -> rename) * to prevent corruption from concurrent or interrupted writes. */ export class FileRunHistoryStore { constructor({ filePath, maxRuns = 200 }) { - this.filePath = filePath || path.join(__dirname, '..', '..', 'data', 'run-history.json') + this.filePath = + filePath || + path.join(__dirname, '..', '..', 'data', 'run-history.json') this.maxRuns = Math.max(10, maxRuns) this.runs = [] } @@ -51,11 +53,13 @@ export class FileRunHistoryStore { const raw = await fs.readFile(this.filePath, 'utf-8') const parsed = JSON.parse(raw) this.runs = Array.isArray(parsed) ? parsed : [] - console.log(`[run-history] Loaded ${this.runs.length} runs from ${this.filePath}`) + console.log( + `[run-history] Loaded ${this.runs.length} runs from ${this.filePath}` + ) } catch (err) { if (err.code === 'ENOENT') { this.runs = [] - console.log(`[run-history] No existing history file, starting fresh`) + console.log('[run-history] No existing history file, starting fresh') } else { console.error(`[run-history] Failed to load: ${err.message}`) this.runs = [] @@ -67,7 +71,11 @@ export class FileRunHistoryStore { const dir = path.dirname(this.filePath) await fs.mkdir(dir, { recursive: true }) const tmp = `${this.filePath}.tmp` - await fs.writeFile(tmp, JSON.stringify(this.runs, null, 2), 'utf-8') + await fs.writeFile( + tmp, + JSON.stringify(this.runs, null, 2), + 'utf-8' + ) await fs.rename(tmp, this.filePath) } From 0f9468b970d35a468f59ba7486eeeb7cae8471a3 Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Fri, 7 Aug 2026 21:18:25 +0200 Subject: [PATCH 3/3] fix: correct FileRunHistoryStore constructor args Signed-off-by: laurentketterle-hub --- src/storage/run-history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage/run-history.js b/src/storage/run-history.js index e09e185..e5c3fc7 100644 --- a/src/storage/run-history.js +++ b/src/storage/run-history.js @@ -165,7 +165,7 @@ export async function createRunHistoryStore(config) { return store } - const store = new FileRunHistoryStore(config.runHistoryFile, config.runHistoryMaxRuns) + const store = new FileRunHistoryStore({ filePath: config.runHistoryFile, maxRuns: config.runHistoryMaxRuns }) await store.init() return store }