1pub const SCHEMA: &str = r#"
8-- NOTE: pragmas (journal_mode=WAL, busy_timeout, foreign_keys) are set by
9-- the connection opener *before* this batch runs — concurrent openers race
10-- if we set them here.
11
12CREATE TABLE IF NOT EXISTS projects (
13 id TEXT PRIMARY KEY,
14 name TEXT NOT NULL
15);
16
17CREATE TABLE IF NOT EXISTS agents (
18 id TEXT PRIMARY KEY, -- "<project>:<agent>"
19 project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
20 role TEXT NOT NULL,
21 runtime TEXT NOT NULL,
22 is_manager INTEGER NOT NULL DEFAULT 0,
23 reports_to TEXT -- short name, resolved within project
24);
25
26CREATE INDEX IF NOT EXISTS agents_project_idx ON agents(project_id);
27
28CREATE TABLE IF NOT EXISTS messages (
29 id INTEGER PRIMARY KEY AUTOINCREMENT,
30 project_id TEXT NOT NULL,
31 sender TEXT NOT NULL, -- "<project>:<agent>" or "user:<handle>" or "cli"
32 recipient TEXT NOT NULL, -- "<project>:<agent>" or "channel:<project>:<name>"
33 text TEXT NOT NULL,
34 thread_id TEXT,
35 sent_at REAL NOT NULL,
36 delivered_at REAL,
37 acked_at REAL
38);
39
40CREATE INDEX IF NOT EXISTS messages_recipient_idx
41 ON messages(recipient, acked_at);
42CREATE INDEX IF NOT EXISTS messages_project_idx
43 ON messages(project_id, sent_at);
44
45-- Channels + subscriptions + per-agent ACLs.
46CREATE TABLE IF NOT EXISTS channels (
47 id TEXT PRIMARY KEY, -- "<project>:<name>"
48 project_id TEXT NOT NULL,
49 name TEXT NOT NULL,
50 wildcard INTEGER NOT NULL DEFAULT 0 -- 1 iff members = "*"
51);
52
53CREATE TABLE IF NOT EXISTS channel_members (
54 channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
55 agent_id TEXT NOT NULL,
56 PRIMARY KEY (channel_id, agent_id)
57);
58
59CREATE INDEX IF NOT EXISTS channel_members_agent_idx
60 ON channel_members(agent_id);
61
62CREATE TABLE IF NOT EXISTS agent_acls (
63 agent_id TEXT PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
64 can_dm_json TEXT NOT NULL DEFAULT '[]', -- ["dev","critic"]
65 can_bcast_json TEXT NOT NULL DEFAULT '[]' -- ["product","all"]
66);
67
68-- Inter-project manager bridges.
69CREATE TABLE IF NOT EXISTS bridges (
70 id INTEGER PRIMARY KEY AUTOINCREMENT,
71 from_agent TEXT NOT NULL, -- "<project>:<agent>", must be a manager
72 to_agent TEXT NOT NULL, -- "<project>:<agent>", must be a manager
73 topic TEXT NOT NULL,
74 opened_by TEXT NOT NULL, -- "user:<handle>" or "cli"
75 opened_at REAL NOT NULL,
76 expires_at REAL NOT NULL,
77 closed_at REAL
78);
79
80CREATE INDEX IF NOT EXISTS bridges_open_idx
81 ON bridges(expires_at, closed_at);
82
83-- Human-in-the-loop permission fabric.
84CREATE TABLE IF NOT EXISTS approvals (
85 id INTEGER PRIMARY KEY AUTOINCREMENT,
86 project_id TEXT NOT NULL,
87 agent_id TEXT NOT NULL,
88 action TEXT NOT NULL, -- "publish", "deploy", ...
89 scope_tag TEXT, -- optional narrower tag
90 summary TEXT NOT NULL,
91 payload_json TEXT,
92 status TEXT NOT NULL, -- pending | approved | denied | expired
93 requested_at REAL NOT NULL,
94 decided_at REAL,
95 decided_by TEXT,
96 decision_note TEXT,
97 expires_at REAL NOT NULL
98);
99
100CREATE INDEX IF NOT EXISTS approvals_pending_idx
101 ON approvals(status, expires_at);
102
103-- Budget ledger. Rows are appended by interface adapters and by runtime
104-- cost parsers. `teamctl budget` aggregates per project/day.
105CREATE TABLE IF NOT EXISTS budget (
106 id INTEGER PRIMARY KEY AUTOINCREMENT,
107 project_id TEXT NOT NULL,
108 agent_id TEXT,
109 runtime TEXT,
110 usd REAL NOT NULL DEFAULT 0,
111 input_tok INTEGER NOT NULL DEFAULT 0,
112 output_tok INTEGER NOT NULL DEFAULT 0,
113 observed_at REAL NOT NULL
114);
115
116CREATE INDEX IF NOT EXISTS budget_project_day_idx
117 ON budget(project_id, observed_at);
118
119-- Rate-limit events. Written by `teamctl rl-watch` whenever a runtime
120-- emits a rate-limit signature. Hooks (notify, webhook, run) run off these
121-- rows; the wrapper loop sleeps until `resets_at` before respawning.
122CREATE TABLE IF NOT EXISTS rate_limits (
123 id INTEGER PRIMARY KEY AUTOINCREMENT,
124 agent_id TEXT NOT NULL,
125 runtime TEXT NOT NULL,
126 hit_at REAL NOT NULL,
127 resets_at REAL, -- nullable: sometimes we can't parse
128 raw_match TEXT NOT NULL,
129 handled_at REAL
130);
131
132CREATE INDEX IF NOT EXISTS rate_limits_agent_idx
133 ON rate_limits(agent_id, hit_at);
134"#;