@osolmaz/pi-workflows 0.12.1 → 0.13.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 +52 -28
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +369 -73
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +6 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -21
- package/dist/builtins/monitor.workflow.js +497 -216
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-change.workflow.d.ts +170 -22
- package/dist/builtins/sanity-check.workflow.d.ts +45 -3
- package/dist/builtins/sanity-check.workflow.js +45 -7
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -0
- package/dist/extension/decision-channels.js +311 -118
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +156 -103
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +6 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +14 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +2 -17
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/composition.js +12 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +183 -103
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -3
- package/dist/workflows/errors.js +3 -3
- package/dist/workflows/human-decision.d.ts +29 -4
- package/dist/workflows/human-decision.js +547 -283
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -5
- package/dist/workflows/index.js +3 -4
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +902 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +55 -81
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +13 -14
- package/docs/MONITOR.md +179 -231
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +19 -19
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
- package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +104 -50
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +1 -11
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -163
- package/skills/pi-workflows/SKILL.md +7 -7
- package/skills/sanity-check/SKILL.md +1 -1
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +6 -4
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +663 -249
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +427 -144
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -118
- package/src/extension/recorder.ts +13 -22
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +8 -0
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +11 -4
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +12 -24
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/composition.ts +12 -3
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +212 -116
- package/src/workflows/errors.ts +3 -3
- package/src/workflows/human-decision.ts +795 -340
- package/src/workflows/index.ts +15 -19
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/store.ts +1295 -1163
- package/src/workflows/types.ts +61 -81
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs";
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
2
|
import fsp from "node:fs/promises";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
7
6
|
import { Key, matchesKey, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
8
|
-
import
|
|
7
|
+
import type { StateDatabase } from "../state/database.js";
|
|
8
|
+
import { resourceIdFor, tokenHash } from "../state/mutation.js";
|
|
9
9
|
import {
|
|
10
10
|
MAX_PRESENTATION_TRANSPORT_PARTS,
|
|
11
11
|
decisionDocumentSegments,
|
|
12
12
|
decisionPresentationFingerprint,
|
|
13
13
|
digestCanonical,
|
|
14
|
+
humanDecisionChannelRequest,
|
|
14
15
|
} from "../workflows/decision-presentation.js";
|
|
15
16
|
import { HumanDecisionStore, createHumanDecisionAttemptId } from "../workflows/human-decision.js";
|
|
16
17
|
import type {
|
|
@@ -154,6 +155,9 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
async deliver(request: HumanDecisionChannelRequest): Promise<HumanDecisionDeliveryResult> {
|
|
158
|
+
if (await this.alreadyDecided(request.decisionId)) {
|
|
159
|
+
return { status: "confirmed", channel: this.id, attemptId: "adopted" };
|
|
160
|
+
}
|
|
157
161
|
const attemptId = createHumanDecisionAttemptId();
|
|
158
162
|
const createdAt = new Date().toISOString();
|
|
159
163
|
const controller = new AbortController();
|
|
@@ -281,6 +285,9 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
281
285
|
}
|
|
282
286
|
response = { choice: selectedChoice, input: { [definition.input.name]: text } };
|
|
283
287
|
}
|
|
288
|
+
if (await this.alreadyDecided(request.decisionId)) {
|
|
289
|
+
return { status: "confirmed", channel: this.id, attemptId: "adopted" };
|
|
290
|
+
}
|
|
284
291
|
const eventId = createHumanDecisionAttemptId();
|
|
285
292
|
await this.options.onAnswer({
|
|
286
293
|
request,
|
|
@@ -307,6 +314,13 @@ export class PiDecisionChannel implements HumanDecisionChannel {
|
|
|
307
314
|
}
|
|
308
315
|
}
|
|
309
316
|
|
|
317
|
+
private async alreadyDecided(decisionId: string): Promise<boolean> {
|
|
318
|
+
return (
|
|
319
|
+
(await this.options.store.readResolved(decisionId)) !== null ||
|
|
320
|
+
(await this.options.store.readCancellation(decisionId)) !== null
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
310
324
|
async settle(decision: SettledHumanDecision): Promise<void> {
|
|
311
325
|
this.promptAbort?.abort();
|
|
312
326
|
if (this.settlementTask !== null) return await this.settlementTask;
|
|
@@ -354,8 +368,6 @@ type ReplyBinding = CallbackBinding & {
|
|
|
354
368
|
promptMessageId: string;
|
|
355
369
|
};
|
|
356
370
|
|
|
357
|
-
type CallbackRow = { request_json: string; choice_id: string };
|
|
358
|
-
type ReplyRow = { request_json: string; choice_id: string; chat_id: string; message_id: string };
|
|
359
371
|
type MessageRow = { chat_id: string; message_id: string };
|
|
360
372
|
type MessagePartRow = {
|
|
361
373
|
recipient_index: number;
|
|
@@ -364,189 +376,214 @@ type MessagePartRow = {
|
|
|
364
376
|
chat_id: string;
|
|
365
377
|
message_id: string;
|
|
366
378
|
};
|
|
367
|
-
type OffsetRow = { next_offset: number };
|
|
368
379
|
|
|
369
380
|
class TelegramProjection {
|
|
370
|
-
private readonly
|
|
381
|
+
private readonly channelId: string;
|
|
382
|
+
private readonly resourceId: string;
|
|
383
|
+
private readonly token = randomBytes(32).toString("base64url");
|
|
384
|
+
private generation = 0;
|
|
371
385
|
|
|
372
386
|
constructor(
|
|
373
|
-
|
|
387
|
+
private readonly state: StateDatabase,
|
|
374
388
|
private readonly profile: string,
|
|
375
389
|
private readonly owner: string,
|
|
376
390
|
) {
|
|
377
|
-
|
|
378
|
-
this.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
chat_id TEXT NOT NULL,
|
|
402
|
-
message_id TEXT NOT NULL,
|
|
403
|
-
request_json TEXT NOT NULL,
|
|
404
|
-
choice_id TEXT NOT NULL,
|
|
405
|
-
PRIMARY KEY (profile, chat_id, message_id)
|
|
406
|
-
);
|
|
407
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
408
|
-
profile TEXT NOT NULL,
|
|
409
|
-
decision_id TEXT NOT NULL,
|
|
410
|
-
chat_id TEXT NOT NULL,
|
|
411
|
-
message_id TEXT NOT NULL,
|
|
412
|
-
PRIMARY KEY (profile, decision_id, chat_id, message_id)
|
|
413
|
-
);
|
|
414
|
-
CREATE TABLE IF NOT EXISTS message_parts (
|
|
415
|
-
profile TEXT NOT NULL,
|
|
416
|
-
decision_id TEXT NOT NULL,
|
|
417
|
-
recipient_index INTEGER NOT NULL,
|
|
418
|
-
part_index INTEGER NOT NULL,
|
|
419
|
-
content_digest TEXT NOT NULL,
|
|
420
|
-
chat_id TEXT NOT NULL,
|
|
421
|
-
message_id TEXT NOT NULL,
|
|
422
|
-
PRIMARY KEY (profile, decision_id, recipient_index, part_index)
|
|
423
|
-
);
|
|
424
|
-
`);
|
|
391
|
+
this.channelId = `telegram-${createHash("sha256").update(profile).digest("hex").slice(0, 40)}`;
|
|
392
|
+
this.resourceId = resourceIdFor("channel", this.channelId);
|
|
393
|
+
this.state.transaction(() => {
|
|
394
|
+
const now = Date.now();
|
|
395
|
+
this.state.connection
|
|
396
|
+
.prepare(
|
|
397
|
+
`INSERT INTO resources(
|
|
398
|
+
resource_id, resource_type, aggregate_key, revision, created_at, updated_at
|
|
399
|
+
) VALUES (?, 'channel', ?, 1, ?, ?)
|
|
400
|
+
ON CONFLICT(resource_type, aggregate_key) DO NOTHING`,
|
|
401
|
+
)
|
|
402
|
+
.run(this.resourceId, this.channelId, now, now);
|
|
403
|
+
this.state.connection
|
|
404
|
+
.prepare(
|
|
405
|
+
"INSERT INTO leases(resource_id, generation) VALUES (?, 0) ON CONFLICT(resource_id) DO NOTHING",
|
|
406
|
+
)
|
|
407
|
+
.run(this.resourceId);
|
|
408
|
+
this.state.connection
|
|
409
|
+
.prepare(
|
|
410
|
+
`INSERT INTO channels(channel_id, resource_id, adapter_type, profile_key, created_at)
|
|
411
|
+
VALUES (?, ?, 'telegram', ?, ?) ON CONFLICT(channel_id) DO NOTHING`,
|
|
412
|
+
)
|
|
413
|
+
.run(this.channelId, this.resourceId, this.profile, now);
|
|
414
|
+
});
|
|
425
415
|
}
|
|
426
416
|
|
|
427
417
|
close(): void {
|
|
428
418
|
this.release();
|
|
429
|
-
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
ownsLease(): boolean {
|
|
422
|
+
try {
|
|
423
|
+
this.assertOwner();
|
|
424
|
+
return true;
|
|
425
|
+
} catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
430
428
|
}
|
|
431
429
|
|
|
432
430
|
acquire(now = Date.now()): boolean {
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
.
|
|
437
|
-
|
|
438
|
-
|
|
431
|
+
return this.state.transaction(() => {
|
|
432
|
+
const lease = this.lease();
|
|
433
|
+
if (
|
|
434
|
+
lease.ownerId !== null &&
|
|
435
|
+
lease.ownerId !== this.owner &&
|
|
436
|
+
lease.expiresAt !== null &&
|
|
437
|
+
lease.expiresAt > now
|
|
438
|
+
) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
const generation = lease.ownerId === this.owner ? lease.generation : lease.generation + 1;
|
|
442
|
+
const result = this.state.connection
|
|
439
443
|
.prepare(
|
|
440
|
-
|
|
444
|
+
`UPDATE leases
|
|
445
|
+
SET generation = ?, owner_type = 'channel', owner_id = ?, token_hash = ?,
|
|
446
|
+
acquired_at = COALESCE(acquired_at, ?), heartbeat_at = ?, expires_at = ?
|
|
447
|
+
WHERE resource_id = ? AND generation = ?`,
|
|
441
448
|
)
|
|
442
|
-
.run(
|
|
449
|
+
.run(
|
|
450
|
+
generation,
|
|
451
|
+
this.owner,
|
|
452
|
+
tokenHash(this.token),
|
|
453
|
+
now,
|
|
454
|
+
now,
|
|
455
|
+
now + LEASE_TTL_MS,
|
|
456
|
+
this.resourceId,
|
|
457
|
+
lease.generation,
|
|
458
|
+
);
|
|
459
|
+
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
460
|
+
if (result.changes !== 1) return false;
|
|
461
|
+
this.generation = generation;
|
|
462
|
+
this.recordMutation("channel.lease_acquired", { expiresAt: now + LEASE_TTL_MS }, now);
|
|
443
463
|
return true;
|
|
444
464
|
});
|
|
445
|
-
return transaction();
|
|
446
465
|
}
|
|
447
466
|
|
|
448
467
|
renew(now = Date.now()): boolean {
|
|
449
468
|
return (
|
|
450
|
-
this.
|
|
451
|
-
.prepare(
|
|
452
|
-
|
|
469
|
+
this.state.connection
|
|
470
|
+
.prepare(
|
|
471
|
+
`UPDATE leases SET heartbeat_at = ?, expires_at = ?
|
|
472
|
+
WHERE resource_id = ? AND owner_id = ? AND token_hash = ? AND generation = ?`,
|
|
473
|
+
)
|
|
474
|
+
.run(
|
|
475
|
+
now,
|
|
476
|
+
now + LEASE_TTL_MS,
|
|
477
|
+
this.resourceId,
|
|
478
|
+
this.owner,
|
|
479
|
+
tokenHash(this.token),
|
|
480
|
+
this.generation,
|
|
481
|
+
).changes === 1
|
|
453
482
|
);
|
|
454
483
|
}
|
|
455
484
|
|
|
456
485
|
release(): void {
|
|
457
|
-
this.
|
|
458
|
-
|
|
459
|
-
|
|
486
|
+
this.state.transaction(() => {
|
|
487
|
+
const result = this.state.connection
|
|
488
|
+
.prepare(
|
|
489
|
+
`UPDATE leases
|
|
490
|
+
SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
|
|
491
|
+
acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
|
|
492
|
+
WHERE resource_id = ? AND owner_id = ? AND token_hash = ? AND generation = ?`,
|
|
493
|
+
)
|
|
494
|
+
.run(this.resourceId, this.owner, tokenHash(this.token), this.generation);
|
|
495
|
+
if (result.changes === 1) {
|
|
496
|
+
this.recordMutation("channel.lease_released", {}, Date.now());
|
|
497
|
+
}
|
|
498
|
+
});
|
|
460
499
|
}
|
|
461
500
|
|
|
462
501
|
offset(): number {
|
|
463
|
-
|
|
464
|
-
(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
);
|
|
502
|
+
const row = this.state.connection
|
|
503
|
+
.prepare(
|
|
504
|
+
"SELECT cursor_value AS cursorValue FROM channel_cursors WHERE channel_id = ? AND cursor_key = 'telegram_update'",
|
|
505
|
+
)
|
|
506
|
+
.get(this.channelId);
|
|
507
|
+
return isCursorRow(row) ? Number.parseInt(row.cursorValue, 10) || 0 : 0;
|
|
470
508
|
}
|
|
471
509
|
|
|
472
510
|
setOffset(offset: number): void {
|
|
473
|
-
this.
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
511
|
+
this.mutate("channel.cursor_updated", { offset }, () => {
|
|
512
|
+
this.state.connection
|
|
513
|
+
.prepare(
|
|
514
|
+
`INSERT INTO channel_cursors(channel_id, cursor_key, cursor_value, updated_at)
|
|
515
|
+
VALUES (?, 'telegram_update', ?, ?)
|
|
516
|
+
ON CONFLICT(channel_id, cursor_key) DO UPDATE SET
|
|
517
|
+
cursor_value = CAST(MAX(CAST(channel_cursors.cursor_value AS INTEGER), CAST(excluded.cursor_value AS INTEGER)) AS TEXT),
|
|
518
|
+
updated_at = excluded.updated_at`,
|
|
519
|
+
)
|
|
520
|
+
.run(this.channelId, String(offset), Date.now());
|
|
521
|
+
});
|
|
478
522
|
}
|
|
479
523
|
|
|
480
524
|
putCallback(token: string, binding: CallbackBinding): void {
|
|
481
|
-
this.
|
|
482
|
-
.prepare(
|
|
483
|
-
"INSERT INTO callbacks(profile, token, request_json, choice_id) VALUES (?, ?, ?, ?) ON CONFLICT(profile, token) DO UPDATE SET request_json = excluded.request_json, choice_id = excluded.choice_id",
|
|
484
|
-
)
|
|
485
|
-
.run(this.profile, token, JSON.stringify(binding.request), binding.choice);
|
|
525
|
+
this.putInbox(`callback:${token}`, "callback", binding);
|
|
486
526
|
}
|
|
487
527
|
|
|
488
528
|
callback(token: string): CallbackBinding | undefined {
|
|
489
|
-
|
|
490
|
-
.prepare("SELECT request_json, choice_id FROM callbacks WHERE profile = ? AND token = ?")
|
|
491
|
-
.get(this.profile, token) as CallbackRow | undefined;
|
|
492
|
-
return row === undefined
|
|
493
|
-
? undefined
|
|
494
|
-
: {
|
|
495
|
-
request: JSON.parse(row.request_json) as HumanDecisionChannelRequest,
|
|
496
|
-
choice: row.choice_id,
|
|
497
|
-
};
|
|
529
|
+
return this.inbox<CallbackBinding>(`callback:${token}`);
|
|
498
530
|
}
|
|
499
531
|
|
|
500
532
|
putReply(binding: ReplyBinding): void {
|
|
501
|
-
this.
|
|
502
|
-
.prepare(
|
|
503
|
-
"INSERT INTO replies(profile, chat_id, message_id, request_json, choice_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(profile, chat_id, message_id) DO UPDATE SET request_json = excluded.request_json, choice_id = excluded.choice_id",
|
|
504
|
-
)
|
|
505
|
-
.run(
|
|
506
|
-
this.profile,
|
|
507
|
-
binding.chatId,
|
|
508
|
-
binding.promptMessageId,
|
|
509
|
-
JSON.stringify(binding.request),
|
|
510
|
-
binding.choice,
|
|
511
|
-
);
|
|
533
|
+
this.putInbox(`reply:${binding.chatId}:${binding.promptMessageId}`, "reply", binding);
|
|
512
534
|
}
|
|
513
535
|
|
|
514
536
|
reply(chatId: string, messageId: string): ReplyBinding | undefined {
|
|
515
|
-
|
|
516
|
-
.prepare(
|
|
517
|
-
"SELECT request_json, choice_id, chat_id, message_id FROM replies WHERE profile = ? AND chat_id = ? AND message_id = ?",
|
|
518
|
-
)
|
|
519
|
-
.get(this.profile, chatId, messageId) as ReplyRow | undefined;
|
|
520
|
-
return row === undefined
|
|
521
|
-
? undefined
|
|
522
|
-
: {
|
|
523
|
-
request: JSON.parse(row.request_json) as HumanDecisionChannelRequest,
|
|
524
|
-
choice: row.choice_id,
|
|
525
|
-
chatId: row.chat_id,
|
|
526
|
-
promptMessageId: row.message_id,
|
|
527
|
-
};
|
|
537
|
+
return this.inbox<ReplyBinding>(`reply:${chatId}:${messageId}`);
|
|
528
538
|
}
|
|
529
539
|
|
|
530
540
|
deleteReply(chatId: string, messageId: string): void {
|
|
531
|
-
this.
|
|
532
|
-
.
|
|
533
|
-
|
|
541
|
+
this.mutate("channel.reply_consumed", { chatId, messageId }, () => {
|
|
542
|
+
this.state.connection
|
|
543
|
+
.prepare("DELETE FROM channel_inbox WHERE channel_id = ? AND external_event_id = ?")
|
|
544
|
+
.run(this.channelId, `reply:${chatId}:${messageId}`);
|
|
545
|
+
});
|
|
534
546
|
}
|
|
535
547
|
|
|
536
548
|
putMessage(decisionId: string, chatId: string, messageId: string): void {
|
|
537
|
-
this.
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
549
|
+
const localId = transportMessageId(this.channelId, decisionId, chatId, messageId);
|
|
550
|
+
this.mutate("channel.message_confirmed", { decisionId, chatId, messageId }, () => {
|
|
551
|
+
const contentHash = this.state.putJson({ chatId, messageId });
|
|
552
|
+
this.state.connection
|
|
553
|
+
.prepare(
|
|
554
|
+
`INSERT INTO channel_messages(
|
|
555
|
+
message_id, channel_id, decision_id, purpose, content_hash,
|
|
556
|
+
external_conversation_ref, external_message_ref, status, created_at, updated_at
|
|
557
|
+
) VALUES (?, ?, ?, 'delivery', ?, ?, ?, 'confirmed', ?, ?)
|
|
558
|
+
ON CONFLICT(message_id) DO NOTHING`,
|
|
559
|
+
)
|
|
560
|
+
.run(
|
|
561
|
+
localId,
|
|
562
|
+
this.channelId,
|
|
563
|
+
decisionId,
|
|
564
|
+
contentHash,
|
|
565
|
+
chatId,
|
|
566
|
+
messageId,
|
|
567
|
+
Date.now(),
|
|
568
|
+
Date.now(),
|
|
569
|
+
);
|
|
570
|
+
});
|
|
542
571
|
}
|
|
543
572
|
|
|
544
573
|
messages(decisionId: string): MessageRow[] {
|
|
545
|
-
|
|
574
|
+
const rows = this.state.connection
|
|
546
575
|
.prepare(
|
|
547
|
-
|
|
576
|
+
`SELECT external_conversation_ref AS chatId, external_message_ref AS messageId
|
|
577
|
+
FROM channel_messages
|
|
578
|
+
WHERE channel_id = ? AND decision_id = ? AND purpose = 'delivery'
|
|
579
|
+
AND external_conversation_ref IS NOT NULL AND external_message_ref IS NOT NULL
|
|
580
|
+
ORDER BY external_conversation_ref, external_message_ref`,
|
|
548
581
|
)
|
|
549
|
-
.all(this.
|
|
582
|
+
.all(this.channelId, decisionId);
|
|
583
|
+
return rows.filter(isTransportMessageRow).map((row) => ({
|
|
584
|
+
chat_id: row.chatId,
|
|
585
|
+
message_id: row.messageId,
|
|
586
|
+
}));
|
|
550
587
|
}
|
|
551
588
|
|
|
552
589
|
putMessagePart(
|
|
@@ -557,11 +594,35 @@ class TelegramProjection {
|
|
|
557
594
|
chatId: string,
|
|
558
595
|
messageId: string,
|
|
559
596
|
): void {
|
|
560
|
-
|
|
561
|
-
.
|
|
562
|
-
|
|
563
|
-
)
|
|
564
|
-
|
|
597
|
+
const localId = `telegram-parts-${createHash("sha256")
|
|
598
|
+
.update(`${this.channelId}\0${decisionId}`)
|
|
599
|
+
.digest("hex")
|
|
600
|
+
.slice(0, 40)}`;
|
|
601
|
+
this.mutate("channel.message_part_confirmed", { decisionId, recipientIndex, partIndex }, () => {
|
|
602
|
+
const parentHash = this.state.putJson({ decisionId });
|
|
603
|
+
this.state.connection
|
|
604
|
+
.prepare(
|
|
605
|
+
`INSERT INTO channel_messages(
|
|
606
|
+
message_id, channel_id, decision_id, purpose, content_hash,
|
|
607
|
+
status, created_at, updated_at
|
|
608
|
+
) VALUES (?, ?, ?, 'delivery', ?, 'confirmed', ?, ?)
|
|
609
|
+
ON CONFLICT(message_id) DO NOTHING`,
|
|
610
|
+
)
|
|
611
|
+
.run(localId, this.channelId, decisionId, parentHash, Date.now(), Date.now());
|
|
612
|
+
const contentHash = this.state.putText(contentDigest);
|
|
613
|
+
this.state.connection
|
|
614
|
+
.prepare(
|
|
615
|
+
`INSERT INTO channel_message_parts(
|
|
616
|
+
message_id, recipient_index, part_index, content_hash,
|
|
617
|
+
external_conversation_ref, external_message_ref
|
|
618
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
619
|
+
ON CONFLICT(message_id, recipient_index, part_index) DO UPDATE SET
|
|
620
|
+
content_hash = excluded.content_hash,
|
|
621
|
+
external_conversation_ref = excluded.external_conversation_ref,
|
|
622
|
+
external_message_ref = excluded.external_message_ref`,
|
|
623
|
+
)
|
|
624
|
+
.run(localId, recipientIndex, partIndex, contentHash, chatId, messageId);
|
|
625
|
+
});
|
|
565
626
|
}
|
|
566
627
|
|
|
567
628
|
messagePart(
|
|
@@ -569,12 +630,182 @@ class TelegramProjection {
|
|
|
569
630
|
recipientIndex: number,
|
|
570
631
|
partIndex: number,
|
|
571
632
|
): MessagePartRow | undefined {
|
|
572
|
-
|
|
633
|
+
const localId = `telegram-parts-${createHash("sha256")
|
|
634
|
+
.update(`${this.channelId}\0${decisionId}`)
|
|
635
|
+
.digest("hex")
|
|
636
|
+
.slice(0, 40)}`;
|
|
637
|
+
const row = this.state.connection
|
|
573
638
|
.prepare(
|
|
574
|
-
|
|
639
|
+
`SELECT recipient_index AS recipientIndex, part_index AS partIndex,
|
|
640
|
+
content_hash AS contentHash,
|
|
641
|
+
external_conversation_ref AS chatId,
|
|
642
|
+
external_message_ref AS messageId
|
|
643
|
+
FROM channel_message_parts
|
|
644
|
+
WHERE message_id = ? AND recipient_index = ? AND part_index = ?`,
|
|
575
645
|
)
|
|
576
|
-
.get(
|
|
646
|
+
.get(localId, recipientIndex, partIndex);
|
|
647
|
+
if (!isMessagePartProjectionRow(row)) return undefined;
|
|
648
|
+
const content = this.state.readBlob(row.contentHash);
|
|
649
|
+
/* istanbul ignore if -- foreign key guarantees the content blob */
|
|
650
|
+
if (content === undefined) throw new Error("Telegram part content is missing");
|
|
651
|
+
return {
|
|
652
|
+
recipient_index: row.recipientIndex,
|
|
653
|
+
part_index: row.partIndex,
|
|
654
|
+
content_digest: content.content.toString("utf8"),
|
|
655
|
+
chat_id: row.chatId,
|
|
656
|
+
message_id: row.messageId,
|
|
657
|
+
};
|
|
577
658
|
}
|
|
659
|
+
|
|
660
|
+
private putInbox(
|
|
661
|
+
eventId: string,
|
|
662
|
+
type: "callback" | "reply",
|
|
663
|
+
binding: CallbackBinding | ReplyBinding,
|
|
664
|
+
): void {
|
|
665
|
+
this.mutate(`channel.${type}_stored`, { eventId }, () => {
|
|
666
|
+
const payloadHash = this.state.putJson(binding);
|
|
667
|
+
this.state.connection
|
|
668
|
+
.prepare(
|
|
669
|
+
`INSERT INTO channel_inbox(
|
|
670
|
+
channel_id, external_event_id, event_type, payload_hash, received_at
|
|
671
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
672
|
+
ON CONFLICT(channel_id, external_event_id) DO UPDATE SET payload_hash = excluded.payload_hash`,
|
|
673
|
+
)
|
|
674
|
+
.run(this.channelId, eventId, type, payloadHash, Date.now());
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private inbox<T>(eventId: string): T | undefined {
|
|
679
|
+
const row = this.state.connection
|
|
680
|
+
.prepare(
|
|
681
|
+
"SELECT payload_hash AS payloadHash FROM channel_inbox WHERE channel_id = ? AND external_event_id = ?",
|
|
682
|
+
)
|
|
683
|
+
.get(this.channelId, eventId);
|
|
684
|
+
return isPayloadHashRow(row) ? (this.state.readJson(row.payloadHash) as T) : undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private mutate(type: string, payload: unknown, operation: () => void): void {
|
|
688
|
+
this.state.transaction(() => {
|
|
689
|
+
this.assertOwner();
|
|
690
|
+
operation();
|
|
691
|
+
this.recordMutation(type, payload, Date.now());
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private recordMutation(type: string, payload: unknown, now: number): void {
|
|
696
|
+
const revisionRow = this.state.connection
|
|
697
|
+
.prepare("SELECT revision FROM resources WHERE resource_id = ?")
|
|
698
|
+
.get(this.resourceId);
|
|
699
|
+
/* istanbul ignore if -- exact schema and internal query shape */
|
|
700
|
+
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
701
|
+
if (!isRevisionProjectionRow(revisionRow))
|
|
702
|
+
throw new Error("Telegram channel resource is missing");
|
|
703
|
+
const revision = revisionRow.revision + 1;
|
|
704
|
+
this.state.connection
|
|
705
|
+
.prepare(
|
|
706
|
+
"UPDATE resources SET revision = ?, updated_at = ? WHERE resource_id = ? AND revision = ?",
|
|
707
|
+
)
|
|
708
|
+
.run(revision, now, this.resourceId, revision - 1);
|
|
709
|
+
const payloadHash = this.state.putJson(payload, now);
|
|
710
|
+
this.state.connection
|
|
711
|
+
.prepare(
|
|
712
|
+
`INSERT INTO events(
|
|
713
|
+
event_id, resource_id, resource_revision, event_type, actor_type,
|
|
714
|
+
actor_id, lease_generation, payload_hash, recorded_at
|
|
715
|
+
) VALUES (?, ?, ?, ?, 'channel', ?, ?, ?, ?)`,
|
|
716
|
+
)
|
|
717
|
+
.run(
|
|
718
|
+
`event-${randomUUID()}`,
|
|
719
|
+
this.resourceId,
|
|
720
|
+
revision,
|
|
721
|
+
type,
|
|
722
|
+
this.owner,
|
|
723
|
+
this.generation === 0 ? null : this.generation,
|
|
724
|
+
payloadHash,
|
|
725
|
+
now,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
private assertOwner(): void {
|
|
730
|
+
const lease = this.lease();
|
|
731
|
+
if (
|
|
732
|
+
lease.ownerId !== this.owner ||
|
|
733
|
+
lease.generation !== this.generation ||
|
|
734
|
+
lease.tokenHash === null ||
|
|
735
|
+
!lease.tokenHash.equals(tokenHash(this.token)) ||
|
|
736
|
+
lease.expiresAt === null ||
|
|
737
|
+
lease.expiresAt <= Date.now()
|
|
738
|
+
) {
|
|
739
|
+
throw new Error(`Telegram profile ${this.profile} is not owned by this channel process`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private lease(): TelegramLeaseRow {
|
|
744
|
+
const row = this.state.connection
|
|
745
|
+
.prepare(
|
|
746
|
+
`SELECT generation, owner_id AS ownerId, token_hash AS tokenHash,
|
|
747
|
+
expires_at AS expiresAt
|
|
748
|
+
FROM leases WHERE resource_id = ?`,
|
|
749
|
+
)
|
|
750
|
+
.get(this.resourceId);
|
|
751
|
+
/* istanbul ignore if -- exact schema and internal query shape */
|
|
752
|
+
/* istanbul ignore if -- impossible after exact schema and transaction checks */
|
|
753
|
+
if (!isTelegramLeaseRow(row)) throw new Error("Telegram channel lease is missing");
|
|
754
|
+
return row;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function transportMessageId(
|
|
759
|
+
channelId: string,
|
|
760
|
+
decisionId: string,
|
|
761
|
+
chatId: string,
|
|
762
|
+
messageId: string,
|
|
763
|
+
): string {
|
|
764
|
+
return `telegram-message-${createHash("sha256")
|
|
765
|
+
.update(`${channelId}\0${decisionId}\0${chatId}\0${messageId}`)
|
|
766
|
+
.digest("hex")
|
|
767
|
+
.slice(0, 40)}`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
type TelegramLeaseRow = {
|
|
771
|
+
generation: number;
|
|
772
|
+
ownerId: string | null;
|
|
773
|
+
tokenHash: Buffer | null;
|
|
774
|
+
expiresAt: number | null;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
function isTelegramLeaseRow(value: unknown): value is TelegramLeaseRow {
|
|
778
|
+
return isProjectionRecord(value);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function isCursorRow(value: unknown): value is { cursorValue: string } {
|
|
782
|
+
return isProjectionRecord(value);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function isPayloadHashRow(value: unknown): value is { payloadHash: Buffer } {
|
|
786
|
+
return isProjectionRecord(value);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function isTransportMessageRow(value: unknown): value is { chatId: string; messageId: string } {
|
|
790
|
+
return isProjectionRecord(value);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function isMessagePartProjectionRow(value: unknown): value is {
|
|
794
|
+
recipientIndex: number;
|
|
795
|
+
partIndex: number;
|
|
796
|
+
contentHash: Buffer;
|
|
797
|
+
chatId: string;
|
|
798
|
+
messageId: string;
|
|
799
|
+
} {
|
|
800
|
+
return isProjectionRecord(value);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function isRevisionProjectionRow(value: unknown): value is { revision: number } {
|
|
804
|
+
return isProjectionRecord(value);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function isProjectionRecord(value: unknown): value is Record<string, unknown> {
|
|
808
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
578
809
|
}
|
|
579
810
|
|
|
580
811
|
class TelegramCallError extends Error {
|
|
@@ -628,7 +859,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
628
859
|
this.fetchFn = options.fetchFn ?? (fetch as TelegramFetch);
|
|
629
860
|
this.apiBase = options.apiBase ?? DEFAULT_API_BASE;
|
|
630
861
|
this.projection = new TelegramProjection(
|
|
631
|
-
|
|
862
|
+
options.store.state,
|
|
632
863
|
options.profileName,
|
|
633
864
|
options.ownerId ?? randomUUID(),
|
|
634
865
|
);
|
|
@@ -653,6 +884,33 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
653
884
|
|
|
654
885
|
async deliver(request: HumanDecisionChannelRequest): Promise<HumanDecisionDeliveryResult> {
|
|
655
886
|
const channelPath = this.channelPath();
|
|
887
|
+
if (!this.projection.ownsLease()) {
|
|
888
|
+
const attemptId = `queued-${createHash("sha256")
|
|
889
|
+
.update(`${this.id}\0${request.decisionId}\0${request.requestDigest}`)
|
|
890
|
+
.digest("hex")
|
|
891
|
+
.slice(0, 40)}`;
|
|
892
|
+
await this.store.recordDelivery(
|
|
893
|
+
request,
|
|
894
|
+
channelPath,
|
|
895
|
+
deliveryRecord(
|
|
896
|
+
request,
|
|
897
|
+
this.id,
|
|
898
|
+
attemptId,
|
|
899
|
+
"complete",
|
|
900
|
+
"intent",
|
|
901
|
+
new Date().toISOString(),
|
|
902
|
+
{
|
|
903
|
+
errorCode: "queued_for_channel_owner",
|
|
904
|
+
},
|
|
905
|
+
),
|
|
906
|
+
);
|
|
907
|
+
return {
|
|
908
|
+
status: "unknown",
|
|
909
|
+
channel: this.id,
|
|
910
|
+
attemptId,
|
|
911
|
+
errorCode: "queued_for_channel_owner",
|
|
912
|
+
};
|
|
913
|
+
}
|
|
656
914
|
const parts = renderTelegramParts(request);
|
|
657
915
|
const partDigests = parts.map((part) => digestCanonical(part));
|
|
658
916
|
const prior = await this.store.listDeliveries(request.decisionId, channelPath);
|
|
@@ -969,6 +1227,7 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
969
1227
|
continue;
|
|
970
1228
|
}
|
|
971
1229
|
try {
|
|
1230
|
+
await this.deliverPendingRequests();
|
|
972
1231
|
const result = await this.call(
|
|
973
1232
|
"getUpdates",
|
|
974
1233
|
{
|
|
@@ -992,6 +1251,28 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
|
|
|
992
1251
|
}
|
|
993
1252
|
}
|
|
994
1253
|
|
|
1254
|
+
private async deliverPendingRequests(): Promise<void> {
|
|
1255
|
+
const channelPath = this.channelPath();
|
|
1256
|
+
for (const request of await this.store.listRequests()) {
|
|
1257
|
+
if (request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now()) continue;
|
|
1258
|
+
if ((await this.store.readResolved(request.decisionId)) !== null) continue;
|
|
1259
|
+
if ((await this.store.readCancellation(request.decisionId)) !== null) continue;
|
|
1260
|
+
const deliveries = await this.store.listDeliveries(request.decisionId, channelPath);
|
|
1261
|
+
if (
|
|
1262
|
+
!deliveries.some(
|
|
1263
|
+
(record) => record.state === "intent" && record.errorCode === "queued_for_channel_owner",
|
|
1264
|
+
)
|
|
1265
|
+
) {
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
await this.deliver(humanDecisionChannelRequest(request));
|
|
1270
|
+
} catch {
|
|
1271
|
+
// Durable delivery evidence controls retries for each request.
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
995
1276
|
private async call(method: string, payload: unknown, signal?: AbortSignal): Promise<unknown> {
|
|
996
1277
|
let response: Awaited<ReturnType<TelegramFetch>>;
|
|
997
1278
|
try {
|
|
@@ -1074,16 +1355,18 @@ export function createTelegramChannels(options: {
|
|
|
1074
1355
|
|
|
1075
1356
|
export async function verifyTelegramTokenFile(
|
|
1076
1357
|
tokenFile: string,
|
|
1077
|
-
fetchFn
|
|
1358
|
+
fetchFn?: TelegramFetch,
|
|
1078
1359
|
apiBase = DEFAULT_API_BASE,
|
|
1079
1360
|
): Promise<void> {
|
|
1361
|
+
/* istanbul ignore next -- global fetch is used only by operator setup */
|
|
1362
|
+
const request = fetchFn ?? (fetch as TelegramFetch);
|
|
1080
1363
|
if (!path.isAbsolute(tokenFile)) throw new Error("Telegram token file path must be absolute");
|
|
1081
1364
|
await requirePrivateFile(tokenFile, "Telegram token file");
|
|
1082
1365
|
const token = (await fsp.readFile(tokenFile, "utf8")).trim();
|
|
1083
1366
|
if (token.length === 0) throw new Error("Telegram token file is empty");
|
|
1084
1367
|
let response;
|
|
1085
1368
|
try {
|
|
1086
|
-
response = await
|
|
1369
|
+
response = await request(`${apiBase}/bot${encodeURIComponent(token)}/getMe`, {
|
|
1087
1370
|
method: "POST",
|
|
1088
1371
|
headers: { "content-type": "application/json" },
|
|
1089
1372
|
body: "{}",
|