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 });
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue