0cc0bd1a1a
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
154 lines
12 KiB
TypeScript
154 lines
12 KiB
TypeScript
/**
|
|
* 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)
|
|
via TEXT -- 'password' | 'desktop'; NULL = legacy row (password)
|
|
);
|
|
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 <provider>/<id>)
|
|
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 | compaction | 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
|
|
);
|
|
CREATE TABLE IF NOT EXISTS server_settings ( -- admin-level server-global settings (GET/PUT /api/admin/settings)
|
|
key TEXT PRIMARY KEY, -- setting name, e.g. 'proxy_for_app'
|
|
value TEXT NOT NULL -- JSON-encoded value; an absent row means the setting's built-in default
|
|
);
|
|
CREATE TABLE IF NOT EXISTS trace_files ( -- DERIVED CACHE of the on-disk Trace tree (services/trace-index.ts): the directories stay the single source of truth, every row is rebuildable from disk, and a row is never authority for absence — consumers reconcile + retry on a miss, so a stale index costs one extra scan, never a false 404
|
|
project_id TEXT NOT NULL,
|
|
agent_id TEXT NOT NULL,
|
|
session_id TEXT NOT NULL,
|
|
file_index INTEGER NOT NULL, -- shard index NNN of <session_id>_NNN.jsonl (name/path are reconstructed from the row, never stored: the data root may move)
|
|
date TEXT NOT NULL, -- date directory name (local yyyy-mm-dd)
|
|
size_bytes INTEGER NOT NULL, -- last observed size (listings stat-refresh the returned page and write back; an actively-appended shard may lag in between)
|
|
page_stats TEXT, -- cached message-window scan state at this shard's END (services/message-window.ts ShardPrefixRecord JSON); immutable shards only, NULL = not yet computed, nulled whenever size_bytes changes
|
|
PRIMARY KEY (project_id, agent_id, session_id, file_index)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_trace_files_agent_date ON trace_files(project_id, agent_id, date);
|
|
CREATE INDEX IF NOT EXISTS idx_trace_files_session ON trace_files(session_id);
|
|
CREATE TABLE IF NOT EXISTS trace_sessions ( -- per-session facts read ONCE at registration from the earliest shard's head (same derived-cache rules as trace_files; listing needs zero head-reads)
|
|
session_id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL,
|
|
agent_id TEXT NOT NULL,
|
|
source TEXT, -- session_meta origin: 'subagent' | 'schedule' | NULL = user-created (or head not yet readable)
|
|
workspace TEXT NOT NULL DEFAULT '',
|
|
title TEXT, -- first-prompt fallback title (sessions.title always wins when present)
|
|
provider TEXT, -- model reference from session_meta (CLI adoption reads it from here; NULL = meta unreadable / legacy without provider)
|
|
model_id TEXT,
|
|
first_ts TEXT, -- first record's timestamp (adoption's createdAt fallback when the id embeds no time)
|
|
meta_read INTEGER NOT NULL DEFAULT 0 -- 1 once the head parsed; 0 = facts unknown, retried by the next reconcile that touches the session
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_trace_sessions_agent ON trace_sessions(project_id, agent_id);
|
|
`;
|