@llblab/pi-telegram 0.52.0 → 0.52.1
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/AGENTS.md +5 -4
- package/BACKLOG.md +16 -1
- package/CHANGELOG.md +6 -0
- package/README.md +3 -2
- package/dist/lib/bindings.js +1 -0
- package/dist/lib/bus-api.js +4 -3
- package/dist/lib/extension.js +3 -0
- package/dist/lib/journal.d.ts +7 -1
- package/dist/lib/journal.js +72 -11
- package/dist/lib/queue.d.ts +2 -0
- package/dist/lib/queue.js +25 -3
- package/dist/lib/routing.d.ts +1 -0
- package/dist/lib/routing.js +118 -44
- package/dist/lib/telegram-api.d.ts +2 -0
- package/dist/lib/telegram-api.js +4 -0
- package/dist/lib/threads.d.ts +6 -4
- package/dist/lib/threads.js +45 -3
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +34 -18
- package/dist/package.json +1 -1
- package/docs/architecture.md +9 -7
- package/docs/ui-style.md +1 -1
- package/lib/bindings.ts +1 -0
- package/lib/bus-api.ts +4 -2
- package/lib/extension.ts +4 -0
- package/lib/journal.ts +80 -11
- package/lib/queue.ts +26 -3
- package/lib/routing.ts +91 -36
- package/lib/telegram-api.ts +5 -0
- package/lib/threads.ts +46 -7
- package/lib/updates.ts +43 -21
- package/package.json +1 -1
package/lib/routing.ts
CHANGED
|
@@ -289,6 +289,7 @@ function formatTelegramTemporaryThreadChooserText(command?: string): string {
|
|
|
289
289
|
"<b>🧵 Choose target thread:</b>",
|
|
290
290
|
"",
|
|
291
291
|
command ? `You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.` : "Choose where to send your message.",
|
|
292
|
+
"Unaccepted routing expires after 60 minutes; disposable tabs are removed when clear. Accepted work and restored Threads are kept.",
|
|
292
293
|
"Select the Pi thread that should handle it, or restore a Pi into this tab:",
|
|
293
294
|
].join("\n");
|
|
294
295
|
}
|
|
@@ -674,6 +675,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
674
675
|
captureWorkspaceExternalProtection?: ReturnType<typeof createTelegramWorkspaceExternalProtectionCapture>;
|
|
675
676
|
/** Strict committed abandonment plus retained original; shared by Restore and temporary-input cancellation. */
|
|
676
677
|
inspectRestoreSourceAbandonment?: (updateId: number, journalBindingKey: string) => Threads.TelegramTemporaryThreadCancellationEvidence | undefined;
|
|
678
|
+
inspectRoutingInputGroupExpiry?: (input: Threads.TelegramTemporaryThreadInput) => readonly Threads.TelegramTemporaryThreadCancellationEvidence[] | undefined;
|
|
677
679
|
/** Strict active-journal observation only; a hint or missing source never substitutes for this ACK. */
|
|
678
680
|
inspectRestoreSourceCompletion?: (expected: Updates.TelegramDeferredSourceEvidence & { completionSha256: string }) =>
|
|
679
681
|
(Updates.TelegramDeferredSourceEvidence & { completionSha256: string }) | undefined;
|
|
@@ -2363,22 +2365,45 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2363
2365
|
* absence of chooser and journal custody, and the ordinary protection checks must all hold again. The attempt is
|
|
2364
2366
|
* durably issued once; a skipped or unknown removal keeps the entry protecting the tab across restart.
|
|
2365
2367
|
*/
|
|
2366
|
-
const runTemporaryThreadCleanup = async (token: string, cap: TemporaryThreadAuthority): Promise<void> => {
|
|
2367
|
-
const store = cap.store
|
|
2368
|
-
if (!
|
|
2368
|
+
const runTemporaryThreadCleanup = async (token: string, cap: TemporaryThreadAuthority, reconcileExpiry: boolean): Promise<void> => {
|
|
2369
|
+
const store = cap.store;
|
|
2370
|
+
if ((!deps.inspectRestoreSourceAbandonment && !deps.inspectRoutingInputGroupExpiry) || !deps.runWorkspaceOperation || !deps.callApi || !cap.isCurrent()) return;
|
|
2369
2371
|
await deps.runWorkspaceOperation({ operationId: `temporary-thread-cleanup-${randomBytes(16).toString("hex")}`,
|
|
2370
2372
|
operationKind: "workspace.temporary-thread", scopes: [{ kind: "profile" }] }, async () => {
|
|
2371
2373
|
if (!cap.isCurrent()) return;
|
|
2372
2374
|
let entry = store.listTemporaryThreads().find(value => value.token === token && value.phase === "created" && !!value.target);
|
|
2373
2375
|
const target = entry?.target;
|
|
2374
|
-
if (!entry || !target || entry.cleanupIssued ||
|
|
2375
|
-
entry.
|
|
2376
|
-
const owner = `telegram-owner:${cap.operatorUserId}
|
|
2377
|
-
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2376
|
+
if (!entry || !target || entry.cleanupIssued || entry.operatorUserId !== cap.operatorUserId ||
|
|
2377
|
+
entry.source.journalBindingKey !== cap.journalBindingKey) return;
|
|
2378
|
+
const owner = `telegram-owner:${cap.operatorUserId}`;
|
|
2379
|
+
if (reconcileExpiry && deps.inspectRoutingInputGroupExpiry) {
|
|
2380
|
+
for (const group of Threads.getTelegramTemporaryThreadInputs(entry)) {
|
|
2381
|
+
if ([...(entry.cancelledInputs ?? []), ...(entry.completedInputs ?? [])].some(value => isDeepStrictEqual(value, group))) continue;
|
|
2382
|
+
const inspect = (id: number) => deps.inspectRoutingInputGroupExpiry!(group)?.find(value => value.updateId === id);
|
|
2383
|
+
if (!group.updateIds.every(id => inspect(id)?.operatorAuthorityId === owner)) continue;
|
|
2384
|
+
entry = cap.adopt(entry);
|
|
2385
|
+
if (!entry || !cap.isCurrent()) return;
|
|
2386
|
+
let recorded: Threads.TelegramTemporaryThreadEntry | undefined, failure: unknown;
|
|
2387
|
+
try { recorded = store.recordTemporaryThreadInputExpiry(entry, group, cap.authority, inspect); }
|
|
2388
|
+
catch (error) { failure = error; }
|
|
2389
|
+
if (!cap.isCurrent()) return;
|
|
2390
|
+
const retained = store.listTemporaryThreads().find(value => value.token === token);
|
|
2391
|
+
// A lost publication ACK may already have recorded the group or retired a bound tab's temporary frame.
|
|
2392
|
+
if (!retained) return;
|
|
2393
|
+
if (!recorded && !retained.cancelledInputs?.some(value => isDeepStrictEqual(value, group)))
|
|
2394
|
+
throw failure ?? new Error("Chooser expiry metadata was not confirmed.");
|
|
2395
|
+
entry = retained;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
if (!Threads.isTelegramTemporaryThreadFullyResolved(entry) || hasPendingRerouteForTarget(target)) return;
|
|
2399
|
+
const cancelled = entry.cancelledInputs ?? [];
|
|
2400
|
+
const cancelledCurrent = () => cancelled.every(input => {
|
|
2401
|
+
const expiry = deps.inspectRoutingInputGroupExpiry?.(input);
|
|
2402
|
+
return input.updateIds.every(updateId => {
|
|
2403
|
+
const evidence = deps.inspectRestoreSourceAbandonment?.(updateId, input.journalBindingKey) ?? expiry?.find(value => value.updateId === updateId);
|
|
2404
|
+
return evidence?.journalBindingKey === input.journalBindingKey && evidence.updateId === updateId && evidence.operatorAuthorityId === owner;
|
|
2405
|
+
});
|
|
2406
|
+
});
|
|
2382
2407
|
if (!cancelledCurrent()) return;
|
|
2383
2408
|
entry = cap.adopt(entry);
|
|
2384
2409
|
if (!entry || !cap.isCurrent() || isRerouteTargetProtected(target, undefined, entry)) return;
|
|
@@ -2394,33 +2419,49 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2394
2419
|
store.retireTemporaryThread(entry, cap.authority);
|
|
2395
2420
|
});
|
|
2396
2421
|
};
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
if (!cap || !deps.runWorkspaceOperation || target.threadId === undefined || hasPendingRerouteForTarget(target)) return;
|
|
2401
|
-
let entry: Threads.TelegramTemporaryThreadEntry | undefined;
|
|
2402
|
-
try { entry = cap.store.listTemporaryThreads().find(value => value.phase === "created" && isDeepStrictEqual(value.target, target)); }
|
|
2403
|
-
catch (error) { deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-schedule" }); return; }
|
|
2404
|
-
if (!entry || entry.cleanupIssued || !Threads.isTelegramTemporaryThreadFullyResolved(entry) ||
|
|
2405
|
-
entry.operatorUserId !== cap.operatorUserId || entry.source.journalBindingKey !== cap.journalBindingKey) return;
|
|
2406
|
-
const token = entry.token;
|
|
2422
|
+
// Reuse the one timer per tab for body-free metadata retries. Future retry timers do not join settlement waits;
|
|
2423
|
+
// only their running attempt does. No retry is licensed once a destructive grant may have been issued.
|
|
2424
|
+
const queueTemporaryThreadCleanup = (token: string, cap: TemporaryThreadAuthority, delay: number, reconcileExpiry: boolean, retry = false): void => {
|
|
2407
2425
|
cancelTemporaryThreadCleanup(token);
|
|
2408
|
-
const requested = deps.temporaryThreadCleanupDelayMs;
|
|
2409
|
-
const delay = typeof requested === "number" && Number.isFinite(requested) ? Math.max(0, requested) : 1000;
|
|
2410
2426
|
let settle = (): void => undefined;
|
|
2411
2427
|
const task = new Promise<void>(resolve => { settle = resolve; });
|
|
2412
2428
|
const timer = setTimeout(() => {
|
|
2413
2429
|
if (temporaryCleanupTimers.get(token)?.timer !== timer) return;
|
|
2414
2430
|
temporaryCleanupTimers.delete(token);
|
|
2415
|
-
|
|
2416
|
-
|
|
2431
|
+
if (retry) restoreSettlementTasks.add(task);
|
|
2432
|
+
runTemporaryThreadCleanup(token, cap, reconcileExpiry)
|
|
2433
|
+
.catch(error => {
|
|
2434
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup" });
|
|
2435
|
+
if (!reconcileExpiry || !cap.isCurrent()) return;
|
|
2436
|
+
try {
|
|
2437
|
+
const retained = cap.store.listTemporaryThreads().find(value => value.token === token);
|
|
2438
|
+
if (retained && !retained.cleanupIssued && cap.isCurrent()) queueTemporaryThreadCleanup(token, cap, 60_000, true, true);
|
|
2439
|
+
} catch { /* Unknown grant state never licenses a retry. */ }
|
|
2440
|
+
})
|
|
2417
2441
|
.finally(settle);
|
|
2418
2442
|
}, delay);
|
|
2419
2443
|
timer.unref?.();
|
|
2420
2444
|
temporaryCleanupTimers.set(token, { timer, settle });
|
|
2421
|
-
restoreSettlementTasks.add(task);
|
|
2445
|
+
if (!retry) restoreSettlementTasks.add(task);
|
|
2422
2446
|
void task.finally(() => restoreSettlementTasks.delete(task));
|
|
2423
2447
|
};
|
|
2448
|
+
/** Starts the quiet period for resolved groups or body-free expiry reconciliation; fresh input/authority revokes it. */
|
|
2449
|
+
const scheduleTemporaryThreadCleanup = (target: Queue.TelegramQueueTarget, ctx: TContext, reconcileExpiry = false): void => {
|
|
2450
|
+
const cap = captureTemporaryThreadAuthority(ctx);
|
|
2451
|
+
if (!cap || !deps.runWorkspaceOperation || target.threadId === undefined || hasPendingRerouteForTarget(target)) return;
|
|
2452
|
+
let entry: Threads.TelegramTemporaryThreadEntry | undefined;
|
|
2453
|
+
try { entry = cap.store.listTemporaryThreads().find(value => value.phase === "created" && isDeepStrictEqual(value.target, target)); }
|
|
2454
|
+
catch (error) { deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-schedule" }); return; }
|
|
2455
|
+
if (!entry || entry.cleanupIssued || entry.operatorUserId !== cap.operatorUserId || entry.source.journalBindingKey !== cap.journalBindingKey) return;
|
|
2456
|
+
if (!reconcileExpiry && !Threads.isTelegramTemporaryThreadFullyResolved(entry)) {
|
|
2457
|
+
try { reconcileExpiry = Threads.getTelegramTemporaryThreadInputs(entry).some(group => !!deps.inspectRoutingInputGroupExpiry?.(group)); }
|
|
2458
|
+
catch (error) { deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-schedule" }); return; }
|
|
2459
|
+
}
|
|
2460
|
+
if (!reconcileExpiry && !Threads.isTelegramTemporaryThreadFullyResolved(entry)) return;
|
|
2461
|
+
const requested = deps.temporaryThreadCleanupDelayMs;
|
|
2462
|
+
const delay = typeof requested === "number" && Number.isFinite(requested) ? Math.max(0, requested) : 1000;
|
|
2463
|
+
queueTemporaryThreadCleanup(entry.token, cap, delay, reconcileExpiry);
|
|
2464
|
+
};
|
|
2424
2465
|
const recordCancelledTemporaryThreadInput = (pending: PendingUnboundReroute, ctx: TContext, isCurrent: () => boolean): boolean => {
|
|
2425
2466
|
const store = deps.getWorkspaceRestoreStore?.(), target = pending.sourceTarget;
|
|
2426
2467
|
if (!store || target.threadId === undefined) return !pending.temporaryThread;
|
|
@@ -2490,10 +2531,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2490
2531
|
await threads.load();
|
|
2491
2532
|
if (!cap.isCurrent()) return;
|
|
2492
2533
|
const self = cap.authority.executor.instanceId;
|
|
2493
|
-
const
|
|
2534
|
+
const entries = cap.store.listTemporaryThreads();
|
|
2535
|
+
const preserved = entries.filter(entry => entry.operatorUserId === cap.operatorUserId && entry.phase === "created" &&
|
|
2536
|
+
cap.store.inspectTemporaryThreadTarget(entry, cap.authority)?.kind === "temporary" &&
|
|
2537
|
+
Threads.getTelegramTemporaryThreadInputs(entry).some(source => source.journalBindingKey === journalBindingKey &&
|
|
2538
|
+
source.updateIds.some(id => input.routingSourceIds?.includes(id)))).map(entry => entry.token);
|
|
2539
|
+
const disposable = entries.flatMap(entry => entry.operatorUserId === cap.operatorUserId && !preserved.includes(entry.token) &&
|
|
2494
2540
|
entry.executor.instanceId !== self && entry.phase === "created" && entry.target &&
|
|
2495
2541
|
cap.store.inspectTemporaryThreadTarget(entry, cap.authority)?.kind === "temporary" ? [entry] : []);
|
|
2496
|
-
const forgotten = cap.store.forgetPreviousWorld(cap.authority);
|
|
2542
|
+
const forgotten = cap.store.forgetPreviousWorld(cap.authority, preserved);
|
|
2497
2543
|
if (!forgotten) return;
|
|
2498
2544
|
result.forgotten = forgotten.operations.length + forgotten.temporaryThreads.length;
|
|
2499
2545
|
for (const entry of disposable) {
|
|
@@ -2514,6 +2560,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2514
2560
|
{ maxAttempts: 1, retrySafety: "non-idempotent" }) === true) result.deleted++;
|
|
2515
2561
|
} catch (error) { deps.recordRuntimeEvent?.("routing", error, { phase: "new-world-tab-delete" }); }
|
|
2516
2562
|
}
|
|
2563
|
+
// Same-instance session replacement may have lost a metadata-only timer after the source was already spent.
|
|
2564
|
+
// Reconstruct from retained tab/source proof under fresh authority, never from a prompt body or an issued delete.
|
|
2565
|
+
for (const entry of cap.store.listTemporaryThreads()) {
|
|
2566
|
+
if (!cap.isCurrent()) break;
|
|
2567
|
+
if (entry.phase !== "created" || !entry.target || entry.cleanupIssued || entry.operatorUserId !== cap.operatorUserId ||
|
|
2568
|
+
entry.source.journalBindingKey !== journalBindingKey) continue;
|
|
2569
|
+
if (Threads.getTelegramTemporaryThreadInputs(entry).some(group => !!deps.inspectRoutingInputGroupExpiry?.(group)))
|
|
2570
|
+
scheduleTemporaryThreadCleanup(entry.target, ctx, true);
|
|
2571
|
+
}
|
|
2517
2572
|
});
|
|
2518
2573
|
} catch (error) { deps.recordRuntimeEvent?.("routing", error, { phase: "new-world-forget" }); }
|
|
2519
2574
|
return result;
|
|
@@ -2585,25 +2640,22 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2585
2640
|
const expireRoutingInput = async (source: Updates.TelegramRoutingInputExpirySource, ctx: TContext, signal: AbortSignal): Promise<void> => {
|
|
2586
2641
|
const operator = deps.configStore.getAllowedUserId(), epoch = deps.getCurrentLeaderEpoch?.();
|
|
2587
2642
|
const message = source.original.update.message as Partial<TMessage> | undefined;
|
|
2588
|
-
const
|
|
2643
|
+
const runtimeCurrent = () => !signal.aborted && operator !== undefined && epoch !== undefined &&
|
|
2589
2644
|
deps.isContextActive?.(ctx) === true && deps.configStore.getAllowedUserId() === operator &&
|
|
2590
2645
|
deps.getCurrentLeaderEpoch?.() === epoch && deps.getAdmissionJournalBinding?.() === source.journalBindingKey &&
|
|
2591
2646
|
source.original.routingInput?.operatorUserId === operator && message?.from?.id === operator && message.from.is_bot !== true && message.chat?.type === "private";
|
|
2647
|
+
const current = () => runtimeCurrent() && source.isCurrent();
|
|
2592
2648
|
if (!current() || !deps.runWorkspaceOperation) return;
|
|
2593
2649
|
await deps.runWorkspaceOperation({ operationId: `routing-expiry-${randomBytes(16).toString("hex")}`,
|
|
2594
2650
|
operationKind: "workspace.expire-unbound-routing", scopes: [{ kind: "profile" }] }, async () => {
|
|
2595
2651
|
if (!current()) return;
|
|
2596
2652
|
const restores = deps.getWorkspaceRestoreStore?.();
|
|
2597
2653
|
if (deps.getWorkspaceRestoreStore && !restores) return;
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
for (const pending of pendingUnboundReroutes.values()) {
|
|
2601
|
-
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId)) continue;
|
|
2602
|
-
if (pending.selectionAttempted || pending.destinationSelected || pending.dispatching || pending.foreignForwardIssued || pending.workspaceRestore || pending.foreignRetry || pending.cleanup || pending.abandonment?.running) return;
|
|
2603
|
-
}
|
|
2654
|
+
const temporary = restores?.listTemporaryThreads().find(entry => Threads.getTelegramTemporaryThreadInputs(entry).some(input =>
|
|
2655
|
+
input.journalBindingKey === source.journalBindingKey && input.updateIds.includes(source.original.updateId)));
|
|
2604
2656
|
const result = source.expire();
|
|
2605
2657
|
if (!result) return;
|
|
2606
|
-
//
|
|
2658
|
+
// Expiry revokes the donor carrier; old controls and late ACKs cannot issue another delivery. No body is archived.
|
|
2607
2659
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
2608
2660
|
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId)) continue;
|
|
2609
2661
|
removePendingReroute(id);
|
|
@@ -2615,6 +2667,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2615
2667
|
catch (error) { deps.recordRuntimeEvent?.("routing", error, { phase: "routing-input-expiry-view" }); }
|
|
2616
2668
|
}
|
|
2617
2669
|
}
|
|
2670
|
+
// The scheduled stage captures only context/epoch and a tab token. It reconstructs disposition from body-free proof,
|
|
2671
|
+
// never closes over this source or asks the worker to retain a terminal prompt while metadata publication fails.
|
|
2672
|
+
if (temporary?.target && runtimeCurrent()) scheduleTemporaryThreadCleanup(temporary.target, ctx, true);
|
|
2618
2673
|
});
|
|
2619
2674
|
};
|
|
2620
2675
|
const renderCancellationReview = async (view: CancellationReview, isCurrent: () => boolean): Promise<void> => {
|
package/lib/telegram-api.ts
CHANGED
|
@@ -1701,6 +1701,11 @@ export async function downloadTelegramFile(
|
|
|
1701
1701
|
return targetPath;
|
|
1702
1702
|
}
|
|
1703
1703
|
|
|
1704
|
+
/** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
|
|
1705
|
+
export function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined {
|
|
1706
|
+
return text?.replace(/(?<!\.)\.\s*$/u, "");
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1704
1709
|
export async function answerTelegramCallbackQuery(
|
|
1705
1710
|
botToken: string | undefined,
|
|
1706
1711
|
callbackQueryId: string,
|
package/lib/threads.ts
CHANGED
|
@@ -332,7 +332,7 @@ export interface TelegramTemporaryThreadInput {
|
|
|
332
332
|
journalBindingKey: string;
|
|
333
333
|
updateIds: number[];
|
|
334
334
|
}
|
|
335
|
-
/**
|
|
335
|
+
/** Journal-owned donor discard evidence: retained manual cancellation or body-free chooser expiry, never recipient cancellation. */
|
|
336
336
|
export interface TelegramTemporaryThreadCancellationEvidence {
|
|
337
337
|
journalBindingKey: string;
|
|
338
338
|
updateId: number;
|
|
@@ -343,7 +343,7 @@ export interface TelegramTemporaryThreadEntry {
|
|
|
343
343
|
source: { journalBindingKey: string; updateId: number };
|
|
344
344
|
/** Append-only known source groups. Missing legacy metadata never proves that the creation source was alone. */
|
|
345
345
|
inputs?: TelegramTemporaryThreadInput[];
|
|
346
|
-
/** Whole known groups with positively observed cancellation; never
|
|
346
|
+
/** Whole known groups with positively observed donor cancellation or expiry; never recipient cancellation or deletion authority. */
|
|
347
347
|
cancelledInputs?: TelegramTemporaryThreadInput[];
|
|
348
348
|
/** Whole known groups whose Forward was positively completed by the journal owner; never deletion authority alone. */
|
|
349
349
|
completedInputs?: TelegramTemporaryThreadInput[];
|
|
@@ -415,6 +415,10 @@ export interface TelegramWorkspaceRestore {
|
|
|
415
415
|
recordTemporaryThreadInputCancellation(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput,
|
|
416
416
|
authority: TelegramWorkspaceRestoreAuthority,
|
|
417
417
|
inspect: (updateId: number) => TelegramTemporaryThreadCancellationEvidence | undefined): TelegramTemporaryThreadEntry | undefined;
|
|
418
|
+
/** Body-free chooser expiry may terminate an uncertain donor Forward/Restore, never accepted recipient work or a bound target. */
|
|
419
|
+
recordTemporaryThreadInputExpiry(expected: TelegramTemporaryThreadEntry, input: TelegramTemporaryThreadInput,
|
|
420
|
+
authority: TelegramWorkspaceRestoreAuthority,
|
|
421
|
+
inspect: (updateId: number) => TelegramTemporaryThreadCancellationEvidence | undefined): TelegramTemporaryThreadEntry | undefined;
|
|
418
422
|
/** Caller holds profile admission and proves fresh source/protection clearance; publication grants one attempt, never retry. */
|
|
419
423
|
issueTemporaryThreadCleanup(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority):
|
|
420
424
|
{ issued: true; entry: TelegramTemporaryThreadEntry } | undefined;
|
|
@@ -423,8 +427,8 @@ export interface TelegramWorkspaceRestore {
|
|
|
423
427
|
retireTemporaryThread(expected: TelegramTemporaryThreadEntry, authority: TelegramWorkspaceRestoreAuthority,
|
|
424
428
|
completed?: TelegramTemporaryThreadInput): TelegramTemporaryThreadEntry | undefined;
|
|
425
429
|
/** New-world restart: atomically forgets this operator's Restore intents and temporary entries from previous runtime instances.
|
|
426
|
-
* Committed bindings stay; nothing is rolled back, replayed or deleted here. */
|
|
427
|
-
forgetPreviousWorld(authority: TelegramWorkspaceRestoreAuthority):
|
|
430
|
+
* Caller may preserve exact unbound temporary tokens for clock-bearing sources. Committed bindings stay; nothing is rolled back, replayed or deleted here. */
|
|
431
|
+
forgetPreviousWorld(authority: TelegramWorkspaceRestoreAuthority, preserveTemporaryTokens?: readonly string[]):
|
|
428
432
|
{ operations: TelegramWorkspaceRestoreIntent[]; temporaryThreads: TelegramTemporaryThreadEntry[] } | undefined;
|
|
429
433
|
}
|
|
430
434
|
export interface TelegramWorkspaceRestoreOptions {
|
|
@@ -3595,6 +3599,40 @@ export function createTelegramTopicTargetStore(
|
|
|
3595
3599
|
});
|
|
3596
3600
|
return recorded && current() ? recorded : undefined;
|
|
3597
3601
|
},
|
|
3602
|
+
recordTemporaryThreadInputExpiry(expected, inputValue, authority, inspect) {
|
|
3603
|
+
const input = structuredClone(inputValue);
|
|
3604
|
+
if (!isTemporaryThreadInput(input) || typeof inspect !== "function") return undefined;
|
|
3605
|
+
const operator = authority.operatorUserId, executor = structuredClone(authority.executor);
|
|
3606
|
+
const bound = classifyTemporaryTarget(read(), expected).kind === "bound";
|
|
3607
|
+
const current = () => authority.isCurrent() && authority.operatorUserId === operator &&
|
|
3608
|
+
isDeepStrictEqual(authority.executor, executor) && input.updateIds.every(updateId => {
|
|
3609
|
+
const evidence = inspect(updateId);
|
|
3610
|
+
return evidence?.journalBindingKey === input.journalBindingKey && evidence.updateId === updateId &&
|
|
3611
|
+
evidence.operatorAuthorityId === `telegram-owner:${operator}`;
|
|
3612
|
+
}) && authority.isCurrent();
|
|
3613
|
+
return advanceTemporary(expected, { ...authority, isCurrent: current }, (entry, file) => {
|
|
3614
|
+
if (entry.phase !== "created" || !getTelegramTemporaryThreadInputs(entry).some(value => isDeepStrictEqual(value, input)) ||
|
|
3615
|
+
entry.completedInputs?.some(value => isDeepStrictEqual(value, input))) return false;
|
|
3616
|
+
const cancelled = entry.cancelledInputs ?? [];
|
|
3617
|
+
if (cancelled.some(value => isDeepStrictEqual(value, input))) return "unchanged";
|
|
3618
|
+
const operations = file.operations.filter(({ request }) => request.source.journalBindingKey === input.journalBindingKey &&
|
|
3619
|
+
request.source.updateIds.some(id => input.updateIds.includes(id)));
|
|
3620
|
+
if (operations.some(({ request }) => !request.source.updateIds.every(id => input.updateIds.includes(id)))) return false;
|
|
3621
|
+
// Forget only expired donor intent; canonical binding, recipient queue and immutable settlement proofs are untouched.
|
|
3622
|
+
file.operations = file.operations.filter(value => !operations.includes(value));
|
|
3623
|
+
// Once bound, this is no longer a disposable tab. Forget its temporary frame, not the binding or other journal sources.
|
|
3624
|
+
if (bound) {
|
|
3625
|
+
file.temporaryThreads = file.temporaryThreads?.filter(value => value !== entry);
|
|
3626
|
+
if (!file.temporaryThreads?.length) delete file.temporaryThreads;
|
|
3627
|
+
return true;
|
|
3628
|
+
}
|
|
3629
|
+
const forwarded = entry.forwardedInputs?.filter(value => !isDeepStrictEqual(value, input));
|
|
3630
|
+
if (forwarded?.length) entry.forwardedInputs = forwarded;
|
|
3631
|
+
else delete entry.forwardedInputs;
|
|
3632
|
+
entry.cancelledInputs = [...cancelled, input];
|
|
3633
|
+
return true;
|
|
3634
|
+
});
|
|
3635
|
+
},
|
|
3598
3636
|
recordTemporaryThreadForwardIssued(expected, inputValue, authority) {
|
|
3599
3637
|
const input = structuredClone(inputValue);
|
|
3600
3638
|
if (!isTemporaryThreadInput(input)) return undefined;
|
|
@@ -3644,7 +3682,7 @@ export function createTelegramTopicTargetStore(
|
|
|
3644
3682
|
return entry;
|
|
3645
3683
|
});
|
|
3646
3684
|
},
|
|
3647
|
-
forgetPreviousWorld(authority) {
|
|
3685
|
+
forgetPreviousWorld(authority, preserveTemporaryTokens = []) {
|
|
3648
3686
|
if (!authority.isCurrent()) return undefined;
|
|
3649
3687
|
const executor = structuredClone(authority.executor), operator = authority.operatorUserId;
|
|
3650
3688
|
if (!isTelegramWorkspaceRestoreExecutor(executor) || !restoreInteger(operator)) return undefined;
|
|
@@ -3652,11 +3690,12 @@ export function createTelegramTopicTargetStore(
|
|
|
3652
3690
|
const previous = (value: { operatorUserId: number; executor: TelegramWorkspaceRestoreExecutor }) =>
|
|
3653
3691
|
value.operatorUserId === operator && value.executor.instanceId !== executor.instanceId;
|
|
3654
3692
|
const before = storage.read(), file = structuredClone(before);
|
|
3655
|
-
const
|
|
3693
|
+
const forgetTemporary = (value: TelegramTemporaryThreadEntry) => previous(value) && !preserveTemporaryTokens.includes(value.token);
|
|
3694
|
+
const operations = file.operations.filter(previous), temporaryThreads = (file.temporaryThreads ?? []).filter(forgetTemporary);
|
|
3656
3695
|
if (!current()) return undefined;
|
|
3657
3696
|
if (!operations.length && !temporaryThreads.length) return { operations, temporaryThreads };
|
|
3658
3697
|
file.operations = file.operations.filter(value => !previous(value));
|
|
3659
|
-
const remaining = (file.temporaryThreads ?? []).filter(value => !
|
|
3698
|
+
const remaining = (file.temporaryThreads ?? []).filter(value => !forgetTemporary(value));
|
|
3660
3699
|
if (remaining.length) file.temporaryThreads = remaining;
|
|
3661
3700
|
else delete file.temporaryThreads;
|
|
3662
3701
|
if (file.revision === Number.MAX_SAFE_INTEGER) throw new Error("Workspace Restore revision exhausted.");
|
package/lib/updates.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
type TelegramUpdateJournalEntry,
|
|
57
57
|
type TelegramUpdateJournalRoutingInput,
|
|
58
58
|
type TelegramRoutingInputJournal,
|
|
59
|
+
type TelegramRoutingInputExpiryResult,
|
|
59
60
|
type TelegramUpdateJournalQueueDiscardResult,
|
|
60
61
|
type TelegramUpdateJournalQueueHandoffAcceptResult,
|
|
61
62
|
type TelegramUpdateJournalQueueHandoffCancelResult,
|
|
@@ -495,6 +496,8 @@ export interface TelegramHeldSourcePreparation<TContext> {
|
|
|
495
496
|
/** Exact prepared worker owner, context, key and authority; the consumer adds its own domain fences. */
|
|
496
497
|
isCurrent: () => boolean;
|
|
497
498
|
signal: AbortSignal;
|
|
499
|
+
/** Chooser-clock sources that must survive cold spending until their fixed expiry. */
|
|
500
|
+
routingSourceIds?: readonly number[];
|
|
498
501
|
}
|
|
499
502
|
|
|
500
503
|
export interface TelegramDeferredAbandonmentRecoveryPage {
|
|
@@ -1922,7 +1925,7 @@ export interface TelegramRoutingInputExpirySource {
|
|
|
1922
1925
|
original: TelegramUpdateJournalEntry;
|
|
1923
1926
|
journalBindingKey: string;
|
|
1924
1927
|
isCurrent(): boolean;
|
|
1925
|
-
expire():
|
|
1928
|
+
expire(): TelegramRoutingInputExpiryResult | undefined;
|
|
1926
1929
|
}
|
|
1927
1930
|
|
|
1928
1931
|
export interface TelegramUpdateWorkerJournalPort {
|
|
@@ -2035,7 +2038,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
|
|
|
2035
2038
|
getRecipientBindingKey?: () => string | undefined;
|
|
2036
2039
|
/** True selects legacy historical review/spending; retain protects unsupported originals without disposition authority. */
|
|
2037
2040
|
shouldReviewHistoricalInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | "retain" | Promise<boolean | "retain">;
|
|
2038
|
-
/** New-world restart: spend
|
|
2041
|
+
/** New-world restart: spend unclocked classified routing input without delivery, copy or completion. Chooser clocks wait for expiry; interrupted private abandonment keeps exact recovery. */
|
|
2039
2042
|
spendHistoricalInput?: boolean;
|
|
2040
2043
|
/** Holds protected live or retry sources before execution; never cancels or disposes them. */
|
|
2041
2044
|
shouldHoldPendingInput?: (entry: TelegramUpdateJournalEntry, ctx: TContext, signal: AbortSignal) => boolean | Promise<boolean>;
|
|
@@ -2090,6 +2093,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
2090
2093
|
}) => void;
|
|
2091
2094
|
armRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => TelegramUpdateJournalRoutingInput | undefined;
|
|
2092
2095
|
selectRoutingInput?: (input: { updateId: number; signal: AbortSignal; operatorUserId: number; sourceUpdateIds: readonly number[] }) => boolean;
|
|
2096
|
+
isRoutingInputCurrent?: (input: { updateId: number; signal: AbortSignal }) => boolean;
|
|
2093
2097
|
supportsDeferredAbandonment?: (input: { updateId: number; signal: AbortSignal; journalBindingKey: string }) => boolean;
|
|
2094
2098
|
inspectAbandoning?: (input: TelegramDeferredAbandonmentRecoveryRequest & { signal: AbortSignal }) =>
|
|
2095
2099
|
TelegramDeferredAbandonmentRecoveryPage | undefined;
|
|
@@ -3215,18 +3219,29 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3215
3219
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !expireRouting || !expireRoutingObserver) return false;
|
|
3216
3220
|
let nextAtMs: number | undefined, changed = false;
|
|
3217
3221
|
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
|
|
3218
|
-
// Lost
|
|
3222
|
+
// Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
|
|
3219
3223
|
const candidates = [...snapshot.entries, ...[...deferredSources.values()]
|
|
3220
3224
|
.filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
|
|
3221
3225
|
.map(source => source.entry)];
|
|
3222
3226
|
for (const entry of candidates) {
|
|
3223
3227
|
const lifetime = entry.routingInput;
|
|
3224
|
-
if (
|
|
3225
|
-
|
|
3226
|
-
|
|
3228
|
+
if (!lifetime || entry.state !== "pending" || !binding || claims.get(entry.updateId) === "queued" ||
|
|
3229
|
+
unsettledExecutionsByUpdateId.get(entry.updateId)?.size) continue;
|
|
3230
|
+
// Classify the first startup snapshot before constructing any historical expiry carrier.
|
|
3231
|
+
if (expectedOwner.startupUpdateIds?.has(entry.updateId) && !claims.has(entry.updateId) && deps.shouldReviewHistoricalInput) {
|
|
3232
|
+
nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3227
3235
|
if (getNowMs() < lifetime.expiresAtMs) { nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs); continue; }
|
|
3228
3236
|
const existing = deferredSources.get(entry.updateId);
|
|
3229
|
-
if (existing && !isDeepStrictEqual(existing.entry, entry))
|
|
3237
|
+
if (existing && !isDeepStrictEqual(existing.entry, entry)) {
|
|
3238
|
+
// Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
|
|
3239
|
+
const previous = existing.entry.routingInput;
|
|
3240
|
+
if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
|
|
3241
|
+
(previous && (!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
|
|
3242
|
+
(previous.phase === "selected" && lifetime.phase !== "selected")))) continue;
|
|
3243
|
+
existing.entry = structuredClone(entry);
|
|
3244
|
+
}
|
|
3230
3245
|
const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true as const };
|
|
3231
3246
|
deferredSources.set(entry.updateId, source);
|
|
3232
3247
|
if (!claims.has(entry.updateId)) claims.set(entry.updateId, "historical");
|
|
@@ -3234,7 +3249,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3234
3249
|
deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
|
|
3235
3250
|
deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
|
|
3236
3251
|
isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
|
|
3237
|
-
let committed:
|
|
3252
|
+
let committed: TelegramRoutingInputExpiryResult | undefined;
|
|
3238
3253
|
try {
|
|
3239
3254
|
if (current()) await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
|
|
3240
3255
|
expire() {
|
|
@@ -3242,9 +3257,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3242
3257
|
if (committed) return { ...committed, duplicate: true };
|
|
3243
3258
|
claims.set(entry.updateId, "abandoning");
|
|
3244
3259
|
committed = expireRouting!({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
|
|
3260
|
+
// Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
|
|
3245
3261
|
claims.delete(entry.updateId); deferredSources.delete(entry.updateId);
|
|
3246
3262
|
state.journalEntryCount = committed.entryCount; state.journalSerializedBytes = committed.serializedBytes;
|
|
3247
|
-
updateClaimCounts(); notifyStateChange(); changed
|
|
3263
|
+
updateClaimCounts(); notifyStateChange(); changed ||= !committed.duplicate;
|
|
3248
3264
|
return committed;
|
|
3249
3265
|
} }, expectedOwner.ctx, expectedOwner.controller.signal);
|
|
3250
3266
|
} catch (error) { recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId }); }
|
|
@@ -3445,7 +3461,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3445
3461
|
transition("deferred", entry.updateId);
|
|
3446
3462
|
continue;
|
|
3447
3463
|
}
|
|
3448
|
-
if (review && historical && spendHistorical && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
3464
|
+
if (review && historical && spendHistorical && !entry.routingInput && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
3449
3465
|
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
3450
3466
|
continue;
|
|
3451
3467
|
}
|
|
@@ -3456,10 +3472,6 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3456
3472
|
}
|
|
3457
3473
|
}
|
|
3458
3474
|
if (entry.state === "pending" && entry.routingInput) {
|
|
3459
|
-
if (historical && spendHistorical) {
|
|
3460
|
-
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
3461
|
-
continue;
|
|
3462
|
-
}
|
|
3463
3475
|
claims.set(entry.updateId, "historical");
|
|
3464
3476
|
transition("deferred", entry.updateId);
|
|
3465
3477
|
continue;
|
|
@@ -3628,7 +3640,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3628
3640
|
expectedOwner.heldSourcesPreparedIssued = true;
|
|
3629
3641
|
// Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
|
|
3630
3642
|
void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
|
|
3631
|
-
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent
|
|
3643
|
+
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent,
|
|
3644
|
+
routingSourceIds: [...deferredSources].filter(([, source]) => source.journalBindingKey === key && !!source.entry.routingInput)
|
|
3645
|
+
.map(([id]) => id) }) : undefined).catch(error => {
|
|
3632
3646
|
try { deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" }); } catch { /* Diagnostic only. */ }
|
|
3633
3647
|
});
|
|
3634
3648
|
} catch (error) {
|
|
@@ -3784,6 +3798,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3784
3798
|
},
|
|
3785
3799
|
armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
|
|
3786
3800
|
selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
|
|
3801
|
+
isRoutingInputCurrent(input) {
|
|
3802
|
+
return owner?.controller.signal === input.signal && !input.signal.aborted &&
|
|
3803
|
+
deferredSources.has(input.updateId) && claims.get(input.updateId) !== "abandoning";
|
|
3804
|
+
},
|
|
3787
3805
|
inspectDeferredSource(input) {
|
|
3788
3806
|
const expectedOwner = owner;
|
|
3789
3807
|
const source = deferredSources.get(input.updateId);
|
|
@@ -3843,7 +3861,8 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3843
3861
|
return;
|
|
3844
3862
|
}
|
|
3845
3863
|
const claim = claims.get(input.updateId);
|
|
3846
|
-
if (claim === "abandoning" || claim === "historical" || claim === "retained"
|
|
3864
|
+
if (claim === "abandoning" || claim === "historical" || claim === "retained" ||
|
|
3865
|
+
(!claim && routingJournal?.inspectExpiry(input.updateId))) return;
|
|
3847
3866
|
if (input.outcome.kind === "complete") {
|
|
3848
3867
|
// Expiry may retire only a still-deferred source, never accepted queue work.
|
|
3849
3868
|
if (claim !== "deferred") return;
|
|
@@ -4453,6 +4472,7 @@ export interface TelegramUpdateAdmissionHandleDeps<
|
|
|
4453
4472
|
abandonDeferred?: TelegramUpdateWorkerRuntime<TContext>["abandonDeferred"];
|
|
4454
4473
|
armRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["armRoutingInput"];
|
|
4455
4474
|
selectRoutingInput?: TelegramUpdateWorkerRuntime<TContext>["selectRoutingInput"];
|
|
4475
|
+
isRoutingInputCurrent?: TelegramUpdateWorkerRuntime<TContext>["isRoutingInputCurrent"];
|
|
4456
4476
|
supportsDeferredAbandonment?: TelegramUpdateWorkerRuntime<TContext>["supportsDeferredAbandonment"];
|
|
4457
4477
|
inspectAbandoning?: TelegramUpdateWorkerRuntime<TContext>["inspectAbandoning"];
|
|
4458
4478
|
inspectHistorical?: TelegramUpdateWorkerRuntime<TContext>["inspectHistorical"];
|
|
@@ -5483,9 +5503,10 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
5483
5503
|
generation,
|
|
5484
5504
|
updateId: update.update_id,
|
|
5485
5505
|
signal,
|
|
5486
|
-
isCurrent: () => !signal.aborted && !suspended
|
|
5506
|
+
isCurrent: () => !signal.aborted && !suspended && ((!routingLifetime && !routingClockUnknown) || immediate || settlementReported ||
|
|
5507
|
+
deps.isRoutingInputCurrent?.({ updateId: update.update_id, signal }) !== false),
|
|
5487
5508
|
assertCurrent() {
|
|
5488
|
-
if (
|
|
5509
|
+
if (!execution.isCurrent()) {
|
|
5489
5510
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
5490
5511
|
}
|
|
5491
5512
|
},
|
|
@@ -5499,7 +5520,7 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
5499
5520
|
let outcome: TelegramUpdateAdmissionOutcome | undefined;
|
|
5500
5521
|
const boundUpdate = bindTelegramUpdateExecutionFence(
|
|
5501
5522
|
bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
5502
|
-
if (
|
|
5523
|
+
if (!execution.isCurrent()) return;
|
|
5503
5524
|
if (next.kind === "deferred" && next.routingReview) {
|
|
5504
5525
|
if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
|
|
5505
5526
|
throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
|
|
@@ -5522,7 +5543,7 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
5522
5543
|
}
|
|
5523
5544
|
void Promise.resolve()
|
|
5524
5545
|
.then(() => {
|
|
5525
|
-
if (
|
|
5546
|
+
if (!execution.isCurrent()) return;
|
|
5526
5547
|
return deps.onLateOutcome!(next, {
|
|
5527
5548
|
updateId: update.update_id,
|
|
5528
5549
|
ctx,
|
|
@@ -5605,7 +5626,7 @@ export function createTelegramCustodiedUpdateAdmissionHandle<
|
|
|
5605
5626
|
TUpdate extends TelegramJournaledUpdate & TelegramUpdateFlow,
|
|
5606
5627
|
TContext,
|
|
5607
5628
|
>(deps: Omit<TelegramUpdateAdmissionHandleDeps<TUpdate, TContext>,
|
|
5608
|
-
"onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
|
|
5629
|
+
"onLateOutcome" | "onLateOutcomeError" | "abandonDeferred" | "armRoutingInput" | "selectRoutingInput" | "isRoutingInputCurrent" | "supportsDeferredAbandonment" | "inspectAbandoning" | "inspectHistorical" | "inspectDeferredSource" | "isHistoricalSource"> & {
|
|
5609
5630
|
journal: TelegramCustodyExecutionJournal;
|
|
5610
5631
|
recipientBindingKey: string;
|
|
5611
5632
|
onLateOutcomeError(error: unknown, updateId: number): void;
|
|
@@ -6983,6 +7004,7 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
|
|
|
6983
7004
|
abandonDeferred: input => worker?.abandonDeferred?.(input),
|
|
6984
7005
|
armRoutingInput: input => worker?.armRoutingInput?.(input),
|
|
6985
7006
|
selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
|
|
7007
|
+
isRoutingInputCurrent: input => worker?.isRoutingInputCurrent?.(input) !== false,
|
|
6986
7008
|
supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
|
|
6987
7009
|
inspectAbandoning: input => worker?.inspectAbandoning?.(input),
|
|
6988
7010
|
inspectHistorical: input => worker?.inspectHistorical?.(input),
|