sf snapshot: uncommitted changes after 72m inactivity

This commit is contained in:
Mikael Hugo 2026-05-10 00:28:55 +02:00
parent 6f174cabc1
commit f66555456f
6 changed files with 118 additions and 26 deletions

Binary file not shown.

Binary file not shown.

BIN
.sf/metrics.db Normal file

Binary file not shown.

View file

@ -1394,9 +1394,12 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
debugLog("startAuto", { phase: "already-active", skipping: true }); debugLog("startAuto", { phase: "already-active", skipping: true });
return; return;
} }
// Gate: if the user is in Ask mode (manual runControl), ask permission to // Gate: if the user is in Ask mode (manual runControl and not already in
// switch to Build mode before starting autonomous execution. // build workMode), ask permission to switch to Build mode.
if (s.runControl === "manual" && !options?.skipModeGate) { // Skip if workMode is already "build" — runControl is reset to "manual" on
// autonomous stop but workMode persists, so this avoids a spurious prompt
// for users who stay in Build mode between autonomous runs.
if (s.runControl === "manual" && s.workMode !== "build" && !options?.skipModeGate) {
const confirmed = await showConfirm(ctx, { const confirmed = await showConfirm(ctx, {
title: "Switch to Build mode?", title: "Switch to Build mode?",
message: message:

View file

@ -20,6 +20,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { sfRoot } from "./paths.js"; import { sfRoot } from "./paths.js";
import { logWarning } from "./workflow-logger.js"; import { logWarning } from "./workflow-logger.js";
@ -28,6 +29,7 @@ const MAX_HISTOGRAM_BUCKETS = 10;
const FLUSH_RETRY_MAX = 3; const FLUSH_RETRY_MAX = 3;
const FLUSH_RETRY_BASE_MS = 1000; const FLUSH_RETRY_BASE_MS = 1000;
const METRIC_NAME_PATTERN = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/; const METRIC_NAME_PATTERN = /^[a-zA-Z_:][a-zA-Z0-9_:]*$/;
const METRICS_DB_ROW_CAP = 10_000; // keep newest N rows; prune on flush when exceeded
// ─── Metrics System Performance Monitoring ────────────────────────────────── // ─── Metrics System Performance Monitoring ──────────────────────────────────
@ -60,7 +62,7 @@ export function getMetricsSystemStats() {
_flushSuccessCount > 0 _flushSuccessCount > 0
? Math.round(_totalFlushDuration / _flushSuccessCount) ? Math.round(_totalFlushDuration / _flushSuccessCount)
: 0, : 0,
databaseStatus: _dbAdapter ? "connected" : "disconnected", databaseStatus: _metricsDb ? "connected" : "disconnected",
}; };
} }
@ -391,7 +393,8 @@ let _flushTimer = null;
let _metricsHealthTimer = null; let _metricsHealthTimer = null;
let _basePath = ""; let _basePath = "";
let _sessionId = ""; let _sessionId = "";
let _dbAdapter = null; let _dbAdapter = null; // kept for API compat but no longer used for metrics writes
let _metricsDb = null; // dedicated metrics.db connection
let _flushFailures = 0; let _flushFailures = 0;
function getRegistry() { function getRegistry() {
@ -405,9 +408,17 @@ function metricsFilePath(basePath) {
// ─── DB Persistence ───────────────────────────────────────────────────────── // ─── DB Persistence ─────────────────────────────────────────────────────────
function ensureMetricsTable(db) { function metricsDbPath(basePath) {
if (!db) return; return join(sfRoot(basePath), "metrics.db");
}
function openMetricsDb(basePath) {
if (_metricsDb) return;
try { try {
mkdirSync(sfRoot(basePath), { recursive: true });
const db = new DatabaseSync(metricsDbPath(basePath));
db.exec("PRAGMA journal_mode=WAL");
db.exec("PRAGMA synchronous=NORMAL");
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS metrics ( CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -420,20 +431,32 @@ function ensureMetricsTable(db) {
) )
`); `);
db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_name ON metrics(name)`);
db.exec( db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_session ON metrics(session_id)`);
`CREATE INDEX IF NOT EXISTS idx_metrics_session ON metrics(session_id)`, db.exec(`CREATE INDEX IF NOT EXISTS idx_metrics_name_ts ON metrics(name, timestamp DESC)`);
); _metricsDb = db;
db.exec(
`CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp)`,
);
} catch (err) { } catch (err) {
logWarning("metrics-central", `DB table creation failed: ${err.message}`); logWarning("metrics-central", `Failed to open metrics.db: ${err.message}`);
} }
} }
function persistMetricsToDb(registry, sessionId, db) { function closeMetricsDb() {
if (!_metricsDb) return;
try {
_metricsDb.close();
} catch {
// swallow
}
_metricsDb = null;
}
function ensureMetricsTable(db) {
// no-op — metrics.db is set up by openMetricsDb
void db;
}
function persistMetricsToDb(registry, sessionId, _ignored) {
const db = _metricsDb;
if (!db) return; if (!db) return;
ensureMetricsTable(db);
const ts = new Date().toISOString(); const ts = new Date().toISOString();
try { try {
const insert = db.prepare( const insert = db.prepare(
@ -476,8 +499,25 @@ function persistMetricsToDb(registry, sessionId, db) {
); );
} }
} catch (err) { } catch (err) {
if (err.message?.includes("database is not open")) {
closeMetricsDb();
return;
}
logWarning("metrics-central", `DB persist failed: ${err.message}`); logWarning("metrics-central", `DB persist failed: ${err.message}`);
} }
// Prune if the table has grown beyond the cap (best-effort; never block flush)
try {
const row = _metricsDb?.prepare("SELECT count(*) as n FROM metrics").get();
if (row && row.n > METRICS_DB_ROW_CAP) {
_metricsDb.prepare(
`DELETE FROM metrics WHERE rowid NOT IN (
SELECT rowid FROM metrics ORDER BY timestamp DESC LIMIT ${METRICS_DB_ROW_CAP}
)`,
).run();
}
} catch (_) {
// swallow — prune failure must never surface to the user
}
} }
// ─── Flush with Retry ─────────────────────────────────────────────────────── // ─── Flush with Retry ───────────────────────────────────────────────────────
@ -493,10 +533,8 @@ function flushMetrics() {
const path = metricsFilePath(_basePath); const path = metricsFilePath(_basePath);
mkdirSync(join(sfRoot(_basePath), "runtime"), { recursive: true }); mkdirSync(join(sfRoot(_basePath), "runtime"), { recursive: true });
writeFileSync(path, text, "utf-8"); writeFileSync(path, text, "utf-8");
// Also persist to DB if available // Persist to dedicated metrics.db
if (_dbAdapter) { persistMetricsToDb(getRegistry(), _sessionId, null);
persistMetricsToDb(getRegistry(), _sessionId, _dbAdapter);
}
// Update performance metrics // Update performance metrics
_flushSuccessCount++; _flushSuccessCount++;
@ -562,7 +600,7 @@ function flushMetrics() {
export function initMetricsCentral(basePath, opts = {}) { export function initMetricsCentral(basePath, opts = {}) {
_basePath = basePath; _basePath = basePath;
_sessionId = opts.sessionId ?? ""; _sessionId = opts.sessionId ?? "";
_dbAdapter = opts.dbAdapter ?? null; _dbAdapter = opts.dbAdapter ?? null; // accepted but no longer used for metrics writes
const interval = opts.flushIntervalMs ?? FLUSH_INTERVAL_MS; const interval = opts.flushIntervalMs ?? FLUSH_INTERVAL_MS;
// Reset metrics system stats on fresh init // Reset metrics system stats on fresh init
@ -582,10 +620,8 @@ export function initMetricsCentral(basePath, opts = {}) {
// Ensure timer doesn't keep process alive // Ensure timer doesn't keep process alive
if (_flushTimer.unref) _flushTimer.unref(); if (_flushTimer.unref) _flushTimer.unref();
// Ensure DB table exists // Open dedicated metrics.db (separate from main sf.db to avoid WAL pressure)
if (_dbAdapter) { openMetricsDb(basePath);
ensureMetricsTable(_dbAdapter);
}
// Start periodic metrics system health reporting // Start periodic metrics system health reporting
if (!_metricsHealthTimer) { if (!_metricsHealthTimer) {
@ -663,6 +699,7 @@ export function stopMetricsCentral() {
_basePath = ""; _basePath = "";
_sessionId = ""; _sessionId = "";
_dbAdapter = null; _dbAdapter = null;
closeMetricsDb();
} }
/** /**

View file

@ -244,7 +244,7 @@ function performDatabaseMaintenance(rawDb, path) {
); );
} }
} }
const SCHEMA_VERSION = 54; const SCHEMA_VERSION = 56;
function indexExists(db, name) { function indexExists(db, name) {
return !!db return !!db
.prepare( .prepare(
@ -1464,6 +1464,22 @@ function initSchema(db, fileBacked) {
db.exec( db.exec(
`CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`, `CREATE VIEW IF NOT EXISTS active_memories AS SELECT * FROM memories WHERE superseded_by IS NULL`,
); );
db.exec(
`CREATE VIEW IF NOT EXISTS active_tasks AS SELECT * FROM tasks WHERE status NOT IN ('done','complete','completed','cancelled')`,
);
db.exec(`
CREATE VIEW IF NOT EXISTS v_task_full AS
SELECT t.*, ts.spec_version, ts.verify AS spec_verify,
ts.inputs AS spec_inputs, ts.expected_output AS spec_expected_output
FROM tasks t
LEFT JOIN task_specs ts
ON t.milestone_id = ts.milestone_id
AND t.slice_id = ts.slice_id
AND t.id = ts.task_id
`);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_audit_events_category ON audit_events(category, type, ts DESC)`,
);
const existing = db const existing = db
.prepare("SELECT count(*) as cnt FROM schema_version") .prepare("SELECT count(*) as cnt FROM schema_version")
.get(); .get();
@ -3173,6 +3189,42 @@ function migrateSchema(db) {
":applied_at": new Date().toISOString(), ":applied_at": new Date().toISOString(),
}); });
} }
if (currentVersion < 55) {
// Schema v55: composite index for audit_events + task access-pattern views
db.exec(
`CREATE INDEX IF NOT EXISTS idx_audit_events_category ON audit_events(category, type, ts DESC)`,
);
db.exec(
`CREATE VIEW IF NOT EXISTS active_tasks AS SELECT * FROM tasks WHERE status NOT IN ('done','complete','completed','cancelled')`,
);
db.exec(`
CREATE VIEW IF NOT EXISTS v_task_full AS
SELECT t.*, ts.spec_version, ts.verify AS spec_verify,
ts.inputs AS spec_inputs, ts.expected_output AS spec_expected_output
FROM tasks t
LEFT JOIN task_specs ts
ON t.milestone_id = ts.milestone_id
AND t.slice_id = ts.slice_id
AND t.id = ts.task_id
`);
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
":version": 55,
":applied_at": new Date().toISOString(),
});
}
if (currentVersion < 56) {
// Schema v56: move metrics table to dedicated metrics.db — drop from main DB
// to eliminate WAL pressure from high-frequency telemetry writes.
db.exec(`DROP TABLE IF EXISTS metrics`);
db.prepare(
"INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)",
).run({
":version": 56,
":applied_at": new Date().toISOString(),
});
}
db.exec("COMMIT"); db.exec("COMMIT");
} catch (err) { } catch (err) {
db.exec("ROLLBACK"); db.exec("ROLLBACK");