sf snapshot: uncommitted changes after 60m inactivity

This commit is contained in:
Mikael Hugo 2026-05-10 01:29:08 +02:00
parent f66555456f
commit 15185c2e7d
10 changed files with 64 additions and 12 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.sf/metrics.db-shm Normal file

Binary file not shown.

BIN
.sf/metrics.db-wal Normal file

Binary file not shown.

View file

@ -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

View file

@ -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 = [

View file

@ -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}`);

View file

@ -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 <id>, 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 <id>.
*/
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.

View file

@ -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");
}