opencode-gateway 0.2.3 → 0.2.5

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 (79) hide show
  1. package/dist/cli.js +0 -0
  2. package/dist/index.js +36910 -56
  3. package/dist/runtime/delay.d.ts +1 -0
  4. package/dist/store/database.d.ts +22 -0
  5. package/dist/store/migrations.d.ts +2 -2
  6. package/dist/store/sqlite.d.ts +2 -2
  7. package/package.json +9 -3
  8. package/dist/binding/execution.js +0 -1
  9. package/dist/binding/gateway.js +0 -1
  10. package/dist/binding/index.js +0 -4
  11. package/dist/binding/opencode.js +0 -1
  12. package/dist/cli/args.js +0 -53
  13. package/dist/cli/doctor.js +0 -49
  14. package/dist/cli/init.js +0 -40
  15. package/dist/cli/opencode-config-file.js +0 -18
  16. package/dist/cli/opencode-config.js +0 -194
  17. package/dist/cli/paths.js +0 -22
  18. package/dist/cli/templates.js +0 -41
  19. package/dist/config/cron.js +0 -52
  20. package/dist/config/gateway.js +0 -148
  21. package/dist/config/memory.js +0 -105
  22. package/dist/config/paths.js +0 -39
  23. package/dist/config/telegram.js +0 -91
  24. package/dist/cron/runtime.js +0 -402
  25. package/dist/delivery/telegram.js +0 -75
  26. package/dist/delivery/text.js +0 -175
  27. package/dist/gateway.js +0 -117
  28. package/dist/host/file-sender.js +0 -59
  29. package/dist/host/logger.js +0 -53
  30. package/dist/host/transport.js +0 -35
  31. package/dist/mailbox/router.js +0 -16
  32. package/dist/media/mime.js +0 -45
  33. package/dist/memory/prompt.js +0 -122
  34. package/dist/opencode/adapter.js +0 -340
  35. package/dist/opencode/driver-hub.js +0 -82
  36. package/dist/opencode/event-normalize.js +0 -48
  37. package/dist/opencode/event-stream.js +0 -65
  38. package/dist/opencode/events.js +0 -1
  39. package/dist/questions/client.js +0 -36
  40. package/dist/questions/format.js +0 -36
  41. package/dist/questions/normalize.js +0 -45
  42. package/dist/questions/parser.js +0 -96
  43. package/dist/questions/runtime.js +0 -195
  44. package/dist/questions/types.js +0 -1
  45. package/dist/runtime/attachments.js +0 -12
  46. package/dist/runtime/conversation-coordinator.js +0 -22
  47. package/dist/runtime/executor.js +0 -407
  48. package/dist/runtime/mailbox.js +0 -112
  49. package/dist/runtime/opencode-runner.js +0 -79
  50. package/dist/runtime/runtime-singleton.js +0 -28
  51. package/dist/session/context.js +0 -23
  52. package/dist/session/conversation-key.js +0 -3
  53. package/dist/session/switcher.js +0 -59
  54. package/dist/session/system-prompt.js +0 -52
  55. package/dist/store/migrations.js +0 -197
  56. package/dist/store/sqlite.js +0 -777
  57. package/dist/telegram/client.js +0 -180
  58. package/dist/telegram/media.js +0 -65
  59. package/dist/telegram/normalize.js +0 -119
  60. package/dist/telegram/poller.js +0 -166
  61. package/dist/telegram/runtime.js +0 -157
  62. package/dist/telegram/state.js +0 -149
  63. package/dist/telegram/types.js +0 -1
  64. package/dist/tools/channel-new-session.js +0 -27
  65. package/dist/tools/channel-send-file.js +0 -27
  66. package/dist/tools/channel-target.js +0 -34
  67. package/dist/tools/cron-run.js +0 -20
  68. package/dist/tools/cron-upsert.js +0 -51
  69. package/dist/tools/gateway-dispatch-cron.js +0 -33
  70. package/dist/tools/gateway-status.js +0 -25
  71. package/dist/tools/schedule-cancel.js +0 -12
  72. package/dist/tools/schedule-format.js +0 -48
  73. package/dist/tools/schedule-list.js +0 -17
  74. package/dist/tools/schedule-once.js +0 -43
  75. package/dist/tools/schedule-status.js +0 -23
  76. package/dist/tools/telegram-send-test.js +0 -26
  77. package/dist/tools/telegram-status.js +0 -49
  78. package/dist/tools/time.js +0 -25
  79. package/dist/utils/error.js +0 -57
@@ -1,777 +0,0 @@
1
- import { Database } from "bun:sqlite";
2
- import { mkdir } from "node:fs/promises";
3
- import { dirname } from "node:path";
4
- import { migrateGatewayDatabase } from "./migrations";
5
- export class SqliteStore {
6
- db;
7
- constructor(db) {
8
- this.db = db;
9
- }
10
- getSessionBinding(conversationKey) {
11
- const row = this.db
12
- .query("SELECT session_id FROM session_bindings WHERE conversation_key = ?1;")
13
- .get(conversationKey);
14
- return row?.session_id ?? null;
15
- }
16
- putSessionBinding(conversationKey, sessionId, recordedAtMs) {
17
- this.db
18
- .query(`
19
- INSERT INTO session_bindings (conversation_key, session_id, updated_at_ms)
20
- VALUES (?1, ?2, ?3)
21
- ON CONFLICT(conversation_key) DO UPDATE SET
22
- session_id = excluded.session_id,
23
- updated_at_ms = excluded.updated_at_ms;
24
- `)
25
- .run(conversationKey, sessionId, recordedAtMs);
26
- }
27
- putSessionBindingIfUnchanged(conversationKey, expectedSessionId, nextSessionId, recordedAtMs) {
28
- const result = expectedSessionId === null
29
- ? this.db
30
- .query(`
31
- INSERT INTO session_bindings (conversation_key, session_id, updated_at_ms)
32
- VALUES (?1, ?2, ?3)
33
- ON CONFLICT(conversation_key) DO NOTHING;
34
- `)
35
- .run(conversationKey, nextSessionId, recordedAtMs)
36
- : this.db
37
- .query(`
38
- UPDATE session_bindings
39
- SET session_id = ?2,
40
- updated_at_ms = ?3
41
- WHERE conversation_key = ?1
42
- AND session_id = ?4;
43
- `)
44
- .run(conversationKey, nextSessionId, recordedAtMs, expectedSessionId);
45
- return result.changes > 0;
46
- }
47
- deleteSessionBinding(conversationKey) {
48
- this.db.query("DELETE FROM session_bindings WHERE conversation_key = ?1;").run(conversationKey);
49
- }
50
- clearSessionReplyTargets(sessionId) {
51
- this.db.query("DELETE FROM session_reply_targets WHERE session_id = ?1;").run(sessionId);
52
- }
53
- replaceSessionReplyTargets(input) {
54
- assertSafeInteger(input.recordedAtMs, "session reply-target recordedAtMs");
55
- const deleteTargets = this.db.query("DELETE FROM session_reply_targets WHERE session_id = ?1;");
56
- const insertTarget = this.db.query(`
57
- INSERT INTO session_reply_targets (
58
- session_id,
59
- ordinal,
60
- conversation_key,
61
- delivery_channel,
62
- delivery_target,
63
- delivery_topic,
64
- updated_at_ms
65
- )
66
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);
67
- `);
68
- this.db.transaction((payload) => {
69
- deleteTargets.run(payload.sessionId);
70
- for (const [ordinal, target] of payload.targets.entries()) {
71
- insertTarget.run(payload.sessionId, ordinal, payload.conversationKey, target.channel, target.target, normalizeKeyField(target.topic), payload.recordedAtMs);
72
- }
73
- })(input);
74
- }
75
- listSessionReplyTargets(sessionId) {
76
- const rows = this.db
77
- .query(`
78
- SELECT
79
- session_id,
80
- ordinal,
81
- conversation_key,
82
- delivery_channel,
83
- delivery_target,
84
- delivery_topic,
85
- updated_at_ms
86
- FROM session_reply_targets
87
- WHERE session_id = ?1
88
- ORDER BY ordinal ASC;
89
- `)
90
- .all(sessionId);
91
- return rows.map(mapSessionReplyTargetRow);
92
- }
93
- getDefaultSessionReplyTarget(sessionId) {
94
- const row = this.db
95
- .query(`
96
- SELECT
97
- session_id,
98
- ordinal,
99
- conversation_key,
100
- delivery_channel,
101
- delivery_target,
102
- delivery_topic,
103
- updated_at_ms
104
- FROM session_reply_targets
105
- WHERE session_id = ?1
106
- ORDER BY ordinal ASC
107
- LIMIT 1;
108
- `)
109
- .get(sessionId);
110
- return row ? mapSessionReplyTargetRow(row) : null;
111
- }
112
- hasGatewaySession(sessionId) {
113
- const binding = this.db
114
- .query(`
115
- SELECT 1 AS present
116
- FROM session_bindings
117
- WHERE session_id = ?1
118
- LIMIT 1;
119
- `)
120
- .get(sessionId);
121
- if (binding?.present === 1) {
122
- return true;
123
- }
124
- const replyTarget = this.db
125
- .query(`
126
- SELECT 1 AS present
127
- FROM session_reply_targets
128
- WHERE session_id = ?1
129
- LIMIT 1;
130
- `)
131
- .get(sessionId);
132
- return replyTarget?.present === 1;
133
- }
134
- appendJournal(entry) {
135
- this.db
136
- .query(`
137
- INSERT INTO runtime_journal (kind, recorded_at_ms, conversation_key, payload_json)
138
- VALUES (?1, ?2, ?3, ?4);
139
- `)
140
- .run(entry.kind, entry.recordedAtMs, entry.conversationKey, JSON.stringify(entry.payload));
141
- }
142
- replacePendingQuestion(input) {
143
- assertSafeInteger(input.recordedAtMs, "pending question recordedAtMs");
144
- const deleteQuestion = this.db.query("DELETE FROM pending_questions WHERE request_id = ?1;");
145
- const insertQuestion = this.db.query(`
146
- INSERT INTO pending_questions (
147
- request_id,
148
- session_id,
149
- delivery_channel,
150
- delivery_target,
151
- delivery_topic,
152
- question_json,
153
- telegram_message_id,
154
- created_at_ms
155
- )
156
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);
157
- `);
158
- this.db.transaction((payload) => {
159
- deleteQuestion.run(payload.requestId);
160
- for (const target of payload.targets) {
161
- insertQuestion.run(payload.requestId, payload.sessionId, target.deliveryTarget.channel, target.deliveryTarget.target, normalizeKeyField(target.deliveryTarget.topic), JSON.stringify(payload.questions), target.telegramMessageId, payload.recordedAtMs);
162
- }
163
- })(input);
164
- }
165
- deletePendingQuestion(requestId) {
166
- this.db.query("DELETE FROM pending_questions WHERE request_id = ?1;").run(requestId);
167
- }
168
- deletePendingQuestionsForSession(sessionId) {
169
- this.db.query("DELETE FROM pending_questions WHERE session_id = ?1;").run(sessionId);
170
- }
171
- getPendingQuestionForTarget(target) {
172
- const row = this.db
173
- .query(`
174
- SELECT
175
- id,
176
- request_id,
177
- session_id,
178
- delivery_channel,
179
- delivery_target,
180
- delivery_topic,
181
- question_json,
182
- telegram_message_id,
183
- created_at_ms
184
- FROM pending_questions
185
- WHERE delivery_channel = ?1
186
- AND delivery_target = ?2
187
- AND delivery_topic = ?3
188
- ORDER BY created_at_ms ASC, id ASC
189
- LIMIT 1;
190
- `)
191
- .get(target.channel, target.target, normalizeKeyField(target.topic));
192
- return row ? mapPendingQuestionRow(row) : null;
193
- }
194
- getPendingQuestionForTelegramMessage(target, telegramMessageId) {
195
- assertSafeInteger(telegramMessageId, "pending question telegramMessageId");
196
- const row = this.db
197
- .query(`
198
- SELECT
199
- id,
200
- request_id,
201
- session_id,
202
- delivery_channel,
203
- delivery_target,
204
- delivery_topic,
205
- question_json,
206
- telegram_message_id,
207
- created_at_ms
208
- FROM pending_questions
209
- WHERE delivery_channel = ?1
210
- AND delivery_target = ?2
211
- AND delivery_topic = ?3
212
- AND telegram_message_id = ?4
213
- ORDER BY created_at_ms ASC, id ASC
214
- LIMIT 1;
215
- `)
216
- .get(target.channel, target.target, normalizeKeyField(target.topic), telegramMessageId);
217
- return row ? mapPendingQuestionRow(row) : null;
218
- }
219
- hasMailboxEntry(sourceKind, externalId) {
220
- const row = this.db
221
- .query(`
222
- SELECT 1 AS present
223
- FROM mailbox_entries
224
- WHERE source_kind = ?1 AND external_id = ?2
225
- LIMIT 1;
226
- `)
227
- .get(sourceKind, externalId);
228
- return row?.present === 1;
229
- }
230
- enqueueMailboxEntry(input) {
231
- assertSafeInteger(input.recordedAtMs, "mailbox recordedAtMs");
232
- const insertEntry = this.db.query(`
233
- INSERT INTO mailbox_entries (
234
- mailbox_key,
235
- source_kind,
236
- external_id,
237
- sender,
238
- body,
239
- reply_channel,
240
- reply_target,
241
- reply_topic,
242
- created_at_ms
243
- )
244
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
245
- ON CONFLICT(source_kind, external_id) DO NOTHING;
246
- `);
247
- const insertAttachment = this.db.query(`
248
- INSERT INTO mailbox_entry_attachments (
249
- mailbox_entry_id,
250
- ordinal,
251
- kind,
252
- mime_type,
253
- file_name,
254
- local_path
255
- )
256
- VALUES (?1, ?2, ?3, ?4, ?5, ?6);
257
- `);
258
- this.db.transaction((payload) => {
259
- const result = insertEntry.run(payload.mailboxKey, payload.sourceKind, payload.externalId, payload.sender, payload.text ?? "", payload.replyChannel, payload.replyTarget, payload.replyTopic, payload.recordedAtMs);
260
- if (result.changes === 0) {
261
- return;
262
- }
263
- const entryId = Number(result.lastInsertRowid);
264
- for (const [index, attachment] of payload.attachments.entries()) {
265
- insertAttachment.run(entryId, index, attachment.kind, attachment.mimeType, attachment.fileName, attachment.localPath);
266
- }
267
- })(input);
268
- }
269
- listPendingMailboxKeys() {
270
- const rows = this.db
271
- .query(`
272
- SELECT DISTINCT mailbox_key
273
- FROM mailbox_entries
274
- ORDER BY mailbox_key ASC;
275
- `)
276
- .all();
277
- return rows.map((row) => row.mailbox_key);
278
- }
279
- listMailboxEntries(mailboxKey) {
280
- const rows = this.db
281
- .query(`
282
- SELECT
283
- id,
284
- mailbox_key,
285
- source_kind,
286
- external_id,
287
- sender,
288
- body,
289
- reply_channel,
290
- reply_target,
291
- reply_topic,
292
- created_at_ms
293
- FROM mailbox_entries
294
- WHERE mailbox_key = ?1
295
- ORDER BY id ASC;
296
- `)
297
- .all(mailboxKey);
298
- const attachments = listMailboxAttachments(this.db, rows.map((row) => row.id));
299
- return rows.map((row) => mapMailboxEntryRow(row, attachments.get(row.id) ?? []));
300
- }
301
- deleteMailboxEntries(ids) {
302
- if (ids.length === 0) {
303
- return;
304
- }
305
- for (const id of ids) {
306
- assertSafeInteger(id, "mailbox entry id");
307
- }
308
- const placeholders = ids.map((_, index) => `?${index + 1}`).join(", ");
309
- this.db.query(`DELETE FROM mailbox_entries WHERE id IN (${placeholders});`).run(...ids);
310
- }
311
- getTelegramUpdateOffset() {
312
- const value = this.getStateValue("telegram.update_offset");
313
- if (value === null) {
314
- return null;
315
- }
316
- return parseStoredInteger(value, "stored telegram.update_offset");
317
- }
318
- putTelegramUpdateOffset(offset, recordedAtMs) {
319
- assertSafeInteger(offset, "telegram update offset");
320
- this.putStateValue("telegram.update_offset", String(offset), recordedAtMs);
321
- }
322
- getStateValue(key) {
323
- const row = this.db.query("SELECT value FROM kv_state WHERE key = ?1;").get(key);
324
- return row?.value ?? null;
325
- }
326
- putStateValue(key, value, recordedAtMs) {
327
- this.db
328
- .query(`
329
- INSERT INTO kv_state (key, value, updated_at_ms)
330
- VALUES (?1, ?2, ?3)
331
- ON CONFLICT(key) DO UPDATE SET
332
- value = excluded.value,
333
- updated_at_ms = excluded.updated_at_ms;
334
- `)
335
- .run(key, value, recordedAtMs);
336
- }
337
- upsertCronJob(input) {
338
- assertSafeInteger(input.nextRunAtMs, "cron next_run_at_ms");
339
- assertSafeInteger(input.recordedAtMs, "cron recordedAtMs");
340
- if (input.runAtMs !== null) {
341
- assertSafeInteger(input.runAtMs, "cron run_at_ms");
342
- }
343
- this.db
344
- .query(`
345
- INSERT INTO cron_jobs (
346
- id,
347
- kind,
348
- schedule,
349
- run_at_ms,
350
- prompt,
351
- delivery_channel,
352
- delivery_target,
353
- delivery_topic,
354
- enabled,
355
- next_run_at_ms,
356
- created_at_ms,
357
- updated_at_ms
358
- )
359
- VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?11)
360
- ON CONFLICT(id) DO UPDATE SET
361
- kind = excluded.kind,
362
- schedule = excluded.schedule,
363
- run_at_ms = excluded.run_at_ms,
364
- prompt = excluded.prompt,
365
- delivery_channel = excluded.delivery_channel,
366
- delivery_target = excluded.delivery_target,
367
- delivery_topic = excluded.delivery_topic,
368
- enabled = excluded.enabled,
369
- next_run_at_ms = excluded.next_run_at_ms,
370
- updated_at_ms = excluded.updated_at_ms;
371
- `)
372
- .run(input.id, input.kind, encodeStoredSchedule(input.kind, input.schedule), input.runAtMs, input.prompt, input.deliveryChannel, input.deliveryTarget, input.deliveryTopic, input.enabled ? 1 : 0, input.nextRunAtMs, input.recordedAtMs);
373
- }
374
- getCronJob(id) {
375
- const row = this.db
376
- .query(`
377
- SELECT
378
- id,
379
- kind,
380
- schedule,
381
- run_at_ms,
382
- prompt,
383
- delivery_channel,
384
- delivery_target,
385
- delivery_topic,
386
- enabled,
387
- next_run_at_ms,
388
- created_at_ms,
389
- updated_at_ms
390
- FROM cron_jobs
391
- WHERE id = ?1;
392
- `)
393
- .get(id);
394
- return row ? mapCronJobRow(row) : null;
395
- }
396
- listCronJobs() {
397
- const rows = this.db
398
- .query(`
399
- SELECT
400
- id,
401
- kind,
402
- schedule,
403
- run_at_ms,
404
- prompt,
405
- delivery_channel,
406
- delivery_target,
407
- delivery_topic,
408
- enabled,
409
- next_run_at_ms,
410
- created_at_ms,
411
- updated_at_ms
412
- FROM cron_jobs
413
- ORDER BY id ASC;
414
- `)
415
- .all();
416
- return rows.map(mapCronJobRow);
417
- }
418
- listOverdueCronJobs(nowMs) {
419
- assertSafeInteger(nowMs, "cron nowMs");
420
- const rows = this.db
421
- .query(`
422
- SELECT
423
- id,
424
- kind,
425
- schedule,
426
- run_at_ms,
427
- prompt,
428
- delivery_channel,
429
- delivery_target,
430
- delivery_topic,
431
- enabled,
432
- next_run_at_ms,
433
- created_at_ms,
434
- updated_at_ms
435
- FROM cron_jobs
436
- WHERE kind = 'cron' AND enabled = 1 AND next_run_at_ms <= ?1
437
- ORDER BY next_run_at_ms ASC, id ASC;
438
- `)
439
- .all(nowMs);
440
- return rows.map(mapCronJobRow);
441
- }
442
- listDueCronJobs(nowMs, limit) {
443
- assertSafeInteger(nowMs, "cron nowMs");
444
- assertSafeInteger(limit, "cron due-job limit");
445
- const rows = this.db
446
- .query(`
447
- SELECT
448
- id,
449
- kind,
450
- schedule,
451
- run_at_ms,
452
- prompt,
453
- delivery_channel,
454
- delivery_target,
455
- delivery_topic,
456
- enabled,
457
- next_run_at_ms,
458
- created_at_ms,
459
- updated_at_ms
460
- FROM cron_jobs
461
- WHERE enabled = 1 AND next_run_at_ms <= ?1
462
- ORDER BY next_run_at_ms ASC, id ASC
463
- LIMIT ?2;
464
- `)
465
- .all(nowMs, limit);
466
- return rows.map(mapCronJobRow);
467
- }
468
- removeCronJob(id) {
469
- const result = this.db.query("DELETE FROM cron_jobs WHERE id = ?1;").run(id);
470
- return result.changes > 0;
471
- }
472
- updateCronJobNextRun(id, nextRunAtMs, recordedAtMs) {
473
- assertSafeInteger(nextRunAtMs, "cron next_run_at_ms");
474
- assertSafeInteger(recordedAtMs, "cron recordedAtMs");
475
- this.db
476
- .query(`
477
- UPDATE cron_jobs
478
- SET next_run_at_ms = ?2, updated_at_ms = ?3
479
- WHERE id = ?1;
480
- `)
481
- .run(id, nextRunAtMs, recordedAtMs);
482
- }
483
- setCronJobEnabled(id, enabled, recordedAtMs) {
484
- assertSafeInteger(recordedAtMs, "cron recordedAtMs");
485
- this.db
486
- .query(`
487
- UPDATE cron_jobs
488
- SET enabled = ?2,
489
- updated_at_ms = ?3
490
- WHERE id = ?1;
491
- `)
492
- .run(id, enabled ? 1 : 0, recordedAtMs);
493
- }
494
- listCronRuns(jobId, limit) {
495
- assertSafeInteger(limit, "cron run limit");
496
- const rows = this.db
497
- .query(`
498
- SELECT
499
- id,
500
- job_id,
501
- scheduled_for_ms,
502
- started_at_ms,
503
- finished_at_ms,
504
- status,
505
- response_text,
506
- error_message
507
- FROM cron_runs
508
- WHERE job_id = ?1
509
- ORDER BY started_at_ms DESC, id DESC
510
- LIMIT ?2;
511
- `)
512
- .all(jobId, limit);
513
- return rows.map((row) => ({
514
- id: row.id,
515
- jobId: row.job_id,
516
- scheduledForMs: row.scheduled_for_ms,
517
- startedAtMs: row.started_at_ms,
518
- finishedAtMs: row.finished_at_ms,
519
- status: row.status,
520
- responseText: row.response_text,
521
- errorMessage: row.error_message,
522
- }));
523
- }
524
- insertCronRun(jobId, scheduledForMs, startedAtMs) {
525
- assertSafeInteger(scheduledForMs, "cron scheduled_for_ms");
526
- assertSafeInteger(startedAtMs, "cron started_at_ms");
527
- const result = this.db
528
- .query(`
529
- INSERT INTO cron_runs (
530
- job_id,
531
- scheduled_for_ms,
532
- started_at_ms,
533
- finished_at_ms,
534
- status,
535
- response_text,
536
- error_message
537
- )
538
- VALUES (?1, ?2, ?3, NULL, 'running', NULL, NULL);
539
- `)
540
- .run(jobId, scheduledForMs, startedAtMs);
541
- return Number(result.lastInsertRowid);
542
- }
543
- finishCronRun(runId, status, finishedAtMs, responseText, errorMessage) {
544
- assertSafeInteger(runId, "cron run id");
545
- assertSafeInteger(finishedAtMs, "cron finished_at_ms");
546
- this.db
547
- .query(`
548
- UPDATE cron_runs
549
- SET
550
- finished_at_ms = ?2,
551
- status = ?3,
552
- response_text = ?4,
553
- error_message = ?5
554
- WHERE id = ?1;
555
- `)
556
- .run(runId, finishedAtMs, status, responseText, errorMessage);
557
- }
558
- abandonRunningCronRuns(finishedAtMs) {
559
- assertSafeInteger(finishedAtMs, "cron abandoned finished_at_ms");
560
- const result = this.db
561
- .query(`
562
- UPDATE cron_runs
563
- SET
564
- finished_at_ms = ?1,
565
- status = 'abandoned',
566
- error_message = 'gateway process stopped before this run completed'
567
- WHERE status = 'running';
568
- `)
569
- .run(finishedAtMs);
570
- return result.changes;
571
- }
572
- close() {
573
- this.db.close();
574
- }
575
- }
576
- function mapCronJobRow(row) {
577
- const kind = parseScheduleJobKind(row.kind);
578
- return {
579
- id: row.id,
580
- kind,
581
- schedule: kind === "cron" ? row.schedule : null,
582
- runAtMs: row.run_at_ms,
583
- prompt: row.prompt,
584
- deliveryChannel: row.delivery_channel,
585
- deliveryTarget: row.delivery_target,
586
- deliveryTopic: row.delivery_topic,
587
- enabled: row.enabled === 1,
588
- nextRunAtMs: row.next_run_at_ms,
589
- createdAtMs: row.created_at_ms,
590
- updatedAtMs: row.updated_at_ms,
591
- };
592
- }
593
- function parseScheduleJobKind(value) {
594
- switch (value) {
595
- case "cron":
596
- case "once":
597
- return value;
598
- default:
599
- throw new Error(`stored schedule job kind is invalid: ${value}`);
600
- }
601
- }
602
- function encodeStoredSchedule(kind, schedule) {
603
- if (kind === "once") {
604
- return "@once";
605
- }
606
- if (schedule === null) {
607
- throw new Error("cron schedule must not be null");
608
- }
609
- return schedule;
610
- }
611
- function mapMailboxEntryRow(row, attachments) {
612
- return {
613
- id: row.id,
614
- mailboxKey: row.mailbox_key,
615
- sourceKind: row.source_kind,
616
- externalId: row.external_id,
617
- sender: row.sender,
618
- text: normalizeStoredMailboxText(row.body),
619
- attachments,
620
- replyChannel: row.reply_channel,
621
- replyTarget: row.reply_target,
622
- replyTopic: row.reply_topic,
623
- createdAtMs: row.created_at_ms,
624
- };
625
- }
626
- function listMailboxAttachments(db, entryIds) {
627
- if (entryIds.length === 0) {
628
- return new Map();
629
- }
630
- for (const entryId of entryIds) {
631
- assertSafeInteger(entryId, "mailbox entry id");
632
- }
633
- const placeholders = entryIds.map((_, index) => `?${index + 1}`).join(", ");
634
- const rows = db
635
- .query(`
636
- SELECT
637
- mailbox_entry_id,
638
- ordinal,
639
- kind,
640
- mime_type,
641
- file_name,
642
- local_path
643
- FROM mailbox_entry_attachments
644
- WHERE mailbox_entry_id IN (${placeholders})
645
- ORDER BY mailbox_entry_id ASC, ordinal ASC;
646
- `)
647
- .all(...entryIds);
648
- const attachments = new Map();
649
- for (const row of rows) {
650
- const records = attachments.get(row.mailbox_entry_id) ?? [];
651
- records.push(mapMailboxEntryAttachmentRow(row));
652
- attachments.set(row.mailbox_entry_id, records);
653
- }
654
- return attachments;
655
- }
656
- function mapMailboxEntryAttachmentRow(row) {
657
- switch (row.kind) {
658
- case "image":
659
- return {
660
- kind: "image",
661
- ordinal: row.ordinal,
662
- mimeType: row.mime_type,
663
- fileName: row.file_name,
664
- localPath: row.local_path,
665
- };
666
- default:
667
- throw new Error(`unsupported mailbox attachment kind: ${row.kind}`);
668
- }
669
- }
670
- function mapSessionReplyTargetRow(row) {
671
- return {
672
- channel: row.delivery_channel,
673
- target: row.delivery_target,
674
- topic: normalizeStoredKeyField(row.delivery_topic),
675
- };
676
- }
677
- function mapPendingQuestionRow(row) {
678
- return {
679
- requestId: row.request_id,
680
- sessionId: row.session_id,
681
- questions: parsePendingQuestions(row.question_json),
682
- deliveryTarget: {
683
- channel: row.delivery_channel,
684
- target: row.delivery_target,
685
- topic: normalizeStoredKeyField(row.delivery_topic),
686
- },
687
- telegramMessageId: row.telegram_message_id,
688
- createdAtMs: row.created_at_ms,
689
- };
690
- }
691
- function parsePendingQuestions(value) {
692
- const parsed = JSON.parse(value);
693
- if (!Array.isArray(parsed)) {
694
- throw new Error("stored pending question payload is invalid");
695
- }
696
- return parsed.map((question, index) => {
697
- if (typeof question !== "object" || question === null) {
698
- throw new Error(`stored pending question ${index} is invalid`);
699
- }
700
- const header = readRequiredStringField(question, "header");
701
- const prompt = readRequiredStringField(question, "question");
702
- const rawOptions = readArrayField(question, "options");
703
- const options = rawOptions.map((option, optionIndex) => {
704
- if (typeof option !== "object" || option === null) {
705
- throw new Error(`stored pending question option ${index}:${optionIndex} is invalid`);
706
- }
707
- return {
708
- label: readRequiredStringField(option, "label"),
709
- description: readRequiredStringField(option, "description"),
710
- };
711
- });
712
- return {
713
- header,
714
- question: prompt,
715
- options,
716
- multiple: readBooleanField(question, "multiple", false),
717
- custom: readBooleanField(question, "custom", true),
718
- };
719
- });
720
- }
721
- function assertSafeInteger(value, field) {
722
- if (!Number.isSafeInteger(value) || value < 0) {
723
- throw new Error(`${field} is out of range: ${value}`);
724
- }
725
- }
726
- function parseStoredInteger(value, field) {
727
- const parsed = Number.parseInt(value, 10);
728
- if (!Number.isSafeInteger(parsed) || parsed < 0) {
729
- throw new Error(`${field} is invalid: ${value}`);
730
- }
731
- return parsed;
732
- }
733
- function normalizeStoredMailboxText(value) {
734
- const trimmed = value.trim();
735
- return trimmed.length === 0 ? null : trimmed;
736
- }
737
- function normalizeKeyField(value) {
738
- if (value === null) {
739
- return "";
740
- }
741
- const trimmed = value.trim();
742
- return trimmed.length === 0 ? "" : trimmed;
743
- }
744
- function normalizeStoredKeyField(value) {
745
- const trimmed = value.trim();
746
- return trimmed.length === 0 ? null : trimmed;
747
- }
748
- function readRequiredStringField(value, field) {
749
- const raw = value[field];
750
- if (typeof raw !== "string" || raw.trim().length === 0) {
751
- throw new Error(`stored field ${field} is invalid`);
752
- }
753
- return raw;
754
- }
755
- function readArrayField(value, field) {
756
- const raw = value[field];
757
- if (!Array.isArray(raw)) {
758
- throw new Error(`stored field ${field} is invalid`);
759
- }
760
- return raw;
761
- }
762
- function readBooleanField(value, field, fallback) {
763
- const raw = value[field];
764
- if (raw === undefined) {
765
- return fallback;
766
- }
767
- if (typeof raw !== "boolean") {
768
- throw new Error(`stored field ${field} is invalid`);
769
- }
770
- return raw;
771
- }
772
- export async function openSqliteStore(path) {
773
- await mkdir(dirname(path), { recursive: true });
774
- const db = new Database(path);
775
- migrateGatewayDatabase(db);
776
- return new SqliteStore(db);
777
- }