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 });
return;
}
// Gate: if the user is in Ask mode (manual runControl), ask permission to
// switch to Build mode before starting autonomous execution.
if (s.runControl === "manual" && !options?.skipModeGate) {
// Gate: if the user is in Ask mode (manual runControl and not already in
// build workMode), ask permission to switch to Build mode.
// 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, {
title: "Switch to Build mode?",
message:

View file

@ -20,6 +20,7 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { sfRoot } from "./paths.js";
import { logWarning } from "./workflow-logger.js";
@ -28,6 +29,7 @@ const MAX_HISTOGRAM_BUCKETS = 10;
const FLUSH_RETRY_MAX = 3;
const FLUSH_RETRY_BASE_MS = 1000;
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 ──────────────────────────────────
@ -60,7 +62,7 @@ export function getMetricsSystemStats() {
_flushSuccessCount > 0
? Math.round(_totalFlushDuration / _flushSuccessCount)
: 0,
databaseStatus: _dbAdapter ? "connected" : "disconnected",
databaseStatus: _metricsDb ? "connected" : "disconnected",
};
}
@ -391,7 +393,8 @@ let _flushTimer = null;
let _metricsHealthTimer = null;
let _basePath = "";
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;
function getRegistry() {
@ -405,9 +408,17 @@ function metricsFilePath(basePath) {
// ─── DB Persistence ─────────────────────────────────────────────────────────
function ensureMetricsTable(db) {
if (!db) return;
function metricsDbPath(basePath) {
return join(sfRoot(basePath), "metrics.db");
}
function openMetricsDb(basePath) {
if (_metricsDb) return;
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(`
CREATE TABLE IF NOT EXISTS metrics (
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_session ON metrics(session_id)`,
);
db.exec(
`CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp)`,
);
db.exec(`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;
} 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;
ensureMetricsTable(db);
const ts = new Date().toISOString();
try {
const insert = db.prepare(
@ -476,8 +499,25 @@ function persistMetricsToDb(registry, sessionId, db) {
);
}
} catch (err) {
if (err.message?.includes("database is not open")) {
closeMetricsDb();
return;
}
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 ───────────────────────────────────────────────────────
@ -493,10 +533,8 @@ function flushMetrics() {
const path = metricsFilePath(_basePath);
mkdirSync(join(sfRoot(_basePath), "runtime"), { recursive: true });
writeFileSync(path, text, "utf-8");
// Also persist to DB if available
if (_dbAdapter) {
persistMetricsToDb(getRegistry(), _sessionId, _dbAdapter);
}
// Persist to dedicated metrics.db
persistMetricsToDb(getRegistry(), _sessionId, null);
// Update performance metrics
_flushSuccessCount++;
@ -562,7 +600,7 @@ function flushMetrics() {
export function initMetricsCentral(basePath, opts = {}) {
_basePath = basePath;
_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;
// Reset metrics system stats on fresh init
@ -582,10 +620,8 @@ export function initMetricsCentral(basePath, opts = {}) {
// Ensure timer doesn't keep process alive
if (_flushTimer.unref) _flushTimer.unref();
// Ensure DB table exists
if (_dbAdapter) {
ensureMetricsTable(_dbAdapter);
}
// Open dedicated metrics.db (separate from main sf.db to avoid WAL pressure)
openMetricsDb(basePath);
// Start periodic metrics system health reporting
if (!_metricsHealthTimer) {
@ -663,6 +699,7 @@ export function stopMetricsCentral() {
_basePath = "";
_sessionId = "";
_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) {
return !!db
.prepare(
@ -1464,6 +1464,22 @@ function initSchema(db, fileBacked) {
db.exec(
`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
.prepare("SELECT count(*) as cnt FROM schema_version")
.get();
@ -3173,6 +3189,42 @@ function migrateSchema(db) {
":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");
} catch (err) {
db.exec("ROLLBACK");