@controlflow-ai/daemon 0.1.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.
- package/README.md +360 -0
- package/bin/console.js +2 -0
- package/bin/daemon.js +2 -0
- package/bin/pal.js +2 -0
- package/bin/server.js +2 -0
- package/package.json +31 -0
- package/src/agent-runtime.ts +285 -0
- package/src/app.ts +745 -0
- package/src/args.ts +54 -0
- package/src/artifacts.ts +85 -0
- package/src/cli.ts +284 -0
- package/src/client.ts +310 -0
- package/src/coco.ts +52 -0
- package/src/codex.ts +41 -0
- package/src/coding-agent-runtime.ts +20 -0
- package/src/config.ts +106 -0
- package/src/console.ts +349 -0
- package/src/daemon-client.ts +91 -0
- package/src/daemon.ts +580 -0
- package/src/db.ts +2830 -0
- package/src/failure-message.ts +17 -0
- package/src/format.ts +13 -0
- package/src/http.ts +55 -0
- package/src/lark/agent-runtime.ts +142 -0
- package/src/lark/cli.ts +549 -0
- package/src/lark/credentials.ts +105 -0
- package/src/lark/daemon-integration.ts +108 -0
- package/src/lark/dispatcher.ts +374 -0
- package/src/lark/event-router.ts +329 -0
- package/src/lark/inbound-events.ts +131 -0
- package/src/lark/server-integration.ts +445 -0
- package/src/lark/setup.ts +326 -0
- package/src/lark/ws-daemon.ts +224 -0
- package/src/lark-fixture-diagnostics.ts +56 -0
- package/src/lark-fixture.ts +277 -0
- package/src/local-api.ts +155 -0
- package/src/local-auth.ts +45 -0
- package/src/migrations/001_initial.ts +61 -0
- package/src/migrations/002_daemon_deliveries.ts +52 -0
- package/src/migrations/003_sessions_runs.ts +49 -0
- package/src/migrations/004_message_idempotency.ts +21 -0
- package/src/migrations/005_artifacts.ts +24 -0
- package/src/migrations/006_lark_channel_foundation.ts +119 -0
- package/src/migrations/007_agents_a0.ts +17 -0
- package/src/migrations/008_b0_chat_history.ts +31 -0
- package/src/migrations/009_b0_transcript_ingest_seq.ts +35 -0
- package/src/migrations/010_b0_transcript_shadow_external_ids.ts +32 -0
- package/src/migrations/011_b0_channel_conversation_audit_only.ts +27 -0
- package/src/migrations/012_b0_cross_conversation_invariant.ts +45 -0
- package/src/migrations/013_b1_0_eng_inbound_raw_events.ts +56 -0
- package/src/migrations/014_agents_runtime.ts +10 -0
- package/src/migrations/015_agent_runtime_sessions.ts +15 -0
- package/src/migrations/016_room_participants.ts +27 -0
- package/src/migrations/017_unified_room_delivery.ts +203 -0
- package/src/migrations/018_room_display_names.ts +36 -0
- package/src/migrations/019_computer_connections.ts +63 -0
- package/src/migrations/020_computer_agent_assignments.ts +20 -0
- package/src/migrations/021_provider_identity_bindings.ts +32 -0
- package/src/migrations.ts +85 -0
- package/src/neeko.ts +23 -0
- package/src/provider-identity.ts +40 -0
- package/src/runtime-registry.ts +41 -0
- package/src/server-auth.ts +13 -0
- package/src/server.ts +63 -0
- package/src/token-file.ts +57 -0
- package/src/types.ts +408 -0
- package/src/web.ts +565 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 5;
|
|
4
|
+
export const name = 'artifacts';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
11
|
+
title TEXT NOT NULL DEFAULT '',
|
|
12
|
+
filename TEXT NOT NULL DEFAULT '',
|
|
13
|
+
mime_type TEXT NOT NULL,
|
|
14
|
+
size_bytes INTEGER NOT NULL,
|
|
15
|
+
content BLOB NOT NULL,
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
expires_at TEXT NOT NULL,
|
|
18
|
+
revoked_at TEXT
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_expires_at ON artifacts(expires_at);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_revoked_at ON artifacts(revoked_at);
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 6;
|
|
4
|
+
export const name = 'lark_channel_foundation';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS channel_accounts (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
channel TEXT NOT NULL CHECK (channel IN ('lark')),
|
|
11
|
+
name TEXT NOT NULL,
|
|
12
|
+
app_id TEXT NOT NULL,
|
|
13
|
+
bot_open_id TEXT,
|
|
14
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
UNIQUE(channel, app_id)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
CREATE TABLE IF NOT EXISTS channel_conversations (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
channel_account_id TEXT NOT NULL REFERENCES channel_accounts(id) ON DELETE CASCADE,
|
|
23
|
+
lock_chat_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
24
|
+
conversation_key TEXT NOT NULL UNIQUE,
|
|
25
|
+
external_chat_id TEXT NOT NULL,
|
|
26
|
+
external_root_id TEXT,
|
|
27
|
+
external_thread_id TEXT,
|
|
28
|
+
scope TEXT NOT NULL CHECK (scope IN ('chat', 'thread', 'p2p')),
|
|
29
|
+
chat_type TEXT NOT NULL CHECK (chat_type IN ('group', 'p2p')),
|
|
30
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
31
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE TABLE IF NOT EXISTS lock_transcript_messages (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
lock_message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
|
37
|
+
conversation_id TEXT NOT NULL REFERENCES channel_conversations(id) ON DELETE CASCADE,
|
|
38
|
+
channel_type TEXT NOT NULL DEFAULT 'lark' CHECK (channel_type IN ('lark', 'internal')),
|
|
39
|
+
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound', 'internal')),
|
|
40
|
+
sender_type TEXT NOT NULL CHECK (sender_type IN ('user', 'bot', 'agent', 'system')),
|
|
41
|
+
sender_id TEXT NOT NULL,
|
|
42
|
+
recipient_ref TEXT,
|
|
43
|
+
content TEXT NOT NULL,
|
|
44
|
+
content_type TEXT NOT NULL DEFAULT 'text',
|
|
45
|
+
source_type TEXT NOT NULL,
|
|
46
|
+
source_unique_key TEXT NOT NULL,
|
|
47
|
+
status TEXT NOT NULL DEFAULT 'recorded' CHECK (status IN ('recorded', 'superseded', 'redacted')),
|
|
48
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
49
|
+
recorded_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
50
|
+
UNIQUE(source_type, source_unique_key)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS lock_provider_evidence (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
transcript_message_id TEXT NOT NULL REFERENCES lock_transcript_messages(id) ON DELETE CASCADE,
|
|
56
|
+
provider TEXT NOT NULL DEFAULT 'lark' CHECK (provider IN ('lark')),
|
|
57
|
+
provider_message_id TEXT,
|
|
58
|
+
provider_event_id TEXT,
|
|
59
|
+
attempt_id TEXT,
|
|
60
|
+
evidence_type TEXT NOT NULL CHECK (evidence_type IN ('send_attempt', 'send_result', 'send_error', 'receive_event')),
|
|
61
|
+
receipt_state TEXT NOT NULL CHECK (receipt_state IN ('pending', 'delivered', 'failed', 'unknown')),
|
|
62
|
+
error_code TEXT,
|
|
63
|
+
error_message TEXT,
|
|
64
|
+
raw_payload_redacted_json TEXT NOT NULL DEFAULT '{}',
|
|
65
|
+
observed_at TEXT,
|
|
66
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS channel_messages (
|
|
70
|
+
id TEXT PRIMARY KEY,
|
|
71
|
+
channel_account_id TEXT NOT NULL REFERENCES channel_accounts(id) ON DELETE CASCADE,
|
|
72
|
+
channel_conversation_id TEXT NOT NULL REFERENCES channel_conversations(id) ON DELETE CASCADE,
|
|
73
|
+
lock_message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
|
74
|
+
transcript_message_id TEXT REFERENCES lock_transcript_messages(id) ON DELETE SET NULL,
|
|
75
|
+
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
|
76
|
+
external_message_id TEXT,
|
|
77
|
+
external_chat_id TEXT NOT NULL,
|
|
78
|
+
external_root_id TEXT,
|
|
79
|
+
external_thread_id TEXT,
|
|
80
|
+
sender_open_id TEXT,
|
|
81
|
+
sender_type TEXT NOT NULL DEFAULT 'unknown',
|
|
82
|
+
raw_type TEXT NOT NULL DEFAULT 'text',
|
|
83
|
+
status TEXT NOT NULL CHECK (status IN ('mapped', 'ignored', 'rejected', 'sent', 'failed')),
|
|
84
|
+
reason_code TEXT,
|
|
85
|
+
source TEXT NOT NULL DEFAULT 'local_fixture' CHECK (source IN ('local_fixture', 'channel_adapter')),
|
|
86
|
+
admitted INTEGER NOT NULL DEFAULT 0 CHECK (admitted IN (0, 1)),
|
|
87
|
+
raw_payload_redacted_json TEXT NOT NULL DEFAULT '{}',
|
|
88
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_messages_external_id
|
|
92
|
+
ON channel_messages(channel_account_id, external_message_id)
|
|
93
|
+
WHERE external_message_id IS NOT NULL;
|
|
94
|
+
|
|
95
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_messages_lock_message
|
|
96
|
+
ON channel_messages(channel_account_id, lock_message_id, channel_conversation_id, direction)
|
|
97
|
+
WHERE lock_message_id IS NOT NULL;
|
|
98
|
+
|
|
99
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_provider_evidence_event_id
|
|
100
|
+
ON lock_provider_evidence(provider, provider_event_id)
|
|
101
|
+
WHERE provider_event_id IS NOT NULL;
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS channel_outbox (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
channel_account_id TEXT NOT NULL REFERENCES channel_accounts(id) ON DELETE CASCADE,
|
|
106
|
+
channel_conversation_id TEXT NOT NULL REFERENCES channel_conversations(id) ON DELETE CASCADE,
|
|
107
|
+
transcript_message_id TEXT NOT NULL REFERENCES lock_transcript_messages(id) ON DELETE CASCADE,
|
|
108
|
+
lock_message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
109
|
+
purpose TEXT NOT NULL DEFAULT 'reply',
|
|
110
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
111
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_channel_conversations_account ON channel_conversations(channel_account_id, external_chat_id);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_conversation ON lock_transcript_messages(conversation_id, created_at);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_provider_evidence_transcript ON lock_provider_evidence(transcript_message_id, created_at);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_channel_outbox_created_at ON channel_outbox(created_at);
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 7;
|
|
4
|
+
export const name = 'agents_a0';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
agent_key TEXT NOT NULL UNIQUE,
|
|
11
|
+
display_name TEXT NOT NULL,
|
|
12
|
+
description TEXT NULL,
|
|
13
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
|
+
);
|
|
16
|
+
`);
|
|
17
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 8;
|
|
4
|
+
export const name = 'b0_chat_history';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
ALTER TABLE lock_transcript_messages ADD COLUMN mentions_json TEXT NOT NULL DEFAULT '[]';
|
|
9
|
+
ALTER TABLE lock_transcript_messages ADD COLUMN reply_to_transcript_id TEXT REFERENCES lock_transcript_messages(id) ON DELETE SET NULL;
|
|
10
|
+
ALTER TABLE lock_transcript_messages ADD COLUMN quote_root_transcript_id TEXT REFERENCES lock_transcript_messages(id) ON DELETE SET NULL;
|
|
11
|
+
ALTER TABLE lock_transcript_messages ADD COLUMN quote_message_transcript_id TEXT REFERENCES lock_transcript_messages(id) ON DELETE SET NULL;
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_conversation_order
|
|
14
|
+
ON lock_transcript_messages(conversation_id, created_at, id);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_reply_to
|
|
17
|
+
ON lock_transcript_messages(reply_to_transcript_id);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_quote_root
|
|
20
|
+
ON lock_transcript_messages(quote_root_transcript_id);
|
|
21
|
+
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_channel_conversations_external
|
|
23
|
+
ON channel_conversations(channel_account_id, external_chat_id, external_thread_id);
|
|
24
|
+
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_channel_messages_external_thread
|
|
26
|
+
ON channel_messages(channel_account_id, external_chat_id, external_thread_id);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_channel_messages_external_root
|
|
29
|
+
ON channel_messages(channel_account_id, external_chat_id, external_root_id);
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 9;
|
|
4
|
+
export const name = 'b0_transcript_ingest_seq';
|
|
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 hasIndex(db: Database, name: string): boolean {
|
|
12
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?").get(name) as { name: string } | null;
|
|
13
|
+
return row !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function up(db: Database): void {
|
|
17
|
+
if (!hasColumn(db, 'lock_transcript_messages', 'ingest_seq')) {
|
|
18
|
+
db.exec('ALTER TABLE lock_transcript_messages ADD COLUMN ingest_seq INTEGER NOT NULL DEFAULT 0');
|
|
19
|
+
db.exec(`
|
|
20
|
+
UPDATE lock_transcript_messages
|
|
21
|
+
SET ingest_seq = (
|
|
22
|
+
SELECT COUNT(*) FROM lock_transcript_messages AS earlier
|
|
23
|
+
WHERE earlier.rowid <= lock_transcript_messages.rowid
|
|
24
|
+
)
|
|
25
|
+
WHERE ingest_seq = 0;
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!hasIndex(db, 'idx_transcript_conversation_ingest_seq')) {
|
|
30
|
+
db.exec('CREATE INDEX idx_transcript_conversation_ingest_seq ON lock_transcript_messages(conversation_id, ingest_seq)');
|
|
31
|
+
}
|
|
32
|
+
if (!hasIndex(db, 'idx_transcript_ingest_seq')) {
|
|
33
|
+
db.exec('CREATE UNIQUE INDEX idx_transcript_ingest_seq ON lock_transcript_messages(ingest_seq)');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 10;
|
|
4
|
+
export const name = 'b0_transcript_shadow_external_ids';
|
|
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 hasIndex(db: Database, name: string): boolean {
|
|
12
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?").get(name) as { name: string } | null;
|
|
13
|
+
return row !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function up(db: Database): void {
|
|
17
|
+
for (const column of ['reply_to_external_message_id', 'quote_root_external_message_id', 'quote_message_external_message_id']) {
|
|
18
|
+
if (!hasColumn(db, 'lock_transcript_messages', column)) {
|
|
19
|
+
db.exec(`ALTER TABLE lock_transcript_messages ADD COLUMN ${column} TEXT`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!hasIndex(db, 'idx_transcript_reply_external')) {
|
|
24
|
+
db.exec('CREATE INDEX idx_transcript_reply_external ON lock_transcript_messages(conversation_id, reply_to_external_message_id) WHERE reply_to_external_message_id IS NOT NULL');
|
|
25
|
+
}
|
|
26
|
+
if (!hasIndex(db, 'idx_transcript_quote_root_external')) {
|
|
27
|
+
db.exec('CREATE INDEX idx_transcript_quote_root_external ON lock_transcript_messages(conversation_id, quote_root_external_message_id) WHERE quote_root_external_message_id IS NOT NULL');
|
|
28
|
+
}
|
|
29
|
+
if (!hasIndex(db, 'idx_transcript_quote_message_external')) {
|
|
30
|
+
db.exec('CREATE INDEX idx_transcript_quote_message_external ON lock_transcript_messages(conversation_id, quote_message_external_message_id) WHERE quote_message_external_message_id IS NOT NULL');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 11;
|
|
4
|
+
export const name = 'b0_channel_conversation_audit_only';
|
|
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 hasIndex(db: Database, name: string): boolean {
|
|
12
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?").get(name) as { name: string } | null;
|
|
13
|
+
return row !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function up(db: Database): void {
|
|
17
|
+
if (!hasColumn(db, 'channel_conversations', 'audit_only')) {
|
|
18
|
+
db.exec('ALTER TABLE channel_conversations ADD COLUMN audit_only INTEGER NOT NULL DEFAULT 0 CHECK (audit_only IN (0, 1))');
|
|
19
|
+
}
|
|
20
|
+
if (!hasIndex(db, 'idx_channel_conversations_canonical')) {
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE INDEX idx_channel_conversations_canonical
|
|
23
|
+
ON channel_conversations(channel_account_id, external_chat_id, external_thread_id)
|
|
24
|
+
WHERE audit_only = 0
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 12;
|
|
4
|
+
export const name = 'b0_cross_conversation_invariant';
|
|
5
|
+
|
|
6
|
+
interface InvariantViolation {
|
|
7
|
+
transcript_id: string;
|
|
8
|
+
conversation_id: string;
|
|
9
|
+
column: 'reply_to_transcript_id' | 'quote_root_transcript_id' | 'quote_message_transcript_id';
|
|
10
|
+
referenced_transcript_id: string;
|
|
11
|
+
referenced_conversation_id: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function up(db: Database): void {
|
|
15
|
+
const violations: InvariantViolation[] = [];
|
|
16
|
+
|
|
17
|
+
for (const column of ['reply_to_transcript_id', 'quote_root_transcript_id', 'quote_message_transcript_id'] as const) {
|
|
18
|
+
const rows = db.query(`
|
|
19
|
+
SELECT child.id AS transcript_id,
|
|
20
|
+
child.conversation_id AS conversation_id,
|
|
21
|
+
child.${column} AS referenced_transcript_id,
|
|
22
|
+
parent.conversation_id AS referenced_conversation_id
|
|
23
|
+
FROM lock_transcript_messages AS child
|
|
24
|
+
INNER JOIN lock_transcript_messages AS parent
|
|
25
|
+
ON parent.id = child.${column}
|
|
26
|
+
WHERE child.${column} IS NOT NULL
|
|
27
|
+
AND parent.conversation_id <> child.conversation_id
|
|
28
|
+
`).all() as Array<{
|
|
29
|
+
transcript_id: string;
|
|
30
|
+
conversation_id: string;
|
|
31
|
+
referenced_transcript_id: string;
|
|
32
|
+
referenced_conversation_id: string;
|
|
33
|
+
}>;
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
violations.push({ ...row, column });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (violations.length > 0) {
|
|
40
|
+
const summary = violations
|
|
41
|
+
.map((violation) => `${violation.transcript_id}.${violation.column} → ${violation.referenced_transcript_id} (${violation.referenced_conversation_id} != ${violation.conversation_id})`)
|
|
42
|
+
.join('; ');
|
|
43
|
+
throw new Error(`CROSS_CONVERSATION_INVARIANT_VIOLATION (migration 012): ${violations.length} row(s): ${summary}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 13;
|
|
4
|
+
export const name = 'b1_0_eng_inbound_raw_events';
|
|
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 hasIndex(db: Database, name: string): boolean {
|
|
12
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?").get(name) as { name: string } | null;
|
|
13
|
+
return row !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasTable(db: Database, name: string): boolean {
|
|
17
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name) as { name: string } | null;
|
|
18
|
+
return row !== null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function up(db: Database): void {
|
|
22
|
+
if (!hasTable(db, 'channel_inbound_raw_events')) {
|
|
23
|
+
db.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS channel_inbound_raw_events (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
received_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
27
|
+
app_id TEXT NOT NULL,
|
|
28
|
+
event_type TEXT NOT NULL,
|
|
29
|
+
event_id TEXT NOT NULL,
|
|
30
|
+
parse_ok INTEGER NOT NULL CHECK (parse_ok IN (0, 1)),
|
|
31
|
+
raw_body_bytes BLOB NOT NULL
|
|
32
|
+
);
|
|
33
|
+
`);
|
|
34
|
+
} else {
|
|
35
|
+
if (!hasColumn(db, 'channel_inbound_raw_events', 'event_id')) {
|
|
36
|
+
db.exec('ALTER TABLE channel_inbound_raw_events ADD COLUMN event_id TEXT NOT NULL DEFAULT ""');
|
|
37
|
+
}
|
|
38
|
+
if (!hasColumn(db, 'channel_inbound_raw_events', 'parse_ok')) {
|
|
39
|
+
db.exec('ALTER TABLE channel_inbound_raw_events ADD COLUMN parse_ok INTEGER NOT NULL DEFAULT 0 CHECK (parse_ok IN (0, 1))');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!hasIndex(db, 'idx_raw_events_event_id')) {
|
|
44
|
+
db.exec(`
|
|
45
|
+
CREATE UNIQUE INDEX idx_raw_events_event_id
|
|
46
|
+
ON channel_inbound_raw_events(event_id)
|
|
47
|
+
WHERE event_id IS NOT NULL;
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
if (!hasIndex(db, 'idx_raw_events_received_at')) {
|
|
51
|
+
db.exec('CREATE INDEX idx_raw_events_received_at ON channel_inbound_raw_events(received_at)');
|
|
52
|
+
}
|
|
53
|
+
if (!hasIndex(db, 'idx_raw_events_app_type')) {
|
|
54
|
+
db.exec('CREATE INDEX idx_raw_events_app_type ON channel_inbound_raw_events(app_id, event_type)');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 15;
|
|
4
|
+
export const name = 'agent_runtime_sessions';
|
|
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, 'agent_sessions', 'runtime_session_id')) {
|
|
13
|
+
db.exec('ALTER TABLE agent_sessions ADD COLUMN runtime_session_id TEXT');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 16;
|
|
4
|
+
export const name = 'room_participants';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS room_participants (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
11
|
+
participant_id TEXT NOT NULL,
|
|
12
|
+
kind TEXT NOT NULL CHECK (kind IN ('user', 'bot', 'agent')),
|
|
13
|
+
display_name TEXT,
|
|
14
|
+
source TEXT NOT NULL CHECK (source IN ('lark_member_api', 'known_bot', 'event', 'local_agent')),
|
|
15
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
UNIQUE(room_id, participant_id)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_room_participants_room
|
|
22
|
+
ON room_participants(room_id, kind, participant_id);
|
|
23
|
+
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_room_participants_seen
|
|
25
|
+
ON room_participants(room_id, last_seen_at);
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 17;
|
|
4
|
+
export const name = 'unified_room_delivery';
|
|
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
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS servers (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
name TEXT NOT NULL,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
display_name TEXT NOT NULL,
|
|
28
|
+
role TEXT NOT NULL DEFAULT 'owner' CHECK (role IN ('owner', 'admin', 'member')),
|
|
29
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
30
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
36
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
37
|
+
expires_at TEXT,
|
|
38
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE TABLE IF NOT EXISTS server_memberships (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
server_id TEXT NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
|
|
44
|
+
participant_id TEXT NOT NULL,
|
|
45
|
+
participant_kind TEXT NOT NULL CHECK (participant_kind IN ('user', 'agent')),
|
|
46
|
+
role TEXT NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
|
|
47
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'removed')),
|
|
48
|
+
joined_at TEXT,
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
50
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
51
|
+
UNIQUE(server_id, participant_id, participant_kind)
|
|
52
|
+
);
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
db.query(`INSERT OR IGNORE INTO servers (id, name) VALUES ('default', 'Default')`).run();
|
|
56
|
+
db.query(`INSERT OR IGNORE INTO users (id, display_name, role) VALUES ('owner', 'Owner', 'owner')`).run();
|
|
57
|
+
db.query(`
|
|
58
|
+
INSERT OR IGNORE INTO server_memberships (id, server_id, participant_id, participant_kind, role, status)
|
|
59
|
+
VALUES ('default-owner', 'default', 'owner', 'user', 'owner', 'active')
|
|
60
|
+
`).run();
|
|
61
|
+
|
|
62
|
+
addColumnIfMissing(db, 'chats', 'server_id', `server_id TEXT NOT NULL DEFAULT 'default'`);
|
|
63
|
+
addColumnIfMissing(db, 'chats', 'provider', `provider TEXT NOT NULL DEFAULT 'web' CHECK (provider IN ('web', 'lark', 'wechat'))`);
|
|
64
|
+
addColumnIfMissing(db, 'chats', 'dm_type', `dm_type TEXT CHECK (dm_type IN ('agent_agent', 'user_agent', 'user_user'))`);
|
|
65
|
+
addColumnIfMissing(db, 'chats', 'capabilities_json', `capabilities_json TEXT NOT NULL DEFAULT '{"topics":"native"}'`);
|
|
66
|
+
addColumnIfMissing(db, 'chats', 'audit_visibility', `audit_visibility TEXT NOT NULL DEFAULT 'members' CHECK (audit_visibility IN ('members', 'admins'))`);
|
|
67
|
+
|
|
68
|
+
addColumnIfMissing(db, 'room_participants', 'status', `status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'removed'))`);
|
|
69
|
+
addColumnIfMissing(db, 'room_participants', 'joined_at', `joined_at TEXT`);
|
|
70
|
+
addColumnIfMissing(db, 'room_participants', 'delivery_cursor_message_id', `delivery_cursor_message_id INTEGER`);
|
|
71
|
+
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS room_channels (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
76
|
+
kind TEXT NOT NULL DEFAULT 'topic' CHECK (kind IN ('topic')),
|
|
77
|
+
name TEXT NOT NULL,
|
|
78
|
+
external_ref TEXT,
|
|
79
|
+
created_by TEXT,
|
|
80
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
81
|
+
UNIQUE(room_id, name)
|
|
82
|
+
);
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
addColumnIfMissing(db, 'messages', 'channel_id', `channel_id TEXT`);
|
|
86
|
+
addColumnIfMissing(db, 'messages', 'provider', `provider TEXT NOT NULL DEFAULT 'web' CHECK (provider IN ('web', 'lark', 'wechat'))`);
|
|
87
|
+
addColumnIfMissing(db, 'messages', 'mentions_json', `mentions_json TEXT NOT NULL DEFAULT '[]'`);
|
|
88
|
+
|
|
89
|
+
db.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS agent_room_subscriptions (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
agent TEXT NOT NULL,
|
|
93
|
+
room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
94
|
+
channel_id TEXT REFERENCES room_channels(id) ON DELETE CASCADE,
|
|
95
|
+
mode TEXT NOT NULL DEFAULT 'mentions' CHECK (mode IN ('all', 'periodic', 'mentions', 'muted', 'off')),
|
|
96
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
97
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
98
|
+
UNIQUE(agent, room_id, channel_id)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS provider_accounts (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
provider TEXT NOT NULL CHECK (provider IN ('lark', 'wechat')),
|
|
104
|
+
external_account_id TEXT NOT NULL,
|
|
105
|
+
agent TEXT NOT NULL,
|
|
106
|
+
bot_external_user_id TEXT,
|
|
107
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
|
|
108
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
109
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
110
|
+
UNIQUE(provider, external_account_id)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
CREATE TABLE IF NOT EXISTS provider_conversations (
|
|
114
|
+
id TEXT PRIMARY KEY,
|
|
115
|
+
provider_account_id TEXT NOT NULL REFERENCES provider_accounts(id) ON DELETE CASCADE,
|
|
116
|
+
room_id TEXT NOT NULL REFERENCES chats(id) ON DELETE CASCADE,
|
|
117
|
+
channel_id TEXT,
|
|
118
|
+
external_room_id TEXT NOT NULL,
|
|
119
|
+
external_channel_id TEXT,
|
|
120
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
121
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
122
|
+
UNIQUE(provider_account_id, external_room_id, external_channel_id)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
CREATE TABLE IF NOT EXISTS pending_inbound_events (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
raw_event_id TEXT NOT NULL,
|
|
128
|
+
provider TEXT NOT NULL CHECK (provider IN ('lark', 'wechat')),
|
|
129
|
+
provider_account_id TEXT,
|
|
130
|
+
reason TEXT NOT NULL,
|
|
131
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
132
|
+
next_retry_at TEXT,
|
|
133
|
+
last_error TEXT NOT NULL DEFAULT '',
|
|
134
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'resolved', 'dead')),
|
|
135
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
136
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
137
|
+
UNIQUE(raw_event_id, reason)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
CREATE TABLE IF NOT EXISTS provider_identity_cache (
|
|
141
|
+
id TEXT PRIMARY KEY,
|
|
142
|
+
provider TEXT NOT NULL CHECK (provider IN ('lark', 'wechat')),
|
|
143
|
+
app_id TEXT NOT NULL,
|
|
144
|
+
open_id TEXT NOT NULL,
|
|
145
|
+
union_id TEXT NOT NULL,
|
|
146
|
+
fetched_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
147
|
+
expires_at TEXT,
|
|
148
|
+
UNIQUE(provider, app_id, open_id)
|
|
149
|
+
);
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
addColumnIfMissing(db, 'channel_accounts', 'agent', `agent TEXT`);
|
|
153
|
+
addColumnIfMissing(db, 'channel_accounts', 'provider_account_id', `provider_account_id TEXT REFERENCES provider_accounts(id) ON DELETE SET NULL`);
|
|
154
|
+
|
|
155
|
+
db.exec(`
|
|
156
|
+
INSERT OR IGNORE INTO provider_accounts (id, provider, external_account_id, agent, bot_external_user_id, status, created_at, updated_at)
|
|
157
|
+
SELECT id, 'lark', app_id, COALESCE(agent, ''), bot_open_id, status, created_at, updated_at
|
|
158
|
+
FROM channel_accounts
|
|
159
|
+
WHERE agent IS NOT NULL AND agent != '';
|
|
160
|
+
|
|
161
|
+
UPDATE channel_accounts
|
|
162
|
+
SET provider_account_id = id
|
|
163
|
+
WHERE provider_account_id IS NULL AND agent IS NOT NULL AND agent != '';
|
|
164
|
+
|
|
165
|
+
UPDATE chats
|
|
166
|
+
SET capabilities_json = '{"topics":"unsupported"}'
|
|
167
|
+
WHERE kind = 'dm';
|
|
168
|
+
|
|
169
|
+
UPDATE chats
|
|
170
|
+
SET provider = 'lark',
|
|
171
|
+
kind = CASE WHEN name LIKE 'lark:group:%' THEN 'group' ELSE 'dm' END,
|
|
172
|
+
capabilities_json = CASE WHEN name LIKE 'lark:group:%' THEN '{"topics":"native"}' ELSE '{"topics":"unsupported"}' END,
|
|
173
|
+
dm_type = CASE WHEN name LIKE 'lark:group:%' THEN dm_type ELSE COALESCE(dm_type, 'user_agent') END
|
|
174
|
+
WHERE name LIKE 'lark:%';
|
|
175
|
+
|
|
176
|
+
UPDATE messages
|
|
177
|
+
SET provider = 'lark'
|
|
178
|
+
WHERE chat_id IN (SELECT id FROM chats WHERE provider = 'lark');
|
|
179
|
+
|
|
180
|
+
DROP VIEW IF EXISTS chat_stats;
|
|
181
|
+
CREATE VIEW chat_stats AS
|
|
182
|
+
SELECT
|
|
183
|
+
c.id,
|
|
184
|
+
c.name,
|
|
185
|
+
c.kind,
|
|
186
|
+
c.server_id,
|
|
187
|
+
c.provider,
|
|
188
|
+
c.dm_type,
|
|
189
|
+
c.capabilities_json,
|
|
190
|
+
c.audit_visibility,
|
|
191
|
+
c.created_at,
|
|
192
|
+
COUNT(m.id) AS message_count,
|
|
193
|
+
MAX(m.created_at) AS last_message_at
|
|
194
|
+
FROM chats c
|
|
195
|
+
LEFT JOIN messages m ON m.chat_id = c.id
|
|
196
|
+
GROUP BY c.id;
|
|
197
|
+
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_room_channels_room ON room_channels(room_id, kind, name);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_agent_room_subscriptions_room ON agent_room_subscriptions(room_id, mode, agent);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_provider_accounts_agent ON provider_accounts(provider, agent, status);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_pending_inbound_events_status ON pending_inbound_events(status, next_retry_at, created_at);
|
|
202
|
+
`);
|
|
203
|
+
}
|