Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,975 @@
-- Migration 001: Baseline schema for nomifun-backend.
--
-- This file is the 2026-06-13 "primary-key redesign" baseline (see
-- docs/superpowers/specs/2026-06-13-primary-key-redesign-design.md). It
-- supersedes and squashes the former 001(seq baseline)/002(attachments)/
-- 003(channel_per_pet). The system has never shipped, so the baseline only
-- serves BRAND-NEW databases: final-state schema + seed data, NO backfill /
-- normalization. Any pre-baseline database is renamed `*.pre-baseline.bak`
-- and recreated by database.rs during the pre-launch window.
--
-- ID model (single id per entity, NO seq dual-track; display == primary key):
-- * Cross-device entities (id leaves this machine via remote protocol /
-- ACP transcript / external IM / cross-device coordination key) use the
-- string global id `{prefix}_{uuidv7}` minted by generate_prefixed_id:
-- messages(msg_) cron_jobs(cron_) agent_metadata(agent_builtin_/agent_)
-- providers(prov_) assistants assistant_plugins assistant_users(achu_)
-- assistant_sessions knowledge_bases(kb_) teams(team_) team_agents(slot_)
-- team_tasks(task_) attachments(att_) pets(pet_, filesystem) device_id.
-- * Local-only entities use INTEGER PRIMARY KEY AUTOINCREMENT (monotonic,
-- ordered, never reused): conversations, requirements, terminal_sessions,
-- conversation_artifacts, mcp_servers, remote_agents, webhooks, mailbox,
-- knowledge_bindings(binding_id). The user-facing conversation/requirement/
-- terminal ids render as `#N`; see
-- docs/superpowers/specs/2026-06-14-numeric-session-requirement-id-design.md.
-- * Natural keys unchanged: users.id, client_preferences.key,
-- oauth_tokens.server_url, requirement_tags.tag, tag_settings.tag,
-- assistant_pairing_codes.code, system_settings(id=1).
--
-- INVARIANT: a cross-device INTEGER id (conversation/requirement/terminal)
-- enters a remote payload only as the client-provided correlation tag (safe;
-- the real remote routing key is the peer-minted sessionKey). FK column type
-- == referenced table PK type. agent-address columns (teams.lead_agent_id,
-- mailbox.to_agent_id/from_agent_id, team_tasks.owner) are NOT foreign keys:
-- they hold a slot_id OR the 'user'/'lead' sentinels, so a FK would reject
-- valid rows.
--
-- Builtin agent seed ids use the stable slug scheme `agent_builtin_{backend}`.
-- Runtime lookup goes through find_builtin_by_backend, never id literals.
--
-- Requires PRAGMA foreign_keys = ON on every connection for the ON DELETE
-- cascades below to fire (set in database.rs connect options).
------------------------------------------------------------------------
-- Core tables
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL,
username TEXT NOT NULL UNIQUE,
email TEXT UNIQUE,
password_hash TEXT NOT NULL,
avatar_path TEXT,
jwt_secret TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_login INTEGER
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Singleton settings row. Deliberately NOT seeded: the settings repository
-- treats a missing row as "defaults" (get_settings returns Option) and lazily
-- creates it on first upsert.
CREATE TABLE IF NOT EXISTS system_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
language TEXT NOT NULL DEFAULT 'en-US',
notification_enabled INTEGER NOT NULL DEFAULT 1,
cron_notification_enabled INTEGER NOT NULL DEFAULT 0,
command_queue_enabled INTEGER NOT NULL DEFAULT 0,
save_upload_to_workspace INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS client_preferences (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
-- Cross-device: provider_id is embedded as a value-object snapshot in
-- conversation.model / pet config / idmm.sidecar and travels with them.
CREATE TABLE IF NOT EXISTS providers (
id TEXT PRIMARY KEY NOT NULL, -- prov_{uuidv7}
platform TEXT NOT NULL,
name TEXT NOT NULL,
base_url TEXT NOT NULL,
api_key_encrypted TEXT NOT NULL,
models TEXT NOT NULL DEFAULT '[]',
enabled INTEGER NOT NULL DEFAULT 1,
capabilities TEXT NOT NULL DEFAULT '[]',
context_limit INTEGER,
model_protocols TEXT,
model_enabled TEXT,
model_health TEXT,
bedrock_config TEXT,
is_full_url INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_providers_platform ON providers(platform);
------------------------------------------------------------------------
-- Conversations & Messages
------------------------------------------------------------------------
-- cron_job_id: the cron job that created this conversation (was the JSON
-- key extra.cronJobId + an expression index; now a real nullable FK column).
-- This forms a ring with cron_jobs.conversation_id: writers must INSERT the
-- cron row with conversation_id=NULL first, then the conversation, then
-- backfill both (see spec §9.A).
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL,
extra TEXT NOT NULL DEFAULT '{}',
model TEXT,
status TEXT NOT NULL DEFAULT 'pending'
CHECK(status IN ('pending', 'running', 'finished')),
source TEXT,
channel_chat_id TEXT,
pinned INTEGER NOT NULL DEFAULT 0,
pinned_at INTEGER,
cron_job_id TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (cron_job_id) REFERENCES cron_jobs(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_conversations_user_id ON conversations(user_id);
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at);
CREATE INDEX IF NOT EXISTS idx_conversations_type ON conversations(type);
CREATE INDEX IF NOT EXISTS idx_conversations_user_updated ON conversations(user_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_conversations_source ON conversations(source);
CREATE INDEX IF NOT EXISTS idx_conversations_source_updated ON conversations(source, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_conversations_source_chat ON conversations(source, channel_chat_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_conversations_cron_job_id ON conversations(cron_job_id);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY NOT NULL, -- msg_{uuidv7}
conversation_id INTEGER NOT NULL,
msg_id TEXT,
type TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '{}',
position TEXT CHECK(position IN ('left', 'right', 'center', 'pop')),
status TEXT CHECK(status IN ('finish', 'pending', 'error', 'work')),
hidden INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_type ON messages(type);
CREATE INDEX IF NOT EXISTS idx_messages_msg_id ON messages(msg_id);
CREATE INDEX IF NOT EXISTS idx_messages_conv_created ON messages(conversation_id, created_at);
CREATE INDEX IF NOT EXISTS idx_messages_conv_created_desc ON messages(conversation_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_messages_type_created ON messages(type, created_at DESC);
-- Local-only INTEGER surrogate id. Idempotency moved off the old composite
-- text id onto a partial unique index: skill_suggest is unique per
-- (conversation, cron_job); cron_trigger has NO unique (one row per fire).
CREATE TABLE IF NOT EXISTS conversation_artifacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id INTEGER NOT NULL,
cron_job_id TEXT,
kind TEXT NOT NULL
CHECK(kind IN ('cron_trigger', 'skill_suggest')),
status TEXT NOT NULL DEFAULT 'active'
CHECK(status IN ('active', 'pending', 'dismissed', 'saved')),
payload TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
FOREIGN KEY (cron_job_id) REFERENCES cron_jobs(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_conversation_artifacts_conversation_id ON conversation_artifacts(conversation_id);
CREATE INDEX IF NOT EXISTS idx_conversation_artifacts_created_at ON conversation_artifacts(created_at);
CREATE INDEX IF NOT EXISTS idx_conversation_artifacts_conversation_created ON conversation_artifacts(conversation_id, created_at);
CREATE INDEX IF NOT EXISTS idx_conversation_artifacts_cron_job ON conversation_artifacts(cron_job_id);
CREATE INDEX IF NOT EXISTS idx_conversation_artifacts_kind_status ON conversation_artifacts(kind, status);
CREATE UNIQUE INDEX IF NOT EXISTS uq_conversation_artifacts_skill_suggest
ON conversation_artifacts(conversation_id, cron_job_id) WHERE kind = 'skill_suggest';
------------------------------------------------------------------------
-- ACP Sessions
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS acp_session (
conversation_id INTEGER PRIMARY KEY,
agent_backend TEXT NOT NULL,
agent_source TEXT NOT NULL,
-- Nullable: an ACP conversation can be created before a concrete catalog
-- agent is picked (legacy clients post only `backend`, or nothing at all).
-- A non-NULL value is RESTRICT-bound to agent_metadata; NULL means
-- "no agent chosen yet" and is exempt from FK enforcement (SQLite does not
-- check FKs on NULL child columns). Replaces the old empty-string sentinel,
-- which the new RESTRICT FK would reject.
agent_id TEXT,
session_id TEXT,
session_status TEXT NOT NULL DEFAULT 'idle',
session_config TEXT NOT NULL DEFAULT '{}',
last_active_at INTEGER,
suspended_at INTEGER,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
FOREIGN KEY (agent_id) REFERENCES agent_metadata(id) ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS idx_acp_session_status ON acp_session(session_status);
CREATE INDEX IF NOT EXISTS idx_acp_session_suspended ON acp_session(session_status, suspended_at) WHERE session_status = 'suspended';
CREATE INDEX IF NOT EXISTS idx_acp_session_agent_id ON acp_session(agent_id);
------------------------------------------------------------------------
-- Agent Metadata
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS agent_metadata (
id TEXT PRIMARY KEY NOT NULL,
icon TEXT,
name TEXT NOT NULL,
name_i18n TEXT,
description TEXT,
description_i18n TEXT,
backend TEXT,
agent_type TEXT NOT NULL,
agent_source TEXT NOT NULL,
agent_source_info TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
command TEXT,
args TEXT,
env TEXT,
native_skills_dirs TEXT,
behavior_policy TEXT,
yolo_id TEXT,
agent_capabilities TEXT,
auth_methods TEXT,
config_options TEXT,
available_modes TEXT,
available_models TEXT,
available_commands TEXT,
sort_order INTEGER NOT NULL DEFAULT 1000,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agent_metadata_backend ON agent_metadata(backend);
CREATE INDEX IF NOT EXISTS idx_agent_metadata_agent_type ON agent_metadata(agent_type);
CREATE INDEX IF NOT EXISTS idx_agent_metadata_sort_order ON agent_metadata(sort_order);
-- Seed agent_metadata with builtin agents.
--
-- Values are the post-001/003/004/010/012 final state of the legacy migration
-- chain: bun package pins from 004, ACP handshake captures (agent_capabilities
-- / auth_methods) from 003, command/binary_name fixes for Qoder/Vibe/Kiro from
-- 003, and the internal agent display name "Nomi" from 012. Agents without a
-- 003 handshake capture (Claude, Codex, Gemini, OpenCode, Cursor, Hermes,
-- Snow, and the non-ACP rows) keep NULL capabilities until first spawn.
INSERT OR IGNORE INTO agent_metadata
(id, icon, name, backend, agent_type, agent_source, agent_source_info,
enabled, command, args, env, native_skills_dirs, behavior_policy, yolo_id,
agent_capabilities, auth_methods,
sort_order, created_at, updated_at)
VALUES
-- ACP builtin agents
('agent_builtin_claude', '/api/assets/logos/ai-major/claude.svg', 'Claude Code',
'claude', 'acp', 'builtin', '{"binary_name":"claude","bridge_binary":"bun"}',
1, 'bun', '["x","--bun","@agentclientprotocol/claude-agent-acp@0.33.1"]', '[]',
'[".claude/skills"]',
'{"supports_side_question":true,"self_identity_sticky":true,"session_load_via_meta_field":true,"supports_team":true}',
'bypassPermissions',
NULL, NULL,
3100,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_codex', '/api/assets/logos/tools/coding/codex.svg', 'Codex CLI',
'codex', 'acp', 'builtin', '{"binary_name":"codex","bridge_binary":"bun"}',
1, 'bun', '["x","--bun","@zed-industries/codex-acp@0.14.0"]', '[]',
'[".codex/skills"]',
'{"supports_side_question":false,"supports_team":true}',
'full-access',
NULL, NULL,
3110,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_gemini', '/api/assets/logos/ai-major/gemini.svg', 'Gemini CLI',
'gemini', 'acp', 'builtin', '{"binary_name":"gemini"}',
1, 'gemini', '["--experimental-acp"]', '[]',
'[".gemini/skills"]',
'{"supports_side_question":false,"supports_team":true}',
'yolo',
NULL, NULL,
3120,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_qwen', '/api/assets/logos/ai-china/qwen.svg', 'Qwen',
'qwen', 'acp', 'builtin', '{"binary_name":"qwen"}',
1, 'qwen', '["--acp"]', '[]',
'[".qwen/skills"]',
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"prompt_capabilities":{"image":true,"audio":true,"embedded_context":true},"session_capabilities":{"list":{},"resume":{}},"mcp_capabilities":{"sse":true,"http":true}}',
'[{"id":"openai","name":"Use OpenAI API key","description":"Requires setting the `OPENAI_API_KEY` environment variable","_meta":{"type":"terminal","args":["--auth-type=openai"]}},{"id":"qwen-oauth","name":"Qwen OAuth","description":"Qwen OAuth (free tier discontinued 2026-04-15)","_meta":{"type":"terminal","args":["--auth-type=qwen-oauth"]}}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_codebuddy', '/api/assets/logos/tools/coding/codebuddy.svg', 'CodeBuddy',
'codebuddy', 'acp', 'builtin', '{"binary_name":"codebuddy","bridge_binary":"bun"}',
1, 'bun', '["x","--bun","@tencent-ai/codebuddy-code@2.97.0","--acp"]', '[]',
'[".codebuddy/skills"]',
'{"supports_side_question":false,"supports_team":true}',
'bypassPermissions',
'{"prompt_capabilities":{"image":true,"embedded_context":true},"mcp_capabilities":{"http":true,"sse":true},"load_session":true,"delegate_tools_support":true}',
'[{"id":"iOA","name":"Login with iOA","description":null},{"id":"external","name":"Login with Google/Github","description":null},{"id":"internal","name":"Login with WeChat","description":null},{"id":"selfhosted","name":"Login with Enterprise Domain","description":null}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_droid', '/api/assets/logos/brand/droid.svg', 'Droid',
'droid', 'acp', 'builtin', '{"binary_name":"droid"}',
1, 'droid', '["exec","--output-format","acp"]', '[]',
'[".factory/skills"]',
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"session_capabilities":{"list":{},"resume":{}},"prompt_capabilities":{"image":true,"embedded_context":true},"_meta":{"terminal_output":true,"terminal-auth":true}}',
'[{"id":"device-pairing","name":"Login","description":"Authenticate with Factory using a device pairing code in your browser."},{"id":"factory-api-key","name":"Factory API Key","description":"Authenticate using a Factory API key set in the FACTORY_API_KEY environment variable."}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_goose', '/api/assets/logos/tools/goose.svg', 'Goose',
'goose', 'acp', 'builtin', '{"binary_name":"goose"}',
1, 'goose', '["acp"]', '[]',
'[".goose/skills"]',
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"prompt_capabilities":{"image":true,"audio":false,"embedded_context":true},"mcp_capabilities":{"http":true,"sse":false},"session_capabilities":{"list":{},"close":{}},"auth":{}}',
'[{"id":"goose-provider","name":"Configure Provider","description":"Run `goose configure` to set up your AI provider and API key"}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_auggie', '/api/assets/logos/brand/auggie.svg', 'Auggie',
'auggie', 'acp', 'builtin', '{"binary_name":"auggie"}',
1, 'auggie', '["--acp"]', '[]',
NULL,
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"prompt_capabilities":{"image":true},"session_capabilities":{"list":{}}}',
'[]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_kimi', '/api/assets/logos/ai-china/kimi.svg', 'Kimi',
'kimi', 'acp', 'builtin', '{"binary_name":"kimi"}',
1, 'kimi', '["acp"]', '[]',
'[".kimi/skills"]',
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"mcp_capabilities":{"http":true,"sse":false},"prompt_capabilities":{"audio":false,"embedded_context":true,"image":true},"session_capabilities":{"list":{},"resume":{}}}',
'[{"_meta":{"terminal-auth":{"command":"kimi","args":["login"],"label":"Kimi Code Login","env":{},"type":"terminal"}},"description":"Run `kimi login` command in the terminal, then follow the instructions to finish login.","id":"login","name":"Login with Kimi account"}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_opencode', '/api/assets/logos/tools/coding/opencode-light.svg', 'OpenCode',
'opencode', 'acp', 'builtin', '{"binary_name":"opencode"}',
1, 'opencode', '["acp"]', '[]',
'[".opencode/skills"]',
'{"supports_side_question":false}',
'build',
NULL, NULL,
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_copilot', '/api/assets/logos/tools/github.svg', 'Copilot',
'copilot', 'acp', 'builtin', '{"binary_name":"copilot"}',
1, 'copilot', '["--acp","--stdio"]', '[]',
NULL,
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"mcp_capabilities":{"http":true,"sse":true},"prompt_capabilities":{"image":true,"audio":false,"embedded_context":true},"session_capabilities":{"list":{}}}',
'[{"id":"copilot-login","name":"Log in with Copilot CLI","description":"Run `copilot login` in the terminal","_meta":{"terminal-auth":{"command":"copilot","args":["login"],"label":"Copilot Login"}}}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_qoder', '/api/assets/logos/tools/coding/qoder.png', 'Qoder',
'qoder', 'acp', 'builtin', '{"binary_name":"qodercli"}',
1, 'qodercli', '["--acp"]', '[]',
NULL,
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"session_capabilities":{"list":{}},"prompt_capabilities":{"image":true,"audio":true,"embedded_context":true},"mcp_capabilities":{"http":true,"sse":true}}',
'[{"id":"qodercli-login","name":"Use qodercli login","description":"Use your existing qodercli login for this agent. If needed, sign in from qodercli first."},{"type":"env_var","id":"qoder-personal-access-token","name":"Use QODER_PERSONAL_ACCESS_TOKEN","description":"Requires `QODER_PERSONAL_ACCESS_TOKEN` in the agent environment.","vars":[{"name":"QODER_PERSONAL_ACCESS_TOKEN"}]}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_vibe', '/api/assets/logos/ai-major/mistral.svg', 'Vibe',
'vibe', 'acp', 'builtin', '{"binary_name":"vibe-acp"}',
1, 'vibe-acp', '[]', '[]',
'[".vibe/skills"]',
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"prompt_capabilities":{"audio":false,"embedded_context":true,"image":false},"session_capabilities":{"close":{},"fork":{},"list":{}}}',
'[]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_cursor', '/api/assets/logos/tools/coding/cursor.png', 'Cursor',
'cursor', 'acp', 'builtin', '{"binary_name":"agent"}',
1, 'agent', '["acp"]', '[]',
'[".cursor/skills"]',
'{"supports_side_question":false}',
'agent',
NULL, NULL,
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_kiro', NULL, 'Kiro',
'kiro', 'acp', 'builtin', '{"binary_name":"kiro-cli"}',
1, 'kiro-cli', '["acp"]', '[]',
NULL,
'{"supports_side_question":false}',
'yolo',
'{"load_session":true,"prompt_capabilities":{"image":true,"audio":false,"embedded_context":false},"mcp_capabilities":{"http":true,"sse":false},"session_capabilities":{}}',
'[{"id":"kiro-login","name":"Kiro Login","description":"Run ''kiro-cli login'' in terminal to authenticate. See https://kiro.dev/docs/cli/authentication/"}]',
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_hermes', '/api/assets/logos/brand/hermes.svg', 'Hermes',
'hermes', 'acp', 'builtin', '{"binary_name":"hermes"}',
1, 'hermes', '["acp"]', '[]',
NULL,
'{"supports_side_question":false}',
'yolo',
NULL, NULL,
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_snow', '/api/assets/logos/tools/coding/snow.png', 'Snow',
'snow', 'acp', 'builtin', '{"binary_name":"snow"}',
1, 'snow', '["--acp"]', '[]',
NULL,
'{"supports_side_question":false}',
'yolo',
NULL, NULL,
3130,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
-- Non-ACP builtins
('agent_builtin_nanobot', '/api/assets/logos/tools/nanobot.svg', 'Nanobot',
NULL, 'nanobot', 'builtin', '{"binary_name":"nanobot"}',
1, 'nanobot', '["--experimental-acp"]', '[]',
NULL,
'{}',
'yolo',
NULL, NULL,
3990,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
('agent_builtin_openclaw', '/api/assets/logos/tools/openclaw.svg', 'OpenClaw',
NULL, 'openclaw-gateway', 'builtin', '{"binary_name":"openclaw"}',
1, 'openclaw', '[]', '[]',
NULL,
'{}',
'yolo',
NULL, NULL,
3900,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000),
-- Internal
('agent_builtin_nomi', '/api/assets/logos/brand/nomi.svg', 'Nomi',
NULL, 'nomi', 'internal', '{}',
1, NULL, '[]', '[]',
'[".nomi/skills"]',
'{"supports_team":true}',
'yolo',
NULL, NULL,
100,
unixepoch('now','subsec')*1000, unixepoch('now','subsec')*1000);
------------------------------------------------------------------------
-- Remote Agents & MCP
------------------------------------------------------------------------
-- Local-only INTEGER id; the cross-device identity is device_id (a derived
-- key), not this row id.
CREATE TABLE IF NOT EXISTS remote_agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
protocol TEXT NOT NULL,
url TEXT NOT NULL,
auth_type TEXT NOT NULL,
auth_token TEXT,
allow_insecure INTEGER NOT NULL DEFAULT 0,
avatar TEXT,
description TEXT,
device_id TEXT,
device_public_key TEXT,
device_private_key TEXT,
device_token TEXT,
status TEXT NOT NULL DEFAULT 'unknown',
last_connected_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_remote_agents_status ON remote_agents(status);
CREATE TABLE IF NOT EXISTS mcp_servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
enabled INTEGER NOT NULL DEFAULT 0,
transport_type TEXT NOT NULL,
transport_config TEXT NOT NULL,
tools TEXT,
last_test_status TEXT NOT NULL DEFAULT 'disconnected',
last_connected INTEGER,
original_json TEXT,
builtin INTEGER NOT NULL DEFAULT 0,
deleted_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_mcp_servers_name ON mcp_servers(name);
CREATE INDEX IF NOT EXISTS idx_mcp_servers_enabled ON mcp_servers(enabled);
CREATE INDEX IF NOT EXISTS idx_mcp_servers_deleted_at ON mcp_servers(deleted_at);
CREATE TABLE IF NOT EXISTS oauth_tokens (
server_url TEXT PRIMARY KEY NOT NULL,
access_token TEXT NOT NULL,
refresh_token TEXT,
token_type TEXT NOT NULL DEFAULT 'bearer',
expires_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
------------------------------------------------------------------------
-- Assistants (channel / IM-facing: cross-device TEXT ids)
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS assistants (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
avatar TEXT,
preset_agent_type TEXT NOT NULL DEFAULT 'gemini',
enabled_skills TEXT,
custom_skill_names TEXT,
disabled_builtin_skills TEXT,
prompts TEXT,
models TEXT,
name_i18n TEXT,
description_i18n TEXT,
prompts_i18n TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_assistants_updated_at ON assistants(updated_at DESC);
-- assistant_id may reference a builtin assistant defined in JSON (not a row
-- in `assistants`), so NO foreign key: orphans are GC'd by the assistant
-- service's delete_orphans(valid_ids).
CREATE TABLE IF NOT EXISTS assistant_overrides (
assistant_id TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
preset_agent_type TEXT,
last_used_at INTEGER,
updated_at INTEGER NOT NULL
);
-- Multi-row channel plugins: one row per connected bot. `pet_id` binds the
-- bot to one pet (filesystem entity, no FK). `bot_key` is the platform-level
-- bot identity; the partial unique index guarantees one bot binds to at most
-- one pet. (Merged from former migration 003; legacy backfill dropped.)
CREATE TABLE IF NOT EXISTS assistant_plugins (
id TEXT PRIMARY KEY NOT NULL,
type TEXT NOT NULL,
name TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
config TEXT NOT NULL,
status TEXT,
last_connected INTEGER,
pet_id TEXT,
bot_key TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_assistant_plugins_type_bot_key
ON assistant_plugins(type, bot_key) WHERE bot_key IS NOT NULL;
CREATE TABLE IF NOT EXISTS assistant_users (
id TEXT PRIMARY KEY NOT NULL, -- achu_{uuidv7}
platform_user_id TEXT NOT NULL,
platform_type TEXT NOT NULL,
display_name TEXT,
authorized_at INTEGER NOT NULL,
last_active INTEGER,
session_id TEXT,
UNIQUE (platform_user_id, platform_type)
);
-- channel_id: which bot plugin owns this session (so two bots sharing a chat
-- get isolated sessions). Merged from former migration 003.
CREATE TABLE IF NOT EXISTS assistant_sessions (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL,
agent_type TEXT NOT NULL,
conversation_id INTEGER,
workspace TEXT,
chat_id TEXT,
channel_id TEXT,
created_at INTEGER NOT NULL,
last_activity INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES assistant_users(id) ON DELETE CASCADE,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL,
FOREIGN KEY (channel_id) REFERENCES assistant_plugins(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_assistant_sessions_user_id ON assistant_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_assistant_sessions_user_chat ON assistant_sessions(user_id, chat_id);
CREATE INDEX IF NOT EXISTS idx_assistant_sessions_channel ON assistant_sessions(channel_id);
CREATE TABLE IF NOT EXISTS assistant_pairing_codes (
code TEXT PRIMARY KEY NOT NULL,
platform_user_id TEXT NOT NULL,
platform_type TEXT NOT NULL,
display_name TEXT,
requested_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'approved', 'rejected', 'expired'))
);
CREATE INDEX IF NOT EXISTS idx_pairing_codes_status ON assistant_pairing_codes(status);
------------------------------------------------------------------------
-- Teams (cross-device TEXT ids: team_/slot_/task_ travel via guide MCP
-- result + team wake prompts + MCP env, recorded in ACP transcripts)
------------------------------------------------------------------------
-- lead_agent_id is an agent-address (a slot_id, or the 'lead'/'user'
-- sentinel), NOT a foreign key.
CREATE TABLE IF NOT EXISTS teams (
id TEXT PRIMARY KEY NOT NULL, -- team_{uuidv7}
user_id TEXT NOT NULL DEFAULT 'system_default_user',
name TEXT NOT NULL,
workspace TEXT NOT NULL DEFAULT '',
workspace_mode TEXT NOT NULL DEFAULT 'shared',
lead_agent_id TEXT,
session_mode TEXT,
agents_version TEXT NOT NULL DEFAULT '1.0.0',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_teams_user_id ON teams(user_id);
CREATE INDEX IF NOT EXISTS idx_teams_updated_at ON teams(updated_at);
-- Columnized from the former teams.agents JSON array. slot_id stays a string
-- PK because it is transmitted in the MCP env (TEAM_AGENT_SLOT_ID) and remote
-- protocol. conversation_id FK CASCADE: create flow inserts the slot's
-- conversation before the slot row (see spec §9.A).
CREATE TABLE IF NOT EXISTS team_agents (
slot_id TEXT PRIMARY KEY NOT NULL, -- slot_{uuidv7}
team_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'teammate',
conversation_id INTEGER,
backend TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
custom_agent_id TEXT,
status TEXT,
conversation_type TEXT,
cli_path TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_team_agents_team ON team_agents(team_id, sort_order);
CREATE INDEX IF NOT EXISTS idx_team_agents_conversation ON team_agents(conversation_id);
-- to_agent_id / from_agent_id are agent-addresses (slot_id or 'user'/'lead'
-- sentinel), NOT foreign keys.
CREATE TABLE IF NOT EXISTS mailbox (
id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id TEXT NOT NULL,
to_agent_id TEXT NOT NULL,
from_agent_id TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('message', 'idle_notification', 'shutdown_request')),
content TEXT NOT NULL,
summary TEXT,
files TEXT,
read INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_mailbox_team_to_read ON mailbox(team_id, to_agent_id, read);
CREATE INDEX IF NOT EXISTS idx_mailbox_team_id ON mailbox(team_id);
-- owner is an agent-address (slot_id or sentinel), NOT a foreign key.
-- blocked_by/blocks JSON arrays are columnized into team_task_deps.
CREATE TABLE IF NOT EXISTS team_tasks (
id TEXT PRIMARY KEY NOT NULL, -- task_{uuidv7}
team_id TEXT NOT NULL,
subject TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'in_progress', 'completed', 'deleted')),
owner TEXT,
metadata TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_team_tasks_team_id ON team_tasks(team_id);
-- Single-directed dependency edge (replaces the bidirectional blocked_by/
-- blocks JSON arrays). "who blocks X" = WHERE blocked_task_id=X; "what X
-- blocks" = WHERE blocker_task_id=X.
CREATE TABLE IF NOT EXISTS team_task_deps (
blocker_task_id TEXT NOT NULL,
blocked_task_id TEXT NOT NULL,
PRIMARY KEY (blocker_task_id, blocked_task_id),
CHECK (blocker_task_id <> blocked_task_id),
FOREIGN KEY (blocker_task_id) REFERENCES team_tasks(id) ON DELETE CASCADE,
FOREIGN KEY (blocked_task_id) REFERENCES team_tasks(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_team_task_deps_blocked ON team_task_deps(blocked_task_id);
------------------------------------------------------------------------
-- Cron Jobs
------------------------------------------------------------------------
-- conversation_id is now NULLABLE with a FK (was NOT NULL, no FK): a
-- new_conversation job has no target until first fire. terminal_session_id
-- FK SET NULL (terminal lazily created).
CREATE TABLE IF NOT EXISTS cron_jobs (
id TEXT PRIMARY KEY NOT NULL, -- cron_{uuidv7}
name TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
schedule_kind TEXT NOT NULL CHECK(schedule_kind IN ('at', 'every', 'cron')),
schedule_value TEXT NOT NULL,
schedule_tz TEXT,
schedule_description TEXT,
payload_message TEXT NOT NULL,
execution_mode TEXT NOT NULL DEFAULT 'existing'
CHECK(execution_mode IN ('existing', 'new_conversation')),
agent_config TEXT,
conversation_id INTEGER,
conversation_title TEXT,
agent_type TEXT NOT NULL,
created_by TEXT NOT NULL CHECK(created_by IN ('user', 'agent')),
skill_content TEXT,
description TEXT,
target_kind TEXT NOT NULL DEFAULT 'agent',
terminal_mode TEXT,
terminal_session_id INTEGER,
terminal_command TEXT,
terminal_args TEXT,
terminal_script TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
next_run_at INTEGER,
last_run_at INTEGER,
last_status TEXT CHECK(last_status IN ('ok', 'error', 'skipped', 'missed')),
last_error TEXT,
run_count INTEGER NOT NULL DEFAULT 0,
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 3,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE SET NULL,
FOREIGN KEY (terminal_session_id) REFERENCES terminal_sessions(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_cron_jobs_conversation ON cron_jobs(conversation_id);
CREATE INDEX IF NOT EXISTS idx_cron_jobs_next_run ON cron_jobs(next_run_at) WHERE enabled = 1;
CREATE INDEX IF NOT EXISTS idx_cron_jobs_agent_type ON cron_jobs(agent_type);
CREATE INDEX IF NOT EXISTS idx_cron_jobs_terminal_session ON cron_jobs(terminal_session_id);
------------------------------------------------------------------------
-- Terminal sessions
------------------------------------------------------------------------
-- PTY-backed interactive sessions. Scrollback kept in-memory (not persisted).
-- autowork: JSON {enabled, tag, max_requirements} — AutoWork config, nullable.
-- idmm: JSON blob — IDMM per-terminal stall-supervision config, nullable.
CREATE TABLE IF NOT EXISTS terminal_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
cwd TEXT NOT NULL,
command TEXT NOT NULL,
args TEXT NOT NULL DEFAULT '[]',
env TEXT,
backend TEXT,
mode TEXT,
cols INTEGER NOT NULL DEFAULT 80,
rows INTEGER NOT NULL DEFAULT 24,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_status TEXT NOT NULL DEFAULT 'running'
CHECK(last_status IN ('running', 'exited', 'error')),
exit_code INTEGER,
user_id TEXT NOT NULL,
pinned INTEGER NOT NULL DEFAULT 0,
pinned_at INTEGER,
autowork TEXT,
idmm TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_terminal_sessions_user ON terminal_sessions(user_id);
------------------------------------------------------------------------
-- Requirements Platform
------------------------------------------------------------------------
-- owner_session_id records the executing session and is a dual-domain
-- address (a conv_* conversation id OR a term_* terminal id), discriminated
-- by owner_kind. No FK (single column cannot reference two tables); when a
-- conversation/terminal is deleted the service clears the matching owner
-- (clear_owner_for_session, spec §9.B). The owner token replaces the former
-- redundant claimed_by column.
CREATE TABLE IF NOT EXISTS requirements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL DEFAULT '',
tag TEXT NOT NULL,
order_key TEXT NOT NULL DEFAULT '',
sort_seq TEXT NOT NULL DEFAULT '', -- normalized sortable form of order_key (NOT a display seq)
status TEXT NOT NULL DEFAULT 'pending',
priority INTEGER NOT NULL DEFAULT 0,
completion_note TEXT,
owner_session_id INTEGER, -- conversation OR terminal id; no FK
owner_kind TEXT CHECK(owner_kind IS NULL OR owner_kind IN ('conversation', 'terminal')),
claimed_at INTEGER,
lease_expires_at INTEGER,
started_at INTEGER,
completed_at INTEGER,
attempt_count INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL DEFAULT 'user',
extra TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
CHECK ((owner_session_id IS NULL) = (owner_kind IS NULL))
);
CREATE INDEX IF NOT EXISTS idx_requirements_tag_status ON requirements(tag, status);
CREATE INDEX IF NOT EXISTS idx_requirements_tag_order ON requirements(tag, sort_seq);
CREATE INDEX IF NOT EXISTS idx_requirements_owner ON requirements(owner_session_id);
CREATE INDEX IF NOT EXISTS idx_requirements_status ON requirements(status);
-- AutoWork tag-level pause. paused_req_id FK SET NULL: the triggering
-- requirement may be deleted while the pause stays.
CREATE TABLE IF NOT EXISTS requirement_tags (
tag TEXT PRIMARY KEY,
paused INTEGER NOT NULL DEFAULT 0,
paused_reason TEXT,
paused_req_id INTEGER,
paused_at INTEGER,
FOREIGN KEY (paused_req_id) REFERENCES requirements(id) ON DELETE SET NULL
);
------------------------------------------------------------------------
-- Webhooks + per-tag settings (AutoWork completion notifications)
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS webhooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
platform TEXT NOT NULL DEFAULT 'lark',
url TEXT NOT NULL,
secret TEXT,
description TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tag_settings (
tag TEXT PRIMARY KEY,
webhook_id INTEGER,
description TEXT NOT NULL DEFAULT '',
updated_at INTEGER NOT NULL,
FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON DELETE SET NULL
);
------------------------------------------------------------------------
-- Knowledge Base platform
------------------------------------------------------------------------
-- Cross-device: kb id is an agent-facing gateway tool argument/result, so it
-- enters the master agent's ACP transcript -> string global id.
CREATE TABLE IF NOT EXISTS knowledge_bases (
id TEXT PRIMARY KEY, -- kb_{uuidv7}
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
root_path TEXT NOT NULL,
managed INTEGER NOT NULL DEFAULT 1,
extra TEXT NOT NULL DEFAULT '{}',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Per-target mount binding. The former composite PK (target_kind,target_id)
-- and JSON kb_ids array are redesigned into a surrogate binding_id +
-- type-discriminated nullable target columns (CHECK exactly-one) +
-- knowledge_binding_bases junction. target_kind set is owned by the
-- nomifun-knowledge service (BINDING_KINDS = workpath/conversation/terminal/pet).
-- workpath: normalized workspace path key (not an entity, no FK)
-- conversation/terminal: real TEXT FK CASCADE (binding dies with the session)
-- pet: pet_{} filesystem entity (no FK; pet service cleans on delete)
-- writeback_mode: 'staged' confines agent writes to {kb}/_inbox/{conversation_id}/
CREATE TABLE IF NOT EXISTS knowledge_bindings (
binding_id INTEGER PRIMARY KEY AUTOINCREMENT,
target_kind TEXT NOT NULL,
target_workpath TEXT,
target_conv_id INTEGER,
target_term_id INTEGER,
target_pet_id TEXT,
enabled INTEGER NOT NULL DEFAULT 0,
writeback INTEGER NOT NULL DEFAULT 0,
writeback_mode TEXT NOT NULL DEFAULT 'staged'
CHECK(writeback_mode IN ('staged', 'direct')),
updated_at INTEGER NOT NULL,
FOREIGN KEY (target_conv_id) REFERENCES conversations(id) ON DELETE CASCADE,
FOREIGN KEY (target_term_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE,
CHECK (
(target_kind = 'workpath' AND target_workpath IS NOT NULL
AND target_conv_id IS NULL AND target_term_id IS NULL AND target_pet_id IS NULL)
OR (target_kind = 'conversation' AND target_conv_id IS NOT NULL
AND target_workpath IS NULL AND target_term_id IS NULL AND target_pet_id IS NULL)
OR (target_kind = 'terminal' AND target_term_id IS NOT NULL
AND target_workpath IS NULL AND target_conv_id IS NULL AND target_pet_id IS NULL)
OR (target_kind = 'pet' AND target_pet_id IS NOT NULL
AND target_workpath IS NULL AND target_conv_id IS NULL AND target_term_id IS NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_workpath ON knowledge_bindings(target_workpath) WHERE target_workpath IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_conv ON knowledge_bindings(target_conv_id) WHERE target_conv_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_term ON knowledge_bindings(target_term_id) WHERE target_term_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_pet ON knowledge_bindings(target_pet_id) WHERE target_pet_id IS NOT NULL;
-- Columnized from the former knowledge_bindings.kb_ids JSON array.
CREATE TABLE IF NOT EXISTS knowledge_binding_bases (
binding_id INTEGER NOT NULL,
kb_id TEXT NOT NULL,
position INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (binding_id, kb_id),
FOREIGN KEY (binding_id) REFERENCES knowledge_bindings(binding_id) ON DELETE CASCADE,
FOREIGN KEY (kb_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_kb_binding_bases_kb ON knowledge_binding_bases(kb_id);
------------------------------------------------------------------------
-- Attachments (requirement images; was migration 002)
------------------------------------------------------------------------
-- Cross-device: att id rides the requirement DTO into the master agent's ACP
-- transcript (nomi_requirement_update result) -> string global id. The former
-- generic (kind, target_id) polymorphism is collapsed to a real
-- requirement_id FK (only the requirement kind was ever used).
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY, -- att_{uuidv7}
requirement_id INTEGER NOT NULL,
file_name TEXT NOT NULL, -- original display name (deduped per requirement)
rel_path TEXT NOT NULL, -- relative to data_dir
mime TEXT NOT NULL,
size_bytes INTEGER NOT NULL,
created_by TEXT,
created_at INTEGER NOT NULL,
FOREIGN KEY (requirement_id) REFERENCES requirements(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_attachments_requirement ON attachments(requirement_id);
------------------------------------------------------------------------
-- Conversation <-> MCP server selection (was conversations.extra.selected_mcp_server_ids)
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS conversation_mcp_servers (
conversation_id INTEGER NOT NULL,
mcp_server_id INTEGER NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (conversation_id, mcp_server_id),
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
FOREIGN KEY (mcp_server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_conversation_mcp_servers_mcp ON conversation_mcp_servers(mcp_server_id);
@@ -0,0 +1,20 @@
-- Migration 002: write-back "eagerness" (回写意识) for knowledge bindings.
--
-- Adds a SECOND, orthogonal write-back axis to `knowledge_bindings`. The
-- existing `writeback_mode` controls WHERE agent writes land (`staged` inbox
-- vs `direct` body); this new `writeback_eagerness` controls HOW EAGERLY the
-- agent writes at all, while write-back is enabled:
-- * 'conservative' — the historical, restrained default: only persist
-- knowledge the model judges clearly worth keeping.
-- * 'aggressive' — capture anything plausibly relevant to a mounted base
-- without much hesitation; the user prunes later.
-- Both are purely prompt-contract wording (rendered by
-- nomifun-knowledge::context); the column only persists the user's pick.
--
-- Additive on purpose: editing 001_baseline would change its checksum and
-- trip the pre-baseline rebuild path (database.rs), wiping existing dev DBs.
-- SQLite allows ADD COLUMN with a NOT NULL DEFAULT + CHECK; the default
-- satisfies the CHECK for every pre-existing row.
ALTER TABLE knowledge_bindings
ADD COLUMN writeback_eagerness TEXT NOT NULL DEFAULT 'conservative'
CHECK(writeback_eagerness IN ('conservative', 'aggressive'));
@@ -0,0 +1,77 @@
-- 002_companion_rename.sql
-- 「pet」域整体更名为「companion」(数字伙伴)。001_baseline 已冻结(sqlx 校验和)
-- 故所有列/值的前向更名集中在本迁移:新库 = 001 建 pet_* 列后由本迁移改为 companion_*
-- 既有库 = 本迁移把存量数据迁到 companion 名。代码侧标识符已全部改为 companion。
-- 1) assistant_plugins.pet_id -> companion_id(该列无索引/约束依赖,直接改名)。
ALTER TABLE assistant_plugins RENAME COLUMN pet_id TO companion_id;
-- 2) knowledge_bindingstarget_pet_id -> target_companion_id 且 kind 'pet' -> 'companion'。
-- CHECK 约束内嵌字面量 'pet' 与列名,SQLite 无法 ALTER CHECK,需整表重建。
-- knowledge_binding_bases 通过 ON DELETE CASCADE 挂在本表下:重建期先备份、后恢复,
-- binding_id 全程保留以维持外键引用;对 foreign_keys 开/关两态都正确(DELETE+INSERT 幂等)。
CREATE TEMP TABLE _kbb_backup AS SELECT * FROM knowledge_binding_bases;
CREATE TABLE knowledge_bindings_new (
binding_id INTEGER PRIMARY KEY AUTOINCREMENT,
target_kind TEXT NOT NULL,
target_workpath TEXT,
target_conv_id INTEGER,
target_term_id INTEGER,
target_companion_id TEXT,
enabled INTEGER NOT NULL DEFAULT 0,
writeback INTEGER NOT NULL DEFAULT 0,
writeback_mode TEXT NOT NULL DEFAULT 'staged'
CHECK(writeback_mode IN ('staged', 'direct')),
writeback_eagerness TEXT NOT NULL DEFAULT 'conservative'
CHECK(writeback_eagerness IN ('conservative', 'aggressive')),
updated_at INTEGER NOT NULL,
FOREIGN KEY (target_conv_id) REFERENCES conversations(id) ON DELETE CASCADE,
FOREIGN KEY (target_term_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE,
CHECK (
(target_kind = 'workpath' AND target_workpath IS NOT NULL
AND target_conv_id IS NULL AND target_term_id IS NULL AND target_companion_id IS NULL)
OR (target_kind = 'conversation' AND target_conv_id IS NOT NULL
AND target_workpath IS NULL AND target_term_id IS NULL AND target_companion_id IS NULL)
OR (target_kind = 'terminal' AND target_term_id IS NOT NULL
AND target_workpath IS NULL AND target_conv_id IS NULL AND target_companion_id IS NULL)
OR (target_kind = 'companion' AND target_companion_id IS NOT NULL
AND target_workpath IS NULL AND target_conv_id IS NULL AND target_term_id IS NULL)
)
);
INSERT INTO knowledge_bindings_new
(binding_id, target_kind, target_workpath, target_conv_id, target_term_id, target_companion_id, enabled, writeback, writeback_mode, writeback_eagerness, updated_at)
SELECT binding_id,
CASE WHEN target_kind = 'pet' THEN 'companion' ELSE target_kind END,
target_workpath, target_conv_id, target_term_id, target_pet_id,
enabled, writeback, writeback_mode, writeback_eagerness, updated_at
FROM knowledge_bindings;
DROP TABLE knowledge_bindings;
ALTER TABLE knowledge_bindings_new RENAME TO knowledge_bindings;
-- 恢复子表(CASCADE 可能已清空,或 FK 关闭时仍在):清后按备份重灌,保证幂等无重复。
DELETE FROM knowledge_binding_bases;
INSERT INTO knowledge_binding_bases SELECT * FROM _kbb_backup;
DROP TABLE _kbb_backup;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_workpath ON knowledge_bindings(target_workpath) WHERE target_workpath IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_conv ON knowledge_bindings(target_conv_id) WHERE target_conv_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_term ON knowledge_bindings(target_term_id) WHERE target_term_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS uq_kb_binding_companion ON knowledge_bindings(target_companion_id) WHERE target_companion_id IS NOT NULL;
-- 3) conversations.extrapetCompanion -> companionSession(保留 JSON 布尔型,后端 as_bool 依赖),
-- petId -> companionId。companion 会话两键恒同时存在;先迁真值行,再清理任何遗留旧键。
UPDATE conversations
SET extra = json_remove(
json_set(
json_set(extra, '$.companionId', json_extract(extra, '$.petId')),
'$.companionSession', json('true')
),
'$.petId', '$.petCompanion')
WHERE json_valid(extra) AND json_extract(extra, '$.petCompanion') = 1;
UPDATE conversations
SET extra = json_remove(extra, '$.petCompanion', '$.petId')
WHERE json_valid(extra) AND (json_extract(extra, '$.petCompanion') IS NOT NULL OR json_extract(extra, '$.petId') IS NOT NULL);
@@ -0,0 +1,50 @@
-- Migration 004: Add branding configuration table
--
-- Stores brand customization including:
-- - Logo paths (light/dark variants)
-- - Theme colors (primary, secondary, accent)
-- - Custom CSS variables
------------------------------------------------------------------------
-- Branding configuration table
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS branding_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
-- Logo configuration
logo_light TEXT, -- Path to light logo
logo_dark TEXT, -- Path to dark logo
logo_favicon TEXT, -- Path to favicon
-- Theme colors (hex format)
primary_color TEXT NOT NULL DEFAULT '#3B82F6',
secondary_color TEXT NOT NULL DEFAULT '#64748B',
accent_color TEXT NOT NULL DEFAULT '#10B981',
-- Background colors
background_light TEXT NOT NULL DEFAULT '#FFFFFF',
background_dark TEXT NOT NULL DEFAULT '#0F172A',
surface_light TEXT NOT NULL DEFAULT '#F8FAFC',
surface_dark TEXT NOT NULL DEFAULT '#1E293B',
-- Text colors
text_primary_light TEXT NOT NULL DEFAULT '#0F172A',
text_primary_dark TEXT NOT NULL DEFAULT '#F8FAFC',
text_secondary_light TEXT NOT NULL DEFAULT '#475569',
text_secondary_dark TEXT NOT NULL DEFAULT '#94A3B8',
-- Border colors
border_light TEXT NOT NULL DEFAULT '#E2E8F0',
border_dark TEXT NOT NULL DEFAULT '#334155',
-- Active preset name (for quick reference)
active_preset TEXT NOT NULL DEFAULT 'default',
-- Custom CSS that will be injected
custom_css TEXT NOT NULL DEFAULT '',
-- Timestamps
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Seed default branding configuration
INSERT OR IGNORE INTO branding_config (id, created_at, updated_at)
VALUES (1, unixepoch('now', 'subsec') * 1000, unixepoch('now', 'subsec') * 1000);
------------------------------------------------------------------------
-- End of migration
------------------------------------------------------------------------
@@ -0,0 +1,57 @@
-- Migration 005: Add audit logging table
--
-- Records sensitive operations for security and compliance:
-- - Authentication events (login, logout, failed attempts)
-- - Configuration changes
-- - User management operations
-- - Data access and exports
------------------------------------------------------------------------
-- Audit log table
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Event classification
action TEXT NOT NULL,
category TEXT NOT NULL
CHECK(category IN (
'auth',
'user_management',
'branding',
'system_config',
'data_access',
'data_export',
'permission_change',
'other'
)),
-- Actor information
user_id TEXT,
username TEXT,
ip_address TEXT,
user_agent TEXT,
-- Event details
resource_type TEXT,
resource_id TEXT,
details TEXT NOT NULL DEFAULT '{}', -- JSON with action-specific data
-- Outcome
status TEXT NOT NULL
CHECK(status IN ('success', 'failure', 'denied'))
DEFAULT 'success',
-- Timestamp (ms since epoch)
created_at INTEGER NOT NULL
);
-- Indexes for common query patterns
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_user_id ON audit_log(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_action ON audit_log(action);
CREATE INDEX IF NOT EXISTS idx_audit_log_category ON audit_log(category);
CREATE INDEX IF NOT EXISTS idx_audit_log_status ON audit_log(status);
-- Composite index for filtered queries
CREATE INDEX IF NOT EXISTS idx_audit_log_category_created ON audit_log(category, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_log_user_created ON audit_log(user_id, created_at DESC);
------------------------------------------------------------------------
-- End of migration
------------------------------------------------------------------------
@@ -0,0 +1,98 @@
-- Migration 006: Add domain configuration table
--
-- Stores domain-specific settings and configurations:
-- - Government: departments, approval workflows
-- - Enterprise: departments, team structures
-- - Education: schools, faculties, semesters
------------------------------------------------------------------------
-- Domain configuration table
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS domain_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
-- Domain type (mirrors system_config.domain_type)
domain_type TEXT NOT NULL DEFAULT 'enterprise'
CHECK(domain_type IN ('government', 'enterprise', 'education')),
-- Domain-specific settings (JSON)
settings TEXT NOT NULL DEFAULT '{}',
-- Government-specific
government_settings TEXT NOT NULL DEFAULT '{}',
-- Enterprise-specific
enterprise_settings TEXT NOT NULL DEFAULT '{}',
-- Education-specific
education_settings TEXT NOT NULL DEFAULT '{}',
-- Enabled features (JSON array)
enabled_features TEXT NOT NULL DEFAULT '[]',
-- Department/division list (JSON array)
departments TEXT NOT NULL DEFAULT '[]',
-- Custom domain parameters (JSON)
custom_params TEXT NOT NULL DEFAULT '{}',
-- Timestamps
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Seed default domain configuration
INSERT OR IGNORE INTO domain_config (id, created_at, updated_at)
VALUES (1, unixepoch('now', 'subsec') * 1000, unixepoch('now', 'subsec') * 1000);
------------------------------------------------------------------------
-- Domain presets (reference data)
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS domain_presets (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
domain_type TEXT NOT NULL
CHECK(domain_type IN ('government', 'enterprise', 'education')),
description TEXT,
-- Default settings for this preset
settings TEXT NOT NULL DEFAULT '{}',
-- Preset configurations
config TEXT NOT NULL DEFAULT '{}',
-- Sort order
sort_order INTEGER NOT NULL DEFAULT 0,
-- Timestamps
created_at INTEGER NOT NULL
);
-- Seed government presets
INSERT OR IGNORE INTO domain_presets (id, name, domain_type, description, settings, config, sort_order, created_at)
VALUES
('gov_office', '政务办公', 'government', '政务办公场景,适合政府机关日常办公',
'{"approval_workflow": true, "document_classification": true, "multi_department": true}',
'{"departments": ["办公室", "人事处", "财务处", "业务处"], "features": ["公文管理", "审批流程", "档案管理"]}',
10, unixepoch('now', 'subsec') * 1000),
('gov_service', '政务服务', 'government', '政务服务场景,适合对外服务窗口',
'{"queue_management": true, "service_rating": true, "id_verification": true}',
'{"departments": ["综合窗口", "专项服务", "投诉受理"], "features": ["排队叫号", "服务评价", "身份核验"]}',
20, unixepoch('now', 'subsec') * 1000);
-- Seed enterprise presets
INSERT OR IGNORE INTO domain_presets (id, name, domain_type, description, settings, config, sort_order, created_at)
VALUES
('ent_corporate', '企业办公', 'enterprise', '企业通用办公场景',
'{"team_structure": true, "project_tracking": true, "knowledge_base": true}',
'{"departments": ["技术部", "市场部", "运营部", "财务部"], "features": ["项目管理", "知识库", "团队协作"]}',
10, unixepoch('now', 'subsec') * 1000),
('ent_sales', '销售团队', 'enterprise', '销售团队专用场景',
'{"crm_integration": true, "lead_tracking": true, "sales_reporting": true}',
'{"departments": ["销售一部", "销售二部", "客服部"], "features": ["客户管理", "销售报表", "线索追踪"]}',
20, unixepoch('now', 'subsec') * 1000);
-- Seed education presets
INSERT OR IGNORE INTO domain_presets (id, name, domain_type, description, settings, config, sort_order, created_at)
VALUES
('edu_university', '高等院校', 'education', '高等院校教学科研场景',
'{"semester_management": true, "course_management": true, "research_support": true}',
'{"departments": ["计算机系", "数学系", "物理系", "外语系"], "features": ["课程管理", "科研助手", "论文写作"]}',
10, unixepoch('now', 'subsec') * 1000),
('edu_school', '中小学', 'education', '中小学教育场景',
'{"grade_management": true, "homework_tracking": true, "parent_communication": true}',
'{"departments": ["语文组", "数学组", "英语组", "综合组"], "features": ["作业管理", "家长通知", "成绩分析"]}',
20, unixepoch('now', 'subsec') * 1000);
------------------------------------------------------------------------
-- End of migration
------------------------------------------------------------------------
@@ -0,0 +1,29 @@
-- Align built-in ACP agent launch metadata with the current product-owned CLI names.
-- Existing databases created before this migration keep their rows, so update
-- only the known stale command/binary pairs.
UPDATE agent_metadata
SET
agent_source_info = '{"binary_name":"agent"}',
command = 'agent',
args = '["acp"]',
updated_at = unixepoch('now','subsec') * 1000
WHERE id = 'agent_builtin_cursor'
AND agent_source = 'builtin'
AND (
agent_source_info = '{"binary_name":"cursor"}'
OR command = 'cursor'
);
UPDATE agent_metadata
SET
agent_source_info = '{"binary_name":"kiro-cli"}',
command = 'kiro-cli',
args = '["acp"]',
updated_at = unixepoch('now','subsec') * 1000
WHERE id = 'agent_builtin_kiro'
AND agent_source = 'builtin'
AND (
agent_source_info = '{"binary_name":"kiro-cli-chat"}'
OR command = 'kiro-cli-chat'
);
@@ -0,0 +1,24 @@
-- IDMM 决策记录:把原本仅存于内存(100 条环形缓冲、重启即丢、前端零渲染)的
-- 介入审计落独立表,使决策"看得见、可追溯"。激进淘汰(数据不重要,只留一点):
-- 每 target 仅留最近 30 条 + 全局 TTL 48h + 全局硬上限兜底;target_id 多态
-- (会话 TEXT / 终端 INTEGER),不设双 FK,删会话/终端时由应用层级联清理。
CREATE TABLE IF NOT EXISTS idmm_interventions (
id TEXT PRIMARY KEY NOT NULL, -- idmmrec_{uuidv7}
target_kind TEXT NOT NULL, -- 'conversation' | 'terminal'
target_id TEXT NOT NULL,
watch TEXT NOT NULL, -- 'fault'(provider/agent 故障)| 'decision'(其余),由信号种类推导
at INTEGER NOT NULL, -- epoch ms
signal TEXT NOT NULL, -- stall_class: provider_error/idle/decision/open_question/scheduled
tier_used TEXT NOT NULL, -- rule | sidecar | rule_fallback
category TEXT, -- option/open_question/permission/fault
action TEXT NOT NULL, -- retry/answer_choice/send_text/confirm/wait/stop
detail TEXT, -- 选了什么/答了什么(截断 ≤2000 字符)
reason TEXT, -- 思路(模型 reason 或规则解释;非选项分支的描述串也落这里)
confidence REAL, -- 模型置信度(规则档 NULL)
bypass_model TEXT, -- provider/model(规则档 NULL)
outcome TEXT NOT NULL -- 规范枚举 applied/resolved/failed/halted/skipped(Phase1 发 applied|halted)
);
CREATE INDEX IF NOT EXISTS idx_idmm_interventions_target
ON idmm_interventions(target_kind, target_id, at DESC);
CREATE INDEX IF NOT EXISTS idx_idmm_interventions_at
ON idmm_interventions(at);
@@ -0,0 +1,22 @@
-- 009_assistant_tags.sql
-- Two-dimension tagging for assistants (audience / scenario).
-- Per-assistant tag keys live as JSON arrays on the assistants table
-- (mirrors enabled_skills). The vocabulary's user-created entries live in
-- assistant_tags; built-in seed tags ship in tags.json (no rows here).
ALTER TABLE assistants ADD COLUMN audience_tags TEXT;
ALTER TABLE assistants ADD COLUMN scenario_tags TEXT;
-- User-created tag vocabulary only. Built-in seed tags are served from the
-- embedded tags.json manifest and merged at the service layer, so they are
-- NOT rows here. `dimension` is 'audience' | 'scenario'. No FK to assistants
-- (assistants reference tags by key in their JSON arrays; deletion cleanup is
-- done by the service).
CREATE TABLE IF NOT EXISTS assistant_tags (
key TEXT PRIMARY KEY,
dimension TEXT NOT NULL CHECK (dimension IN ('audience', 'scenario')),
label TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_assistant_tags_dimension ON assistant_tags(dimension, sort_order);
@@ -0,0 +1,15 @@
-- Migration 009: external-channel write re-enable toggle for knowledge bindings.
--
-- P1/P2 hard-disable knowledge write-back for external IM channel master-agent
-- sessions (discord/slack/lark/…) by default: an unattended bot writing to a
-- shared knowledge base is a standing risk. This column lets the user opt a
-- specific binding back in. When enabled, channel writes are still forced to
-- STAGED placement (review inbox) — never direct — so the human review gate
-- remains the safety net (enforced in `resolve_write_policy`).
--
-- Additive on purpose: editing 001_baseline would change its checksum and trip
-- the pre-baseline rebuild path (database.rs), wiping existing dev DBs. SQLite
-- allows ADD COLUMN with a NOT NULL DEFAULT; the default (0 = disabled)
-- preserves the prior behavior for every pre-existing row.
ALTER TABLE knowledge_bindings
ADD COLUMN channel_write_enabled INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,20 @@
-- Migration 010: encrypted credentials for source connectors (feishu/notion/…).
--
-- Stores per-connector credentials as an opaque AES-256-GCM ciphertext blob
-- (`payload_encrypted`); the service layer holds the encryption key and the
-- JSON payload shape (e.g. `{ "app_id": ..., "app_secret": ... }`), exactly
-- like the providers table's `api_key_encrypted`. Multiple credentials of the
-- same kind are allowed (different tenants/accounts), so the key is a surrogate
-- id, not the kind.
--
-- Additive: never touches 001_baseline (checksum stability).
CREATE TABLE IF NOT EXISTS connector_credentials (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
name TEXT NOT NULL,
payload_encrypted TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_connector_credentials_kind ON connector_credentials(kind);
@@ -0,0 +1,14 @@
-- 010_skill_tags.sql
-- Per-skill tag assignments. Skills are filesystem folders (not DB rows),
-- keyed by their unique `name`. Tags are decoupled from skill files so ANY
-- source (builtin/custom/extension/external) is taggable. Built-in seed
-- assignments ship in skill-tags.json and are merged at the route layer;
-- this table holds user assignments/overrides only. JSON-array TEXT columns
-- mirror the assistants' audience_tags/scenario_tags storage. Tag keys
-- reference the shared vocabulary (assistant_tags + tags.json).
CREATE TABLE IF NOT EXISTS skill_tags (
skill_name TEXT PRIMARY KEY,
audience_tags TEXT,
scenario_tags TEXT,
updated_at INTEGER NOT NULL
);
@@ -0,0 +1,14 @@
-- 013_knowledge_tags.sql
-- User-defined tags for knowledge bases. The `tags` column on knowledge_bases
-- stores a JSON array of tag keys assigned to that base (NULL = no tags).
-- The `knowledge_tags` table holds the tag definitions (palette).
ALTER TABLE knowledge_bases ADD COLUMN tags TEXT; -- JSON array text; NULL = no tags
CREATE TABLE knowledge_tags (
key TEXT PRIMARY KEY,
label TEXT NOT NULL,
color TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
@@ -0,0 +1,3 @@
-- Per-tag notification event filter. Comma-separated subset of done/failed/needs_review.
-- Default keeps current behavior (all three fire).
ALTER TABLE tag_settings ADD COLUMN notify_events TEXT NOT NULL DEFAULT 'done,failed,needs_review';
@@ -0,0 +1,9 @@
-- Single-row table holding the SHA-256 hash of the long-lived instance API
-- token used by the Remote capability front door (`/mcp`). All-or-nothing
-- trust: one token per instance, hash-only at rest, revocable by deleting the
-- row. CHECK(id = 1) enforces the singleton invariant.
CREATE TABLE IF NOT EXISTS instance_api_token (
id INTEGER PRIMARY KEY CHECK (id = 1),
token_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
);
@@ -0,0 +1,14 @@
-- Per-companion access tokens for the Remote capability front door.
-- Replaces the singleton `instance_api_token` (015): every external connection
-- binds to exactly one companion. Only the SHA-256 hash is stored; the plaintext
-- is shown once at mint time and never persisted.
CREATE TABLE IF NOT EXISTS companion_access_token (
companion_id TEXT PRIMARY KEY,
token_hash TEXT NOT NULL,
created_at INTEGER NOT NULL
);
-- Retire the singleton instance token. Safe whether or not 015 was ever applied
-- on this DB (fresh branch DB: 015 creates it, 016 drops it; already-migrated DB:
-- 016 drops the existing table). No dead table remains.
DROP TABLE IF EXISTS instance_api_token;
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS cron_job_runs (
id TEXT PRIMARY KEY NOT NULL,
job_id TEXT NOT NULL,
executed_at_ms INTEGER NOT NULL,
status TEXT NOT NULL CHECK(status IN ('ok', 'error', 'skipped', 'missed')),
created_at_ms INTEGER NOT NULL,
FOREIGN KEY (job_id) REFERENCES cron_jobs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_cron_job_runs_job_time
ON cron_job_runs(job_id, executed_at_ms DESC, created_at_ms DESC);
@@ -0,0 +1,30 @@
-- Migration 002: Add user role support
--
-- Single-tenant architecture: all tables retain tenant_id as extension point,
-- but the first deployed system operates in single-tenant mode.
--
-- This migration adds role-based access control to the users table.
------------------------------------------------------------------------
-- Extend users table with role field
------------------------------------------------------------------------
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'
CHECK(role IN ('admin', 'user'));
-- Create index for role-based queries
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
------------------------------------------------------------------------
-- Seed data: upgrade existing users to admin if they are the first user
------------------------------------------------------------------------
-- The first registered user (by created_at) becomes admin
-- This handles the case where users existed before this migration
UPDATE users
SET role = 'admin'
WHERE id = (SELECT id FROM users ORDER BY created_at ASC LIMIT 1);
------------------------------------------------------------------------
-- End of migration
------------------------------------------------------------------------
@@ -0,0 +1,36 @@
-- Migration 003: Add system configuration table
--
-- Stores organization-level configuration including:
-- - Organization name
-- - Domain type (government/enterprise/education)
-- - Initialization state
------------------------------------------------------------------------
-- System configuration table
------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS system_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
organization_name TEXT NOT NULL DEFAULT 'My Organization',
domain_type TEXT NOT NULL DEFAULT 'enterprise'
CHECK(domain_type IN ('government', 'enterprise', 'education')),
initialized INTEGER NOT NULL DEFAULT 0,
init_completed_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- Seed default configuration row
INSERT OR IGNORE INTO system_config (id, created_at, updated_at)
VALUES (1, unixepoch('now', 'subsec') * 1000, unixepoch('now', 'subsec') * 1000);
------------------------------------------------------------------------
-- Extend system_settings for additional preferences
------------------------------------------------------------------------
ALTER TABLE system_settings ADD COLUMN theme_mode TEXT NOT NULL DEFAULT 'light'
CHECK(theme_mode IN ('light', 'dark', 'auto'));
------------------------------------------------------------------------
-- End of migration
------------------------------------------------------------------------
@@ -0,0 +1,72 @@
-- 004_channel_scoped_pairing.sql
-- 配对/授权域从全局 (platform_user_id, platform_type) 收敛到 per-bot:
-- 两张表加 channel_id + FK→assistant_plugins(id) ON DELETE CASCADE,
-- 唯一约束改为含 channel_id。回填到对应平台"最可能在用"的那一行。
-- ── 1) assistant_users 重建(子表 assistant_sessions 需备份/恢复)──
CREATE TEMP TABLE _sessions_backup AS SELECT * FROM assistant_sessions;
CREATE TABLE assistant_users_new (
id TEXT PRIMARY KEY NOT NULL,
platform_user_id TEXT NOT NULL,
platform_type TEXT NOT NULL,
channel_id TEXT,
display_name TEXT,
authorized_at INTEGER NOT NULL,
last_active INTEGER,
session_id TEXT,
UNIQUE (platform_user_id, platform_type, channel_id),
FOREIGN KEY (channel_id) REFERENCES assistant_plugins(id) ON DELETE CASCADE
);
INSERT INTO assistant_users_new
(id, platform_user_id, platform_type, channel_id, display_name, authorized_at, last_active, session_id)
SELECT u.id, u.platform_user_id, u.platform_type,
(SELECT p.id FROM assistant_plugins p
WHERE p.type = u.platform_type
ORDER BY (p.companion_id IS NOT NULL) DESC, p.created_at ASC
LIMIT 1),
u.display_name, u.authorized_at, u.last_active, u.session_id
FROM assistant_users u;
DROP TABLE assistant_users;
ALTER TABLE assistant_users_new RENAME TO assistant_users;
DELETE FROM assistant_sessions;
INSERT INTO assistant_sessions SELECT * FROM _sessions_backup;
DROP TABLE _sessions_backup;
CREATE INDEX IF NOT EXISTS idx_assistant_sessions_user_id ON assistant_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_assistant_sessions_user_chat ON assistant_sessions(user_id, chat_id);
CREATE INDEX IF NOT EXISTS idx_assistant_sessions_channel ON assistant_sessions(channel_id);
CREATE INDEX IF NOT EXISTS idx_assistant_users_channel ON assistant_users(channel_id);
-- ── 2) assistant_pairing_codes 重建(无子表)──
CREATE TABLE assistant_pairing_codes_new (
code TEXT PRIMARY KEY NOT NULL,
platform_user_id TEXT NOT NULL,
platform_type TEXT NOT NULL,
channel_id TEXT,
display_name TEXT,
requested_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'approved', 'rejected', 'expired')),
FOREIGN KEY (channel_id) REFERENCES assistant_plugins(id) ON DELETE CASCADE
);
INSERT INTO assistant_pairing_codes_new
(code, platform_user_id, platform_type, channel_id, display_name, requested_at, expires_at, status)
SELECT c.code, c.platform_user_id, c.platform_type,
(SELECT p.id FROM assistant_plugins p
WHERE p.type = c.platform_type
ORDER BY (p.companion_id IS NOT NULL) DESC, p.created_at ASC
LIMIT 1),
c.display_name, c.requested_at, c.expires_at, c.status
FROM assistant_pairing_codes c;
DROP TABLE assistant_pairing_codes;
ALTER TABLE assistant_pairing_codes_new RENAME TO assistant_pairing_codes;
CREATE INDEX IF NOT EXISTS idx_pairing_codes_status ON assistant_pairing_codes(status);
CREATE INDEX IF NOT EXISTS idx_pairing_codes_channel ON assistant_pairing_codes(channel_id);
@@ -0,0 +1,15 @@
-- 005_terminal_scrollback.sql
-- 终端 scrollback 跨重启持久化:把原本仅存于内存(256KB 有界缓冲)的输出历史
-- 落到独立表,使应用重启后仍能回放历史显示(配合 boot 对账把幽灵 running
-- 行改成 exited,前端即出现 relaunch 入口并回放这段历史)。
--
-- 用独立表而非给 terminal_sessions 加列:list 查询天然不拉这块大数据(保持
-- 列表轻量),且 ON DELETE CASCADE 随会话删除自动清理。
-- 写入由后端去抖驱动(仅脏会话、~5s 一次 + 进程退出时),绝不每输出块写。
CREATE TABLE IF NOT EXISTS terminal_scrollback (
session_id INTEGER PRIMARY KEY NOT NULL,
data BLOB NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (session_id) REFERENCES terminal_sessions(id) ON DELETE CASCADE
);
@@ -0,0 +1,22 @@
-- Keyset (cursor) pagination for conversation message history.
--
-- An ever-growing single conversation — notably a desktop companion's ONE
-- session, which now also absorbs every IM-channel turn — is loaded in
-- newest-first windows via a (created_at, id) keyset cursor instead of one
-- giant fetch:
--
-- SELECT * FROM messages
-- WHERE conversation_id = ?
-- AND type NOT IN ('cron_trigger','skill_suggest')
-- AND (created_at < ? OR (created_at = ? AND id < ?))
-- ORDER BY created_at DESC, id DESC
-- LIMIT ?
--
-- The existing idx_messages_conv_created / idx_messages_conv_created_desc cover
-- (conversation_id, created_at) but NOT the `id` tiebreaker, so the cursor's
-- (created_at, id) comparison and the matching ORDER BY degrade to a sort. This
-- composite covers both, keeping deep "load older" pages index-only and stable
-- under concurrent streaming appends. `id` is msg_{uuidv7} (time-ordered), so it
-- is a sound, monotonic tiebreaker for rows sharing a created_at millisecond.
CREATE INDEX IF NOT EXISTS idx_messages_conv_created_id
ON messages (conversation_id, created_at DESC, id DESC);