agent-relay-server 0.33.0 → 0.34.0

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.
@@ -0,0 +1,758 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk";
4
+ import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "../config.ts";
5
+ import { parseJson } from "../utils";
6
+ import { isLiveIsolatedWorkspace } from "../workspace-phase";
7
+ import {
8
+ CONTRACT_REQUIREMENTS,
9
+ contractCompatibility,
10
+ parseRuntimeCapabilities,
11
+ parseRuntimeContracts,
12
+ parseRuntimePackage,
13
+ type RuntimeContracts,
14
+ } from "../contracts";
15
+ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
+ import { matchAgents } from "../agent-ref";
17
+ import { applyConnectionPragmas, getDb, setDb } from "./connection.ts";
18
+ import { applyMigrations } from "./migrations.ts";
19
+ import type {
20
+ AgentCard,
21
+ ActivityEvent,
22
+ ActivityEventInput,
23
+ AgentKind,
24
+ AgentSessionGuard,
25
+ Artifact,
26
+ ArtifactBlob,
27
+ ArtifactKind,
28
+ ArtifactLink,
29
+ ArtifactSensitivity,
30
+ ArtifactVisibility,
31
+ AttachmentRef,
32
+ ChannelBinding,
33
+ ChannelBindingMode,
34
+ ChannelRouteTarget,
35
+ ChatHistoryImport,
36
+ ChatHistoryImportEntry,
37
+ ChannelSummary,
38
+ ChannelTargetHealth,
39
+ CreatePairInput,
40
+ HealthCheck,
41
+ HealthReport,
42
+ ManagedAgent,
43
+ ManagedSessionExitDiagnostics,
44
+ Message,
45
+ MessageDeliveryAttempt,
46
+ MessageDeliveryStatus,
47
+ Orchestrator,
48
+ OrchestratorHealth,
49
+ OrchestratorRuntimeInput,
50
+ OrchestratorStatus,
51
+ OrchestratorUpgradeState,
52
+ PairActionInput,
53
+ PairMessageInput,
54
+ PairSession,
55
+ PairStatus,
56
+ RegisterAgentInput,
57
+ ReplyObligation,
58
+ RegisterOrchestratorInput,
59
+ SendMessageInput,
60
+ PollQuery,
61
+ SpawnApprovalMode,
62
+ SpawnProvider,
63
+ Task,
64
+ TaskEvent,
65
+ TaskSeverity,
66
+ TaskStatus,
67
+ IntegrationEventInput,
68
+ IntegrationSummary,
69
+ IntegrationTaskStats,
70
+ InboxDraft,
71
+ InboxState,
72
+ InboxThreadState,
73
+ ContextSnapshot,
74
+ ContextState,
75
+ ProviderCapabilities,
76
+ TaskStatusInput,
77
+ WorkspaceMetadata,
78
+ WorkspaceRecord,
79
+ WorkspaceStatus,
80
+ } from "../types";
81
+
82
+ // Schema bootstrap. initDb opens the handle, applies connection pragmas, creates
83
+ // the full table/index catalog (the inline DDL below — kept in one place so the
84
+ // schema reads top-to-bottom), then hands off to applyMigrations() for the
85
+ // incremental ALTER/backfill history. The DDL block is declarative and large by
86
+ // nature; that is why this file carries a file-size-ratchet baseline.
87
+ export function initDb(path: string = "agent-relay.db"): Database {
88
+ const conn = new Database(path, { create: true });
89
+ applyConnectionPragmas(conn);
90
+ setDb(conn);
91
+
92
+ getDb().run(`
93
+ CREATE TABLE IF NOT EXISTS agents (
94
+ id TEXT PRIMARY KEY,
95
+ name TEXT NOT NULL,
96
+ kind TEXT NOT NULL DEFAULT 'provider',
97
+ tags TEXT NOT NULL DEFAULT '[]',
98
+ machine TEXT,
99
+ rig TEXT,
100
+ capabilities TEXT NOT NULL DEFAULT '[]',
101
+ status TEXT NOT NULL DEFAULT 'idle',
102
+ instance_id TEXT,
103
+ epoch INTEGER NOT NULL DEFAULT 0,
104
+ provider_capabilities TEXT,
105
+ context_state TEXT,
106
+ meta TEXT NOT NULL DEFAULT '{}',
107
+ spawned_by TEXT,
108
+ last_seen INTEGER NOT NULL,
109
+ created_at INTEGER NOT NULL
110
+ );
111
+
112
+ CREATE TABLE IF NOT EXISTS messages (
113
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ from_agent TEXT NOT NULL,
115
+ to_target TEXT NOT NULL,
116
+ kind TEXT NOT NULL DEFAULT 'chat',
117
+ channel TEXT,
118
+ subject TEXT,
119
+ body TEXT NOT NULL,
120
+ thread_id INTEGER,
121
+ reply_to INTEGER REFERENCES messages(id),
122
+ reply_expected INTEGER NOT NULL DEFAULT 1,
123
+ claimable INTEGER NOT NULL DEFAULT 0,
124
+ claimed_by TEXT,
125
+ claimed_at INTEGER,
126
+ claim_expires_at INTEGER,
127
+ idempotency_key TEXT,
128
+ delivery_status TEXT NOT NULL DEFAULT 'pending',
129
+ delivery_attempts INTEGER NOT NULL DEFAULT 0,
130
+ delivery_last_error TEXT,
131
+ delivery_next_retry_at INTEGER,
132
+ delivery_poison_reason TEXT,
133
+ delivery_updated_at INTEGER,
134
+ queued_at INTEGER,
135
+ max_age_seconds INTEGER,
136
+ resolved_to_agent TEXT,
137
+ payload TEXT NOT NULL DEFAULT '{}',
138
+ meta TEXT NOT NULL DEFAULT '{}',
139
+ read_by TEXT NOT NULL DEFAULT '[]',
140
+ created_at INTEGER NOT NULL,
141
+ occurred_at INTEGER
142
+ );
143
+
144
+ CREATE INDEX IF NOT EXISTS idx_msg_to ON messages(to_target);
145
+ CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(created_at);
146
+ CREATE INDEX IF NOT EXISTS idx_msg_channel ON messages(channel);
147
+ -- (reply_to, from_agent) powers the reply-obligation NOT EXISTS subquery in
148
+ -- listPendingReplyObligations. Without it that subquery full-scans messages
149
+ -- per candidate row (O(n^2)): ~8.6s at 4k rows, which blew the 5s Stop-hook
150
+ -- timeout and wedged turns in "busy" (#199).
151
+ CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent);
152
+
153
+ CREATE TABLE IF NOT EXISTS message_reactions (
154
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
155
+ actor_id TEXT NOT NULL,
156
+ emoji TEXT NOT NULL,
157
+ created_at INTEGER NOT NULL,
158
+ updated_at INTEGER NOT NULL,
159
+ PRIMARY KEY (message_id, actor_id, emoji)
160
+ );
161
+ CREATE INDEX IF NOT EXISTS idx_message_reactions_message ON message_reactions(message_id);
162
+
163
+ CREATE TABLE IF NOT EXISTS context_snapshots (
164
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
165
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
166
+ utilization REAL NOT NULL,
167
+ lifecycle_state TEXT NOT NULL,
168
+ tokens_used INTEGER,
169
+ tokens_max INTEGER,
170
+ source TEXT NOT NULL,
171
+ confidence TEXT NOT NULL,
172
+ context_state TEXT NOT NULL,
173
+ captured_at INTEGER NOT NULL
174
+ );
175
+ CREATE INDEX IF NOT EXISTS idx_context_snapshots_agent_time ON context_snapshots(agent_id, captured_at DESC);
176
+
177
+ CREATE TABLE IF NOT EXISTS memories (
178
+ id TEXT PRIMARY KEY,
179
+ type TEXT NOT NULL,
180
+ scope TEXT NOT NULL,
181
+ title TEXT NOT NULL,
182
+ content TEXT NOT NULL,
183
+ tags TEXT NOT NULL DEFAULT '[]',
184
+ visibility TEXT NOT NULL DEFAULT 'project',
185
+ sensitivity TEXT NOT NULL DEFAULT 'normal',
186
+ confidence TEXT NOT NULL DEFAULT 'reported',
187
+ redaction_state TEXT NOT NULL DEFAULT 'raw',
188
+ relevance_score REAL DEFAULT 1.0,
189
+ source_agent TEXT,
190
+ source_task INTEGER REFERENCES tasks(id) ON DELETE SET NULL,
191
+ created_by TEXT,
192
+ content_hash TEXT,
193
+ metadata TEXT NOT NULL DEFAULT '{}',
194
+ access_count INTEGER DEFAULT 0,
195
+ last_accessed_at INTEGER,
196
+ created_at INTEGER NOT NULL,
197
+ updated_at INTEGER NOT NULL,
198
+ expires_at INTEGER
199
+ );
200
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
201
+ CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
202
+ CREATE INDEX IF NOT EXISTS idx_memories_tags ON memories(tags);
203
+ CREATE INDEX IF NOT EXISTS idx_memories_relevance ON memories(relevance_score);
204
+ CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at);
205
+
206
+ CREATE TABLE IF NOT EXISTS agent_active_memories (
207
+ agent_id TEXT NOT NULL,
208
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
209
+ loaded_at INTEGER NOT NULL,
210
+ PRIMARY KEY (agent_id, memory_id)
211
+ );
212
+ CREATE INDEX IF NOT EXISTS idx_agent_active_memories_agent ON agent_active_memories(agent_id);
213
+
214
+ CREATE TABLE IF NOT EXISTS artifact_blobs (
215
+ digest TEXT PRIMARY KEY,
216
+ storage_uri TEXT NOT NULL,
217
+ media_type TEXT NOT NULL,
218
+ size INTEGER NOT NULL,
219
+ created_at INTEGER NOT NULL
220
+ );
221
+
222
+ CREATE TABLE IF NOT EXISTS artifacts (
223
+ id TEXT PRIMARY KEY,
224
+ blob_digest TEXT NOT NULL REFERENCES artifact_blobs(digest),
225
+ media_type TEXT NOT NULL,
226
+ kind TEXT NOT NULL DEFAULT 'other',
227
+ filename TEXT,
228
+ size INTEGER NOT NULL,
229
+ visibility TEXT NOT NULL DEFAULT 'project',
230
+ sensitivity TEXT NOT NULL DEFAULT 'normal',
231
+ created_by TEXT NOT NULL,
232
+ created_at INTEGER NOT NULL,
233
+ expires_at INTEGER,
234
+ metadata TEXT NOT NULL DEFAULT '{}'
235
+ );
236
+ CREATE INDEX IF NOT EXISTS idx_artifacts_blob ON artifacts(blob_digest);
237
+ CREATE INDEX IF NOT EXISTS idx_artifacts_created ON artifacts(created_at);
238
+ CREATE INDEX IF NOT EXISTS idx_artifacts_expires ON artifacts(expires_at);
239
+ CREATE INDEX IF NOT EXISTS idx_artifacts_created_by ON artifacts(created_by);
240
+
241
+ CREATE TABLE IF NOT EXISTS artifact_links (
242
+ id TEXT PRIMARY KEY,
243
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
244
+ entity_type TEXT NOT NULL,
245
+ entity_id TEXT NOT NULL,
246
+ role TEXT,
247
+ title TEXT,
248
+ created_by TEXT NOT NULL,
249
+ created_at INTEGER NOT NULL,
250
+ UNIQUE (artifact_id, entity_type, entity_id, role)
251
+ );
252
+ CREATE INDEX IF NOT EXISTS idx_artifact_links_entity ON artifact_links(entity_type, entity_id);
253
+ CREATE INDEX IF NOT EXISTS idx_artifact_links_artifact ON artifact_links(artifact_id);
254
+
255
+ CREATE TABLE IF NOT EXISTS message_delivery_attempts (
256
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
257
+ message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
258
+ agent_id TEXT,
259
+ action TEXT NOT NULL DEFAULT 'attempt',
260
+ status TEXT NOT NULL,
261
+ error TEXT,
262
+ next_retry_at INTEGER,
263
+ poison_reason TEXT,
264
+ created_at INTEGER NOT NULL
265
+ );
266
+ CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC);
267
+
268
+ CREATE TABLE IF NOT EXISTS config (
269
+ namespace TEXT NOT NULL,
270
+ key TEXT NOT NULL,
271
+ value TEXT NOT NULL,
272
+ version INTEGER NOT NULL DEFAULT 1,
273
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
274
+ updated_by TEXT,
275
+ PRIMARY KEY (namespace, key)
276
+ );
277
+
278
+ CREATE TABLE IF NOT EXISTS config_history (
279
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
280
+ namespace TEXT NOT NULL,
281
+ key TEXT NOT NULL,
282
+ value TEXT NOT NULL,
283
+ version INTEGER NOT NULL,
284
+ changed_at TEXT NOT NULL DEFAULT (datetime('now')),
285
+ changed_by TEXT
286
+ );
287
+ CREATE INDEX IF NOT EXISTS idx_config_history_key ON config_history(namespace, key, version);
288
+
289
+ CREATE TABLE IF NOT EXISTS managed_agent_state (
290
+ policy_name TEXT PRIMARY KEY,
291
+ status TEXT NOT NULL,
292
+ agent_id TEXT,
293
+ orchestrator_id TEXT NOT NULL,
294
+ provider TEXT NOT NULL,
295
+ tmux_session TEXT,
296
+ spawn_request_id TEXT,
297
+ workspace_id TEXT,
298
+ workspace_path TEXT,
299
+ workspace_branch TEXT,
300
+ last_spawn_at INTEGER,
301
+ last_stop_at INTEGER,
302
+ healthy_since INTEGER,
303
+ restart_count INTEGER NOT NULL DEFAULT 0,
304
+ consecutive_failures INTEGER NOT NULL DEFAULT 0,
305
+ backoff_until INTEGER,
306
+ last_error TEXT,
307
+ updated_at INTEGER NOT NULL
308
+ );
309
+ CREATE INDEX IF NOT EXISTS idx_managed_agent_state_status ON managed_agent_state(status);
310
+ CREATE INDEX IF NOT EXISTS idx_managed_agent_state_agent ON managed_agent_state(agent_id);
311
+ CREATE INDEX IF NOT EXISTS idx_managed_agent_state_spawn_request ON managed_agent_state(policy_name, spawn_request_id);
312
+
313
+ CREATE TABLE IF NOT EXISTS workspaces (
314
+ id TEXT PRIMARY KEY,
315
+ repo_root TEXT NOT NULL,
316
+ source_cwd TEXT NOT NULL,
317
+ worktree_path TEXT NOT NULL,
318
+ branch TEXT,
319
+ base_ref TEXT,
320
+ base_sha TEXT,
321
+ mode TEXT NOT NULL,
322
+ requested_mode TEXT,
323
+ status TEXT NOT NULL DEFAULT 'active',
324
+ owner_agent_id TEXT,
325
+ owner_policy_name TEXT,
326
+ owner_automation_run_id TEXT,
327
+ steward_agent_id TEXT,
328
+ metadata TEXT NOT NULL DEFAULT '{}',
329
+ created_at INTEGER NOT NULL,
330
+ updated_at INTEGER NOT NULL,
331
+ ready_at INTEGER,
332
+ cleaned_at INTEGER
333
+ );
334
+ CREATE INDEX IF NOT EXISTS idx_workspaces_repo_status ON workspaces(repo_root, status);
335
+ CREATE INDEX IF NOT EXISTS idx_workspaces_owner_agent ON workspaces(owner_agent_id);
336
+ CREATE INDEX IF NOT EXISTS idx_workspaces_policy ON workspaces(owner_policy_name);
337
+
338
+ -- Persistent per-repo steward record. Keyed to the repo, not a live agent, so
339
+ -- it survives a full all-agents-offline gap: steward_agent_id goes NULL
340
+ -- (dormant) while last_steward_agent_id preserves continuity, and the row is
341
+ -- re-filled when an agent rejoins the repo. This is the durable backing store
342
+ -- the steward column on workspace rows mirrors for display/maintenance.
343
+ CREATE TABLE IF NOT EXISTS repo_stewards (
344
+ repo_root TEXT PRIMARY KEY,
345
+ steward_agent_id TEXT,
346
+ last_steward_agent_id TEXT,
347
+ elected_at INTEGER,
348
+ updated_at INTEGER NOT NULL
349
+ );
350
+
351
+ -- Per-repo merge serialization lease. Exactly one base merge may be in flight
352
+ -- per repo; a second merge request is rejected until the holder settles or the
353
+ -- lease expires. Atomicity comes from the repo_root PRIMARY KEY + expiry guard.
354
+ CREATE TABLE IF NOT EXISTS workspace_merge_leases (
355
+ repo_root TEXT PRIMARY KEY,
356
+ workspace_id TEXT NOT NULL,
357
+ command_id TEXT,
358
+ holder TEXT,
359
+ acquired_at INTEGER NOT NULL,
360
+ expires_at INTEGER NOT NULL
361
+ );
362
+
363
+ CREATE TABLE IF NOT EXISTS tasks (
364
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
365
+ source TEXT NOT NULL,
366
+ title TEXT NOT NULL,
367
+ body TEXT NOT NULL,
368
+ severity TEXT NOT NULL,
369
+ status TEXT NOT NULL,
370
+ target TEXT NOT NULL,
371
+ channel TEXT,
372
+ dedupe_key TEXT,
373
+ external_url TEXT,
374
+ occurrence_count INTEGER NOT NULL DEFAULT 1,
375
+ claimed_by TEXT,
376
+ claimed_at INTEGER,
377
+ claim_expires_at INTEGER,
378
+ message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
379
+ result TEXT,
380
+ metadata TEXT NOT NULL DEFAULT '{}',
381
+ created_at INTEGER NOT NULL,
382
+ updated_at INTEGER NOT NULL,
383
+ last_seen_at INTEGER NOT NULL
384
+ );
385
+
386
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_source_dedupe ON tasks(source, dedupe_key) WHERE dedupe_key IS NOT NULL AND status NOT IN ('done', 'failed', 'canceled');
387
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
388
+ CREATE INDEX IF NOT EXISTS idx_tasks_target ON tasks(target);
389
+ CREATE INDEX IF NOT EXISTS idx_tasks_updated ON tasks(updated_at);
390
+
391
+ CREATE TABLE IF NOT EXISTS automations (
392
+ id TEXT PRIMARY KEY,
393
+ kind TEXT NOT NULL,
394
+ name TEXT NOT NULL,
395
+ description TEXT,
396
+ enabled INTEGER NOT NULL DEFAULT 1,
397
+ schedule TEXT NOT NULL,
398
+ timezone TEXT NOT NULL,
399
+ next_run_at INTEGER,
400
+ catch_up_policy TEXT NOT NULL,
401
+ concurrency_policy TEXT NOT NULL,
402
+ orchestrator_id TEXT NOT NULL,
403
+ target_policy TEXT NOT NULL,
404
+ task_template TEXT NOT NULL,
405
+ created_at INTEGER NOT NULL,
406
+ updated_at INTEGER NOT NULL
407
+ );
408
+ CREATE INDEX IF NOT EXISTS idx_automations_enabled_next_run ON automations(enabled, next_run_at);
409
+
410
+ CREATE TABLE IF NOT EXISTS automation_runs (
411
+ id TEXT PRIMARY KEY,
412
+ automation_id TEXT NOT NULL,
413
+ status TEXT NOT NULL,
414
+ scheduled_for INTEGER NOT NULL,
415
+ started_at INTEGER,
416
+ finished_at INTEGER,
417
+ orchestrator_id TEXT NOT NULL,
418
+ target_agent_id TEXT,
419
+ spawned_agent_id TEXT,
420
+ task_id INTEGER,
421
+ message_id INTEGER,
422
+ control_message_id INTEGER,
423
+ error TEXT,
424
+ result TEXT,
425
+ meta TEXT NOT NULL DEFAULT '{}',
426
+ created_at INTEGER NOT NULL,
427
+ updated_at INTEGER NOT NULL,
428
+ shutdown_requested_at INTEGER
429
+ );
430
+ CREATE INDEX IF NOT EXISTS idx_automation_runs_automation ON automation_runs(automation_id, created_at);
431
+ CREATE INDEX IF NOT EXISTS idx_automation_runs_status ON automation_runs(status);
432
+ CREATE INDEX IF NOT EXISTS idx_automation_runs_task ON automation_runs(task_id);
433
+
434
+ CREATE TABLE IF NOT EXISTS task_events (
435
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
436
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
437
+ source TEXT NOT NULL,
438
+ type TEXT NOT NULL,
439
+ severity TEXT NOT NULL,
440
+ title TEXT NOT NULL,
441
+ body TEXT NOT NULL,
442
+ metadata TEXT NOT NULL DEFAULT '{}',
443
+ created_at INTEGER NOT NULL
444
+ );
445
+
446
+ CREATE TABLE IF NOT EXISTS task_callback_deliveries (
447
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
448
+ task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
449
+ url TEXT NOT NULL,
450
+ event_type TEXT NOT NULL,
451
+ payload TEXT NOT NULL,
452
+ status TEXT NOT NULL,
453
+ attempts INTEGER NOT NULL DEFAULT 0,
454
+ last_error TEXT,
455
+ created_at INTEGER NOT NULL,
456
+ updated_at INTEGER NOT NULL
457
+ );
458
+
459
+ CREATE TABLE IF NOT EXISTS pairs (
460
+ id TEXT PRIMARY KEY,
461
+ requester_id TEXT NOT NULL,
462
+ target_id TEXT NOT NULL,
463
+ status TEXT NOT NULL,
464
+ objective TEXT,
465
+ meta TEXT NOT NULL DEFAULT '{}',
466
+ created_at INTEGER NOT NULL,
467
+ updated_at INTEGER NOT NULL,
468
+ expires_at INTEGER NOT NULL,
469
+ accepted_at INTEGER,
470
+ ended_at INTEGER,
471
+ ended_by TEXT,
472
+ last_message_at INTEGER
473
+ );
474
+
475
+ CREATE INDEX IF NOT EXISTS idx_pairs_requester ON pairs(requester_id);
476
+ CREATE INDEX IF NOT EXISTS idx_pairs_target ON pairs(target_id);
477
+ CREATE INDEX IF NOT EXISTS idx_pairs_status ON pairs(status);
478
+
479
+ CREATE TABLE IF NOT EXISTS inbox_thread_state (
480
+ operator_id TEXT NOT NULL,
481
+ peer_id TEXT NOT NULL,
482
+ read_cursor_message_id INTEGER,
483
+ archived_at_message_id INTEGER,
484
+ updated_at INTEGER NOT NULL,
485
+ PRIMARY KEY (operator_id, peer_id)
486
+ );
487
+ CREATE INDEX IF NOT EXISTS idx_inbox_thread_operator ON inbox_thread_state(operator_id);
488
+
489
+ CREATE TABLE IF NOT EXISTS inbox_drafts (
490
+ operator_id TEXT NOT NULL,
491
+ peer_id TEXT NOT NULL,
492
+ body TEXT NOT NULL,
493
+ subject TEXT,
494
+ channel TEXT,
495
+ updated_at INTEGER NOT NULL,
496
+ PRIMARY KEY (operator_id, peer_id)
497
+ );
498
+ CREATE INDEX IF NOT EXISTS idx_inbox_drafts_operator ON inbox_drafts(operator_id);
499
+
500
+ CREATE TABLE IF NOT EXISTS chat_history_imports (
501
+ id TEXT PRIMARY KEY,
502
+ target_agent_id TEXT,
503
+ target_spawn_request_id TEXT,
504
+ source_peer_id TEXT NOT NULL,
505
+ source_agent_id TEXT,
506
+ source_thread_id TEXT,
507
+ source_agent_label TEXT,
508
+ imported_by TEXT NOT NULL,
509
+ imported_at INTEGER NOT NULL
510
+ );
511
+ CREATE INDEX IF NOT EXISTS idx_chat_history_imports_target_agent ON chat_history_imports(target_agent_id, imported_at);
512
+ CREATE INDEX IF NOT EXISTS idx_chat_history_imports_target_spawn ON chat_history_imports(target_spawn_request_id, imported_at);
513
+
514
+ CREATE TABLE IF NOT EXISTS chat_history_import_entries (
515
+ import_id TEXT NOT NULL REFERENCES chat_history_imports(id) ON DELETE CASCADE,
516
+ position INTEGER NOT NULL,
517
+ original_message_id INTEGER NOT NULL,
518
+ original_from TEXT NOT NULL,
519
+ original_to TEXT NOT NULL,
520
+ original_created_at INTEGER NOT NULL,
521
+ message_snapshot TEXT NOT NULL,
522
+ PRIMARY KEY (import_id, position)
523
+ );
524
+ CREATE INDEX IF NOT EXISTS idx_chat_history_import_entries_import ON chat_history_import_entries(import_id, position);
525
+
526
+ CREATE TABLE IF NOT EXISTS activity_events (
527
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
528
+ operator_id TEXT,
529
+ client_id TEXT UNIQUE,
530
+ kind TEXT NOT NULL,
531
+ title TEXT NOT NULL,
532
+ body TEXT,
533
+ meta_text TEXT,
534
+ icon TEXT,
535
+ view TEXT,
536
+ peer_id TEXT,
537
+ message_id INTEGER,
538
+ pair_id TEXT,
539
+ task_id INTEGER,
540
+ agent_id TEXT,
541
+ metadata TEXT NOT NULL DEFAULT '{}',
542
+ created_at INTEGER NOT NULL
543
+ );
544
+ CREATE INDEX IF NOT EXISTS idx_activity_operator ON activity_events(operator_id, created_at);
545
+ CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_events(created_at);
546
+ CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity_events(agent_id, created_at);
547
+
548
+ CREATE TABLE IF NOT EXISTS channels (
549
+ id TEXT PRIMARY KEY,
550
+ provider TEXT NOT NULL,
551
+ account_id TEXT NOT NULL,
552
+ display_name TEXT NOT NULL,
553
+ agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
554
+ transport TEXT NOT NULL,
555
+ direction TEXT NOT NULL DEFAULT 'bidirectional',
556
+ topic_channels TEXT NOT NULL DEFAULT '[]',
557
+ capabilities TEXT NOT NULL DEFAULT '[]',
558
+ meta TEXT NOT NULL DEFAULT '{}',
559
+ created_at INTEGER NOT NULL,
560
+ updated_at INTEGER NOT NULL
561
+ );
562
+ CREATE INDEX IF NOT EXISTS idx_channels_agent ON channels(agent_id);
563
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_channels_provider_account ON channels(provider, account_id);
564
+
565
+ CREATE TABLE IF NOT EXISTS channel_bindings (
566
+ id TEXT PRIMARY KEY,
567
+ channel_id TEXT NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
568
+ conversation_key TEXT NOT NULL DEFAULT '',
569
+ conversation_id TEXT,
570
+ target_type TEXT NOT NULL,
571
+ target_id TEXT NOT NULL,
572
+ mode TEXT NOT NULL DEFAULT 'exclusive',
573
+ priority INTEGER NOT NULL DEFAULT 0,
574
+ created_at INTEGER NOT NULL,
575
+ updated_at INTEGER NOT NULL
576
+ );
577
+ CREATE INDEX IF NOT EXISTS idx_channel_bindings_channel ON channel_bindings(channel_id, priority);
578
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_bindings_target ON channel_bindings(channel_id, conversation_key, target_type, target_id);
579
+
580
+ CREATE TABLE IF NOT EXISTS orchestrators (
581
+ id TEXT PRIMARY KEY,
582
+ hostname TEXT NOT NULL,
583
+ status TEXT NOT NULL DEFAULT 'online',
584
+ agent_id TEXT NOT NULL,
585
+ providers TEXT NOT NULL DEFAULT '[]',
586
+ base_dir TEXT NOT NULL,
587
+ env_keys TEXT NOT NULL DEFAULT '[]',
588
+ meta TEXT NOT NULL DEFAULT '{}',
589
+ managed_agents TEXT NOT NULL DEFAULT '[]',
590
+ last_seen INTEGER NOT NULL,
591
+ created_at INTEGER NOT NULL
592
+ );
593
+
594
+ CREATE TABLE IF NOT EXISTS bus_outbox (
595
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
596
+ event_type TEXT NOT NULL,
597
+ source TEXT NOT NULL,
598
+ subject TEXT,
599
+ data TEXT NOT NULL,
600
+ timestamp INTEGER NOT NULL
601
+ );
602
+ CREATE INDEX IF NOT EXISTS idx_outbox_type ON bus_outbox(event_type);
603
+ CREATE INDEX IF NOT EXISTS idx_outbox_timestamp ON bus_outbox(timestamp);
604
+
605
+ CREATE TABLE IF NOT EXISTS commands (
606
+ id TEXT PRIMARY KEY,
607
+ type TEXT NOT NULL,
608
+ source TEXT NOT NULL,
609
+ target TEXT NOT NULL,
610
+ params TEXT NOT NULL DEFAULT '{}',
611
+ status TEXT NOT NULL DEFAULT 'pending',
612
+ result TEXT,
613
+ error TEXT,
614
+ correlation_id TEXT,
615
+ created_at INTEGER NOT NULL,
616
+ updated_at INTEGER NOT NULL,
617
+ expires_at INTEGER
618
+ );
619
+ CREATE INDEX IF NOT EXISTS idx_commands_target ON commands(target);
620
+ CREATE INDEX IF NOT EXISTS idx_commands_status ON commands(status);
621
+ CREATE INDEX IF NOT EXISTS idx_commands_created ON commands(created_at);
622
+ CREATE INDEX IF NOT EXISTS idx_commands_correlation ON commands(correlation_id);
623
+
624
+ CREATE TABLE IF NOT EXISTS recipe_instances (
625
+ id TEXT PRIMARY KEY,
626
+ recipe_name TEXT NOT NULL,
627
+ recipe_source TEXT NOT NULL,
628
+ cwd TEXT NOT NULL,
629
+ orchestrator_id TEXT NOT NULL,
630
+ status TEXT NOT NULL,
631
+ started_by TEXT NOT NULL,
632
+ error TEXT,
633
+ started_at INTEGER NOT NULL,
634
+ stopped_at INTEGER
635
+ );
636
+ CREATE TABLE IF NOT EXISTS recipe_agent_instances (
637
+ instance_id TEXT NOT NULL REFERENCES recipe_instances(id) ON DELETE CASCADE,
638
+ role TEXT NOT NULL,
639
+ agent_id TEXT NOT NULL,
640
+ provider TEXT NOT NULL,
641
+ status TEXT NOT NULL,
642
+ idx INTEGER,
643
+ PRIMARY KEY (instance_id, role, agent_id)
644
+ );
645
+ CREATE INDEX IF NOT EXISTS idx_recipe_instances_status ON recipe_instances(status);
646
+
647
+ CREATE TABLE IF NOT EXISTS tokens (
648
+ jti TEXT PRIMARY KEY,
649
+ sub TEXT NOT NULL,
650
+ role TEXT NOT NULL,
651
+ scope TEXT NOT NULL,
652
+ profile_id TEXT,
653
+ issued_at INTEGER NOT NULL,
654
+ expires_at INTEGER,
655
+ revoked_at INTEGER,
656
+ created_by TEXT
657
+ );
658
+
659
+ CREATE TABLE IF NOT EXISTS token_profiles (
660
+ id TEXT PRIMARY KEY,
661
+ name TEXT NOT NULL,
662
+ description TEXT,
663
+ role TEXT NOT NULL,
664
+ scope TEXT NOT NULL,
665
+ constraints TEXT,
666
+ ttl_seconds INTEGER,
667
+ built_in INTEGER NOT NULL DEFAULT 0,
668
+ created_at INTEGER NOT NULL,
669
+ updated_at INTEGER NOT NULL,
670
+ created_by TEXT
671
+ );
672
+
673
+ CREATE TABLE IF NOT EXISTS maintenance_jobs (
674
+ id TEXT PRIMARY KEY,
675
+ title TEXT NOT NULL,
676
+ description TEXT,
677
+ interval_ms INTEGER NOT NULL,
678
+ timeout_ms INTEGER NOT NULL,
679
+ enabled INTEGER NOT NULL DEFAULT 1,
680
+ run_on_start INTEGER NOT NULL DEFAULT 0,
681
+ last_run_at INTEGER,
682
+ next_run_at INTEGER,
683
+ last_duration_ms INTEGER,
684
+ last_status TEXT NOT NULL DEFAULT 'idle',
685
+ last_error TEXT,
686
+ last_result TEXT,
687
+ consecutive_failures INTEGER NOT NULL DEFAULT 0,
688
+ lease_owner TEXT,
689
+ lease_until INTEGER,
690
+ updated_at INTEGER NOT NULL
691
+ );
692
+ CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_next_run ON maintenance_jobs(enabled, next_run_at);
693
+
694
+ CREATE TABLE IF NOT EXISTS provider_model_overrides (
695
+ provider TEXT NOT NULL,
696
+ alias TEXT NOT NULL,
697
+ entry TEXT NOT NULL DEFAULT '{}',
698
+ deleted INTEGER NOT NULL DEFAULT 0,
699
+ updated_at INTEGER NOT NULL,
700
+ updated_by TEXT,
701
+ PRIMARY KEY (provider, alias)
702
+ );
703
+ CREATE INDEX IF NOT EXISTS idx_provider_model_overrides_provider ON provider_model_overrides(provider);
704
+
705
+ CREATE TABLE IF NOT EXISTS integration_registry (
706
+ name TEXT PRIMARY KEY,
707
+ display_name TEXT,
708
+ description TEXT,
709
+ enabled INTEGER NOT NULL DEFAULT 1,
710
+ scopes TEXT NOT NULL DEFAULT '[]',
711
+ targets TEXT NOT NULL DEFAULT '[]',
712
+ channels TEXT NOT NULL DEFAULT '[]',
713
+ type TEXT,
714
+ icon TEXT,
715
+ accent_color TEXT,
716
+ tags TEXT NOT NULL DEFAULT '[]',
717
+ homepage_url TEXT,
718
+ repository_url TEXT,
719
+ docs_url TEXT,
720
+ manifest TEXT NOT NULL DEFAULT '{}',
721
+ source TEXT NOT NULL DEFAULT 'api',
722
+ created_at INTEGER NOT NULL,
723
+ updated_at INTEGER NOT NULL,
724
+ last_event_at INTEGER,
725
+ last_task_at INTEGER,
726
+ last_auth_success_at INTEGER,
727
+ last_auth_failure_at INTEGER
728
+ );
729
+
730
+ CREATE TABLE IF NOT EXISTS insights_observations (
731
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
732
+ session_id TEXT NOT NULL,
733
+ agent_id TEXT,
734
+ project TEXT NOT NULL DEFAULT 'unknown',
735
+ signal TEXT NOT NULL,
736
+ value TEXT NOT NULL DEFAULT '{}',
737
+ outcome TEXT,
738
+ source TEXT NOT NULL DEFAULT 'server',
739
+ created_at INTEGER NOT NULL
740
+ );
741
+ CREATE INDEX IF NOT EXISTS idx_insights_obs_project_signal ON insights_observations(project, signal, created_at DESC);
742
+ CREATE INDEX IF NOT EXISTS idx_insights_obs_signal ON insights_observations(signal, created_at DESC);
743
+ CREATE INDEX IF NOT EXISTS idx_insights_obs_session ON insights_observations(session_id);
744
+ `);
745
+
746
+ applyMigrations();
747
+
748
+ // Bootstrap planner statistics on first run (sqlite_stat1 absent means ANALYZE
749
+ // has never run — the planner would otherwise rely on heuristics only), then
750
+ // let PRAGMA optimize apply/refresh them cheaply on every startup.
751
+ const hasStats = getDb()
752
+ .query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_stat1'")
753
+ .get();
754
+ if (!hasStats) getDb().run("ANALYZE");
755
+ getDb().run("PRAGMA optimize");
756
+
757
+ return conn;
758
+ }