@llblab/pi-telegram 0.54.3 → 0.54.4
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/CHANGELOG.md +10 -0
- package/dist/lib/bindings.d.ts +3 -1
- package/dist/lib/bindings.js +2 -1
- package/dist/lib/extension.js +8 -0
- package/dist/lib/routing.d.ts +8 -1
- package/dist/lib/routing.js +282 -67
- package/dist/lib/threads.d.ts +2 -0
- package/dist/lib/threads.js +4 -2
- package/dist/package.json +1 -1
- package/docs/architecture.md +5 -5
- package/docs/multi-instance-bus.md +2 -2
- package/docs/ui-style.md +6 -4
- package/lib/bindings.ts +4 -0
- package/lib/extension.ts +8 -0
- package/lib/routing.ts +357 -83
- package/lib/threads.ts +10 -2
- package/package.json +1 -1
package/lib/routing.ts
CHANGED
|
@@ -462,7 +462,8 @@ function formatTelegramUnboundRerouteChooserText(command?: string): string {
|
|
|
462
462
|
function formatTelegramUnboundRerouteRestoreChooserText(
|
|
463
463
|
command?: string,
|
|
464
464
|
): string {
|
|
465
|
-
|
|
465
|
+
const subject = command ? ` for ${formatTelegramRouteSubject(command)}` : "";
|
|
466
|
+
return `<b>🔁 Restore a Pi into this tab${subject}:</b>`;
|
|
466
467
|
}
|
|
467
468
|
|
|
468
469
|
function formatTelegramUnboundTopicGuidance(): string {
|
|
@@ -585,7 +586,10 @@ function classifyLiveRebindCleanupFailure(
|
|
|
585
586
|
: "unknown";
|
|
586
587
|
}
|
|
587
588
|
|
|
588
|
-
/**
|
|
589
|
+
/**
|
|
590
|
+
* Default live-rebind cleanup pacing: quick early attempts, then minutely, within a 15-minute window. The first
|
|
591
|
+
* delay is not cosmetic: a leader's detached command reply still needs the released, cleanup-free row.
|
|
592
|
+
*/
|
|
589
593
|
export const TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE = Object.freeze({
|
|
590
594
|
delaysMs: Object.freeze([1_000, 2_000, 5_000, 10_000, 30_000]),
|
|
591
595
|
intervalMs: 60_000,
|
|
@@ -643,6 +647,8 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
643
647
|
/** Explicit staged singleton command branch; absence preserves ordinary live input behavior. */
|
|
644
648
|
selectedCommand?: Bus.TelegramBusSelectedCommandInput;
|
|
645
649
|
};
|
|
650
|
+
/** Restore moves the tab to its new owner: once applied, show that owner's title (best-effort, never blocking). */
|
|
651
|
+
retitle?: (isCurrent: () => boolean) => Promise<void>;
|
|
646
652
|
recordRuntimeEvent?: (
|
|
647
653
|
category: string,
|
|
648
654
|
error: unknown,
|
|
@@ -773,6 +779,7 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
773
779
|
let saved = false,
|
|
774
780
|
commitIssued = false,
|
|
775
781
|
applyIssued = false,
|
|
782
|
+
retitleIssued = false,
|
|
776
783
|
peerReleaseIssued = false,
|
|
777
784
|
donorSettlementAttempted = false,
|
|
778
785
|
continuationIssued = false,
|
|
@@ -1030,6 +1037,17 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
1030
1037
|
}
|
|
1031
1038
|
if (!sourceSaved() || (preparation && !preparation.confirmSaved()))
|
|
1032
1039
|
return "protected";
|
|
1040
|
+
if (input.retitle && !retitleIssued) {
|
|
1041
|
+
retitleIssued = true;
|
|
1042
|
+
try {
|
|
1043
|
+
await input.retitle(current);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
input.recordRuntimeEvent?.("telegram", error, {
|
|
1046
|
+
phase: "live-rebind-title",
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (!current() || !canonical(intent)) return "unknown";
|
|
1050
|
+
}
|
|
1033
1051
|
if (intent.phase === "rebound") {
|
|
1034
1052
|
const expected = intent;
|
|
1035
1053
|
input.restoreStore.advanceLiveRebind(
|
|
@@ -1799,6 +1817,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1799
1817
|
TContext
|
|
1800
1818
|
>
|
|
1801
1819
|
>["canPrepareHeldCommand"];
|
|
1820
|
+
/** Agent-settled hint: wakes waiting leader cleanup retries early; every readiness gate is still sampled fresh. */
|
|
1821
|
+
onSessionSettled(ctx: TContext): void;
|
|
1802
1822
|
/** Fresh body-free recipient observation only; never reconstructs an attempt or grants cleanup. */
|
|
1803
1823
|
observeLiveRebindLeaderWork(
|
|
1804
1824
|
intent: Threads.TelegramWorkspaceLiveRebindIntent,
|
|
@@ -1888,6 +1908,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1888
1908
|
temporaryThread?: Threads.TelegramTemporaryThreadEntry;
|
|
1889
1909
|
/** Source copies left in All (a menu-picked command); deleted once, when the input is consumed. */
|
|
1890
1910
|
allTabCopies?: number[];
|
|
1911
|
+
/** The tab holds a forwarded copy of the threadless original, which then left All at once. */
|
|
1912
|
+
allTabCopiesForwarded?: true;
|
|
1891
1913
|
/** Acknowledged same-tab membership stays protective when its reader disappears; never a dispatch/cleanup grant. */
|
|
1892
1914
|
temporaryMembership?: true;
|
|
1893
1915
|
/** Re-entrancy lock for one command route in flight; orthogonal to `phase`. */
|
|
@@ -1918,7 +1940,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1918
1940
|
/** Some Forward outcome is unknown; process-local, so restart-safe issuance remains a release gate. */
|
|
1919
1941
|
| { kind: "forward-unknown" }
|
|
1920
1942
|
| { kind: "cleanup"; cleanup: PendingRerouteCleanup }
|
|
1921
|
-
|
|
1943
|
+
/** `retainRecord`: a live rebind's chooser-scoped command reference outlives its deleted menu. */
|
|
1944
|
+
| { kind: "finalizing"; message: string; retainRecord?: true };
|
|
1922
1945
|
/** The command a chooser routes, read from its own source exactly as the root heading named it. */
|
|
1923
1946
|
const getPendingRerouteCommandName = (
|
|
1924
1947
|
pending: PendingUnboundReroute,
|
|
@@ -2781,6 +2804,52 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2781
2804
|
pending.sourceTarget.chatId,
|
|
2782
2805
|
pending.allTabCopies?.splice(0) ?? [],
|
|
2783
2806
|
);
|
|
2807
|
+
/**
|
|
2808
|
+
* Restore keeps the tab as the Thread, so the input must stay visible there: the All original goes only when the tab
|
|
2809
|
+
* holds its forwarded copy. Without one it stays where the operator can still read it.
|
|
2810
|
+
*/
|
|
2811
|
+
const releaseAllTabCopiesIntoSurvivingTab = (
|
|
2812
|
+
pending: PendingUnboundReroute,
|
|
2813
|
+
): Promise<void> => {
|
|
2814
|
+
if (pending.allTabCopiesForwarded) return deleteAllTabCopies(pending);
|
|
2815
|
+
pending.allTabCopies?.splice(0);
|
|
2816
|
+
return Promise.resolve();
|
|
2817
|
+
};
|
|
2818
|
+
/**
|
|
2819
|
+
* The Bot API files a threadless All input in All even when a mobile client first draws it in its new tab. One silent
|
|
2820
|
+
* best-effort forward moves it into the routing tab it opened, and only a confirmed copy lets the original leave All;
|
|
2821
|
+
* any failure keeps the original where the operator can still read it.
|
|
2822
|
+
*/
|
|
2823
|
+
const moveAllTabSourceIntoTab = async (
|
|
2824
|
+
pending: PendingUnboundReroute,
|
|
2825
|
+
target: { chatId: number; threadId: number },
|
|
2826
|
+
isCurrent: () => boolean,
|
|
2827
|
+
): Promise<number | undefined> => {
|
|
2828
|
+
const [messageId] = pending.allTabCopies ?? [];
|
|
2829
|
+
if (messageId === undefined || !deps.callApi || !isCurrent()) return;
|
|
2830
|
+
try {
|
|
2831
|
+
const copy = await deps.callApi<{ message_id?: unknown }>(
|
|
2832
|
+
"forwardMessage",
|
|
2833
|
+
{
|
|
2834
|
+
chat_id: target.chatId,
|
|
2835
|
+
message_thread_id: target.threadId,
|
|
2836
|
+
from_chat_id: pending.sourceTarget.chatId,
|
|
2837
|
+
message_id: messageId,
|
|
2838
|
+
disable_notification: true,
|
|
2839
|
+
},
|
|
2840
|
+
);
|
|
2841
|
+
const copyId = copy?.message_id;
|
|
2842
|
+
if (typeof copyId !== "number" || !Number.isSafeInteger(copyId)) return;
|
|
2843
|
+
pending.allTabCopiesForwarded = true;
|
|
2844
|
+
await deleteAllTabCopies(pending);
|
|
2845
|
+
return copyId;
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
deps.recordRuntimeEvent?.("routing", error, {
|
|
2848
|
+
phase: "temporary-thread-source-forward",
|
|
2849
|
+
});
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2784
2853
|
const dismissRerouteChooserMessage = async (
|
|
2785
2854
|
query: TCallbackQuery,
|
|
2786
2855
|
assertExecutionCurrent?: () => void,
|
|
@@ -2810,6 +2879,36 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2810
2879
|
return false;
|
|
2811
2880
|
}
|
|
2812
2881
|
};
|
|
2882
|
+
/** Choosers are one-shot: a final outcome deletes the menu; only a failed delete leaves an inert, button-free notice. */
|
|
2883
|
+
const retireRerouteChooserMessage = async (
|
|
2884
|
+
chatId: number,
|
|
2885
|
+
messageId: number,
|
|
2886
|
+
fallbackText: string,
|
|
2887
|
+
assertCurrent?: () => void,
|
|
2888
|
+
): Promise<void> => {
|
|
2889
|
+
if (deps.deleteMessage) {
|
|
2890
|
+
try {
|
|
2891
|
+
await deps.deleteMessage(chatId, messageId);
|
|
2892
|
+
return;
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
deps.recordRuntimeEvent?.("telegram", error, {
|
|
2895
|
+
phase: "reroute-chooser-delete",
|
|
2896
|
+
chatId,
|
|
2897
|
+
messageId,
|
|
2898
|
+
});
|
|
2899
|
+
}
|
|
2900
|
+
assertCurrent?.();
|
|
2901
|
+
}
|
|
2902
|
+
await deps.editInteractiveMessage?.(
|
|
2903
|
+
chatId,
|
|
2904
|
+
messageId,
|
|
2905
|
+
fallbackText,
|
|
2906
|
+
"html",
|
|
2907
|
+
{
|
|
2908
|
+
inline_keyboard: [],
|
|
2909
|
+
},
|
|
2910
|
+
);
|
|
2911
|
+
};
|
|
2813
2912
|
const closeReroutedUnboundTopic = async (
|
|
2814
2913
|
target: { chatId: number; threadId: number } | undefined,
|
|
2815
2914
|
messageId: number | undefined,
|
|
@@ -2975,8 +3074,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2975
3074
|
continue;
|
|
2976
3075
|
if (!recordCancelledTemporaryThreadInput(pending, ctx, current))
|
|
2977
3076
|
continue;
|
|
3077
|
+
// A sibling cancelled by Restore stays visible in the restored tab, like a desktop input typed there.
|
|
2978
3078
|
if (await retireCancellationChooser(id, pending, current))
|
|
2979
|
-
await
|
|
3079
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
2980
3080
|
} catch (error) {
|
|
2981
3081
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
2982
3082
|
phase: "temporary-thread-restore-sibling",
|
|
@@ -3706,6 +3806,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
3706
3806
|
);
|
|
3707
3807
|
if (!intent) continue;
|
|
3708
3808
|
}
|
|
3809
|
+
// The chooser follows the success proof, never old-thread cleanup, which may stay protected indefinitely.
|
|
3810
|
+
const retireRestoreChoosers = async (): Promise<void> => {
|
|
3811
|
+
for (const [id, pending] of pendingUnboundReroutes) {
|
|
3812
|
+
if (pending.workspaceRestore?.operationId !== operationId)
|
|
3813
|
+
continue;
|
|
3814
|
+
pending.phase = { kind: "finalizing", message: "Restored" };
|
|
3815
|
+
if (pending.chooserMessageId === undefined) continue;
|
|
3816
|
+
assertSettlementCurrent();
|
|
3817
|
+
await retireRerouteChooserMessage(
|
|
3818
|
+
pending.sourceTarget.chatId,
|
|
3819
|
+
pending.chooserMessageId,
|
|
3820
|
+
"<b>✅ Message routed.</b>",
|
|
3821
|
+
assertSettlementCurrent,
|
|
3822
|
+
);
|
|
3823
|
+
assertSettlementCurrent();
|
|
3824
|
+
if (pendingUnboundReroutes.get(id) === pending)
|
|
3825
|
+
removePendingReroute(id);
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3709
3828
|
if (intent.routing?.cleanup === undefined) {
|
|
3710
3829
|
if (
|
|
3711
3830
|
!intent.request.source.updateIds.every((id) =>
|
|
@@ -3716,13 +3835,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
3716
3835
|
)
|
|
3717
3836
|
)
|
|
3718
3837
|
continue;
|
|
3719
|
-
// Positive settlement of every selected source is the success proof
|
|
3838
|
+
// Positive settlement of every selected source is the success proof.
|
|
3720
3839
|
await disposeTemporaryThreadSiblingsAfterRestore(
|
|
3721
3840
|
intent,
|
|
3722
3841
|
ctx,
|
|
3723
3842
|
settlementAuthority.isCurrent,
|
|
3724
3843
|
);
|
|
3725
3844
|
assertSettlementCurrent();
|
|
3845
|
+
await retireRestoreChoosers();
|
|
3726
3846
|
if (
|
|
3727
3847
|
isRerouteTargetProtected(
|
|
3728
3848
|
intent.request.binding.target,
|
|
@@ -3758,27 +3878,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
3758
3878
|
}
|
|
3759
3879
|
if (!store.retire(intent, settlementAuthority)) continue;
|
|
3760
3880
|
retired = true;
|
|
3761
|
-
|
|
3762
|
-
if (pending.workspaceRestore?.operationId !== operationId)
|
|
3763
|
-
continue;
|
|
3764
|
-
pending.phase = { kind: "finalizing", message: "Restored" };
|
|
3765
|
-
if (
|
|
3766
|
-
!deps.editInteractiveMessage ||
|
|
3767
|
-
pending.chooserMessageId === undefined
|
|
3768
|
-
)
|
|
3769
|
-
continue;
|
|
3770
|
-
assertSettlementCurrent();
|
|
3771
|
-
await deps.editInteractiveMessage(
|
|
3772
|
-
pending.sourceTarget.chatId,
|
|
3773
|
-
pending.chooserMessageId,
|
|
3774
|
-
"<b>✅ Message routed.</b>",
|
|
3775
|
-
"html",
|
|
3776
|
-
{ inline_keyboard: [] },
|
|
3777
|
-
);
|
|
3778
|
-
assertSettlementCurrent();
|
|
3779
|
-
if (pendingUnboundReroutes.get(id) === pending)
|
|
3780
|
-
removePendingReroute(id);
|
|
3781
|
-
}
|
|
3881
|
+
await retireRestoreChoosers();
|
|
3782
3882
|
}
|
|
3783
3883
|
},
|
|
3784
3884
|
),
|
|
@@ -5094,6 +5194,23 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5094
5194
|
messages: selection.messages,
|
|
5095
5195
|
restoreStore: store,
|
|
5096
5196
|
threadStore: threads,
|
|
5197
|
+
// Same title as Workspace Restore: the display-mode title of the restored binding on its new tab.
|
|
5198
|
+
retitle: deps.callApi
|
|
5199
|
+
? async (isCurrent) => {
|
|
5200
|
+
if (!isCurrent()) return;
|
|
5201
|
+
const slot = binding.slot ?? "";
|
|
5202
|
+
await deps.callApi!("editForumTopic", {
|
|
5203
|
+
chat_id: target.chatId,
|
|
5204
|
+
message_thread_id: target.threadId,
|
|
5205
|
+
name:
|
|
5206
|
+
deps.getDisplayTitle?.(target) ??
|
|
5207
|
+
ThreadNaming.getTelegramTopicTitleForThreadName(
|
|
5208
|
+
getRestoredThreadName(request.owner, slot),
|
|
5209
|
+
slot,
|
|
5210
|
+
),
|
|
5211
|
+
});
|
|
5212
|
+
}
|
|
5213
|
+
: undefined,
|
|
5097
5214
|
authority: {
|
|
5098
5215
|
executor: { instanceId, leaderEpoch: String(epoch) },
|
|
5099
5216
|
operatorUserId,
|
|
@@ -5788,6 +5905,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5788
5905
|
string,
|
|
5789
5906
|
ReturnType<typeof setTimeout> | "running"
|
|
5790
5907
|
>();
|
|
5908
|
+
// A leader retry waiting out its delay; a settled session wakes it at once, since its own busy run is the usual wait.
|
|
5909
|
+
const liveRebindCleanupWakers = new Map<string, () => void>();
|
|
5791
5910
|
/** End an unissued attempt honestly under fresh admission and current operator authority; no executor grant is borrowed. */
|
|
5792
5911
|
const terminalizeUnissuedLiveRebindCleanup = async (
|
|
5793
5912
|
operationId: string,
|
|
@@ -5860,7 +5979,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5860
5979
|
liveRebindCleanupChains.delete(operationId);
|
|
5861
5980
|
return;
|
|
5862
5981
|
}
|
|
5863
|
-
const
|
|
5982
|
+
const attemptNow = () => {
|
|
5983
|
+
liveRebindCleanupWakers.delete(operationId);
|
|
5864
5984
|
if (liveRebindCleanupChains.get(operationId) !== timer) return;
|
|
5865
5985
|
liveRebindCleanupChains.set(operationId, "running");
|
|
5866
5986
|
let continued = false;
|
|
@@ -5906,9 +6026,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5906
6026
|
});
|
|
5907
6027
|
restoreSettlementTasks.add(task);
|
|
5908
6028
|
void task.finally(() => restoreSettlementTasks.delete(task));
|
|
5909
|
-
}
|
|
6029
|
+
};
|
|
6030
|
+
const timer = setTimeout(
|
|
6031
|
+
attemptNow,
|
|
6032
|
+
pacing.delaysMs[attempt] ?? pacing.intervalMs,
|
|
6033
|
+
);
|
|
5910
6034
|
timer.unref?.();
|
|
5911
6035
|
liveRebindCleanupChains.set(operationId, timer);
|
|
6036
|
+
// The first delay stays whole: a detached command reply still needs the released, cleanup-free row.
|
|
6037
|
+
if (role === "leader" && attempt > 0)
|
|
6038
|
+
liveRebindCleanupWakers.set(operationId, () => {
|
|
6039
|
+
if (liveRebindCleanupChains.get(operationId) !== timer) return;
|
|
6040
|
+
clearTimeout(timer);
|
|
6041
|
+
attemptNow();
|
|
6042
|
+
});
|
|
6043
|
+
};
|
|
6044
|
+
/** The agent settled: every gate is re-sampled now, so an old tab goes right after the answer, not at the next tick. */
|
|
6045
|
+
const onSessionSettled = (ctx: TContext): void => {
|
|
6046
|
+
if (deps.isContextActive?.(ctx) !== true) return;
|
|
6047
|
+
for (const wake of [...liveRebindCleanupWakers.values()]) wake();
|
|
5912
6048
|
};
|
|
5913
6049
|
const restoreWorkspace = deps.workspaceRestoreRecipient
|
|
5914
6050
|
? async (input: {
|
|
@@ -6446,6 +6582,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6446
6582
|
assertExecutionCurrent = Updates.createTelegramUpdateExecutionFenceGuard(
|
|
6447
6583
|
query,
|
|
6448
6584
|
),
|
|
6585
|
+
retainRecord = false,
|
|
6449
6586
|
): Promise<void> => {
|
|
6450
6587
|
assertExecutionCurrent();
|
|
6451
6588
|
const dismissed = await dismissRerouteChooserMessage(
|
|
@@ -6454,12 +6591,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6454
6591
|
);
|
|
6455
6592
|
assertExecutionCurrent();
|
|
6456
6593
|
if (dismissed) {
|
|
6457
|
-
|
|
6594
|
+
// A retained record no longer matches any chooser, so a late duplicate tap reads as expired.
|
|
6595
|
+
if (retainRecord) pending.chooserMessageId = undefined;
|
|
6596
|
+
else removePendingReroute(rerouteId);
|
|
6458
6597
|
await deps.answerCallbackQuery(query.id, successMessage);
|
|
6459
6598
|
await deleteAllTabCopies(pending);
|
|
6460
6599
|
return;
|
|
6461
6600
|
}
|
|
6462
|
-
pending.phase = {
|
|
6601
|
+
pending.phase = {
|
|
6602
|
+
kind: "finalizing",
|
|
6603
|
+
message: successMessage,
|
|
6604
|
+
...(retainRecord ? { retainRecord: true as const } : {}),
|
|
6605
|
+
};
|
|
6463
6606
|
await deps.answerCallbackQuery(
|
|
6464
6607
|
query.id,
|
|
6465
6608
|
`${successMessage}; tap again to clear the menu`,
|
|
@@ -7017,16 +7160,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7017
7160
|
if (
|
|
7018
7161
|
!isPendingRerouteCancelled(pending) ||
|
|
7019
7162
|
!isCurrent() ||
|
|
7020
|
-
!deps.editInteractiveMessage ||
|
|
7163
|
+
(!deps.deleteMessage && !deps.editInteractiveMessage) ||
|
|
7021
7164
|
pending.chooserMessageId === undefined
|
|
7022
7165
|
)
|
|
7023
7166
|
return false;
|
|
7024
|
-
await
|
|
7167
|
+
await retireRerouteChooserMessage(
|
|
7025
7168
|
pending.sourceTarget.chatId,
|
|
7026
7169
|
pending.chooserMessageId,
|
|
7027
7170
|
"<b>⛔️ Routing cancelled.</b>",
|
|
7028
|
-
"html",
|
|
7029
|
-
{ inline_keyboard: [] },
|
|
7030
7171
|
);
|
|
7031
7172
|
if (!isCurrent() || pendingUnboundReroutes.get(id) !== pending)
|
|
7032
7173
|
return false;
|
|
@@ -7523,6 +7664,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7523
7664
|
const result = source.expire();
|
|
7524
7665
|
if (!result) return;
|
|
7525
7666
|
// Expiry revokes the donor carrier; old controls and late ACKs cannot issue another delivery. No body is archived.
|
|
7667
|
+
let movedIntoTab = false;
|
|
7526
7668
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
7527
7669
|
if (
|
|
7528
7670
|
!Updates.collectTelegramAdmissionSourceUpdateIds(
|
|
@@ -7530,6 +7672,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7530
7672
|
).includes(source.original.updateId)
|
|
7531
7673
|
)
|
|
7532
7674
|
continue;
|
|
7675
|
+
movedIntoTab ||= pending.allTabCopiesForwarded === true;
|
|
7533
7676
|
removePendingReroute(id);
|
|
7534
7677
|
if (
|
|
7535
7678
|
signal.aborted ||
|
|
@@ -7562,13 +7705,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7562
7705
|
// never closes over this source or asks the worker to retain a terminal prompt while metadata publication fails.
|
|
7563
7706
|
if (temporary?.target && runtimeCurrent()) {
|
|
7564
7707
|
scheduleTemporaryThreadCleanup(temporary.target, ctx, true);
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
[message!],
|
|
7708
|
+
// A moved original already left All with its forward; only an unmoved one is deleted here.
|
|
7709
|
+
if (!movedIntoTab)
|
|
7710
|
+
await deleteAllTabMessages(
|
|
7569
7711
|
temporary.target.chatId,
|
|
7570
|
-
|
|
7571
|
-
|
|
7712
|
+
getTelegramAllTabSourceMessageIds(
|
|
7713
|
+
[message!],
|
|
7714
|
+
temporary.target.chatId,
|
|
7715
|
+
),
|
|
7716
|
+
);
|
|
7572
7717
|
}
|
|
7573
7718
|
},
|
|
7574
7719
|
);
|
|
@@ -8172,6 +8317,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8172
8317
|
query,
|
|
8173
8318
|
pending.phase.message,
|
|
8174
8319
|
assertExecutionCurrent,
|
|
8320
|
+
pending.phase.retainRecord === true,
|
|
8175
8321
|
);
|
|
8176
8322
|
return true;
|
|
8177
8323
|
}
|
|
@@ -8566,7 +8712,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8566
8712
|
selection.record.owner?.kind === "leader" ? "leader" : "follower",
|
|
8567
8713
|
ctx,
|
|
8568
8714
|
);
|
|
8569
|
-
if (outcome === "released")
|
|
8715
|
+
if (outcome === "released")
|
|
8716
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8570
8717
|
} catch (error) {
|
|
8571
8718
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
8572
8719
|
phase: "live-rebind-chooser",
|
|
@@ -8575,31 +8722,41 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8575
8722
|
selection.isCurrent = undefined;
|
|
8576
8723
|
}
|
|
8577
8724
|
assertExecutionCurrent();
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8725
|
+
if (outcome !== "released") {
|
|
8726
|
+
await deps.answerCallbackQuery(
|
|
8727
|
+
query.id,
|
|
8728
|
+
"Live rebind is unconfirmed; this choice never replays issued input",
|
|
8729
|
+
);
|
|
8730
|
+
return true;
|
|
8731
|
+
}
|
|
8732
|
+
// The tab now belongs to the rebound thread; only the one-shot chooser message goes.
|
|
8733
|
+
await finalizePendingReroute(
|
|
8734
|
+
parsed.rerouteId,
|
|
8735
|
+
pending,
|
|
8736
|
+
query,
|
|
8737
|
+
selection.extensionKind === "generated-prompt"
|
|
8738
|
+
? "Input queued in the rebound Thread; old-Thread cleanup is scheduled, not confirmed"
|
|
8739
|
+
: selection.extensionCommand
|
|
8740
|
+
? "Command source disposal confirmed; reply delivery and old-Thread cleanup are not confirmed"
|
|
8741
|
+
: selection.nameCommand
|
|
8742
|
+
? selection.nameCommand.args.trim()
|
|
8743
|
+
? "Command source disposal confirmed; title/reply delivery and old-Thread cleanup are not confirmed"
|
|
8744
|
+
: "Command source disposal confirmed; name-dialog publication was handled, not title change or old-Thread cleanup"
|
|
8745
|
+
: selection.confirmationCommand
|
|
8746
|
+
? `Command source disposal confirmed; confirmation delivery and actual ${selection.confirmationCommand.name === "new" ? "session replacement" : "compaction"} or old-Thread cleanup are not confirmed`
|
|
8747
|
+
: selection.helpCommand
|
|
8748
|
+
? "Command source disposal confirmed; menu/sync delivery and old-Thread cleanup are not confirmed"
|
|
8749
|
+
: selection.stopCommand
|
|
8750
|
+
? "Command source disposal confirmed; abort settlement, reply delivery and old-Thread cleanup are not confirmed"
|
|
8751
|
+
: selection.nextCommand
|
|
8752
|
+
? "Command source disposal confirmed; queue dispatch, transition notices and old-Thread cleanup are not confirmed"
|
|
8753
|
+
: selection.menuCommand || selection.abortCommand
|
|
8754
|
+
? `Command source disposal confirmed; ${selection.menuCommand ? "menu" : "reply"} delivery and old-Thread cleanup are not confirmed`
|
|
8755
|
+
: selection.coordinator?.selectedCommandReference()
|
|
8756
|
+
? "Command source disposal confirmed; recipient command effects and old-Thread cleanup are not confirmed"
|
|
8757
|
+
: "Input queued in the rebound Thread; old-Thread cleanup is scheduled, not confirmed",
|
|
8758
|
+
assertExecutionCurrent,
|
|
8759
|
+
true,
|
|
8603
8760
|
);
|
|
8604
8761
|
return true;
|
|
8605
8762
|
}
|
|
@@ -8611,6 +8768,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8611
8768
|
messages: [...pending.messages],
|
|
8612
8769
|
});
|
|
8613
8770
|
let active = true;
|
|
8771
|
+
// Delivery proof for the toast only; chooser retirement still waits for the positive settlement that follows.
|
|
8772
|
+
let delivered = false;
|
|
8614
8773
|
const ownerUserId = deps.configStore.getAllowedUserId();
|
|
8615
8774
|
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
8616
8775
|
const admissionScope = deps.getAdmissionScope?.();
|
|
@@ -8676,7 +8835,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8676
8835
|
: routed;
|
|
8677
8836
|
await dispatchPendingRerouteMessages(pending, messages, ctx);
|
|
8678
8837
|
assertRecipientCurrent();
|
|
8679
|
-
|
|
8838
|
+
delivered = true;
|
|
8839
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8680
8840
|
return true;
|
|
8681
8841
|
}
|
|
8682
8842
|
const forwarded = await forwardPendingRerouteMessages(
|
|
@@ -8687,7 +8847,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8687
8847
|
assertRecipientCurrent,
|
|
8688
8848
|
recordForwardAcceptance,
|
|
8689
8849
|
);
|
|
8690
|
-
if (forwarded)
|
|
8850
|
+
if (forwarded) {
|
|
8851
|
+
delivered = true;
|
|
8852
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8853
|
+
}
|
|
8691
8854
|
return forwarded;
|
|
8692
8855
|
},
|
|
8693
8856
|
});
|
|
@@ -8698,7 +8861,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8698
8861
|
if (!selectionCurrent()) return true;
|
|
8699
8862
|
await deps.answerCallbackQuery(
|
|
8700
8863
|
query.id,
|
|
8701
|
-
"Restore unconfirmed; not resending",
|
|
8864
|
+
delivered ? "Restored" : "Restore unconfirmed; not resending",
|
|
8702
8865
|
);
|
|
8703
8866
|
return true;
|
|
8704
8867
|
}
|
|
@@ -10080,7 +10243,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10080
10243
|
}
|
|
10081
10244
|
return undefined;
|
|
10082
10245
|
};
|
|
10083
|
-
/**
|
|
10246
|
+
/**
|
|
10247
|
+
* A fresh implicit native tab observed just before the All input in the same exact scope: mobile names it after
|
|
10248
|
+
* the input, while other clients may use a generic name, so the immediately preceding creation also qualifies.
|
|
10249
|
+
*/
|
|
10084
10250
|
const findImplicitCreationForAllInput = (
|
|
10085
10251
|
message: TMessage,
|
|
10086
10252
|
updateId: number,
|
|
@@ -10098,8 +10264,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10098
10264
|
| undefined;
|
|
10099
10265
|
for (const [key, implicit] of implicitThreadCreations) {
|
|
10100
10266
|
if (
|
|
10101
|
-
implicit.name &&
|
|
10102
|
-
|
|
10267
|
+
((implicit.name && text.startsWith(implicit.name)) ||
|
|
10268
|
+
implicit.updateId === updateId - 1) &&
|
|
10103
10269
|
implicit.updateId < updateId &&
|
|
10104
10270
|
implicit.createdAtSec > 0 &&
|
|
10105
10271
|
message.date !== undefined &&
|
|
@@ -10159,6 +10325,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10159
10325
|
entry.source.updateId === updateId,
|
|
10160
10326
|
);
|
|
10161
10327
|
let entry: Threads.TelegramTemporaryThreadEntry | undefined;
|
|
10328
|
+
// A tab opened by this call, by the bot or adopted from the client, gets the input's one forwarded copy; re-entry
|
|
10329
|
+
// after a restart finds its tab already prepared and never forwards again.
|
|
10330
|
+
let openedNow = false;
|
|
10162
10331
|
await deps.runWorkspaceOperation(
|
|
10163
10332
|
{
|
|
10164
10333
|
operationId: `temporary-thread-${randomBytes(16).toString("hex")}`,
|
|
@@ -10175,7 +10344,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10175
10344
|
}
|
|
10176
10345
|
// Mobile clients sending from All first create a native tab named after the input, then deliver
|
|
10177
10346
|
// the input itself to All: adopt that tab rather than leaving it behind next to a second one.
|
|
10178
|
-
const native = findImplicitCreationForAllInput(
|
|
10347
|
+
const native = findImplicitCreationForAllInput(
|
|
10348
|
+
message,
|
|
10349
|
+
updateId,
|
|
10350
|
+
cap,
|
|
10351
|
+
ctx,
|
|
10352
|
+
);
|
|
10179
10353
|
if (native) {
|
|
10180
10354
|
const adopted = store.registerImplicitTemporaryThread(
|
|
10181
10355
|
{ journalBindingKey, updateIds: [updateId] },
|
|
@@ -10191,6 +10365,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10191
10365
|
if (adopted && current()) {
|
|
10192
10366
|
implicitThreadCreations.delete(native.key);
|
|
10193
10367
|
entry = adopted;
|
|
10368
|
+
openedNow = true;
|
|
10194
10369
|
// Adopted native tabs share the routing name; a failed rename never blocks routing.
|
|
10195
10370
|
try {
|
|
10196
10371
|
await deps.callApi!("editForumTopic", {
|
|
@@ -10237,12 +10412,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10237
10412
|
threadId <= 0
|
|
10238
10413
|
)
|
|
10239
10414
|
return;
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10415
|
+
const acknowledged = store.acknowledgeTemporaryThread(
|
|
10416
|
+
reservation.entry,
|
|
10417
|
+
{ chatId: operatorUserId, threadId },
|
|
10418
|
+
authority,
|
|
10419
|
+
);
|
|
10420
|
+
openedNow = !!acknowledged;
|
|
10421
|
+
entry = acknowledged ?? entry;
|
|
10246
10422
|
},
|
|
10247
10423
|
);
|
|
10248
10424
|
if (!current() || !entry)
|
|
@@ -10298,6 +10474,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10298
10474
|
pending.rootChooserText = formatTelegramTemporaryThreadChooserText(
|
|
10299
10475
|
command?.name,
|
|
10300
10476
|
);
|
|
10477
|
+
const tabCopyId = openedNow
|
|
10478
|
+
? await moveAllTabSourceIntoTab(pending, target, current)
|
|
10479
|
+
: undefined;
|
|
10301
10480
|
try {
|
|
10302
10481
|
if (!deps.sendInteractiveMessage)
|
|
10303
10482
|
throw new Error("Temporary Thread chooser publication is unavailable.");
|
|
@@ -10309,7 +10488,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10309
10488
|
canCancel: !!pending.abandonment,
|
|
10310
10489
|
getDisplayTitle: deps.getDisplayTitle,
|
|
10311
10490
|
}),
|
|
10312
|
-
|
|
10491
|
+
tabCopyId === undefined
|
|
10492
|
+
? { target }
|
|
10493
|
+
: { target, replyToMessageId: tabCopyId },
|
|
10313
10494
|
);
|
|
10314
10495
|
rememberRerouteChooser(rerouteId, chooserId);
|
|
10315
10496
|
} catch (error) {
|
|
@@ -10705,7 +10886,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10705
10886
|
chatId: lifecycle.target.chatId,
|
|
10706
10887
|
threadId: lifecycle.target.threadId!,
|
|
10707
10888
|
},
|
|
10708
|
-
name:
|
|
10889
|
+
name:
|
|
10890
|
+
"name" in created && typeof created.name === "string"
|
|
10891
|
+
? created.name
|
|
10892
|
+
: "",
|
|
10709
10893
|
createdAtSec: message.date ?? 0,
|
|
10710
10894
|
});
|
|
10711
10895
|
} else implicitThreadCreations.delete(key);
|
|
@@ -11097,6 +11281,33 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11097
11281
|
threadId: target.threadId,
|
|
11098
11282
|
};
|
|
11099
11283
|
/** Rebinds the leader record to this unbound tab, publishes its identity and handles the message there. */
|
|
11284
|
+
// The leader's own Workspace binding follows a reclaimed tab, keeping its slot and name; without it, the tab
|
|
11285
|
+
// would gain a second slot while Restore, labels and titles still pointed at the deleted target.
|
|
11286
|
+
const leaderWorkspaceCwd =
|
|
11287
|
+
typeof (ctx as { cwd?: unknown }).cwd === "string"
|
|
11288
|
+
? (ctx as { cwd: string }).cwd
|
|
11289
|
+
: undefined;
|
|
11290
|
+
const claimLeaderWorkspaceBinding = ():
|
|
11291
|
+
Threads.TelegramWorkspaceThreadBinding | undefined => {
|
|
11292
|
+
const sessionId = deps.workspaceRestoreRecipient?.getSessionId(ctx);
|
|
11293
|
+
if (!leaderWorkspaceCwd || !sessionId || !instanceId)
|
|
11294
|
+
return undefined;
|
|
11295
|
+
const identity = deps.threadStore!.claimWorkspaceIdentity(
|
|
11296
|
+
leaderWorkspaceCwd,
|
|
11297
|
+
instanceId,
|
|
11298
|
+
undefined,
|
|
11299
|
+
{ existingBindingOnly: true, sessionId },
|
|
11300
|
+
);
|
|
11301
|
+
const binding = identity
|
|
11302
|
+
? deps.threadStore!.getWorkspaceBinding(
|
|
11303
|
+
identity.cwd,
|
|
11304
|
+
identity.instanceSlot,
|
|
11305
|
+
identity.sessionId,
|
|
11306
|
+
)
|
|
11307
|
+
: undefined;
|
|
11308
|
+
if (!binding) deps.threadStore!.releaseWorkspaceClaim(instanceId);
|
|
11309
|
+
return binding;
|
|
11310
|
+
};
|
|
11100
11311
|
const reclaimUnboundTargetForLeader = async (
|
|
11101
11312
|
base: Partial<Threads.TelegramTopicTargetRecord> &
|
|
11102
11313
|
Pick<Threads.TelegramTopicTargetRecord, "createdAtMs">,
|
|
@@ -11105,7 +11316,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11105
11316
|
threadName: string,
|
|
11106
11317
|
event: string,
|
|
11107
11318
|
details: Record<string, unknown>,
|
|
11319
|
+
leaderBinding?: Threads.TelegramWorkspaceThreadBinding,
|
|
11108
11320
|
): Promise<void> => {
|
|
11321
|
+
if (leaderBinding) {
|
|
11322
|
+
const {
|
|
11323
|
+
inactiveSinceMs: _inactiveSinceMs,
|
|
11324
|
+
displayTitle: _displayTitle,
|
|
11325
|
+
...moved
|
|
11326
|
+
} = leaderBinding;
|
|
11327
|
+
const committed = deps.threadStore!.upsertWorkspaceBinding(
|
|
11328
|
+
{
|
|
11329
|
+
...moved,
|
|
11330
|
+
target: { ...unboundTarget },
|
|
11331
|
+
updatedAtMs: Date.now(),
|
|
11332
|
+
},
|
|
11333
|
+
instanceId,
|
|
11334
|
+
);
|
|
11335
|
+
deps.threadStore!.releaseWorkspaceClaim(instanceId!);
|
|
11336
|
+
if (committed) threadName = committed.threadName ?? threadName;
|
|
11337
|
+
}
|
|
11109
11338
|
deps.threadStore!.upsert({
|
|
11110
11339
|
...base,
|
|
11111
11340
|
profileKey,
|
|
@@ -11137,6 +11366,35 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11137
11366
|
slot,
|
|
11138
11367
|
profileKey,
|
|
11139
11368
|
});
|
|
11369
|
+
if (leaderBinding && deps.callApi) {
|
|
11370
|
+
const title = deps.getDisplayTitle?.(unboundTarget);
|
|
11371
|
+
if (title)
|
|
11372
|
+
try {
|
|
11373
|
+
await deps.callApi("editForumTopic", {
|
|
11374
|
+
chat_id: unboundTarget.chatId,
|
|
11375
|
+
message_thread_id: unboundTarget.threadId,
|
|
11376
|
+
name: title,
|
|
11377
|
+
});
|
|
11378
|
+
const current = deps
|
|
11379
|
+
.threadStore!.listWorkspaceBindings()
|
|
11380
|
+
.find(
|
|
11381
|
+
(value) =>
|
|
11382
|
+
value.bindingKey === leaderBinding.bindingKey &&
|
|
11383
|
+
isDeepStrictEqual(value.target, unboundTarget),
|
|
11384
|
+
);
|
|
11385
|
+
if (current) {
|
|
11386
|
+
deps.threadStore!.upsertWorkspaceBinding({
|
|
11387
|
+
...current,
|
|
11388
|
+
displayTitle: title,
|
|
11389
|
+
});
|
|
11390
|
+
await deps.threadStore!.persist();
|
|
11391
|
+
}
|
|
11392
|
+
} catch (error) {
|
|
11393
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
11394
|
+
phase: "leader-topic-reclaim-title",
|
|
11395
|
+
});
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11140
11398
|
await textDispatch.handleMessage(message as TMessage, ctx);
|
|
11141
11399
|
};
|
|
11142
11400
|
const records = deps.threadStore.list();
|
|
@@ -11277,7 +11535,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11277
11535
|
if (!currentLeaderIsStale) throw error;
|
|
11278
11536
|
}
|
|
11279
11537
|
if (currentLeaderIsStale) {
|
|
11280
|
-
const
|
|
11538
|
+
const leaderBinding = claimLeaderWorkspaceBinding();
|
|
11539
|
+
const slot = deps.threadStore.allocateSlot(
|
|
11540
|
+
leaderProfileKey,
|
|
11541
|
+
leaderBinding?.slot,
|
|
11542
|
+
leaderBinding?.bindingKey,
|
|
11543
|
+
);
|
|
11544
|
+
if (leaderBinding && leaderBinding.slot !== slot)
|
|
11545
|
+
deps.threadStore.releaseWorkspaceClaim(instanceId!);
|
|
11281
11546
|
if (!slot) {
|
|
11282
11547
|
deps.threadStore.markStaleByTarget(
|
|
11283
11548
|
currentLeaderRecord.target,
|
|
@@ -11313,6 +11578,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11313
11578
|
phase: "leader-topic-unbound-stale-reclaim",
|
|
11314
11579
|
staleThreadId: currentLeaderRecord.target.threadId,
|
|
11315
11580
|
},
|
|
11581
|
+
leaderBinding?.slot === slot ? leaderBinding : undefined,
|
|
11316
11582
|
);
|
|
11317
11583
|
return;
|
|
11318
11584
|
}
|
|
@@ -11327,10 +11593,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11327
11593
|
deps.threadStore.getByProfileKey(leaderProfileKey);
|
|
11328
11594
|
const priorLeaderIdentity =
|
|
11329
11595
|
deps.threadStore.getIdentityByProfileKey(leaderProfileKey);
|
|
11596
|
+
const leaderBinding = claimLeaderWorkspaceBinding();
|
|
11330
11597
|
const slot = deps.threadStore.allocateSlot(
|
|
11331
11598
|
leaderProfileKey,
|
|
11332
|
-
|
|
11599
|
+
leaderBinding?.slot ??
|
|
11600
|
+
priorLeaderRecord?.slot ??
|
|
11601
|
+
priorLeaderIdentity?.slot,
|
|
11602
|
+
leaderBinding?.bindingKey,
|
|
11333
11603
|
);
|
|
11604
|
+
if (leaderBinding && leaderBinding.slot !== slot)
|
|
11605
|
+
deps.threadStore.releaseWorkspaceClaim(instanceId!);
|
|
11334
11606
|
if (!slot) {
|
|
11335
11607
|
await deps.sendTextReply(
|
|
11336
11608
|
target.chatId,
|
|
@@ -11360,6 +11632,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11360
11632
|
threadName,
|
|
11361
11633
|
"Bus leader reclaimed unbound thread",
|
|
11362
11634
|
{ phase: "leader-topic-reclaim" },
|
|
11635
|
+
leaderBinding?.slot === slot ? leaderBinding : undefined,
|
|
11363
11636
|
);
|
|
11364
11637
|
return;
|
|
11365
11638
|
}
|
|
@@ -11389,6 +11662,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11389
11662
|
onUpdateCompleted,
|
|
11390
11663
|
prepareHeldCommand: commandHandler.prepareHeldCommand,
|
|
11391
11664
|
canPrepareHeldCommand: commandHandler.canPrepareHeldCommand,
|
|
11665
|
+
onSessionSettled,
|
|
11392
11666
|
observeLiveRebindLeaderWork,
|
|
11393
11667
|
prepareLiveRebindLeaderCleanup,
|
|
11394
11668
|
prepareLiveRebindFollowerCleanup,
|