@controlflow-ai/daemon 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +54 -6
  2. package/bin/daemon.js +6 -1
  3. package/package.json +3 -1
  4. package/src/agent-avatar.ts +30 -0
  5. package/src/agent-key.ts +28 -0
  6. package/src/agent-permissions.ts +359 -0
  7. package/src/agent-runtime.ts +795 -28
  8. package/src/agent-workspace.ts +183 -0
  9. package/src/app.ts +1970 -79
  10. package/src/args.ts +54 -7
  11. package/src/cli.ts +873 -14
  12. package/src/client.ts +472 -10
  13. package/src/coco.ts +9 -40
  14. package/src/codex.ts +33 -5
  15. package/src/config.ts +28 -4
  16. package/src/console.ts +230 -20
  17. package/src/daemon-client.ts +116 -3
  18. package/src/daemon.ts +937 -99
  19. package/src/db.ts +3128 -122
  20. package/src/delivery-ws.ts +269 -0
  21. package/src/format.ts +4 -1
  22. package/src/lark/cli.ts +3 -3
  23. package/src/lark/event-router.ts +60 -4
  24. package/src/lark/inbound-events.ts +156 -3
  25. package/src/lark/server-integration.ts +659 -111
  26. package/src/lark/ws-daemon.ts +136 -10
  27. package/src/local-api.ts +545 -15
  28. package/src/local-auth.ts +33 -1
  29. package/src/message-attachments.ts +71 -0
  30. package/src/messaging-cli.ts +741 -0
  31. package/src/messaging-status.ts +669 -0
  32. package/src/migrations/024_agents_model.ts +10 -0
  33. package/src/migrations/025_room_archive.ts +44 -0
  34. package/src/migrations/026_project_archive.ts +44 -0
  35. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  36. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  37. package/src/migrations/029_held_message_drafts.ts +32 -0
  38. package/src/migrations/030_agent_room_read_state.ts +25 -0
  39. package/src/migrations/031_room_tasks.ts +29 -0
  40. package/src/migrations/032_room_reminders.ts +29 -0
  41. package/src/migrations/033_room_saved_messages.ts +25 -0
  42. package/src/migrations/034_agent_activity_events.ts +27 -0
  43. package/src/migrations/035_agent_avatars.ts +17 -0
  44. package/src/migrations/036_project_agent_defaults.ts +21 -0
  45. package/src/migrations/037_message_attachments.ts +36 -0
  46. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  47. package/src/migrations/039_message_attachments_path.ts +34 -0
  48. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  49. package/src/migrations/041_room_system_events.ts +30 -0
  50. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  51. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  52. package/src/migrations/044_workflow_runtime.ts +69 -0
  53. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  54. package/src/migrations.ts +69 -1
  55. package/src/neeko.ts +40 -4
  56. package/src/runtime-env.ts +179 -0
  57. package/src/runtime-registry.ts +83 -13
  58. package/src/server.ts +244 -4
  59. package/src/token-file.ts +13 -6
  60. package/src/types.ts +362 -0
  61. package/src/workflow-runtime.ts +275 -0
  62. package/src/web.ts +0 -904
@@ -0,0 +1,44 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 25;
4
+ export const name = 'room_archive';
5
+
6
+ function hasColumn(db: Database, table: string, column: string): boolean {
7
+ const rows = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
8
+ return rows.some((row) => row.name === column);
9
+ }
10
+
11
+ export function up(db: Database): void {
12
+ if (!hasColumn(db, 'chats', 'status')) {
13
+ db.exec(`ALTER TABLE chats ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived'))`);
14
+ }
15
+
16
+ db.exec(`
17
+ DROP VIEW IF EXISTS chat_stats;
18
+ CREATE VIEW chat_stats AS
19
+ SELECT
20
+ c.id,
21
+ c.name,
22
+ c.display_name,
23
+ c.kind,
24
+ c.server_id,
25
+ c.provider,
26
+ c.dm_type,
27
+ c.capabilities_json,
28
+ c.audit_visibility,
29
+ c.status,
30
+ c.project_id,
31
+ p.name AS project_name,
32
+ p.root_path AS project_root_path,
33
+ p.computer_id AS project_computer_id,
34
+ pc.name AS project_computer_name,
35
+ c.created_at,
36
+ COUNT(m.id) AS message_count,
37
+ MAX(m.created_at) AS last_message_at
38
+ FROM chats c
39
+ LEFT JOIN projects p ON p.id = c.project_id
40
+ LEFT JOIN computers pc ON pc.id = p.computer_id
41
+ LEFT JOIN messages m ON m.chat_id = c.id
42
+ GROUP BY c.id;
43
+ `);
44
+ }
@@ -0,0 +1,44 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 26;
4
+ export const name = 'project_archive';
5
+
6
+ function hasColumn(db: Database, table: string, column: string): boolean {
7
+ const rows = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
8
+ return rows.some((row) => row.name === column);
9
+ }
10
+
11
+ export function up(db: Database): void {
12
+ if (!hasColumn(db, 'projects', 'status')) {
13
+ db.exec(`ALTER TABLE projects ADD COLUMN status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived'))`);
14
+ }
15
+
16
+ db.exec(`
17
+ DROP VIEW IF EXISTS chat_stats;
18
+ CREATE VIEW chat_stats AS
19
+ SELECT
20
+ c.id,
21
+ c.name,
22
+ c.display_name,
23
+ c.kind,
24
+ c.server_id,
25
+ c.provider,
26
+ c.dm_type,
27
+ c.capabilities_json,
28
+ c.audit_visibility,
29
+ c.status,
30
+ c.project_id,
31
+ p.name AS project_name,
32
+ p.root_path AS project_root_path,
33
+ p.computer_id AS project_computer_id,
34
+ pc.name AS project_computer_name,
35
+ c.created_at,
36
+ COUNT(m.id) AS message_count,
37
+ MAX(m.created_at) AS last_message_at
38
+ FROM chats c
39
+ LEFT JOIN projects p ON p.id = c.project_id
40
+ LEFT JOIN computers pc ON pc.id = p.computer_id
41
+ LEFT JOIN messages m ON m.chat_id = c.id
42
+ GROUP BY c.id;
43
+ `);
44
+ }
@@ -0,0 +1,16 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 27;
4
+ export const name = 'agent_permission_profiles';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS agent_permission_profiles (
9
+ agent_key TEXT PRIMARY KEY REFERENCES agents(agent_key) ON DELETE CASCADE,
10
+ filesystem_mode TEXT NOT NULL DEFAULT 'project-write' CHECK (filesystem_mode IN ('project-write', 'scoped-write', 'full-access')),
11
+ extra_writable_roots_json TEXT NOT NULL DEFAULT '[]',
12
+ created_at TEXT NOT NULL,
13
+ updated_at TEXT NOT NULL
14
+ );
15
+ `);
16
+ }
@@ -0,0 +1,16 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 28;
4
+ export const name = 'lark_websocket_restart_state';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS lark_websocket_restart_state (
9
+ app_id TEXT PRIMARY KEY,
10
+ restart_count INTEGER NOT NULL DEFAULT 0 CHECK (restart_count >= 0),
11
+ last_restart_at TEXT,
12
+ last_restart_reason TEXT,
13
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
14
+ );
15
+ `);
16
+ }
@@ -0,0 +1,32 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 29;
4
+ export const name = 'held_message_drafts';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS held_message_drafts (
9
+ id TEXT PRIMARY KEY,
10
+ chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
11
+ agent TEXT NOT NULL,
12
+ sender TEXT NOT NULL,
13
+ content TEXT NOT NULL,
14
+ mentions_json TEXT NOT NULL DEFAULT '[]',
15
+ base_message_id INTEGER NOT NULL,
16
+ latest_message_id_at_hold INTEGER NOT NULL,
17
+ status TEXT NOT NULL CHECK (status IN ('held', 'abandoned', 'sent_anyway')),
18
+ hold_reason TEXT NOT NULL,
19
+ hold_count INTEGER NOT NULL DEFAULT 1 CHECK (hold_count >= 1),
20
+ resolved_message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
21
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
22
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
23
+ resolved_at TEXT
24
+ );
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_held_message_drafts_agent_status_created
27
+ ON held_message_drafts(agent, status, created_at);
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_held_message_drafts_chat_status
30
+ ON held_message_drafts(chat_id, status);
31
+ `);
32
+ }
@@ -0,0 +1,25 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 30;
4
+ export const name = 'agent_room_read_state';
5
+
6
+ function hasColumn(db: Database, table: string, column: string): boolean {
7
+ const rows = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
8
+ return rows.some((row) => row.name === column);
9
+ }
10
+
11
+ function addColumnIfMissing(db: Database, table: string, column: string, ddl: string): void {
12
+ if (!hasColumn(db, table, column)) {
13
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
14
+ }
15
+ }
16
+
17
+ export function up(db: Database): void {
18
+ addColumnIfMissing(db, 'agent_room_subscriptions', 'last_read_message_id', 'last_read_message_id INTEGER NOT NULL DEFAULT 0');
19
+ addColumnIfMissing(db, 'agent_room_subscriptions', 'last_read_at', 'last_read_at TEXT');
20
+
21
+ db.exec(`
22
+ CREATE INDEX IF NOT EXISTS idx_agent_room_subscriptions_read_state
23
+ ON agent_room_subscriptions(agent, room_id, last_read_message_id);
24
+ `);
25
+ }
@@ -0,0 +1,29 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 31;
4
+ export const name = 'room_tasks';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS room_tasks (
9
+ id TEXT PRIMARY KEY,
10
+ room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
11
+ task_number INTEGER NOT NULL,
12
+ title TEXT NOT NULL,
13
+ status TEXT NOT NULL DEFAULT 'todo' CHECK (status IN ('todo', 'in_progress', 'in_review', 'done', 'closed')),
14
+ assignee TEXT,
15
+ created_by TEXT,
16
+ source_message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
18
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
19
+ completed_at TEXT,
20
+ UNIQUE(room_id, task_number)
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_room_tasks_room_status
24
+ ON room_tasks(room_id, status, task_number);
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_room_tasks_assignee_status
27
+ ON room_tasks(assignee, status, updated_at);
28
+ `);
29
+ }
@@ -0,0 +1,29 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 32;
4
+ export const name = 'room_reminders';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS room_reminders (
9
+ id TEXT PRIMARY KEY,
10
+ room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
11
+ source_message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
12
+ title TEXT NOT NULL,
13
+ status TEXT NOT NULL DEFAULT 'scheduled' CHECK (status IN ('scheduled', 'fired', 'canceled')),
14
+ created_by TEXT,
15
+ fire_at TEXT NOT NULL,
16
+ repeat TEXT,
17
+ fired_at TEXT,
18
+ canceled_at TEXT,
19
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
20
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_room_reminders_room_status_fire_at
24
+ ON room_reminders(room_id, status, fire_at);
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_room_reminders_created_by_status_fire_at
27
+ ON room_reminders(created_by, status, fire_at);
28
+ `);
29
+ }
@@ -0,0 +1,25 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 33;
4
+ export const name = 'room_saved_messages';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS room_saved_messages (
9
+ id TEXT PRIMARY KEY,
10
+ room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
11
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
12
+ saved_by TEXT NOT NULL,
13
+ note TEXT,
14
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
15
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
16
+ UNIQUE(message_id, saved_by)
17
+ );
18
+
19
+ CREATE INDEX IF NOT EXISTS idx_room_saved_messages_room_saved_by_created_at
20
+ ON room_saved_messages(room_id, saved_by, created_at);
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_room_saved_messages_saved_by_created_at
23
+ ON room_saved_messages(saved_by, created_at);
24
+ `);
25
+ }
@@ -0,0 +1,27 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 34;
4
+ export const name = 'agent_activity_events';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS agent_activity_events (
9
+ id TEXT PRIMARY KEY,
10
+ run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
11
+ session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
12
+ agent TEXT NOT NULL,
13
+ chat_id TEXT NOT NULL,
14
+ kind TEXT NOT NULL CHECK (kind IN ('lifecycle', 'working', 'thinking', 'output', 'tool', 'error')),
15
+ title TEXT NOT NULL,
16
+ detail TEXT NOT NULL DEFAULT '',
17
+ metadata TEXT NOT NULL DEFAULT '{}',
18
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_events_run_created
22
+ ON agent_activity_events(run_id, datetime(created_at) DESC);
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_events_agent_created
25
+ ON agent_activity_events(agent, datetime(created_at) DESC);
26
+ `);
27
+ }
@@ -0,0 +1,17 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import { generateAgentAvatar } from '../agent-avatar.js';
3
+
4
+ export const version = 35;
5
+ export const name = 'agent_avatars';
6
+
7
+ export function up(db: Database): void {
8
+ db.exec(`
9
+ ALTER TABLE agents ADD COLUMN avatar TEXT NOT NULL DEFAULT 'sage';
10
+ `);
11
+
12
+ const rows = db.query('SELECT agent_key, display_name FROM agents').all() as Array<{ agent_key: string; display_name: string }>;
13
+ const update = db.query('UPDATE agents SET avatar = ? WHERE agent_key = ?');
14
+ for (const row of rows) {
15
+ update.run(generateAgentAvatar(row.agent_key, row.display_name), row.agent_key);
16
+ }
17
+ }
@@ -0,0 +1,21 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 36;
4
+ export const name = 'project_agent_defaults';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS project_agent_defaults (
9
+ id TEXT PRIMARY KEY,
10
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
11
+ agent TEXT NOT NULL,
12
+ mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'periodic', 'mentions', 'muted', 'off')),
13
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
14
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
15
+ UNIQUE(project_id, agent)
16
+ );
17
+
18
+ CREATE INDEX IF NOT EXISTS idx_project_agent_defaults_project
19
+ ON project_agent_defaults(project_id, agent);
20
+ `);
21
+ }
@@ -0,0 +1,36 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 37;
4
+ export const name = 'message_attachments';
5
+
6
+ function ensureColumn(db: Database, table: string, column: string, definition: string): void {
7
+ const columns = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
8
+ if (columns.some((row) => row.name === column)) return;
9
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
10
+ }
11
+
12
+ export function up(db: Database): void {
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS message_attachments (
15
+ id TEXT PRIMARY KEY,
16
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
17
+ kind TEXT NOT NULL CHECK (kind IN ('image', 'file')),
18
+ mime_type TEXT NOT NULL,
19
+ filename TEXT NOT NULL DEFAULT '',
20
+ size_bytes INTEGER NOT NULL,
21
+ path TEXT NOT NULL,
22
+ source_provider TEXT,
23
+ source_ref TEXT,
24
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
25
+ );
26
+ `);
27
+
28
+ ensureColumn(db, 'message_attachments', 'path', "TEXT NOT NULL DEFAULT ''");
29
+ ensureColumn(db, 'message_attachments', 'source_provider', 'TEXT');
30
+ ensureColumn(db, 'message_attachments', 'source_ref', 'TEXT');
31
+ ensureColumn(db, 'message_attachments', 'created_at', "TEXT NOT NULL DEFAULT (datetime('now'))");
32
+
33
+ db.exec(`
34
+ CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id, id);
35
+ `);
36
+ }
@@ -0,0 +1,64 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 38;
4
+ export const name = 'agent_activity_room_scope';
5
+
6
+ export function up(db: Database): void {
7
+ const columns = db.query('PRAGMA table_info(agent_activity_events)').all() as Array<{ name: string }>;
8
+ const hasRoomId = columns.some((column) => column.name === 'room_id');
9
+ const hasChatId = columns.some((column) => column.name === 'chat_id');
10
+
11
+ if (hasChatId) {
12
+ const roomIdExpression = hasRoomId ? "COALESCE(NULLIF(room_id, ''), chat_id)" : 'chat_id';
13
+ db.exec(`
14
+ DROP INDEX IF EXISTS idx_agent_activity_events_run_created;
15
+ DROP INDEX IF EXISTS idx_agent_activity_events_agent_created;
16
+ DROP INDEX IF EXISTS idx_agent_activity_events_room_agent_created;
17
+
18
+ CREATE TABLE agent_activity_events_room_scoped (
19
+ id TEXT PRIMARY KEY,
20
+ run_id TEXT NOT NULL REFERENCES agent_runs(id) ON DELETE CASCADE,
21
+ session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
22
+ agent TEXT NOT NULL,
23
+ room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
24
+ kind TEXT NOT NULL CHECK (kind IN ('lifecycle', 'working', 'thinking', 'output', 'tool', 'error')),
25
+ title TEXT NOT NULL,
26
+ detail TEXT NOT NULL DEFAULT '',
27
+ metadata TEXT NOT NULL DEFAULT '{}',
28
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
30
+
31
+ INSERT INTO agent_activity_events_room_scoped (
32
+ id, run_id, session_id, agent, room_id, kind, title, detail, metadata, created_at
33
+ )
34
+ SELECT
35
+ id,
36
+ run_id,
37
+ session_id,
38
+ agent,
39
+ ${roomIdExpression},
40
+ kind,
41
+ title,
42
+ detail,
43
+ metadata,
44
+ created_at
45
+ FROM agent_activity_events;
46
+
47
+ DROP TABLE agent_activity_events;
48
+ ALTER TABLE agent_activity_events_room_scoped RENAME TO agent_activity_events;
49
+ `);
50
+ } else if (!hasRoomId) {
51
+ db.exec('ALTER TABLE agent_activity_events ADD COLUMN room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE');
52
+ }
53
+
54
+ db.exec(`
55
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_events_run_created
56
+ ON agent_activity_events(run_id, datetime(created_at) DESC);
57
+
58
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_events_agent_created
59
+ ON agent_activity_events(agent, datetime(created_at) DESC);
60
+
61
+ CREATE INDEX IF NOT EXISTS idx_agent_activity_events_room_agent_created
62
+ ON agent_activity_events(room_id, agent, datetime(created_at) DESC);
63
+ `);
64
+ }
@@ -0,0 +1,34 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 39;
4
+ export const name = 'message_attachments_path';
5
+
6
+ function ensureColumn(db: Database, table: string, column: string, definition: string): void {
7
+ const columns = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
8
+ if (columns.some((row) => row.name === column)) return;
9
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
10
+ }
11
+
12
+ export function up(db: Database): void {
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS message_attachments (
15
+ id TEXT PRIMARY KEY,
16
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
17
+ kind TEXT NOT NULL CHECK (kind IN ('image', 'file')),
18
+ mime_type TEXT NOT NULL,
19
+ filename TEXT NOT NULL DEFAULT '',
20
+ size_bytes INTEGER NOT NULL,
21
+ path TEXT NOT NULL DEFAULT '',
22
+ source_provider TEXT,
23
+ source_ref TEXT,
24
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
25
+ );
26
+ `);
27
+
28
+ ensureColumn(db, 'message_attachments', 'path', "TEXT NOT NULL DEFAULT ''");
29
+ ensureColumn(db, 'message_attachments', 'source_provider', 'TEXT');
30
+ ensureColumn(db, 'message_attachments', 'source_ref', 'TEXT');
31
+ ensureColumn(db, 'message_attachments', 'created_at', "TEXT NOT NULL DEFAULT (datetime('now'))");
32
+
33
+ db.exec('CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id, id)');
34
+ }
@@ -0,0 +1,80 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 40;
4
+ export const name = 'message_attachments_file_schema';
5
+
6
+ function tableColumns(db: Database, table: string): string[] {
7
+ return (db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map((row) => row.name);
8
+ }
9
+
10
+ function hasTable(db: Database, table: string): boolean {
11
+ const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) as { name: string } | null;
12
+ return Boolean(row);
13
+ }
14
+
15
+ export function up(db: Database): void {
16
+ if (!hasTable(db, 'message_attachments')) {
17
+ db.exec(`
18
+ CREATE TABLE message_attachments (
19
+ id TEXT PRIMARY KEY,
20
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
21
+ kind TEXT NOT NULL CHECK (kind IN ('image', 'file')),
22
+ mime_type TEXT NOT NULL,
23
+ filename TEXT NOT NULL DEFAULT '',
24
+ size_bytes INTEGER NOT NULL,
25
+ path TEXT NOT NULL DEFAULT '',
26
+ source_provider TEXT,
27
+ source_ref TEXT,
28
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
29
+ );
30
+ CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id, id);
31
+ `);
32
+ return;
33
+ }
34
+
35
+ const columns = tableColumns(db, 'message_attachments');
36
+ const shouldRebuild = columns.includes('content') || !columns.includes('path');
37
+ if (!shouldRebuild) {
38
+ db.exec('CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id, id)');
39
+ return;
40
+ }
41
+
42
+ db.exec(`
43
+ DROP INDEX IF EXISTS idx_message_attachments_message_id;
44
+
45
+ ALTER TABLE message_attachments RENAME TO message_attachments_legacy;
46
+
47
+ CREATE TABLE message_attachments (
48
+ id TEXT PRIMARY KEY,
49
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
50
+ kind TEXT NOT NULL CHECK (kind IN ('image', 'file')),
51
+ mime_type TEXT NOT NULL,
52
+ filename TEXT NOT NULL DEFAULT '',
53
+ size_bytes INTEGER NOT NULL,
54
+ path TEXT NOT NULL DEFAULT '',
55
+ source_provider TEXT,
56
+ source_ref TEXT,
57
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
58
+ );
59
+
60
+ INSERT INTO message_attachments (
61
+ id, message_id, kind, mime_type, filename, size_bytes, path, source_provider, source_ref, created_at
62
+ )
63
+ SELECT
64
+ id,
65
+ message_id,
66
+ kind,
67
+ mime_type,
68
+ filename,
69
+ size_bytes,
70
+ COALESCE(path, ''),
71
+ source_provider,
72
+ source_ref,
73
+ created_at
74
+ FROM message_attachments_legacy;
75
+
76
+ DROP TABLE message_attachments_legacy;
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id, id);
79
+ `);
80
+ }
@@ -0,0 +1,30 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 41;
4
+ export const name = 'room_system_events';
5
+
6
+ export function up(db: Database): void {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS room_system_events (
9
+ id TEXT PRIMARY KEY,
10
+ room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
11
+ message_id INTEGER NOT NULL UNIQUE REFERENCES messages(id) ON DELETE CASCADE,
12
+ event_type TEXT NOT NULL CHECK (event_type IN ('agent_invited', 'agent_removed', 'agent_receive_mode_changed')),
13
+ actor_kind TEXT NOT NULL DEFAULT 'system' CHECK (actor_kind IN ('user', 'agent', 'system')),
14
+ actor_id TEXT,
15
+ subject_agent TEXT,
16
+ metadata_json TEXT NOT NULL DEFAULT '{}',
17
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
18
+ );
19
+
20
+ CREATE INDEX IF NOT EXISTS idx_room_system_events_room_created
21
+ ON room_system_events(room_id, created_at, message_id);
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_room_system_events_type_created
24
+ ON room_system_events(event_type, created_at);
25
+
26
+ CREATE INDEX IF NOT EXISTS idx_room_system_events_subject_created
27
+ ON room_system_events(subject_agent, created_at)
28
+ WHERE subject_agent IS NOT NULL;
29
+ `);
30
+ }
@@ -0,0 +1,52 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export const version = 42;
4
+ export const name = 'message_attachment_file_kind';
5
+
6
+ function tableSql(db: Database): string {
7
+ const row = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'message_attachments'").get() as { sql: string } | null;
8
+ return row?.sql ?? '';
9
+ }
10
+
11
+ export function up(db: Database): void {
12
+ if (!tableSql(db).includes("kind IN ('image')")) return;
13
+
14
+ db.exec(`
15
+ DROP INDEX IF EXISTS idx_message_attachments_message_id;
16
+
17
+ ALTER TABLE message_attachments RENAME TO message_attachments_legacy;
18
+
19
+ CREATE TABLE message_attachments (
20
+ id TEXT PRIMARY KEY,
21
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
22
+ kind TEXT NOT NULL CHECK (kind IN ('image', 'file')),
23
+ mime_type TEXT NOT NULL,
24
+ filename TEXT NOT NULL DEFAULT '',
25
+ size_bytes INTEGER NOT NULL,
26
+ path TEXT NOT NULL DEFAULT '',
27
+ source_provider TEXT,
28
+ source_ref TEXT,
29
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
30
+ );
31
+
32
+ INSERT INTO message_attachments (
33
+ id, message_id, kind, mime_type, filename, size_bytes, path, source_provider, source_ref, created_at
34
+ )
35
+ SELECT
36
+ id,
37
+ message_id,
38
+ kind,
39
+ mime_type,
40
+ filename,
41
+ size_bytes,
42
+ path,
43
+ source_provider,
44
+ source_ref,
45
+ created_at
46
+ FROM message_attachments_legacy;
47
+
48
+ DROP TABLE message_attachments_legacy;
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_message_attachments_message_id ON message_attachments(message_id, id);
51
+ `);
52
+ }