@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.
Files changed (67) hide show
  1. package/README.md +360 -0
  2. package/bin/console.js +2 -0
  3. package/bin/daemon.js +2 -0
  4. package/bin/pal.js +2 -0
  5. package/bin/server.js +2 -0
  6. package/package.json +31 -0
  7. package/src/agent-runtime.ts +285 -0
  8. package/src/app.ts +745 -0
  9. package/src/args.ts +54 -0
  10. package/src/artifacts.ts +85 -0
  11. package/src/cli.ts +284 -0
  12. package/src/client.ts +310 -0
  13. package/src/coco.ts +52 -0
  14. package/src/codex.ts +41 -0
  15. package/src/coding-agent-runtime.ts +20 -0
  16. package/src/config.ts +106 -0
  17. package/src/console.ts +349 -0
  18. package/src/daemon-client.ts +91 -0
  19. package/src/daemon.ts +580 -0
  20. package/src/db.ts +2830 -0
  21. package/src/failure-message.ts +17 -0
  22. package/src/format.ts +13 -0
  23. package/src/http.ts +55 -0
  24. package/src/lark/agent-runtime.ts +142 -0
  25. package/src/lark/cli.ts +549 -0
  26. package/src/lark/credentials.ts +105 -0
  27. package/src/lark/daemon-integration.ts +108 -0
  28. package/src/lark/dispatcher.ts +374 -0
  29. package/src/lark/event-router.ts +329 -0
  30. package/src/lark/inbound-events.ts +131 -0
  31. package/src/lark/server-integration.ts +445 -0
  32. package/src/lark/setup.ts +326 -0
  33. package/src/lark/ws-daemon.ts +224 -0
  34. package/src/lark-fixture-diagnostics.ts +56 -0
  35. package/src/lark-fixture.ts +277 -0
  36. package/src/local-api.ts +155 -0
  37. package/src/local-auth.ts +45 -0
  38. package/src/migrations/001_initial.ts +61 -0
  39. package/src/migrations/002_daemon_deliveries.ts +52 -0
  40. package/src/migrations/003_sessions_runs.ts +49 -0
  41. package/src/migrations/004_message_idempotency.ts +21 -0
  42. package/src/migrations/005_artifacts.ts +24 -0
  43. package/src/migrations/006_lark_channel_foundation.ts +119 -0
  44. package/src/migrations/007_agents_a0.ts +17 -0
  45. package/src/migrations/008_b0_chat_history.ts +31 -0
  46. package/src/migrations/009_b0_transcript_ingest_seq.ts +35 -0
  47. package/src/migrations/010_b0_transcript_shadow_external_ids.ts +32 -0
  48. package/src/migrations/011_b0_channel_conversation_audit_only.ts +27 -0
  49. package/src/migrations/012_b0_cross_conversation_invariant.ts +45 -0
  50. package/src/migrations/013_b1_0_eng_inbound_raw_events.ts +56 -0
  51. package/src/migrations/014_agents_runtime.ts +10 -0
  52. package/src/migrations/015_agent_runtime_sessions.ts +15 -0
  53. package/src/migrations/016_room_participants.ts +27 -0
  54. package/src/migrations/017_unified_room_delivery.ts +203 -0
  55. package/src/migrations/018_room_display_names.ts +36 -0
  56. package/src/migrations/019_computer_connections.ts +63 -0
  57. package/src/migrations/020_computer_agent_assignments.ts +20 -0
  58. package/src/migrations/021_provider_identity_bindings.ts +32 -0
  59. package/src/migrations.ts +85 -0
  60. package/src/neeko.ts +23 -0
  61. package/src/provider-identity.ts +40 -0
  62. package/src/runtime-registry.ts +41 -0
  63. package/src/server-auth.ts +13 -0
  64. package/src/server.ts +63 -0
  65. package/src/token-file.ts +57 -0
  66. package/src/types.ts +408 -0
  67. package/src/web.ts +565 -0
@@ -0,0 +1,329 @@
1
+ import type { MessageStore, CreateMessageInput } from '../db.js';
2
+ import type { Message } from '../types.js';
3
+ import { palIdentityHandle } from '../provider-identity.js';
4
+
5
+ /**
6
+ * Map a Lark `im.message.receive_v1` event into a `CreateMessageInput`
7
+ * suitable for `MessageStore.createMessage`.
8
+ *
9
+ * Mapping rules (M2 minimal viable):
10
+ *
11
+ * | Lark field | pal field |
12
+ * |-----------------------------------|---------------------------|
13
+ * | message.chat_id | chatName (prefixed `lark:`) |
14
+ * | message.message_id | idempotencyKey |
15
+ * | sender.sender_id.open_id | sender |
16
+ * | message.mentions[0]?.id.open_id | recipient (first @mention)|
17
+ * | message.content (parsed JSON text)| content |
18
+ * | message.root_id (thread reply) | parentId via lookup |
19
+ *
20
+ * Thread mapping: when `message.root_id` is set (i.e. this is a reply inside
21
+ * a Lark thread/topic), we look up the previously-stored lock message whose
22
+ * idempotency_key matches `root_id`. If found, the new message becomes a
23
+ * topic (depth=1, parent=root). If the root is not yet stored (out-of-order
24
+ * delivery), the reply is stored at top-level under the same chat, with the
25
+ * Lark root_id preserved in the `recipient` field as `root:<id>` so a later
26
+ * backfill can wire it up.
27
+ *
28
+ * Idempotency: Lark `message_id` is guaranteed-unique per tenant; we feed it
29
+ * into `idempotency_key` so any retried delivery gets coalesced by the
30
+ * existing dedupe path in `createMessage`.
31
+ */
32
+ export interface LarkMessageEnvelope {
33
+ sender?: {
34
+ sender_id?: { open_id?: string; user_id?: string; union_id?: string };
35
+ sender_type?: string;
36
+ };
37
+ message?: {
38
+ message_id?: string;
39
+ chat_id?: string;
40
+ chat_type?: string;
41
+ root_id?: string;
42
+ thread_id?: string;
43
+ parent_id?: string;
44
+ message_type?: string;
45
+ create_time?: string;
46
+ content?: string;
47
+ mentions?: Array<{ id?: { open_id?: string; user_id?: string }; name?: string; key?: string }>;
48
+ };
49
+ }
50
+
51
+ export interface MapLarkMessageInput {
52
+ /** appId of the receiving bot; used to namespace chat names so multiple bots don't collide. */
53
+ appId: string;
54
+ envelope: LarkMessageEnvelope;
55
+ recipientByMentionOpenId?: Map<string, string>;
56
+ recipientOverride?: string | null;
57
+ }
58
+
59
+ export interface MapLarkMessageResult {
60
+ status: 'ok' | 'skipped';
61
+ reason?: 'missing_message_id' | 'missing_chat_id' | 'missing_sender' | 'unsupported_message_type' | 'empty_text';
62
+ input?: CreateMessageInput;
63
+ /** Lark root_id when present (for thread replies). The router pre-resolves the parentId from it if possible. */
64
+ rootMessageId?: string;
65
+ }
66
+
67
+ export function buildLockChatName(appId: string, chatId: string, chatType?: string): string {
68
+ if (chatType === 'group') {
69
+ return `lark:group:${chatId}`;
70
+ }
71
+ return `lark:${appId}:${chatId}`;
72
+ }
73
+
74
+ /** Extract @mention open_ids from a Lark envelope (may be empty). */
75
+ export function extractMentionOpenIds(envelope: LarkMessageEnvelope): string[] {
76
+ const mentions = envelope.message?.mentions ?? [];
77
+ const ids: string[] = [];
78
+ for (const m of mentions) {
79
+ const id = m.id?.open_id;
80
+ if (typeof id === 'string' && id.length > 0) ids.push(id);
81
+ }
82
+ return ids;
83
+ }
84
+
85
+ function normalizeMappedMentions(values: Array<string | null>): string[] {
86
+ const out: string[] = [];
87
+ const seen = new Set<string>();
88
+ for (const value of values) {
89
+ if (!value || seen.has(value)) continue;
90
+ seen.add(value);
91
+ out.push(value);
92
+ }
93
+ return out;
94
+ }
95
+
96
+ function parseLarkTextContent(rawContent: string | undefined, messageType: string | undefined): string {
97
+ if (!rawContent) return '';
98
+ if (messageType !== 'text' && messageType !== undefined) {
99
+ // Non-text messages: keep the raw JSON so downstream agents see *something*.
100
+ return rawContent.length > 4000 ? rawContent.slice(0, 4000) + '…' : rawContent;
101
+ }
102
+ try {
103
+ const parsed = JSON.parse(rawContent) as { text?: unknown };
104
+ if (typeof parsed.text === 'string') return parsed.text;
105
+ return rawContent;
106
+ } catch {
107
+ return rawContent;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Pure mapper: no DB access. Returns a `CreateMessageInput` skeleton (without
113
+ * parentId resolution — that happens in `resolveThreadParent`).
114
+ */
115
+ export function mapLarkMessageToCreateInput(input: MapLarkMessageInput): MapLarkMessageResult {
116
+ const envelope = input.envelope;
117
+ const msg = envelope.message;
118
+ const sender = envelope.sender;
119
+ if (!msg?.message_id) return { status: 'skipped', reason: 'missing_message_id' };
120
+ if (!msg.chat_id) return { status: 'skipped', reason: 'missing_chat_id' };
121
+ const senderOpenId = sender?.sender_id?.open_id;
122
+ if (!senderOpenId) return { status: 'skipped', reason: 'missing_sender' };
123
+
124
+ const text = parseLarkTextContent(msg.content, msg.message_type);
125
+ if (!text.trim()) return { status: 'skipped', reason: 'empty_text' };
126
+
127
+ const firstMention = msg.mentions?.find((m) => m.id?.open_id)?.id?.open_id;
128
+ const mappedMentions = normalizeMappedMentions(msg.mentions?.map((m) => m.id?.open_id ? input.recipientByMentionOpenId?.get(m.id.open_id) ?? null : null) ?? []);
129
+ const recipient = input.recipientOverride !== undefined
130
+ ? input.recipientOverride
131
+ : firstMention ? input.recipientByMentionOpenId?.get(firstMention) ?? firstMention : null;
132
+ const chatName = buildLockChatName(input.appId, msg.chat_id, msg.chat_type);
133
+ const rootMessageId = msg.root_id?.trim() || undefined;
134
+
135
+ const createInput: CreateMessageInput = {
136
+ chatName,
137
+ sender: senderOpenId,
138
+ recipient,
139
+ content: text,
140
+ type: 'message',
141
+ idempotencyKey: msg.message_id,
142
+ provider: 'lark',
143
+ mentions: mappedMentions,
144
+ };
145
+
146
+ return {
147
+ status: 'ok',
148
+ input: createInput,
149
+ rootMessageId,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Resolve a Lark `root_id` to a previously-stored lock message (by
155
+ * `idempotency_key == root_id`). Returns null if not found.
156
+ *
157
+ * We use `messages.idempotency_key` here because the Lark message_id of the
158
+ * root was stored there when the root was originally ingested. This is
159
+ * deterministic and avoids needing a side-table.
160
+ */
161
+ export function resolveLarkRootMessage(store: MessageStore, rootLarkMessageId: string): Message | null {
162
+ const row = store.db
163
+ .query(
164
+ `SELECT m.*, c.name AS chat_name
165
+ FROM messages m
166
+ JOIN chats c ON c.id = m.chat_id
167
+ WHERE m.idempotency_key = ? AND m.parent_id IS NULL
168
+ LIMIT 1`,
169
+ )
170
+ .get(rootLarkMessageId) as Record<string, unknown> | null;
171
+ if (!row) return null;
172
+ return rowToMessage(row);
173
+ }
174
+
175
+ // Local copy of rowToMessage shape to avoid importing db.ts internals. Keep
176
+ // in sync with src/db.ts:rowToMessage().
177
+ function rowToMessage(row: Record<string, unknown>): Message {
178
+ return {
179
+ id: Number(row.id),
180
+ chat_id: String(row.chat_id),
181
+ chat_name: String(row.chat_name),
182
+ parent_id: row.parent_id === null || row.parent_id === undefined ? null : Number(row.parent_id),
183
+ depth: Number(row.depth) as 0 | 1,
184
+ sender: String(row.sender),
185
+ recipient: row.recipient === null || row.recipient === undefined ? null : String(row.recipient),
186
+ content: String(row.content),
187
+ type: row.type === 'system' ? 'system' : 'message',
188
+ created_at: String(row.created_at),
189
+ idempotency_key: row.idempotency_key === null || row.idempotency_key === undefined ? null : String(row.idempotency_key),
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Full pipeline: map + thread resolution + insert. Returns the resulting
195
+ * Message (either freshly inserted or coalesced via idempotency).
196
+ *
197
+ * Returns null when the event was skipped (non-mappable; e.g. missing fields).
198
+ */
199
+ export interface IngestLarkMessageInput extends MapLarkMessageInput {
200
+ store: MessageStore;
201
+ }
202
+
203
+ export interface IngestLarkMessageResult {
204
+ status: 'ok' | 'skipped';
205
+ reason?: MapLarkMessageResult['reason'];
206
+ message?: Message;
207
+ /** True when the root_id was set but no parent message was found in the local DB. */
208
+ threadOrphan?: boolean;
209
+ /** True when createMessage coalesced via idempotency (i.e. this Lark message_id was already stored). */
210
+ deduped?: boolean;
211
+ }
212
+
213
+ export function ingestLarkMessage(input: IngestLarkMessageInput): IngestLarkMessageResult {
214
+ const account = input.store.getChannelAccountByAppId(input.appId);
215
+ const providerAccountId = account?.provider_account_id ?? null;
216
+ const senderOpenId = input.envelope.sender?.sender_id?.open_id?.trim() ?? '';
217
+ const senderKind = input.envelope.sender?.sender_type === 'app' ? 'bot' : 'user';
218
+ const senderDisplayName = input.envelope.sender?.sender_id?.union_id ?? input.envelope.sender?.sender_id?.user_id ?? null;
219
+ const senderHandle = providerAccountId && senderOpenId
220
+ ? input.store.getOrCreateProviderIdentity({
221
+ provider: 'lark',
222
+ providerAccountId,
223
+ externalType: senderKind,
224
+ externalId: senderOpenId,
225
+ displayName: senderDisplayName,
226
+ }).identity.stable_handle
227
+ : senderOpenId ? palIdentityHandle(senderKind, `lark:${input.appId}:${senderOpenId}`) : '';
228
+ const mentionHandles = new Map<string, string>();
229
+ for (const mention of input.envelope.message?.mentions ?? []) {
230
+ const mentionOpenId = mention.id?.open_id?.trim();
231
+ if (!mentionOpenId) continue;
232
+ if (input.recipientByMentionOpenId?.has(mentionOpenId)) continue;
233
+ const externalType = account?.bot_open_id === mentionOpenId ? 'bot' : 'user';
234
+ const handle = providerAccountId
235
+ ? input.store.getOrCreateProviderIdentity({
236
+ provider: 'lark',
237
+ providerAccountId,
238
+ externalType,
239
+ externalId: mentionOpenId,
240
+ displayName: mention.name,
241
+ }).identity.stable_handle
242
+ : palIdentityHandle(externalType, `lark:${input.appId}:${mentionOpenId}`);
243
+ mentionHandles.set(mentionOpenId, handle);
244
+ }
245
+ const recipientByMentionOpenId = new Map(input.recipientByMentionOpenId ?? []);
246
+ for (const [openId, handle] of mentionHandles) {
247
+ recipientByMentionOpenId.set(openId, handle);
248
+ }
249
+ const mapped = mapLarkMessageToCreateInput({
250
+ appId: input.appId,
251
+ envelope: input.envelope,
252
+ recipientByMentionOpenId,
253
+ recipientOverride: input.recipientOverride,
254
+ });
255
+ if (mapped.status !== 'ok' || !mapped.input) {
256
+ return { status: 'skipped', reason: mapped.reason };
257
+ }
258
+ let parentResolved: Message | null = null;
259
+ let threadOrphan = false;
260
+ if (mapped.rootMessageId) {
261
+ parentResolved = resolveLarkRootMessage(input.store, mapped.rootMessageId);
262
+ if (!parentResolved) {
263
+ threadOrphan = true;
264
+ }
265
+ }
266
+ const createInput: CreateMessageInput = parentResolved
267
+ ? {
268
+ parentId: parentResolved.id,
269
+ sender: senderHandle || mapped.input.sender,
270
+ recipient: mapped.input.recipient,
271
+ content: mapped.input.content,
272
+ type: mapped.input.type,
273
+ idempotencyKey: mapped.input.idempotencyKey,
274
+ }
275
+ : threadOrphan
276
+ ? {
277
+ chatName: mapped.input.chatName,
278
+ sender: senderHandle || mapped.input.sender,
279
+ recipient: `root:${mapped.rootMessageId}`,
280
+ content: mapped.input.content,
281
+ type: mapped.input.type,
282
+ idempotencyKey: mapped.input.idempotencyKey,
283
+ }
284
+ : { ...mapped.input, sender: senderHandle || mapped.input.sender };
285
+
286
+ let deduped = false;
287
+ if (createInput.idempotencyKey) {
288
+ const existing = input.store.db
289
+ .query(
290
+ `SELECT m.*, c.name AS chat_name
291
+ FROM messages m
292
+ JOIN chats c ON c.id = m.chat_id
293
+ WHERE m.idempotency_key = ?`,
294
+ )
295
+ .get(createInput.idempotencyKey) as Record<string, unknown> | null;
296
+ if (existing) {
297
+ deduped = true;
298
+ return {
299
+ status: 'ok',
300
+ message: rowToMessage(existing),
301
+ threadOrphan,
302
+ deduped,
303
+ };
304
+ }
305
+ }
306
+ const message = input.store.createMessage(createInput);
307
+ input.store.upsertRoomParticipant({
308
+ roomId: message.chat_id,
309
+ participantId: message.sender,
310
+ kind: senderKind,
311
+ displayName: senderDisplayName,
312
+ source: 'event',
313
+ });
314
+
315
+ for (const mention of input.envelope.message?.mentions ?? []) {
316
+ const mentionOpenId = mention.id?.open_id;
317
+ if (!mentionOpenId) continue;
318
+ const participantId = input.recipientByMentionOpenId?.get(mentionOpenId) ?? mentionHandles.get(mentionOpenId);
319
+ if (!participantId) continue;
320
+ input.store.upsertRoomParticipant({
321
+ roomId: message.chat_id,
322
+ participantId,
323
+ kind: account?.bot_open_id === mentionOpenId ? 'bot' : 'user',
324
+ displayName: mention.name,
325
+ source: account?.bot_open_id === mentionOpenId ? 'known_bot' : 'event',
326
+ });
327
+ }
328
+ return { status: 'ok', message, threadOrphan, deduped };
329
+ }
@@ -0,0 +1,131 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface InboundRawEvent {
4
+ id: string;
5
+ received_at: string;
6
+ app_id: string;
7
+ event_type: string;
8
+ event_id: string;
9
+ parse_ok: 0 | 1;
10
+ raw_body_bytes: Uint8Array;
11
+ }
12
+
13
+ export interface StoreInboundEventInput {
14
+ appId: string;
15
+ rawBody: string | Uint8Array;
16
+ }
17
+
18
+ export interface StoreInboundEventResult {
19
+ id: string;
20
+ event_id: string;
21
+ event_type: string;
22
+ parse_ok: 0 | 1;
23
+ inserted: boolean;
24
+ duplicate: boolean;
25
+ }
26
+
27
+ const ENCODER = new TextEncoder();
28
+
29
+ function toBytes(body: string | Uint8Array): Uint8Array {
30
+ return typeof body === 'string' ? ENCODER.encode(body) : body;
31
+ }
32
+
33
+ async function sha256Hex(bytes: Uint8Array): Promise<string> {
34
+ const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
35
+ const digest = await crypto.subtle.digest('SHA-256', buf);
36
+ const arr = Array.from(new Uint8Array(digest));
37
+ return arr.map((b) => b.toString(16).padStart(2, '0')).join('');
38
+ }
39
+
40
+ export interface ParsedEnvelope {
41
+ event_id: string;
42
+ event_type: string;
43
+ parse_ok: 0 | 1;
44
+ }
45
+
46
+ export async function parseEventEnvelope(rawBytes: Uint8Array): Promise<ParsedEnvelope> {
47
+ try {
48
+ const text = new TextDecoder('utf8', { fatal: false }).decode(rawBytes);
49
+ const json = JSON.parse(text) as Record<string, unknown>;
50
+ const header = (json.header && typeof json.header === 'object') ? json.header as Record<string, unknown> : null;
51
+ const eventId = (header?.event_id ?? json.uuid) as unknown;
52
+ const eventType = (header?.event_type ?? json.type) as unknown;
53
+ if (typeof eventId === 'string' && eventId.length > 0 && typeof eventType === 'string' && eventType.length > 0) {
54
+ return { event_id: eventId, event_type: eventType, parse_ok: 1 };
55
+ }
56
+ const fallbackId = `sha256:${await sha256Hex(rawBytes)}`;
57
+ return {
58
+ event_id: fallbackId,
59
+ event_type: typeof eventType === 'string' ? eventType : 'unknown',
60
+ parse_ok: 0,
61
+ };
62
+ } catch {
63
+ const fallbackId = `sha256:${await sha256Hex(rawBytes)}`;
64
+ return { event_id: fallbackId, event_type: 'unknown', parse_ok: 0 };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Insert a raw Lark event into channel_inbound_raw_events.
70
+ *
71
+ * Deduplication: the partial UNIQUE index on event_id (WHERE event_id IS NOT
72
+ * NULL) backs an ON CONFLICT DO NOTHING insert. A duplicate event_id returns
73
+ * { inserted: false, duplicate: true } so the caller can skip side effects.
74
+ */
75
+ export async function storeInboundEvent(db: Database, input: StoreInboundEventInput): Promise<StoreInboundEventResult> {
76
+ const rawBytes = toBytes(input.rawBody);
77
+ const envelope = await parseEventEnvelope(rawBytes);
78
+ const id = crypto.randomUUID();
79
+ const result = db
80
+ .query(
81
+ `INSERT INTO channel_inbound_raw_events (id, app_id, event_type, event_id, parse_ok, raw_body_bytes)
82
+ VALUES (?, ?, ?, ?, ?, ?)
83
+ ON CONFLICT(event_id) WHERE event_id IS NOT NULL DO NOTHING
84
+ RETURNING id`,
85
+ )
86
+ .get(id, input.appId, envelope.event_type, envelope.event_id, envelope.parse_ok, rawBytes) as { id: string } | null;
87
+
88
+ if (result) {
89
+ return {
90
+ id: result.id,
91
+ event_id: envelope.event_id,
92
+ event_type: envelope.event_type,
93
+ parse_ok: envelope.parse_ok,
94
+ inserted: true,
95
+ duplicate: false,
96
+ };
97
+ }
98
+ const existing = db
99
+ .query('SELECT id FROM channel_inbound_raw_events WHERE event_id = ? LIMIT 1')
100
+ .get(envelope.event_id) as { id: string } | null;
101
+ return {
102
+ id: existing?.id ?? id,
103
+ event_id: envelope.event_id,
104
+ event_type: envelope.event_type,
105
+ parse_ok: envelope.parse_ok,
106
+ inserted: false,
107
+ duplicate: true,
108
+ };
109
+ }
110
+
111
+ export function getInboundEvent(db: Database, id: string): InboundRawEvent | null {
112
+ const row = db
113
+ .query('SELECT id, received_at, app_id, event_type, event_id, parse_ok, raw_body_bytes FROM channel_inbound_raw_events WHERE id = ?')
114
+ .get(id) as InboundRawEvent | null;
115
+ return row;
116
+ }
117
+
118
+ export function listRecentInboundEvents(db: Database, limit = 20): InboundRawEvent[] {
119
+ return db
120
+ .query('SELECT id, received_at, app_id, event_type, event_id, parse_ok, raw_body_bytes FROM channel_inbound_raw_events ORDER BY received_at DESC, id DESC LIMIT ?')
121
+ .all(Math.max(1, Math.min(limit, 500))) as InboundRawEvent[];
122
+ }
123
+
124
+ export function countInboundEvents(db: Database, appId?: string): number {
125
+ if (appId) {
126
+ const row = db.query('SELECT COUNT(*) AS n FROM channel_inbound_raw_events WHERE app_id = ?').get(appId) as { n: number };
127
+ return row.n;
128
+ }
129
+ const row = db.query('SELECT COUNT(*) AS n FROM channel_inbound_raw_events').get() as { n: number };
130
+ return row.n;
131
+ }