diff --git a/.sf/backups/db/sf.db.2026-05-09T22-35-23-473Z b/.sf/backups/db/sf.db.2026-05-09T22-35-23-473Z new file mode 100644 index 000000000..4df773fd4 Binary files /dev/null and b/.sf/backups/db/sf.db.2026-05-09T22-35-23-473Z differ diff --git a/.sf/backups/db/sf.db.2026-05-09T22-51-01-800Z b/.sf/backups/db/sf.db.2026-05-09T22-51-01-800Z new file mode 100644 index 000000000..bb2d0ddb1 Binary files /dev/null and b/.sf/backups/db/sf.db.2026-05-09T22-51-01-800Z differ diff --git a/.sf/metrics.db b/.sf/metrics.db index dbf369b39..e75c08b0b 100644 Binary files a/.sf/metrics.db and b/.sf/metrics.db differ diff --git a/.sf/metrics.db-shm b/.sf/metrics.db-shm new file mode 100644 index 000000000..73090e026 Binary files /dev/null and b/.sf/metrics.db-shm differ diff --git a/.sf/metrics.db-wal b/.sf/metrics.db-wal new file mode 100644 index 000000000..e064a1bdd Binary files /dev/null and b/.sf/metrics.db-wal differ diff --git a/src/resources/extensions/sf/bootstrap/register-hooks.js b/src/resources/extensions/sf/bootstrap/register-hooks.js index f633a9e64..d9fc59ee1 100644 --- a/src/resources/extensions/sf/bootstrap/register-hooks.js +++ b/src/resources/extensions/sf/bootstrap/register-hooks.js @@ -162,7 +162,7 @@ export function registerHooks(pi, ecosystemHandlers = []) { const sid = ctx.sessionManager?.getSessionId?.() ?? ""; const sfile = ctx.sessionManager?.getSessionFile?.() ?? ""; if (sid) { - process.stderr.write(`[forge] session ${sid.slice(0, 8)} ยท ${sfile}\n`); + process.stderr.write(`[forge] session ${sid.slice(0, 8)}\n`); } // Establish the session row so all subsequent turns have a parent. // Git context (repo, branch) is patched in before_agent_start once the diff --git a/src/resources/extensions/sf/cost-command.js b/src/resources/extensions/sf/cost-command.js index 384214987..08979597d 100644 --- a/src/resources/extensions/sf/cost-command.js +++ b/src/resources/extensions/sf/cost-command.js @@ -8,18 +8,16 @@ */ import { formatCost, getLedger, loadLedgerFromDisk } from "./metrics.js"; import { queryMetrics } from "./metrics-central.js"; -import { getDatabase } from "./sf-db.js"; export async function handleCost(args, ctx, basePath) { const showSession = args.includes("--session"); const showAll = args.includes("--all"); const showPrometheus = args.includes("--prometheus"); - // Try metrics-central DB first - const db = getDatabase(); - if (db && (showSession || showAll)) { + // Try metrics-central DB first (queryMetrics uses its own metrics.db connection) + if (showSession || showAll) { const sessionId = showSession ? extractSessionId() : null; - const rows = queryMetrics(db, sessionId, "sf_cost_total", 1000); + const rows = queryMetrics(null, sessionId, "sf_cost_total", 1000); if (rows.length > 0) { const totalCost = rows.reduce((sum, r) => sum + (r.value || 0), 0); const lines = [ diff --git a/src/resources/extensions/sf/metrics-central.js b/src/resources/extensions/sf/metrics-central.js index f96ef4ecb..c17658edc 100644 --- a/src/resources/extensions/sf/metrics-central.js +++ b/src/resources/extensions/sf/metrics-central.js @@ -659,7 +659,7 @@ function updateMetricsSystemHealth() { "Database connection status (1=connected, 0=disconnected)", ["project_path"], ) - .set({ project_path: _basePath || "unknown" }, _dbAdapter ? 1 : 0); + .set({ project_path: _basePath || "unknown" }, _metricsDb ? 1 : 0); // Record in-memory metrics count let totalMetrics = 0; @@ -910,8 +910,8 @@ export function readMetricsFile(basePath) { * @param {number} [limit] โ€” max rows to return * @returns {Array} โ€” metric rows */ -export function queryMetrics(db, sessionId = null, name = null, limit = 1000) { - if (!db) return []; +export function queryMetrics(_db, sessionId = null, name = null, limit = 1000) { + if (!_metricsDb) return []; try { let sql = "SELECT * FROM metrics WHERE 1=1"; const params = []; @@ -925,7 +925,7 @@ export function queryMetrics(db, sessionId = null, name = null, limit = 1000) { } sql += " ORDER BY timestamp DESC LIMIT ?"; params.push(limit); - const stmt = db.prepare(sql); + const stmt = _metricsDb.prepare(sql); return stmt.all(...params); } catch (err) { logWarning("metrics-central", `Query failed: ${err.message}`); diff --git a/src/resources/extensions/sf/sf-db.js b/src/resources/extensions/sf/sf-db.js index 142b8a8b8..ebc6401ed 100644 --- a/src/resources/extensions/sf/sf-db.js +++ b/src/resources/extensions/sf/sf-db.js @@ -244,7 +244,7 @@ function performDatabaseMaintenance(rawDb, path) { ); } } -const SCHEMA_VERSION = 56; +const SCHEMA_VERSION = 57; function indexExists(db, name) { return !!db .prepare( @@ -3225,6 +3225,19 @@ function migrateSchema(db) { ":applied_at": new Date().toISOString(), }); } + if (currentVersion < 57) { + // Schema v57: add archived_at to sessions for soft-delete / archive support. + db.exec(`ALTER TABLE sessions ADD COLUMN archived_at TEXT DEFAULT NULL`); + db.exec( + `CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(archived_at) WHERE archived_at IS NOT NULL`, + ); + db.prepare( + "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)", + ).run({ + ":version": 57, + ":applied_at": new Date().toISOString(), + }); + } db.exec("COMMIT"); } catch (err) { db.exec("ROLLBACK"); @@ -7252,6 +7265,38 @@ export function upsertSession(entry) { ":now": now, }); } + +/** + * Mark a session as archived. Archived sessions are hidden from default + * session listings but retained for search and audit. + * + * Purpose: soft-delete sessions without losing their turn history or refs. + * Consumer: /sf sessions --archive , autonomous cleanup. + */ +export function archiveSession(sessionId) { + if (!currentDb) return; + currentDb + .prepare( + `UPDATE sessions SET archived_at = :now, updated_at = :now WHERE session_id = :session_id`, + ) + .run({ ":session_id": sessionId, ":now": new Date().toISOString() }); +} + +/** + * Restore an archived session to active status. + * + * Purpose: undo an accidental archive without data loss. + * Consumer: /sf sessions --unarchive . + */ +export function unarchiveSession(sessionId) { + if (!currentDb) return; + currentDb + .prepare( + `UPDATE sessions SET archived_at = NULL, updated_at = :now WHERE session_id = :session_id`, + ) + .run({ ":session_id": sessionId, ":now": new Date().toISOString() }); +} + /** * Insert a turn row for a session. Returns the new turn's integer id so the * caller can link subsequent file-touches and refs to it. diff --git a/src/resources/extensions/sf/steerable-autonomous-extension.js b/src/resources/extensions/sf/steerable-autonomous-extension.js index a3fa031e3..2a20368d5 100644 --- a/src/resources/extensions/sf/steerable-autonomous-extension.js +++ b/src/resources/extensions/sf/steerable-autonomous-extension.js @@ -15,6 +15,8 @@ import { } from "./steerable-autonomous-panel.js"; import { SF_MODE_PRESET_NAMES, inferPresetName, resolvePreset } from "./operating-model.js"; import { getAutoSession } from "./auto/session.js"; +import { isAutoActive, startAutoDetached } from "./auto.js"; +import { projectRoot } from "./commands/context.js"; export default function steerableAutonomousExtension(api) { let panel = null; @@ -77,7 +79,7 @@ export default function steerableAutonomousExtension(api) { }); api.registerShortcut("ctrl+y", { - description: "Toggle YOLO mode (build + autonomous + deep + unrestricted; bypass git prompts)", + description: "Toggle YOLO mode (build + autonomous + deep + unrestricted; bypass git prompts). If not running, starts the autonomous loop immediately.", handler: async (ctx) => { const session = getAutoSession(); // Toggle full-autonomy preset in AutoSession (handles mode slam + restore) @@ -92,6 +94,13 @@ export default function steerableAutonomousExtension(api) { ? "๐Ÿš€ YOLO โ€” Build mode ยท no git prompts ยท no confirmation dialogs" : "๐Ÿš€ YOLO โ€” no git prompts ยท no confirmation dialogs ยท deep model"; ctx.ui.notify(msg, "success"); + // Start autonomous loop immediately if not already running + if (!isAutoActive()) { + startAutoDetached(ctx, api, projectRoot(), false, { + canAskUser: false, + skipModeGate: true, + }); + } } else { ctx.ui.notify("YOLO OFF โ€” Build mode (git prompts restored)", "info"); }