/** * SQLite table-creation SQL. * * SQLite stores only indexes and aggregates: users / login sessions / Project authorization / * Agent & Session indexes / usage summaries / error records / UI preferences. Agent State, * Trace, and Workspace still follow the local directory-based storage rules. * Product not yet released: no migration branches — everything is CREATE IF NOT EXISTS, formed * once. The one exception: columns added to an existing table after release of a web.db are * ALTERed in by the idempotent per-column guard in database.ts (ensureColumn), since CREATE * TABLE IF NOT EXISTS never touches an existing table. */ export const SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS users ( user_id TEXT PRIMARY KEY, -- semantic id doubles as login name: ^[a-z][a-z0-9_-]{1,31}$ password_hash TEXT NOT NULL, -- scrypt$N$r$p$salt$hash(base64) is_admin INTEGER NOT NULL DEFAULT 0, -- 1 for the built-in admin (seeded at startup) password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1=initial password (seeded/admin-set); cleared once the user changes it created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS auth_sessions ( token_hash TEXT PRIMARY KEY, -- sha256(token) hex; the cookie stores only the raw token user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, created_at TEXT NOT NULL, expires_at TEXT NOT NULL -- 7-day sliding renewal (topped up when <6 days remain) ); CREATE TABLE IF NOT EXISTS projects ( project_id TEXT PRIMARY KEY, -- directory name doubles as id; display name lives in project_config.toml owner_user_id TEXT NOT NULL REFERENCES users(user_id), created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS project_members ( -- member grants only; owners are not stored here project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY (project_id, user_id) ); CREATE TABLE IF NOT EXISTS agents ( -- index only; name/description live in system_config.yaml project_id TEXT NOT NULL, agent_id TEXT NOT NULL, created_at TEXT NOT NULL, PRIMARY KEY (project_id, agent_id) ); CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, agent_id TEXT NOT NULL, provider TEXT NOT NULL, -- provider group of the session model (paired with model_id as a model reference) model_id TEXT NOT NULL, -- upstream model id (sent to AgentHub as-is; never concatenate /) workspace TEXT NOT NULL, approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows "New chat") archived_at TEXT, -- archive time; NULL=not archived (shown by default; archived ones move under "Archived") client TEXT, -- creating client: 'web' (created via the Web App) | 'cli' (adopted from a CLI Trace); NULL = legacy row, treated as web has_trace INTEGER NOT NULL DEFAULT 0, -- cache: a Trace record exists (set at task start / adoption / subagent registration; lazily backfilled by list hydration) created_at TEXT NOT NULL ); -- the schedule/subagent SOURCE is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts); "client" is a different, DB-only axis (who created the row) CREATE INDEX IF NOT EXISTS idx_sessions_agent_created ON sessions(project_id, agent_id, created_at DESC); CREATE TABLE IF NOT EXISTS usage_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, date TEXT NOT NULL, -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories) project_id TEXT NOT NULL, agent_id TEXT NOT NULL, session_id TEXT NOT NULL, -- top-level Session (child-session usage counts toward its main Session) origin_session_id TEXT, -- immediate source child Session (last of the origin chain); NULL=main session provider TEXT NOT NULL, -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id model_id TEXT NOT NULL, -- upstream model id (paired with provider; same model_id across providers aggregates separately) cache_read INTEGER NOT NULL, cache_write INTEGER NOT NULL, output INTEGER NOT NULL, total INTEGER NOT NULL, -- from token_usage.request (one row per Request) status TEXT NOT NULL DEFAULT 'completed' -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate) ); -- cost is not stored: computed at query time from current pricing CREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date); CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_records(session_id); CREATE TABLE IF NOT EXISTS error_records ( -- server-side error capture (the Costs page's "Errors" tab) id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, date TEXT NOT NULL, -- local date yyyy-mm-dd (same convention as usage_records) project_id TEXT, -- nullable: sign-in/registration and process-level errors have no Project context agent_id TEXT, session_id TEXT, source TEXT NOT NULL, -- http | session | usage | title | subagent | process | llm | environment | schedule kind TEXT NOT NULL, -- expected (HttpError, business 4xx) | unexpected (500/runtime) code TEXT NOT NULL, -- HttpError.code / internal / session_run_failed / ... status INTEGER, -- HTTP status code; NULL for non-HTTP sources message TEXT NOT NULL -- truncated to 500 chars (no stack stored: stacks go to logs only) ); CREATE INDEX IF NOT EXISTS idx_error_project_date ON error_records(project_id, date); CREATE TABLE IF NOT EXISTS schedule_state ( -- schedule runtime state (files are declarative intent; the system never writes back) project_id TEXT NOT NULL, agent_id TEXT NOT NULL, name TEXT NOT NULL, -- file name (without .toml) doubles as the identifier creator_user_id TEXT, -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner) start_at_ms INTEGER NOT NULL, -- defines identity: a start_at change counts as a new task instance, resetting fire state def_hash TEXT NOT NULL, -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits) last_slot_ms INTEGER, -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts) last_fired_at TEXT, -- last actual fire time (for display) fired_once INTEGER NOT NULL DEFAULT 0, -- one-shot task has fired missed INTEGER NOT NULL DEFAULT 0, -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled) invalid_reason TEXT, -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy PRIMARY KEY (project_id, agent_id, name) ); CREATE TABLE IF NOT EXISTS goal_state ( -- goal-mode runtime state (GOAL.yaml on disk is the model-facing protocol; this row is what the UI reads) id INTEGER PRIMARY KEY AUTOINCREMENT, -- one row per goal run; a Session may run goals repeatedly, latest row wins for display session_id TEXT NOT NULL, project_id TEXT NOT NULL, agent_id TEXT NOT NULL, objective TEXT NOT NULL, status TEXT NOT NULL, -- active | complete | blocked | budget_limited | aborted (aborted is server-side only; on-disk status stays active) budget INTEGER NOT NULL, -- token budget; -1 = unlimited used INTEGER NOT NULL DEFAULT 0, -- uncached input + output, refreshed per round (mirrors the runner's accounting) rounds INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_goal_session ON goal_state(session_id); CREATE TABLE IF NOT EXISTS ui_prefs ( user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE, prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON ); `;