sf snapshot: uncommitted changes after 72m inactivity
This commit is contained in:
parent
6f174cabc1
commit
f66555456f
6 changed files with 118 additions and 26 deletions
BIN
.sf/backups/db/sf.db.2026-05-09T21-41-14-119Z
Normal file
BIN
.sf/backups/db/sf.db.2026-05-09T21-41-14-119Z
Normal file
Binary file not shown.
BIN
.sf/backups/db/sf.db.2026-05-09T22-17-48-976Z
Normal file
BIN
.sf/backups/db/sf.db.2026-05-09T22-17-48-976Z
Normal file
Binary file not shown.
BIN
.sf/metrics.db
Normal file
BIN
.sf/metrics.db
Normal file
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue