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,402 +0,0 @@
1
- import { formatError } from "../utils/error";
2
- const CRON_EFFECTIVE_TIME_ZONE_KEY = "cron.effective_timezone";
3
- const LEGACY_CRON_TIME_ZONE = "UTC";
4
- const MAX_STATUS_RUNS = 20;
5
- export class GatewayCronRuntime {
6
- executor;
7
- contract;
8
- store;
9
- logger;
10
- config;
11
- effectiveTimeZone;
12
- resolveConversationKeyForTarget;
13
- runningJobIds = new Set();
14
- running = false;
15
- constructor(executor, contract, store, logger, config, effectiveTimeZone, resolveConversationKeyForTarget) {
16
- this.executor = executor;
17
- this.contract = contract;
18
- this.store = store;
19
- this.logger = logger;
20
- this.config = config;
21
- this.effectiveTimeZone = effectiveTimeZone;
22
- this.resolveConversationKeyForTarget = resolveConversationKeyForTarget;
23
- }
24
- isEnabled() {
25
- return this.config.enabled;
26
- }
27
- isRunning() {
28
- return this.running;
29
- }
30
- runningJobs() {
31
- return this.runningJobIds.size;
32
- }
33
- timeZone() {
34
- return this.effectiveTimeZone;
35
- }
36
- start() {
37
- if (!this.config.enabled || this.running) {
38
- return;
39
- }
40
- this.running = true;
41
- void this.runLoop().finally(() => {
42
- this.running = false;
43
- });
44
- }
45
- listJobs(includeTerminal = false) {
46
- const jobs = this.store.listCronJobs();
47
- if (includeTerminal) {
48
- return jobs;
49
- }
50
- return jobs.filter((job) => !isTerminalJob(job));
51
- }
52
- getJobStatus(id, limit = 5) {
53
- const job = this.requireJob(normalizeId(id));
54
- const runs = this.store.listCronRuns(job.id, clampStatusLimit(limit));
55
- return {
56
- job,
57
- state: deriveJobState(job, runs[0] ?? null),
58
- runs,
59
- };
60
- }
61
- upsertJob(input) {
62
- const normalized = normalizeUpsertInput(input);
63
- const recordedAtMs = Date.now();
64
- const nextRunAtMs = computeNextRunAt(this.contract, normalized, recordedAtMs, this.effectiveTimeZone);
65
- this.store.upsertCronJob({
66
- ...normalized,
67
- nextRunAtMs,
68
- recordedAtMs,
69
- });
70
- return this.requireJob(normalized.id);
71
- }
72
- scheduleOnce(input) {
73
- const normalized = normalizeOnceInput(input);
74
- const recordedAtMs = Date.now();
75
- this.store.upsertCronJob({
76
- ...normalized,
77
- nextRunAtMs: normalized.runAtMs ?? recordedAtMs,
78
- recordedAtMs,
79
- });
80
- return this.requireJob(normalized.id);
81
- }
82
- cancelJob(id) {
83
- const job = this.store.getCronJob(normalizeId(id));
84
- if (job === null || !job.enabled) {
85
- return false;
86
- }
87
- this.store.setCronJobEnabled(job.id, false, Date.now());
88
- return true;
89
- }
90
- async runNow(id) {
91
- const job = this.requireJob(normalizeId(id));
92
- if (!job.enabled) {
93
- throw new Error(`schedule job is not active: ${job.id}`);
94
- }
95
- if (this.runningJobIds.has(job.id)) {
96
- throw new Error(`schedule job is already running: ${job.id}`);
97
- }
98
- this.runningJobIds.add(job.id);
99
- try {
100
- return await this.executeJob(job, Date.now(), null);
101
- }
102
- finally {
103
- this.runningJobIds.delete(job.id);
104
- }
105
- }
106
- async runLoop() {
107
- await this.reconcileOnce();
108
- for (;;) {
109
- await this.tickOnce();
110
- await sleep(this.config.tickSeconds * 1_000);
111
- }
112
- }
113
- async reconcileOnce(nowMs = Date.now()) {
114
- const abandoned = this.store.abandonRunningCronRuns(nowMs);
115
- if (abandoned > 0) {
116
- this.logger.log("warn", `abandoned ${abandoned} stale cron runs on startup`);
117
- }
118
- const storedTimeZone = this.readStoredEffectiveTimeZone();
119
- const previousTimeZone = storedTimeZone ?? LEGACY_CRON_TIME_ZONE;
120
- if (previousTimeZone !== this.effectiveTimeZone) {
121
- const message = storedTimeZone === null
122
- ? `rebasing enabled cron jobs from legacy ${LEGACY_CRON_TIME_ZONE} semantics to ${this.effectiveTimeZone}`
123
- : `cron time zone changed from ${previousTimeZone} to ${this.effectiveTimeZone}; rebasing enabled jobs`;
124
- this.logger.log("warn", message);
125
- this.rebaseJobs(this.store.listCronJobs().filter((job) => job.enabled && job.kind === "cron"), nowMs);
126
- }
127
- else {
128
- this.rebaseJobs(this.store.listOverdueCronJobs(nowMs), nowMs);
129
- }
130
- this.store.putStateValue(CRON_EFFECTIVE_TIME_ZONE_KEY, this.effectiveTimeZone, nowMs);
131
- }
132
- async tickOnce(nowMs = Date.now()) {
133
- const capacity = this.config.maxConcurrentRuns - this.runningJobIds.size;
134
- if (capacity <= 0) {
135
- return;
136
- }
137
- const dueJobs = this.store.listDueCronJobs(nowMs, capacity);
138
- for (const job of dueJobs) {
139
- if (this.runningJobIds.has(job.id)) {
140
- continue;
141
- }
142
- this.runningJobIds.add(job.id);
143
- void this.executeJob(job, job.nextRunAtMs, nowMs)
144
- .catch((error) => {
145
- this.logger.log("error", `schedule job ${job.id} failed: ${formatError(error)}`);
146
- })
147
- .finally(() => {
148
- this.runningJobIds.delete(job.id);
149
- });
150
- if (this.runningJobIds.size >= this.config.maxConcurrentRuns) {
151
- return;
152
- }
153
- }
154
- }
155
- async executeJob(job, scheduledForMs, nextRunBaseMs) {
156
- const startedAtMs = Date.now();
157
- if (job.kind === "cron" && nextRunBaseMs !== null) {
158
- const nextRunAtMs = computeNextRunAt(this.contract, job, Math.max(nextRunBaseMs, scheduledForMs), this.effectiveTimeZone);
159
- this.store.updateCronJobNextRun(job.id, nextRunAtMs, startedAtMs);
160
- }
161
- else if (job.kind === "once") {
162
- this.store.setCronJobEnabled(job.id, false, startedAtMs);
163
- }
164
- const runId = this.store.insertCronRun(job.id, scheduledForMs, startedAtMs);
165
- try {
166
- const report = await this.executor.dispatchScheduledJob({
167
- jobId: job.id,
168
- jobKind: job.kind,
169
- conversationKey: conversationKeyForJob(job),
170
- prompt: job.prompt,
171
- replyTarget: toReplyTarget(job),
172
- });
173
- this.store.finishCronRun(runId, "succeeded", Date.now(), report.responseText, null);
174
- await this.appendScheduleResultToTarget(job, scheduledForMs, {
175
- kind: "success",
176
- responseText: report.responseText,
177
- });
178
- return report;
179
- }
180
- catch (error) {
181
- const message = formatError(error);
182
- this.store.finishCronRun(runId, "failed", Date.now(), null, message);
183
- await this.appendScheduleResultToTarget(job, scheduledForMs, {
184
- kind: "failure",
185
- errorMessage: message,
186
- });
187
- throw error;
188
- }
189
- }
190
- async appendScheduleResultToTarget(job, scheduledForMs, outcome) {
191
- const replyTarget = toReplyTarget(job);
192
- if (replyTarget === null) {
193
- return;
194
- }
195
- try {
196
- await this.executor.appendContextToConversation({
197
- conversationKey: this.resolveConversationKeyForTarget(replyTarget),
198
- replyTarget,
199
- body: formatScheduleContextNote(job, scheduledForMs, outcome),
200
- recordedAtMs: Date.now(),
201
- });
202
- }
203
- catch (error) {
204
- this.logger.log("warn", `failed to append schedule result to ${replyTarget.channel}:${replyTarget.target}: ${formatError(error)}`);
205
- }
206
- }
207
- requireJob(id) {
208
- const job = this.store.getCronJob(id);
209
- if (job === null) {
210
- throw new Error(`unknown schedule job: ${id}`);
211
- }
212
- return job;
213
- }
214
- rebaseJobs(jobs, nowMs) {
215
- for (const job of jobs) {
216
- try {
217
- const nextRunAtMs = computeNextRunAt(this.contract, job, nowMs, this.effectiveTimeZone);
218
- this.store.updateCronJobNextRun(job.id, nextRunAtMs, nowMs);
219
- }
220
- catch (error) {
221
- this.logger.log("error", `failed to rebase cron job ${job.id}: ${formatError(error)}`);
222
- }
223
- }
224
- }
225
- readStoredEffectiveTimeZone() {
226
- const stored = this.store.getStateValue(CRON_EFFECTIVE_TIME_ZONE_KEY);
227
- if (stored === null) {
228
- return null;
229
- }
230
- try {
231
- return this.contract.normalizeCronTimeZone(stored);
232
- }
233
- catch (error) {
234
- this.logger.log("warn", `stored cron time zone is invalid (${stored}); treating as legacy ${LEGACY_CRON_TIME_ZONE}: ${formatError(error)}`);
235
- return null;
236
- }
237
- }
238
- }
239
- function normalizeUpsertInput(input) {
240
- const id = normalizeId(input.id);
241
- const schedule = normalizeRequiredField(input.schedule, "cron schedule");
242
- const prompt = normalizeRequiredField(input.prompt, "cron prompt");
243
- const deliveryChannel = normalizeOptionalField(input.deliveryChannel);
244
- const deliveryTarget = normalizeOptionalField(input.deliveryTarget);
245
- const deliveryTopic = normalizeOptionalField(input.deliveryTopic);
246
- if ((deliveryChannel === null) !== (deliveryTarget === null)) {
247
- throw new Error("cron delivery_channel and delivery_target must be provided together");
248
- }
249
- if (deliveryChannel === null && deliveryTopic !== null) {
250
- throw new Error("cron delivery_topic requires delivery_channel and delivery_target");
251
- }
252
- if (deliveryChannel !== null && deliveryChannel !== "telegram") {
253
- throw new Error(`unsupported cron delivery channel: ${deliveryChannel}`);
254
- }
255
- return {
256
- id,
257
- kind: "cron",
258
- schedule,
259
- runAtMs: null,
260
- prompt,
261
- enabled: input.enabled,
262
- deliveryChannel,
263
- deliveryTarget,
264
- deliveryTopic,
265
- nextRunAtMs: 0,
266
- recordedAtMs: 0,
267
- };
268
- }
269
- function normalizeOnceInput(input) {
270
- const id = normalizeId(input.id);
271
- const prompt = normalizeRequiredField(input.prompt, "schedule prompt");
272
- const deliveryChannel = normalizeOptionalField(input.deliveryChannel);
273
- const deliveryTarget = normalizeOptionalField(input.deliveryTarget);
274
- const deliveryTopic = normalizeOptionalField(input.deliveryTopic);
275
- if ((deliveryChannel === null) !== (deliveryTarget === null)) {
276
- throw new Error("schedule delivery_channel and delivery_target must be provided together");
277
- }
278
- if (deliveryChannel === null && deliveryTopic !== null) {
279
- throw new Error("schedule delivery_topic requires delivery_channel and delivery_target");
280
- }
281
- if (deliveryChannel !== null && deliveryChannel !== "telegram") {
282
- throw new Error(`unsupported schedule delivery channel: ${deliveryChannel}`);
283
- }
284
- const runAtMs = resolveOnceRunAt(input);
285
- return {
286
- id,
287
- kind: "once",
288
- schedule: null,
289
- runAtMs,
290
- prompt,
291
- enabled: true,
292
- deliveryChannel,
293
- deliveryTarget,
294
- deliveryTopic,
295
- nextRunAtMs: runAtMs,
296
- recordedAtMs: 0,
297
- };
298
- }
299
- function resolveOnceRunAt(input) {
300
- if (input.delaySeconds === null && input.runAtMs === null) {
301
- throw new Error("schedule_once requires delay_seconds or run_at_ms");
302
- }
303
- if (input.delaySeconds !== null && input.runAtMs !== null) {
304
- throw new Error("schedule_once accepts only one of delay_seconds or run_at_ms");
305
- }
306
- if (input.runAtMs !== null) {
307
- if (!Number.isSafeInteger(input.runAtMs) || input.runAtMs < 0) {
308
- throw new Error("schedule run_at_ms must be a non-negative integer");
309
- }
310
- return input.runAtMs;
311
- }
312
- const delaySeconds = input.delaySeconds ?? 0;
313
- if (!Number.isSafeInteger(delaySeconds) || delaySeconds < 0) {
314
- throw new Error("schedule delay_seconds must be a non-negative integer");
315
- }
316
- return Date.now() + delaySeconds * 1_000;
317
- }
318
- function normalizeId(id) {
319
- return normalizeRequiredField(id, "schedule id");
320
- }
321
- function normalizeRequiredField(value, field) {
322
- const trimmed = value.trim();
323
- if (trimmed.length === 0) {
324
- throw new Error(`${field} must not be empty`);
325
- }
326
- return trimmed;
327
- }
328
- function normalizeOptionalField(value) {
329
- if (value === null) {
330
- return null;
331
- }
332
- const trimmed = value.trim();
333
- return trimmed.length === 0 ? null : trimmed;
334
- }
335
- function toBindingCronJobSpec(job) {
336
- return {
337
- id: job.id,
338
- schedule: normalizeRequiredField(job.schedule ?? "", "cron schedule"),
339
- prompt: job.prompt,
340
- deliveryChannel: job.deliveryChannel,
341
- deliveryTarget: job.deliveryTarget,
342
- deliveryTopic: job.deliveryTopic,
343
- };
344
- }
345
- function computeNextRunAt(contract, job, afterMs, timeZone) {
346
- const nextRunAt = contract.nextCronRunAt(toBindingCronJobSpec(job), afterMs, timeZone);
347
- if (!Number.isSafeInteger(nextRunAt) || nextRunAt < 0) {
348
- throw new Error(`next cron run at is out of range for JavaScript: ${nextRunAt}`);
349
- }
350
- return nextRunAt;
351
- }
352
- function sleep(durationMs) {
353
- return new Promise((resolve) => {
354
- setTimeout(resolve, durationMs);
355
- });
356
- }
357
- function clampStatusLimit(limit) {
358
- if (!Number.isSafeInteger(limit) || limit <= 0) {
359
- throw new Error("schedule_status limit must be a positive integer");
360
- }
361
- return Math.min(limit, MAX_STATUS_RUNS);
362
- }
363
- function deriveJobState(job, latestRun) {
364
- if (latestRun?.status === "running") {
365
- return "running";
366
- }
367
- if (job.enabled) {
368
- return "scheduled";
369
- }
370
- if (latestRun !== null) {
371
- return latestRun.status;
372
- }
373
- return "canceled";
374
- }
375
- function isTerminalJob(job) {
376
- return !job.enabled;
377
- }
378
- function toReplyTarget(job) {
379
- if (job.deliveryChannel === null || job.deliveryTarget === null) {
380
- return null;
381
- }
382
- return {
383
- channel: job.deliveryChannel,
384
- target: job.deliveryTarget,
385
- topic: job.deliveryTopic,
386
- };
387
- }
388
- function conversationKeyForJob(job) {
389
- return job.kind === "cron" ? `cron:${job.id}` : `once:${job.id}`;
390
- }
391
- function formatScheduleContextNote(job, scheduledForMs, outcome) {
392
- const header = [
393
- "[Gateway schedule result]",
394
- `job_id=${job.id}`,
395
- `job_kind=${job.kind}`,
396
- `scheduled_for_ms=${scheduledForMs}`,
397
- ];
398
- if (outcome.kind === "success") {
399
- return [...header, "status=succeeded", "", outcome.responseText].join("\n");
400
- }
401
- return [...header, "status=failed", "", outcome.errorMessage].join("\n");
402
- }
@@ -1,75 +0,0 @@
1
- import { readTelegramChatType, recordTelegramChatType, recordTelegramDraftFailure, recordTelegramDraftSuccess, recordTelegramStreamFallback, } from "../telegram/state";
2
- import { formatError } from "../utils/error";
3
- export class TelegramProgressiveSupport {
4
- client;
5
- store;
6
- logger;
7
- constructor(client, store, logger) {
8
- this.client = client;
9
- this.store = store;
10
- this.logger = logger;
11
- }
12
- async resolveMode(target, preference) {
13
- if (target.channel !== "telegram" || preference === "oneshot") {
14
- return "oneshot";
15
- }
16
- const isPrivateChat = await this.isPrivateChat(target.target);
17
- if (preference === "stream") {
18
- if (!isPrivateChat) {
19
- throw new Error("telegram draft stream is only supported for private chats");
20
- }
21
- return "progressive";
22
- }
23
- if (!isPrivateChat) {
24
- recordTelegramStreamFallback(this.store, "non_private_chat", Date.now());
25
- return "oneshot";
26
- }
27
- return "progressive";
28
- }
29
- async sendDraft(target, draftId, text) {
30
- if (this.client === null) {
31
- throw new Error("telegram transport is not configured");
32
- }
33
- try {
34
- await this.client.sendMessageDraft(target.target, draftId, text, target.topic);
35
- recordTelegramDraftSuccess(this.store, Date.now());
36
- }
37
- catch (error) {
38
- const message = formatError(error);
39
- recordTelegramDraftFailure(this.store, message, Date.now());
40
- recordTelegramStreamFallback(this.store, "draft_send_failed", Date.now());
41
- this.logger.log("warn", `telegram draft send failed: ${message}`);
42
- throw error;
43
- }
44
- }
45
- startTyping(target) {
46
- if (this.client === null) {
47
- return;
48
- }
49
- void this.client.sendChatAction(target.target, "typing", target.topic).catch(() => {
50
- // Typing hints are best-effort only.
51
- });
52
- }
53
- async isPrivateChat(chatId) {
54
- const cachedChatType = readTelegramChatType(this.store, chatId);
55
- if (cachedChatType !== null) {
56
- return cachedChatType === "private";
57
- }
58
- if (this.client === null) {
59
- return false;
60
- }
61
- try {
62
- const chat = await this.client.getChat(chatId);
63
- recordTelegramChatType(this.store, chatId, chat.type, Date.now());
64
- return chat.type === "private";
65
- }
66
- catch (error) {
67
- this.logger.log("warn", `failed to resolve Telegram chat type for ${chatId}: ${formatError(error)}`);
68
- return false;
69
- }
70
- }
71
- }
72
- export function createDraftId() {
73
- const draftId = crypto.getRandomValues(new Uint32Array(1))[0];
74
- return draftId === 0 ? 1 : draftId;
75
- }
@@ -1,175 +0,0 @@
1
- import { recordTelegramPreviewEmit, recordTelegramStreamFallback } from "../telegram/state";
2
- import { createDraftId } from "./telegram";
3
- export class GatewayTextDelivery {
4
- transport;
5
- store;
6
- telegramSupport;
7
- constructor(transport, store, telegramSupport) {
8
- this.transport = transport;
9
- this.store = store;
10
- this.telegramSupport = telegramSupport;
11
- }
12
- async open(target, preference) {
13
- const [session] = await this.openMany([target], preference);
14
- return session;
15
- }
16
- async openMany(targets, preference) {
17
- const uniqueTargets = dedupeTargets(targets);
18
- if (uniqueTargets.length === 0) {
19
- return [new NoopTextDeliverySession()];
20
- }
21
- const sessions = await Promise.all(uniqueTargets.map(async (target) => {
22
- const mode = await this.telegramSupport.resolveMode(target, preference);
23
- if (mode === "progressive") {
24
- const session = new ProgressiveTextDeliverySession(target, this.transport, this.telegramSupport, this.store);
25
- session.start();
26
- return session;
27
- }
28
- return new OneshotTextDeliverySession(target, this.transport);
29
- }));
30
- if (sessions.length === 1) {
31
- return sessions;
32
- }
33
- return [new FanoutTextDeliverySession(sessions)];
34
- }
35
- async sendTest(target, text, preference) {
36
- const session = await this.open(target, preference);
37
- if (session.mode === "progressive") {
38
- await session.preview(text.slice(0, Math.max(1, Math.ceil(text.length / 2))));
39
- }
40
- return {
41
- delivered: await session.finish(text),
42
- mode: session.mode,
43
- };
44
- }
45
- }
46
- class NoopTextDeliverySession {
47
- mode = "oneshot";
48
- async preview(_text) { }
49
- async finish(_finalText) {
50
- return false;
51
- }
52
- }
53
- class FanoutTextDeliverySession {
54
- sessions;
55
- mode;
56
- constructor(sessions) {
57
- this.sessions = sessions;
58
- this.mode = sessions.some((session) => session.mode === "progressive") ? "progressive" : "oneshot";
59
- }
60
- async preview(text) {
61
- await Promise.all(this.sessions.map((session) => session.preview(text)));
62
- }
63
- async finish(finalText) {
64
- const results = await Promise.allSettled(this.sessions.map((session) => session.finish(finalText)));
65
- const firstFailure = results.find((result) => result.status === "rejected");
66
- if (firstFailure?.status === "rejected") {
67
- throw firstFailure.reason;
68
- }
69
- return results.some((result) => result.status === "fulfilled" && result.value);
70
- }
71
- }
72
- class OneshotTextDeliverySession {
73
- target;
74
- transport;
75
- mode = "oneshot";
76
- constructor(target, transport) {
77
- this.target = target;
78
- this.transport = transport;
79
- }
80
- async preview(_text) { }
81
- async finish(finalText) {
82
- if (finalText === null || finalText.trim().length === 0) {
83
- return false;
84
- }
85
- const ack = await this.transport.sendMessage({
86
- deliveryTarget: this.target,
87
- body: finalText,
88
- });
89
- if (ack.errorMessage !== null) {
90
- throw new Error(ack.errorMessage);
91
- }
92
- return true;
93
- }
94
- }
95
- class ProgressiveTextDeliverySession {
96
- target;
97
- transport;
98
- telegramSupport;
99
- store;
100
- mode = "progressive";
101
- previewFailed = false;
102
- previewDelivered = false;
103
- closed = false;
104
- pendingPreviewCount = 0;
105
- pendingPreview = Promise.resolve();
106
- draftId = createDraftId();
107
- constructor(target, transport, telegramSupport, store) {
108
- this.target = target;
109
- this.transport = transport;
110
- this.telegramSupport = telegramSupport;
111
- this.store = store;
112
- }
113
- start() {
114
- this.telegramSupport.startTyping(this.target);
115
- }
116
- async preview(text) {
117
- if (this.previewFailed || this.closed || text.trim().length === 0) {
118
- return;
119
- }
120
- const runPreview = async () => {
121
- try {
122
- if (this.previewFailed || this.closed) {
123
- return;
124
- }
125
- try {
126
- recordTelegramPreviewEmit(this.store, Date.now());
127
- await this.telegramSupport.sendDraft(this.target, this.draftId, text);
128
- this.previewDelivered = true;
129
- }
130
- catch {
131
- this.previewFailed = true;
132
- }
133
- }
134
- finally {
135
- this.pendingPreviewCount = Math.max(0, this.pendingPreviewCount - 1);
136
- }
137
- };
138
- this.pendingPreviewCount += 1;
139
- this.pendingPreview = this.pendingPreview.then(runPreview, runPreview);
140
- await this.pendingPreview;
141
- }
142
- async finish(finalText) {
143
- this.closed = true;
144
- if (this.pendingPreviewCount > 0) {
145
- await this.pendingPreview;
146
- }
147
- if (finalText === null || finalText.trim().length === 0) {
148
- return false;
149
- }
150
- if (!this.previewDelivered && !this.previewFailed) {
151
- recordTelegramStreamFallback(this.store, "preview_not_established", Date.now());
152
- }
153
- const ack = await this.transport.sendMessage({
154
- deliveryTarget: this.target,
155
- body: finalText,
156
- });
157
- if (ack.errorMessage !== null) {
158
- throw new Error(ack.errorMessage);
159
- }
160
- return true;
161
- }
162
- }
163
- function dedupeTargets(targets) {
164
- const seen = new Set();
165
- const unique = [];
166
- for (const target of targets) {
167
- const key = `${target.channel}:${target.target}:${target.topic ?? ""}`;
168
- if (seen.has(key)) {
169
- continue;
170
- }
171
- seen.add(key);
172
- unique.push(target);
173
- }
174
- return unique;
175
- }