@llblab/pi-telegram 0.54.4 → 0.55.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 +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +3 -1
- package/dist/lib/bindings.js +8 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +50 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +7 -3
- package/dist/lib/bus.js +24 -16
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +48 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +7 -2
- package/dist/lib/routing.js +585 -229
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +18 -5
- package/dist/lib/threads.js +107 -30
- package/dist/lib/turns.d.ts +6 -0
- package/dist/lib/turns.js +12 -1
- package/dist/lib/updates.d.ts +13 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +14 -11
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +20 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +2 -2
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +9 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +30 -18
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +51 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +614 -257
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +120 -36
- package/lib/turns.ts +15 -1
- package/lib/updates.ts +60 -21
- package/package.json +1 -1
package/lib/routing.ts
CHANGED
|
@@ -335,6 +335,10 @@ function getTelegramAllTabSourceMessageIds(
|
|
|
335
335
|
);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
/** Failure retains the source for recovery; Threaded Mode never emits a fallback answer into All. */
|
|
339
|
+
const TELEGRAM_THREADED_ROUTING_UNAVAILABLE_ERROR =
|
|
340
|
+
"All is input-only in Threaded Mode; a routing Thread could not be prepared.";
|
|
341
|
+
|
|
338
342
|
/** What a route chooser moves: `<code>/name</code>` for a command, otherwise "this message". */
|
|
339
343
|
function formatTelegramRouteSubject(command?: string): string {
|
|
340
344
|
return command ? `<code>/${escapeHtml(command)}</code>` : "this message";
|
|
@@ -387,14 +391,6 @@ function formatTelegramRoutingInputPreview(message: object): string {
|
|
|
387
391
|
);
|
|
388
392
|
}
|
|
389
393
|
|
|
390
|
-
function formatTelegramAllTabMenuChooserText(command: string): string {
|
|
391
|
-
return [
|
|
392
|
-
`<b>🚦 Route ${formatTelegramRouteSubject(command)}:</b>`,
|
|
393
|
-
"",
|
|
394
|
-
"<i>To restore a Pi instead, send a message in a new tab.</i>",
|
|
395
|
-
].join("\n");
|
|
396
|
-
}
|
|
397
|
-
|
|
398
394
|
function buildTelegramUnboundRerouteChooserMarkup(
|
|
399
395
|
rerouteId: string,
|
|
400
396
|
records: readonly Threads.TelegramTopicTargetRecord[],
|
|
@@ -603,7 +599,7 @@ type TelegramLiveRebindSelectedCommandReference = {
|
|
|
603
599
|
selectedCommand: Bus.TelegramBusSelectedCommandInput;
|
|
604
600
|
};
|
|
605
601
|
|
|
606
|
-
/** One warm live attempt with exact
|
|
602
|
+
/** One warm live attempt with independent leader input/cleanup grants and exact peer donor settlement; no startup recovery or legacy Restore ACK. */
|
|
607
603
|
export function createTelegramLiveRebindCoordinator(input: {
|
|
608
604
|
request: Threads.TelegramWorkspaceRestoreRequest;
|
|
609
605
|
/** Captures caller source/reference, admission, profile, transport and Pi lifetime. */
|
|
@@ -647,6 +643,8 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
647
643
|
/** Explicit staged singleton command branch; absence preserves ordinary live input behavior. */
|
|
648
644
|
selectedCommand?: Bus.TelegramBusSelectedCommandInput;
|
|
649
645
|
};
|
|
646
|
+
/** Existing profile admission spans the applied recipient, fresh cleanup decision and one native input grant. */
|
|
647
|
+
cleanupBeforeRelease?(intent: Threads.TelegramWorkspaceLiveRebindIntent, isCurrent: () => boolean): Promise<boolean>;
|
|
650
648
|
/** Restore moves the tab to its new owner: once applied, show that owner's title (best-effort, never blocking). */
|
|
651
649
|
retitle?: (isCurrent: () => boolean) => Promise<void>;
|
|
652
650
|
recordRuntimeEvent?: (
|
|
@@ -662,6 +660,7 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
662
660
|
const leader = input.leader && { ...input.leader },
|
|
663
661
|
follower = input.follower,
|
|
664
662
|
followerRun = follower?.run;
|
|
663
|
+
const beforeCleanup = input.cleanupBeforeRelease;
|
|
665
664
|
const selectedCommand =
|
|
666
665
|
follower?.selectedCommand && structuredClone(follower.selectedCommand),
|
|
667
666
|
held = selectedCommand;
|
|
@@ -783,6 +782,8 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
783
782
|
peerReleaseIssued = false,
|
|
784
783
|
donorSettlementAttempted = false,
|
|
785
784
|
continuationIssued = false,
|
|
785
|
+
cleanupBeforeReleaseIssued = false,
|
|
786
|
+
cleanupBeforeReleaseDecided = false,
|
|
786
787
|
released = false,
|
|
787
788
|
advancing = false;
|
|
788
789
|
let preparedSource: Bus.TelegramBusPreparedCommandSource | undefined;
|
|
@@ -836,6 +837,13 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
836
837
|
generation: recipient!.generation,
|
|
837
838
|
bindingKey: recipient!.bindingKey,
|
|
838
839
|
});
|
|
840
|
+
const acceptInput = (expected: Threads.TelegramWorkspaceLiveRebindIntent) => {
|
|
841
|
+
if (!expected.inputRelease || expected.inputRelease === "accepted") return expected;
|
|
842
|
+
let accepted: Threads.TelegramWorkspaceLiveRebindIntent | undefined;
|
|
843
|
+
try { accepted = input.restoreStore.advanceLiveRebind(expected, "accept-input", authority); }
|
|
844
|
+
catch { accepted = retained(); }
|
|
845
|
+
return accepted?.inputRelease === "accepted" && canonical(accepted) ? accepted : undefined;
|
|
846
|
+
};
|
|
839
847
|
const releaseFollower = async (
|
|
840
848
|
intent: Threads.TelegramWorkspaceLiveRebindIntent,
|
|
841
849
|
): Promise<"unknown" | "released"> => {
|
|
@@ -864,6 +872,9 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
864
872
|
)
|
|
865
873
|
return "unknown";
|
|
866
874
|
}
|
|
875
|
+
const accepted = acceptInput(intent);
|
|
876
|
+
if (!accepted) return "unknown";
|
|
877
|
+
intent = accepted;
|
|
867
878
|
donorSettlementAttempted = true;
|
|
868
879
|
if (preparation?.settleTransferred(() => canonical(intent)) !== "settled")
|
|
869
880
|
return "unknown";
|
|
@@ -874,8 +885,7 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
874
885
|
const intent = retained();
|
|
875
886
|
if (
|
|
876
887
|
!intent ||
|
|
877
|
-
intent
|
|
878
|
-
intent.cleanup ||
|
|
888
|
+
!Threads.isTelegramLiveRebindInputAuthorized(intent) ||
|
|
879
889
|
!canonical(intent) ||
|
|
880
890
|
!leader?.isApplied(intent)
|
|
881
891
|
)
|
|
@@ -883,16 +893,14 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
883
893
|
const completions = messages.map(
|
|
884
894
|
Updates.inspectTelegramDeferredSourceCompletion,
|
|
885
895
|
);
|
|
886
|
-
|
|
896
|
+
const completed = completions.every(
|
|
887
897
|
(completion, index) =>
|
|
888
898
|
completion && isDeepStrictEqual(completion, sources[index]),
|
|
889
899
|
) &&
|
|
890
900
|
current() &&
|
|
891
901
|
canonical(intent) &&
|
|
892
|
-
leader.isApplied(intent) &&
|
|
893
|
-
|
|
894
|
-
? "released"
|
|
895
|
-
: "unknown";
|
|
902
|
+
leader.isApplied(intent) && current();
|
|
903
|
+
return completed && acceptInput(intent) ? "released" : "unknown";
|
|
896
904
|
};
|
|
897
905
|
return {
|
|
898
906
|
/** Correlation only; a later cleanup attempt rereads and revalidates the canonical row itself. */
|
|
@@ -926,8 +934,7 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
926
934
|
const intent = retained();
|
|
927
935
|
if (
|
|
928
936
|
!intent ||
|
|
929
|
-
intent
|
|
930
|
-
intent.cleanup ||
|
|
937
|
+
!Threads.isTelegramLiveRebindInputAuthorized(intent) ||
|
|
931
938
|
!canonical(intent) ||
|
|
932
939
|
!leader?.isApplied(intent) ||
|
|
933
940
|
!leader.observeAdmission?.() ||
|
|
@@ -936,6 +943,7 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
936
943
|
!leader.isApplied(intent)
|
|
937
944
|
)
|
|
938
945
|
return "unknown";
|
|
946
|
+
if (!acceptInput(intent)) return "unknown";
|
|
939
947
|
released = true;
|
|
940
948
|
return "released";
|
|
941
949
|
}
|
|
@@ -1001,23 +1009,24 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
1001
1009
|
generation: recipient!.generation,
|
|
1002
1010
|
},
|
|
1003
1011
|
authority,
|
|
1012
|
+
beforeCleanup ? { holdInput: true } : undefined,
|
|
1004
1013
|
);
|
|
1005
1014
|
} else if (!commitIssued) return "protected";
|
|
1006
1015
|
if (
|
|
1007
1016
|
!intent ||
|
|
1008
1017
|
!sourceSaved() ||
|
|
1009
1018
|
!canonical(intent) ||
|
|
1010
|
-
intent.phase === "finished" ||
|
|
1011
|
-
intent.cleanup
|
|
1019
|
+
(intent.phase === "finished" && !cleanupBeforeReleaseIssued) ||
|
|
1020
|
+
(intent.cleanup && intent.inputRelease === undefined)
|
|
1012
1021
|
)
|
|
1013
1022
|
return "protected";
|
|
1014
1023
|
// A released peer may already have completed its sources; only its retained settlement can reobserve that outcome.
|
|
1015
1024
|
if (recipient!.kind === "follower" && peerReleaseIssued)
|
|
1016
|
-
return intent
|
|
1025
|
+
return Threads.isTelegramLiveRebindInputAuthorized(intent)
|
|
1017
1026
|
? await releaseFollower(intent)
|
|
1018
1027
|
: "protected";
|
|
1019
1028
|
const mode =
|
|
1020
|
-
applyIssued || intent.phase === "released" ? "inspect" : "apply";
|
|
1029
|
+
applyIssued || intent.phase === "released" || intent.phase === "finished" ? "inspect" : "apply";
|
|
1021
1030
|
applyIssued = true;
|
|
1022
1031
|
if (recipient!.kind === "leader") {
|
|
1023
1032
|
await leader!.apply(structuredClone(intent), mode, current);
|
|
@@ -1064,11 +1073,27 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
1064
1073
|
}
|
|
1065
1074
|
if (
|
|
1066
1075
|
!intent ||
|
|
1067
|
-
intent.phase !== "released" ||
|
|
1068
|
-
intent.cleanup ||
|
|
1076
|
+
(intent.phase !== "released" && !(intent.phase === "finished" && intent.inputRelease === "held")) ||
|
|
1077
|
+
(intent.cleanup && intent.inputRelease === undefined) ||
|
|
1069
1078
|
!canonical(intent)
|
|
1070
1079
|
)
|
|
1071
1080
|
return "protected";
|
|
1081
|
+
if (beforeCleanup) {
|
|
1082
|
+
if (!cleanupBeforeReleaseIssued) {
|
|
1083
|
+
cleanupBeforeReleaseIssued = true;
|
|
1084
|
+
cleanupBeforeReleaseDecided = await beforeCleanup(intent, current);
|
|
1085
|
+
}
|
|
1086
|
+
intent = retained();
|
|
1087
|
+
if (!cleanupBeforeReleaseDecided && intent?.phase !== "finished") return "unknown";
|
|
1088
|
+
if (!intent || !canonical(intent) ||
|
|
1089
|
+
(recipient!.kind === "leader" && !leader!.isApplied(intent)) ||
|
|
1090
|
+
intent.inputRelease !== "held" || intent.cleanup === "issued") return "unknown";
|
|
1091
|
+
let issued: Threads.TelegramWorkspaceLiveRebindIntent | undefined;
|
|
1092
|
+
try { issued = input.restoreStore.advanceLiveRebind(intent, "issue-input", authority); }
|
|
1093
|
+
catch { issued = retained(); }
|
|
1094
|
+
if (!issued || issued.inputRelease !== "issued" || !canonical(issued)) return "unknown";
|
|
1095
|
+
intent = issued;
|
|
1096
|
+
}
|
|
1072
1097
|
if (recipient!.kind === "leader") {
|
|
1073
1098
|
const expected = intent;
|
|
1074
1099
|
if (
|
|
@@ -1078,14 +1103,21 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
1078
1103
|
)
|
|
1079
1104
|
return "protected";
|
|
1080
1105
|
continuationIssued = true;
|
|
1081
|
-
const canContinue = () =>
|
|
1082
|
-
|
|
1106
|
+
const canContinue = () => {
|
|
1107
|
+
const observed = expected.inputRelease ? retained() : expected;
|
|
1108
|
+
return current() && !!observed &&
|
|
1109
|
+
(!expected.inputRelease || (observed.inputRelease === "issued" || observed.inputRelease === "accepted")) &&
|
|
1110
|
+
canonical(observed) && leader!.isApplied(observed);
|
|
1111
|
+
};
|
|
1083
1112
|
if (leader!.complete) {
|
|
1084
1113
|
leader!.complete([...messages], canContinue);
|
|
1114
|
+
// The detached native producer has taken this one input grant; its source ACK remains a separate observation.
|
|
1115
|
+
if (!acceptInput(intent)) return "unknown";
|
|
1085
1116
|
return observeCommandCompletion();
|
|
1086
1117
|
}
|
|
1087
1118
|
if (!(await leader!.continue!(messages, canContinue)))
|
|
1088
1119
|
return "unknown";
|
|
1120
|
+
if (!acceptInput(intent)) return "unknown";
|
|
1089
1121
|
} else return await releaseFollower(intent);
|
|
1090
1122
|
if (!current()) return "unknown";
|
|
1091
1123
|
released = true;
|
|
@@ -1519,13 +1551,14 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1519
1551
|
hasWorkspaceRestoreAuthority?: () => boolean;
|
|
1520
1552
|
/** Exact live root authority; capability advertisement alone cannot replace owned profile/session/recipient fences. */
|
|
1521
1553
|
hasWorkspaceLiveRebindAuthority?: () => boolean;
|
|
1554
|
+
canUsePreDispatchRebind?: (recipient?: Threads.TelegramWorkspaceRestoreRecipient) => boolean;
|
|
1522
1555
|
/** Strict complete namespace plus exact current/historical references; only an empty result clears journal protection. */
|
|
1523
1556
|
inspectTemporaryThreadSources?: (
|
|
1524
1557
|
target: Queue.TelegramQueueTarget,
|
|
1525
1558
|
requiredJournalBindingKeys: readonly string[],
|
|
1526
1559
|
ownInputs?: readonly Threads.TelegramTemporaryThreadInput[],
|
|
1527
1560
|
) => readonly number[] | undefined;
|
|
1528
|
-
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to
|
|
1561
|
+
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
|
|
1529
1562
|
temporaryThreadCleanupDelayMs?: number;
|
|
1530
1563
|
/** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
|
|
1531
1564
|
liveRebindCleanupSchedule?: {
|
|
@@ -1819,6 +1852,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1819
1852
|
>["canPrepareHeldCommand"];
|
|
1820
1853
|
/** Agent-settled hint: wakes waiting leader cleanup retries early; every readiness gate is still sampled fresh. */
|
|
1821
1854
|
onSessionSettled(ctx: TContext): void;
|
|
1855
|
+
/** Lifetime-fenced hint from drained publication/activity/delivery/API owners; never a cleanup grant. */
|
|
1856
|
+
onDeliverySettled(ctx: TContext): void;
|
|
1822
1857
|
/** Fresh body-free recipient observation only; never reconstructs an attempt or grants cleanup. */
|
|
1823
1858
|
observeLiveRebindLeaderWork(
|
|
1824
1859
|
intent: Threads.TelegramWorkspaceLiveRebindIntent,
|
|
@@ -1908,10 +1943,21 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1908
1943
|
temporaryThread?: Threads.TelegramTemporaryThreadEntry;
|
|
1909
1944
|
/** Source copies left in All (a menu-picked command); deleted once, when the input is consumed. */
|
|
1910
1945
|
allTabCopies?: number[];
|
|
1911
|
-
/**
|
|
1912
|
-
|
|
1946
|
+
/** This live opening attempt did not remove its All originals; replay/revival cannot reconstruct that fact. */
|
|
1947
|
+
allTabSourceRetained?: true;
|
|
1948
|
+
/** Complete warm copy identity; journal originals stay unchanged even after their All messages leave. */
|
|
1949
|
+
allTabCopiesForwarded?: {
|
|
1950
|
+
target: { chatId: number; threadId: number };
|
|
1951
|
+
sourceMessageIds: readonly number[];
|
|
1952
|
+
messageIds: readonly number[];
|
|
1953
|
+
};
|
|
1913
1954
|
/** Acknowledged same-tab membership stays protective when its reader disappears; never a dispatch/cleanup grant. */
|
|
1914
1955
|
temporaryMembership?: true;
|
|
1956
|
+
/** Confirmed display copies only; the durable Forward fact remains the native one-shot authority. */
|
|
1957
|
+
rerouteCopies?: {
|
|
1958
|
+
target: { chatId: number; threadId: number };
|
|
1959
|
+
bySourceUpdateId: ReadonlyMap<number, number>;
|
|
1960
|
+
};
|
|
1915
1961
|
/** Re-entrancy lock for one command route in flight; orthogonal to `phase`. */
|
|
1916
1962
|
dispatching?: boolean;
|
|
1917
1963
|
abandonment?: {
|
|
@@ -2380,15 +2426,22 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2380
2426
|
const cloneTelegramMessagesForThread = (
|
|
2381
2427
|
messages: TMessage[],
|
|
2382
2428
|
threadId: number,
|
|
2429
|
+
copies?: ReadonlyMap<number, number>,
|
|
2383
2430
|
): TMessage[] => {
|
|
2384
|
-
return messages.map((message) =>
|
|
2385
|
-
Updates.
|
|
2431
|
+
return messages.map((message) => {
|
|
2432
|
+
const ids = Updates.collectTelegramAdmissionSourceUpdateIds([message]);
|
|
2433
|
+
const copyId = copies && ids.length === 1 ? copies.get(ids[0]!) : undefined;
|
|
2434
|
+
if (copies && (!Number.isSafeInteger(copyId) || copyId! <= 0))
|
|
2435
|
+
throw new Error("Reroute destination copy identity is unavailable.");
|
|
2436
|
+
const routed = {
|
|
2386
2437
|
...message,
|
|
2387
|
-
message_id: 0,
|
|
2438
|
+
message_id: copyId ?? 0,
|
|
2388
2439
|
message_thread_id: threadId,
|
|
2389
2440
|
reply_to_message: undefined,
|
|
2390
|
-
} as TMessage
|
|
2391
|
-
|
|
2441
|
+
} as TMessage;
|
|
2442
|
+
return Updates.carryTelegramUpdateExecutionFence(message,
|
|
2443
|
+
copies ? Turns.withTelegramTurnReplyAnchor(routed) : routed);
|
|
2444
|
+
});
|
|
2392
2445
|
};
|
|
2393
2446
|
/** Whether a chooser is still pending in this Thread, ignoring the ones the caller itself owns. */
|
|
2394
2447
|
const hasPendingRerouteForTarget = (
|
|
@@ -2784,9 +2837,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2784
2837
|
const deleteAllTabMessages = async (
|
|
2785
2838
|
chatId: number,
|
|
2786
2839
|
messageIds: readonly number[],
|
|
2840
|
+
isCurrent?: () => boolean,
|
|
2787
2841
|
): Promise<void> => {
|
|
2788
2842
|
if (!deps.deleteMessage) return;
|
|
2789
2843
|
for (const messageId of messageIds) {
|
|
2844
|
+
if (isCurrent?.() === false) return;
|
|
2790
2845
|
try {
|
|
2791
2846
|
await deps.deleteMessage(chatId, messageId);
|
|
2792
2847
|
} catch (error) {
|
|
@@ -2799,10 +2854,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2799
2854
|
}
|
|
2800
2855
|
};
|
|
2801
2856
|
/** One-shot per chooser: the copies are taken before deletion, so a later consuming path finds none. */
|
|
2802
|
-
const deleteAllTabCopies = (
|
|
2857
|
+
const deleteAllTabCopies = (
|
|
2858
|
+
pending: PendingUnboundReroute,
|
|
2859
|
+
isCurrent?: () => boolean,
|
|
2860
|
+
): Promise<void> =>
|
|
2803
2861
|
deleteAllTabMessages(
|
|
2804
2862
|
pending.sourceTarget.chatId,
|
|
2805
2863
|
pending.allTabCopies?.splice(0) ?? [],
|
|
2864
|
+
isCurrent,
|
|
2806
2865
|
);
|
|
2807
2866
|
/**
|
|
2808
2867
|
* Restore keeps the tab as the Thread, so the input must stay visible there: the All original goes only when the tab
|
|
@@ -2811,7 +2870,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2811
2870
|
const releaseAllTabCopiesIntoSurvivingTab = (
|
|
2812
2871
|
pending: PendingUnboundReroute,
|
|
2813
2872
|
): Promise<void> => {
|
|
2814
|
-
if (
|
|
2873
|
+
if (
|
|
2874
|
+
pending.allTabCopiesForwarded &&
|
|
2875
|
+
isDeepStrictEqual(pending.allTabCopiesForwarded.target, pending.sourceTarget)
|
|
2876
|
+
)
|
|
2877
|
+
return deleteAllTabCopies(pending);
|
|
2815
2878
|
pending.allTabCopies?.splice(0);
|
|
2816
2879
|
return Promise.resolve();
|
|
2817
2880
|
};
|
|
@@ -2825,31 +2888,129 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2825
2888
|
target: { chatId: number; threadId: number },
|
|
2826
2889
|
isCurrent: () => boolean,
|
|
2827
2890
|
): Promise<number | undefined> => {
|
|
2828
|
-
const
|
|
2829
|
-
if (
|
|
2891
|
+
const messageIds = [...(pending.allTabCopies ?? [])].sort((a, b) => a - b);
|
|
2892
|
+
if (messageIds.length === 0 || !deps.callApi || !isCurrent()) return;
|
|
2893
|
+
const copyTarget = { ...target };
|
|
2894
|
+
const route = {
|
|
2895
|
+
chat_id: copyTarget.chatId,
|
|
2896
|
+
message_thread_id: copyTarget.threadId,
|
|
2897
|
+
from_chat_id: pending.sourceTarget.chatId,
|
|
2898
|
+
disable_notification: true,
|
|
2899
|
+
};
|
|
2900
|
+
const options = {
|
|
2901
|
+
maxAttempts: 1,
|
|
2902
|
+
retrySafety: "non-idempotent" as const,
|
|
2903
|
+
assertAuthority() {
|
|
2904
|
+
if (!isCurrent())
|
|
2905
|
+
throw new Error("Temporary Thread source forwarding authority changed.");
|
|
2906
|
+
},
|
|
2907
|
+
};
|
|
2830
2908
|
try {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2909
|
+
// An album moves as one album; forwardMessages skips what it cannot forward, so a short reply is not a move.
|
|
2910
|
+
const copies =
|
|
2911
|
+
messageIds.length === 1
|
|
2912
|
+
? [
|
|
2913
|
+
await deps.callApi<{ message_id?: unknown }>(
|
|
2914
|
+
"forwardMessage",
|
|
2915
|
+
{ ...route, message_id: messageIds[0] },
|
|
2916
|
+
options,
|
|
2917
|
+
),
|
|
2918
|
+
]
|
|
2919
|
+
: await deps.callApi<Array<{ message_id?: unknown }>>(
|
|
2920
|
+
"forwardMessages",
|
|
2921
|
+
{ ...route, message_ids: messageIds },
|
|
2922
|
+
options,
|
|
2923
|
+
);
|
|
2924
|
+
if (!isCurrent()) return;
|
|
2925
|
+
const copyIds = Array.isArray(copies)
|
|
2926
|
+
? copies.map((copy) => copy?.message_id)
|
|
2927
|
+
: [];
|
|
2928
|
+
if (
|
|
2929
|
+
copyIds.length !== messageIds.length ||
|
|
2930
|
+
!copyIds.every(
|
|
2931
|
+
(id): id is number =>
|
|
2932
|
+
typeof id === "number" && Number.isSafeInteger(id) && id > 0,
|
|
2933
|
+
) ||
|
|
2934
|
+
new Set(copyIds).size !== copyIds.length
|
|
2935
|
+
)
|
|
2936
|
+
return;
|
|
2937
|
+
pending.allTabCopiesForwarded = {
|
|
2938
|
+
target: copyTarget,
|
|
2939
|
+
sourceMessageIds: messageIds,
|
|
2940
|
+
messageIds: copyIds,
|
|
2941
|
+
};
|
|
2942
|
+
await deleteAllTabCopies(pending, isCurrent);
|
|
2943
|
+
if (!isCurrent()) return;
|
|
2944
|
+
return pending.allTabCopiesForwarded.messageIds[0];
|
|
2846
2945
|
} catch (error) {
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2946
|
+
if (isCurrent())
|
|
2947
|
+
deps.recordRuntimeEvent?.("routing", error, {
|
|
2948
|
+
phase: "temporary-thread-source-forward",
|
|
2949
|
+
});
|
|
2850
2950
|
return;
|
|
2851
2951
|
}
|
|
2852
2952
|
};
|
|
2953
|
+
/** Read original source addresses, not warm target rewrites; unknown copy provenance never grants a Reroute. */
|
|
2954
|
+
const resolvePendingRerouteForwardSource = (
|
|
2955
|
+
pending: PendingUnboundReroute,
|
|
2956
|
+
): { chatId: number; sourceUpdateIds: number[]; sourceMessageIds: number[]; messageIds: number[] } | undefined => {
|
|
2957
|
+
try {
|
|
2958
|
+
const originals = pending.messages.map((message) => {
|
|
2959
|
+
const snapshot = Updates.inspectTelegramDeferredSourceSnapshot(message);
|
|
2960
|
+
const ids = Updates.collectTelegramAdmissionSourceUpdateIds([message]);
|
|
2961
|
+
const original = snapshot?.update.message as TMessage | undefined;
|
|
2962
|
+
if (
|
|
2963
|
+
!snapshot ||
|
|
2964
|
+
ids.length !== 1 ||
|
|
2965
|
+
snapshot.source.updateId !== ids[0] ||
|
|
2966
|
+
snapshot.update.update_id !== ids[0] ||
|
|
2967
|
+
Object.keys(snapshot.update).some((key) => key !== "update_id" && key !== "message") ||
|
|
2968
|
+
snapshot.source.journalBindingKey !== deps.getAdmissionJournalBinding?.() ||
|
|
2969
|
+
!original ||
|
|
2970
|
+
original.chat?.type !== "private" ||
|
|
2971
|
+
original.chat.id !== pending.sourceTarget.chatId ||
|
|
2972
|
+
original.from?.id !== deps.configStore.getAllowedUserId() ||
|
|
2973
|
+
original.from?.is_bot !== false ||
|
|
2974
|
+
!Number.isSafeInteger(original.message_id) ||
|
|
2975
|
+
original.message_id <= 0
|
|
2976
|
+
)
|
|
2977
|
+
return undefined;
|
|
2978
|
+
return { message: original, updateId: ids[0]! };
|
|
2979
|
+
});
|
|
2980
|
+
if (originals.length === 0 || originals.some((source) => !source)) return;
|
|
2981
|
+
originals.sort((a, b) => a!.message.message_id - b!.message.message_id);
|
|
2982
|
+
const sourceMessageIds = originals.map((source) => source!.message.message_id);
|
|
2983
|
+
const sourceUpdateIds = originals.map((source) => source!.updateId);
|
|
2984
|
+
if (
|
|
2985
|
+
new Set(sourceMessageIds).size !== sourceMessageIds.length ||
|
|
2986
|
+
new Set(sourceUpdateIds).size !== sourceUpdateIds.length
|
|
2987
|
+
)
|
|
2988
|
+
return;
|
|
2989
|
+
const copied = pending.allTabCopiesForwarded;
|
|
2990
|
+
if (copied) {
|
|
2991
|
+
if (
|
|
2992
|
+
!isDeepStrictEqual(copied.target, pending.sourceTarget) ||
|
|
2993
|
+
!isDeepStrictEqual(copied.sourceMessageIds, sourceMessageIds) ||
|
|
2994
|
+
copied.messageIds.length !== sourceMessageIds.length ||
|
|
2995
|
+
!copied.messageIds.every((id) => Number.isSafeInteger(id) && id > 0) ||
|
|
2996
|
+
new Set(copied.messageIds).size !== copied.messageIds.length
|
|
2997
|
+
)
|
|
2998
|
+
return;
|
|
2999
|
+
return { chatId: copied.target.chatId, sourceUpdateIds, sourceMessageIds, messageIds: [...copied.messageIds] };
|
|
3000
|
+
}
|
|
3001
|
+
const nativeOriginals = originals.every((source) =>
|
|
3002
|
+
typeof pending.sourceTarget.threadId === "number" &&
|
|
3003
|
+
source!.message.message_thread_id === pending.sourceTarget.threadId);
|
|
3004
|
+
const retainedAllOriginals = pending.allTabSourceRetained === true &&
|
|
3005
|
+
originals.every((source) => source!.message.message_thread_id === undefined) &&
|
|
3006
|
+
isDeepStrictEqual([...(pending.allTabCopies ?? [])].sort((a, b) => a - b), sourceMessageIds);
|
|
3007
|
+
if (nativeOriginals || retainedAllOriginals)
|
|
3008
|
+
return { chatId: pending.sourceTarget.chatId, sourceUpdateIds, sourceMessageIds, messageIds: [...sourceMessageIds] };
|
|
3009
|
+
} catch (error) {
|
|
3010
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-source-proof" });
|
|
3011
|
+
}
|
|
3012
|
+
return undefined;
|
|
3013
|
+
};
|
|
2853
3014
|
const dismissRerouteChooserMessage = async (
|
|
2854
3015
|
query: TCallbackQuery,
|
|
2855
3016
|
assertExecutionCurrent?: () => void,
|
|
@@ -4774,8 +4935,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
4774
4935
|
.listLiveRebindings()
|
|
4775
4936
|
.find((value) => value.request.operationId === request.operationId);
|
|
4776
4937
|
if (
|
|
4777
|
-
intent
|
|
4778
|
-
!intent.cleanup &&
|
|
4938
|
+
intent && Threads.isTelegramLiveRebindInputAuthorized(intent) &&
|
|
4779
4939
|
isDeepStrictEqual(intent.request, request) &&
|
|
4780
4940
|
intent.operatorUserId === operatorUserId &&
|
|
4781
4941
|
isDeepStrictEqual(intent.executor, {
|
|
@@ -5188,6 +5348,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5188
5348
|
name ||
|
|
5189
5349
|
confirmation ||
|
|
5190
5350
|
extension;
|
|
5351
|
+
const preDispatchCleanup =
|
|
5352
|
+
deps.runWorkspaceOperation !== undefined && deps.canUsePreDispatchRebind?.(recipient) === true;
|
|
5191
5353
|
const createCoordinator = () =>
|
|
5192
5354
|
createTelegramLiveRebindCoordinator({
|
|
5193
5355
|
request,
|
|
@@ -5198,25 +5360,55 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5198
5360
|
retitle: deps.callApi
|
|
5199
5361
|
? async (isCurrent) => {
|
|
5200
5362
|
if (!isCurrent()) return;
|
|
5363
|
+
const expected = threads.getWorkspaceBindingByTarget(target, binding.sessionId);
|
|
5364
|
+
if (!expected || expected.bindingKey !== binding.bindingKey || expected.slot !== binding.slot)
|
|
5365
|
+
throw new Error("Live-rebind title binding is unavailable.");
|
|
5366
|
+
const callApi = deps.callApi!;
|
|
5201
5367
|
const slot = binding.slot ?? "";
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
)
|
|
5211
|
-
}
|
|
5368
|
+
const resolveTitle = () => deps.getDisplayTitle?.(expected.target) ??
|
|
5369
|
+
ThreadNaming.getTelegramTopicTitleForThreadName(getRestoredThreadName(request.owner, slot), slot);
|
|
5370
|
+
const title = resolveTitle();
|
|
5371
|
+
const bindingCurrent = (frame: Threads.TelegramWorkspaceThreadBinding) =>
|
|
5372
|
+
isCurrent() && deps.callApi === callApi &&
|
|
5373
|
+
isDeepStrictEqual(threads.getWorkspaceBindingByTarget(frame.target, frame.sessionId), frame);
|
|
5374
|
+
const assertTitle = () => {
|
|
5375
|
+
if (!bindingCurrent(expected) || resolveTitle() !== title)
|
|
5376
|
+
throw new Error("Live-rebind title authority changed.");
|
|
5377
|
+
};
|
|
5378
|
+
assertTitle();
|
|
5379
|
+
const renamed = await callApi.call(deps, "editForumTopic", {
|
|
5380
|
+
chat_id: expected.target.chatId,
|
|
5381
|
+
message_thread_id: expected.target.threadId,
|
|
5382
|
+
name: title,
|
|
5383
|
+
}, { maxAttempts: 1, assertAuthority: assertTitle });
|
|
5384
|
+
assertTitle();
|
|
5385
|
+
if (renamed !== true || !threads.setWorkspaceDisplayTitle(expected, title))
|
|
5386
|
+
throw new Error("Live-rebind title acknowledgement is unconfirmed.");
|
|
5387
|
+
const acknowledged = { ...expected, displayTitle: title };
|
|
5388
|
+
await threads.persist(() => bindingCurrent(acknowledged));
|
|
5389
|
+
if (!bindingCurrent(acknowledged))
|
|
5390
|
+
throw new Error("Live-rebind title publication authority changed.");
|
|
5212
5391
|
}
|
|
5213
5392
|
: undefined,
|
|
5214
5393
|
authority: {
|
|
5215
5394
|
executor: { instanceId, leaderEpoch: String(epoch) },
|
|
5216
5395
|
operatorUserId,
|
|
5217
|
-
isCurrent: current,
|
|
5396
|
+
isCurrent: () => current() && (!preDispatchCleanup || deps.canUsePreDispatchRebind?.(recipient) === true),
|
|
5218
5397
|
},
|
|
5219
5398
|
getRecipient,
|
|
5399
|
+
...(preDispatchCleanup ? {
|
|
5400
|
+
async cleanupBeforeRelease(intent: Threads.TelegramWorkspaceLiveRebindIntent, isCurrent: () => boolean) {
|
|
5401
|
+
const delay = deps.liveRebindCleanupSchedule?.delaysMs[0] ?? 1_000;
|
|
5402
|
+
await new Promise<void>(resolve => { const timer = setTimeout(resolve, delay); timer.unref?.(); });
|
|
5403
|
+
if (!isCurrent()) return false;
|
|
5404
|
+
const result = await inspectLiveRebindRecipient(intent, ctx, isLeader ? "issue" : "follower-issue", isCurrent);
|
|
5405
|
+
if (!isCurrent()) return false;
|
|
5406
|
+
// A session-replacing producer cannot retain an old-session cleanup grant across its gateway.
|
|
5407
|
+
if (result?.status === "not-ready" && selection.confirmationCommand?.name === "new")
|
|
5408
|
+
return !!store.advanceLiveRebind(intent, "protect-old-target", { executor: intent.executor, operatorUserId, isCurrent });
|
|
5409
|
+
return result?.status === "not-ready" || result?.status === "finished" && result.recorded;
|
|
5410
|
+
},
|
|
5411
|
+
} : {}),
|
|
5220
5412
|
follower: isLeader
|
|
5221
5413
|
? undefined
|
|
5222
5414
|
: {
|
|
@@ -5258,9 +5450,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5258
5450
|
instanceId,
|
|
5259
5451
|
leaderEpoch: String(epoch),
|
|
5260
5452
|
}) &&
|
|
5261
|
-
!intent.cleanup &&
|
|
5262
|
-
(intent.phase === "rebound" ||
|
|
5263
|
-
intent.phase === "
|
|
5453
|
+
(!intent.cleanup || intent.inputRelease !== undefined && intent.cleanup !== "issued") &&
|
|
5454
|
+
(intent.phase === "rebound" || intent.phase === "released" ||
|
|
5455
|
+
intent.phase === "finished" && intent.inputRelease !== undefined) &&
|
|
5264
5456
|
isDeepStrictEqual(intent.recipient, {
|
|
5265
5457
|
kind: recipient.kind,
|
|
5266
5458
|
instanceId: recipient.instanceId,
|
|
@@ -5320,6 +5512,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5320
5512
|
});
|
|
5321
5513
|
},
|
|
5322
5514
|
isApplied,
|
|
5515
|
+
|
|
5323
5516
|
...(completeCommand
|
|
5324
5517
|
? {
|
|
5325
5518
|
complete(
|
|
@@ -5512,11 +5705,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5512
5705
|
intent: Threads.TelegramWorkspaceLiveRebindIntent,
|
|
5513
5706
|
ctx: TContext,
|
|
5514
5707
|
mode: "issue" | "follower-issue",
|
|
5708
|
+
admittedOrigin?: () => boolean,
|
|
5515
5709
|
): Promise<TelegramLiveRebindCleanupIssue | undefined>;
|
|
5516
5710
|
async function inspectLiveRebindRecipient(
|
|
5517
5711
|
intent: Threads.TelegramWorkspaceLiveRebindIntent,
|
|
5518
5712
|
ctx: TContext,
|
|
5519
5713
|
mode: "work" | "cleanup" | "follower-cleanup" | "issue" | "follower-issue",
|
|
5714
|
+
admittedOrigin?: () => boolean,
|
|
5520
5715
|
): Promise<
|
|
5521
5716
|
| Bus.TelegramBusLiveRebindWorkObservation
|
|
5522
5717
|
| ThreadReconciler.TelegramLiveRebindCleanupPreparation
|
|
@@ -5544,8 +5739,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5544
5739
|
!run ||
|
|
5545
5740
|
(issueMode && !callApi) ||
|
|
5546
5741
|
deps.isContextActive?.(ctx) !== true
|
|
5547
|
-
)
|
|
5742
|
+
) {
|
|
5548
5743
|
return undefined;
|
|
5744
|
+
}
|
|
5549
5745
|
try {
|
|
5550
5746
|
const expected = structuredClone(intent),
|
|
5551
5747
|
{ request, recipient, executor } = expected;
|
|
@@ -5651,6 +5847,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5651
5847
|
);
|
|
5652
5848
|
};
|
|
5653
5849
|
const current = () =>
|
|
5850
|
+
(!admittedOrigin || admittedOrigin()) &&
|
|
5654
5851
|
deps.hasWorkspaceLiveRebindAuthority?.() === true &&
|
|
5655
5852
|
deps.isContextActive?.(ctx) === true &&
|
|
5656
5853
|
deps.workspaceRestoreRecipient === ports &&
|
|
@@ -5701,8 +5898,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5701
5898
|
};
|
|
5702
5899
|
const sample = async () => {
|
|
5703
5900
|
assertCanonical();
|
|
5704
|
-
if (cleanupMode
|
|
5705
|
-
|
|
5901
|
+
if (cleanupMode) {
|
|
5902
|
+
const protectedTarget = protection.call(threads, expected);
|
|
5903
|
+
if (protectedTarget !== false) return undefined;
|
|
5904
|
+
}
|
|
5706
5905
|
const target = { ...request.binding.target };
|
|
5707
5906
|
let value: Bus.TelegramBusLiveRebindWorkState | undefined;
|
|
5708
5907
|
if (followerMode) {
|
|
@@ -5711,7 +5910,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5711
5910
|
instanceId: recipient.instanceId,
|
|
5712
5911
|
sessionId: recipient.sessionId,
|
|
5713
5912
|
recipientBindingKey: recipientJournalKey!,
|
|
5714
|
-
mode: "observe",
|
|
5913
|
+
mode: expected.inputRelease === "held" ? "observe-held" : "observe",
|
|
5715
5914
|
sourceUpdateIds: [...request.source.updateIds],
|
|
5716
5915
|
slot: request.binding.slot!,
|
|
5717
5916
|
target: { ...request.target },
|
|
@@ -5766,12 +5965,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5766
5965
|
);
|
|
5767
5966
|
assertCanonical();
|
|
5768
5967
|
if (cleanupMode) {
|
|
5968
|
+
const targetProtected = protection.call(threads, expected);
|
|
5769
5969
|
const prepared = ThreadReconciler.prepareLiveRebindThreadCleanup({
|
|
5770
5970
|
operationId: request.operationId,
|
|
5771
5971
|
oldTarget: request.binding.target,
|
|
5772
5972
|
recipientTarget: request.target,
|
|
5773
5973
|
leaderEpoch: executor.leaderEpoch,
|
|
5774
|
-
targetProtected
|
|
5974
|
+
targetProtected,
|
|
5775
5975
|
work,
|
|
5776
5976
|
});
|
|
5777
5977
|
assertCanonical();
|
|
@@ -5794,21 +5994,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5794
5994
|
if (!current()) return undefined;
|
|
5795
5995
|
if (issueMode) {
|
|
5796
5996
|
// One admission spans the fresh sample, durable issue marker, single deletion and terminal record.
|
|
5797
|
-
|
|
5798
|
-
{
|
|
5799
|
-
operationId: `live-${mode}-${randomBytes(16).toString("hex")}`,
|
|
5800
|
-
operationKind: "workspace.live-rebind-cleanup",
|
|
5801
|
-
scopes: [{ kind: "profile" }],
|
|
5802
|
-
},
|
|
5803
|
-
async (): Promise<TelegramLiveRebindCleanupIssue | undefined> => {
|
|
5997
|
+
const execute = async (): Promise<TelegramLiveRebindCleanupIssue | undefined> => {
|
|
5804
5998
|
const prepared = await sample();
|
|
5805
5999
|
if (
|
|
5806
6000
|
!prepared ||
|
|
5807
6001
|
prepared.status !== "prepared" ||
|
|
5808
6002
|
!current() ||
|
|
5809
6003
|
protection.call(threads, expected) !== false
|
|
5810
|
-
)
|
|
6004
|
+
) {
|
|
5811
6005
|
return { status: "not-ready" };
|
|
6006
|
+
}
|
|
5812
6007
|
const authority = {
|
|
5813
6008
|
executor: structuredClone(executor),
|
|
5814
6009
|
operatorUserId: expected.operatorUserId,
|
|
@@ -5819,21 +6014,19 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5819
6014
|
"issue-cleanup",
|
|
5820
6015
|
authority,
|
|
5821
6016
|
);
|
|
5822
|
-
if (!grant)
|
|
6017
|
+
if (!grant) {
|
|
6018
|
+
return undefined;
|
|
6019
|
+
}
|
|
5823
6020
|
const assertGranted = () => assertCanonical(grant);
|
|
5824
6021
|
const cleanup = await ThreadReconciler.issueLiveRebindThreadCleanup(
|
|
5825
6022
|
prepared,
|
|
5826
6023
|
{
|
|
5827
6024
|
assertCurrent: assertGranted,
|
|
5828
|
-
deleteTopic: (target) =>
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
message_thread_id: target.threadId,
|
|
5834
|
-
},
|
|
5835
|
-
{ maxAttempts: 1, assertAuthority: assertGranted },
|
|
5836
|
-
),
|
|
6025
|
+
deleteTopic: (target) => callApi!(
|
|
6026
|
+
"deleteForumTopic",
|
|
6027
|
+
{ chat_id: target.chatId, message_thread_id: target.threadId },
|
|
6028
|
+
{ maxAttempts: 1, assertAuthority: assertGranted },
|
|
6029
|
+
),
|
|
5837
6030
|
classifyFailure: classifyLiveRebindCleanupFailure,
|
|
5838
6031
|
},
|
|
5839
6032
|
);
|
|
@@ -5851,8 +6044,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5851
6044
|
cleanup,
|
|
5852
6045
|
recorded,
|
|
5853
6046
|
};
|
|
5854
|
-
|
|
5855
|
-
|
|
6047
|
+
};
|
|
6048
|
+
// Pre-dispatch calls are private to the chooser callback already holding the same profile admission/gate.
|
|
6049
|
+
return admittedOrigin ? await execute() : await run({
|
|
6050
|
+
operationId: `live-${mode}-${randomBytes(16).toString("hex")}`,
|
|
6051
|
+
operationKind: "workspace.live-rebind-cleanup", scopes: [{ kind: "profile" }],
|
|
6052
|
+
}, execute);
|
|
5856
6053
|
}
|
|
5857
6054
|
const observation = await run(
|
|
5858
6055
|
{
|
|
@@ -5905,8 +6102,22 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5905
6102
|
string,
|
|
5906
6103
|
ReturnType<typeof setTimeout> | "running"
|
|
5907
6104
|
>();
|
|
5908
|
-
//
|
|
5909
|
-
const liveRebindCleanupWakers = new Map<string, () => void>();
|
|
6105
|
+
// One hint slot per existing leader chain; a running attempt retains at most one coalesced recheck.
|
|
6106
|
+
const liveRebindCleanupWakers = new Map<string, (reason: "session-settled" | "delivery-drained") => void>();
|
|
6107
|
+
const captureLiveRebindCleanupScope = (ctx: TContext): (() => boolean) => {
|
|
6108
|
+
try {
|
|
6109
|
+
const generation = deps.getSessionGeneration?.(), instanceId = deps.getCurrentInstanceId?.(),
|
|
6110
|
+
epoch = deps.getCurrentLeaderEpoch?.(), store = deps.getWorkspaceRestoreStore?.(),
|
|
6111
|
+
operator = deps.configStore.getAllowedUserId();
|
|
6112
|
+
return () => {
|
|
6113
|
+
try {
|
|
6114
|
+
return deps.isContextActive?.(ctx) === true && deps.getSessionGeneration?.() === generation &&
|
|
6115
|
+
deps.getCurrentInstanceId?.() === instanceId && deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
6116
|
+
deps.getWorkspaceRestoreStore?.() === store && deps.configStore.getAllowedUserId() === operator;
|
|
6117
|
+
} catch { return false; }
|
|
6118
|
+
};
|
|
6119
|
+
} catch { return () => false; }
|
|
6120
|
+
};
|
|
5910
6121
|
/** End an unissued attempt honestly under fresh admission and current operator authority; no executor grant is borrowed. */
|
|
5911
6122
|
const terminalizeUnissuedLiveRebindCleanup = async (
|
|
5912
6123
|
operationId: string,
|
|
@@ -5967,25 +6178,48 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5967
6178
|
ctx: TContext,
|
|
5968
6179
|
startedAtMs = Date.now(),
|
|
5969
6180
|
attempt = 0,
|
|
6181
|
+
current = captureLiveRebindCleanupScope(ctx),
|
|
5970
6182
|
): void => {
|
|
5971
6183
|
const pacing = deps.liveRebindCleanupSchedule;
|
|
5972
6184
|
const chain = liveRebindCleanupChains.get(operationId);
|
|
5973
6185
|
if (
|
|
5974
6186
|
!pacing ||
|
|
5975
6187
|
(attempt === 0 ? chain !== undefined : chain !== "running") ||
|
|
5976
|
-
|
|
6188
|
+
!current()
|
|
5977
6189
|
) {
|
|
5978
|
-
if (attempt > 0 && chain === "running")
|
|
6190
|
+
if (attempt > 0 && chain === "running") {
|
|
5979
6191
|
liveRebindCleanupChains.delete(operationId);
|
|
6192
|
+
liveRebindCleanupWakers.delete(operationId);
|
|
6193
|
+
}
|
|
5980
6194
|
return;
|
|
5981
6195
|
}
|
|
6196
|
+
let pendingWake: "session-settled" | "delivery-drained" | undefined;
|
|
6197
|
+
const wake = (reason: "session-settled" | "delivery-drained") => {
|
|
6198
|
+
if (!current()) return;
|
|
6199
|
+
if (liveRebindCleanupChains.get(operationId) === "running") {
|
|
6200
|
+
pendingWake = reason; return;
|
|
6201
|
+
}
|
|
6202
|
+
if (liveRebindCleanupChains.get(operationId) !== timer) return;
|
|
6203
|
+
if (reason === "delivery-drained") {
|
|
6204
|
+
let idle: unknown;
|
|
6205
|
+
try { idle = deps.isIdle?.(ctx); } catch { /* A missing idle observation is not a reason to hurry cleanup. */ }
|
|
6206
|
+
if (idle !== true) {
|
|
6207
|
+
// A streamed technical task can drain between deltas while Pi still runs. Keep one hint, not a polling loop.
|
|
6208
|
+
pendingWake = reason;
|
|
6209
|
+
return;
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
clearTimeout(timer);
|
|
6213
|
+
attemptNow();
|
|
6214
|
+
};
|
|
5982
6215
|
const attemptNow = () => {
|
|
5983
|
-
liveRebindCleanupWakers.delete(operationId);
|
|
5984
6216
|
if (liveRebindCleanupChains.get(operationId) !== timer) return;
|
|
6217
|
+
pendingWake = undefined; // This fresh timer/settlement attempt consumes any previously deferred hint.
|
|
5985
6218
|
liveRebindCleanupChains.set(operationId, "running");
|
|
6219
|
+
if (role === "leader") liveRebindCleanupWakers.set(operationId, wake);
|
|
5986
6220
|
let continued = false;
|
|
5987
6221
|
const task = (async () => {
|
|
5988
|
-
if (
|
|
6222
|
+
if (!current()) return;
|
|
5989
6223
|
const intent = deps
|
|
5990
6224
|
.getWorkspaceRestoreStore?.()
|
|
5991
6225
|
?.listLiveRebindings()
|
|
@@ -5998,11 +6232,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5998
6232
|
: await issueLiveRebindFollowerCleanup(intent, ctx);
|
|
5999
6233
|
if (
|
|
6000
6234
|
result?.status === "finished" ||
|
|
6001
|
-
|
|
6235
|
+
!current()
|
|
6002
6236
|
)
|
|
6003
6237
|
return;
|
|
6004
|
-
if (Date.now() - startedAtMs >= pacing.windowMs)
|
|
6238
|
+
if (Date.now() - startedAtMs >= pacing.windowMs) {
|
|
6005
6239
|
return terminalizeUnissuedLiveRebindCleanup(operationId, ctx);
|
|
6240
|
+
}
|
|
6006
6241
|
continued = true;
|
|
6007
6242
|
scheduleLiveRebindCleanup(
|
|
6008
6243
|
operationId,
|
|
@@ -6010,7 +6245,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6010
6245
|
ctx,
|
|
6011
6246
|
startedAtMs,
|
|
6012
6247
|
attempt + 1,
|
|
6248
|
+
current,
|
|
6013
6249
|
);
|
|
6250
|
+
if (pendingWake) liveRebindCleanupWakers.get(operationId)?.(pendingWake);
|
|
6014
6251
|
})()
|
|
6015
6252
|
.catch((error) =>
|
|
6016
6253
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
@@ -6021,8 +6258,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6021
6258
|
if (
|
|
6022
6259
|
!continued &&
|
|
6023
6260
|
liveRebindCleanupChains.get(operationId) === "running"
|
|
6024
|
-
)
|
|
6261
|
+
) {
|
|
6025
6262
|
liveRebindCleanupChains.delete(operationId);
|
|
6263
|
+
liveRebindCleanupWakers.delete(operationId);
|
|
6264
|
+
}
|
|
6026
6265
|
});
|
|
6027
6266
|
restoreSettlementTasks.add(task);
|
|
6028
6267
|
void task.finally(() => restoreSettlementTasks.delete(task));
|
|
@@ -6034,17 +6273,27 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6034
6273
|
timer.unref?.();
|
|
6035
6274
|
liveRebindCleanupChains.set(operationId, timer);
|
|
6036
6275
|
// 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
|
-
});
|
|
6276
|
+
if (role === "leader" && attempt > 0) liveRebindCleanupWakers.set(operationId, wake);
|
|
6043
6277
|
};
|
|
6044
|
-
|
|
6045
|
-
const onSessionSettled = (ctx: TContext): void => {
|
|
6278
|
+
const wakeLiveRebindCleanup = (ctx: TContext, reason: "session-settled" | "delivery-drained"): void => {
|
|
6046
6279
|
if (deps.isContextActive?.(ctx) !== true) return;
|
|
6047
|
-
for (const wake of [...liveRebindCleanupWakers.values()]) wake();
|
|
6280
|
+
for (const wake of [...liveRebindCleanupWakers.values()]) wake(reason);
|
|
6281
|
+
};
|
|
6282
|
+
const onSessionSettled = (ctx: TContext): void => wakeLiveRebindCleanup(ctx, "session-settled");
|
|
6283
|
+
const onDeliverySettled = (ctx: TContext): void => {
|
|
6284
|
+
wakeLiveRebindCleanup(ctx, "delivery-drained");
|
|
6285
|
+
const cap = captureTemporaryThreadAuthority(ctx);
|
|
6286
|
+
if (!cap?.isCurrent()) return;
|
|
6287
|
+
try {
|
|
6288
|
+
for (const entry of cap.store.listTemporaryThreads()) {
|
|
6289
|
+
if (!cap.isCurrent()) return;
|
|
6290
|
+
if (entry.phase === "created" && entry.target && !entry.cleanupIssued &&
|
|
6291
|
+
Threads.isTelegramTemporaryThreadFullyResolved(entry))
|
|
6292
|
+
scheduleTemporaryThreadCleanup(entry.target, ctx);
|
|
6293
|
+
}
|
|
6294
|
+
} catch (error) {
|
|
6295
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-drain" });
|
|
6296
|
+
}
|
|
6048
6297
|
};
|
|
6049
6298
|
const restoreWorkspace = deps.workspaceRestoreRecipient
|
|
6050
6299
|
? async (input: {
|
|
@@ -6675,6 +6924,97 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6675
6924
|
return false;
|
|
6676
6925
|
}
|
|
6677
6926
|
};
|
|
6927
|
+
/** Publish display copies under the native one-shot grant; positive output is not input/source settlement. */
|
|
6928
|
+
const publishPendingRerouteCopies = async (
|
|
6929
|
+
pending: PendingUnboundReroute,
|
|
6930
|
+
record: Threads.TelegramTopicTargetRecord,
|
|
6931
|
+
ctx: TContext,
|
|
6932
|
+
assertExecutionCurrent: () => void,
|
|
6933
|
+
foreign: boolean,
|
|
6934
|
+
): Promise<(() => void) | undefined> => {
|
|
6935
|
+
const source = resolvePendingRerouteForwardSource(pending);
|
|
6936
|
+
const target = { ...record.target };
|
|
6937
|
+
const recordIdentity = (value: Threads.TelegramTopicTargetRecord | undefined) => value && ({
|
|
6938
|
+
target: value.target, profileKey: value.profileKey, owner: value.owner,
|
|
6939
|
+
instanceId: value.instanceId, slot: value.slot, createdAtMs: value.createdAtMs,
|
|
6940
|
+
});
|
|
6941
|
+
const bindingIdentity = () => deps.threadStore!.listWorkspaceBindings()
|
|
6942
|
+
.filter((value) => isDeepStrictEqual(value.target, target))
|
|
6943
|
+
.map((value) => ({ bindingKey: value.bindingKey, sessionId: value.sessionId, sessionKey: value.sessionKey,
|
|
6944
|
+
cwd: value.cwd, workspaceKey: value.workspaceKey, instanceSlot: value.instanceSlot, slot: value.slot,
|
|
6945
|
+
target: value.target }));
|
|
6946
|
+
const expectedRecord = structuredClone(recordIdentity(record));
|
|
6947
|
+
const expectedBindings = structuredClone(bindingIdentity());
|
|
6948
|
+
const ownership = deps.getTargetOwnership?.(target);
|
|
6949
|
+
if (
|
|
6950
|
+
!source || !deps.callApi || expectedBindings.length > 1 ||
|
|
6951
|
+
(foreign && (!ownership || ownership.instanceId !== record.instanceId ||
|
|
6952
|
+
!ownership.ownerGeneration || !ownership.recipientBindingKey))
|
|
6953
|
+
)
|
|
6954
|
+
return;
|
|
6955
|
+
const expectedOwnership = ownership && structuredClone(ownership);
|
|
6956
|
+
const cap = captureTemporaryThreadAuthority(ctx, () => {
|
|
6957
|
+
try {
|
|
6958
|
+
assertExecutionCurrent();
|
|
6959
|
+
const current = getTelegramRoutableThreadRecords(deps.threadStore!.list(), deps.getLiveThreadTargets?.())
|
|
6960
|
+
.find((value) => isDeepStrictEqual(value.target, target));
|
|
6961
|
+
return isDeepStrictEqual(recordIdentity(current), expectedRecord) &&
|
|
6962
|
+
isDeepStrictEqual(bindingIdentity(), expectedBindings) &&
|
|
6963
|
+
(!foreign || Bus.isTelegramBusForwardOwnershipCurrent(expectedOwnership!, deps.getTargetOwnership?.(target)));
|
|
6964
|
+
} catch { return false; }
|
|
6965
|
+
});
|
|
6966
|
+
if (!cap?.isCurrent()) return;
|
|
6967
|
+
const temporary = pending.temporaryThread ?? findCreatedTemporaryThread(pending.sourceTarget);
|
|
6968
|
+
if (!temporary || !issueTemporaryThreadForward(pending, ctx)) return;
|
|
6969
|
+
pending.phase = { kind: "forward-unknown" };
|
|
6970
|
+
const assertRecipientCurrent = (): void => {
|
|
6971
|
+
assertExecutionCurrent();
|
|
6972
|
+
if (!cap.isCurrent()) throw new Error("Reroute publication recipient authority changed.");
|
|
6973
|
+
};
|
|
6974
|
+
const assertPublicationCurrent = (): void => {
|
|
6975
|
+
assertRecipientCurrent();
|
|
6976
|
+
const entry = cap.store.listTemporaryThreads().find((value) => value.token === temporary.token);
|
|
6977
|
+
const ids = [...source.sourceUpdateIds].sort((a, b) => a - b);
|
|
6978
|
+
if (
|
|
6979
|
+
!entry || entry.phase !== "created" || entry.cleanupIssued ||
|
|
6980
|
+
entry.operatorUserId !== cap.operatorUserId ||
|
|
6981
|
+
!isDeepStrictEqual(entry.executor, cap.authority.executor) ||
|
|
6982
|
+
!isDeepStrictEqual(entry.target, pending.sourceTarget) ||
|
|
6983
|
+
!entry.forwardedInputs?.some((input) => input.journalBindingKey === cap.journalBindingKey &&
|
|
6984
|
+
isDeepStrictEqual(input.updateIds, ids)) ||
|
|
6985
|
+
!isDeepStrictEqual(resolvePendingRerouteForwardSource(pending), source)
|
|
6986
|
+
)
|
|
6987
|
+
throw new Error("Reroute publication source authority changed.");
|
|
6988
|
+
};
|
|
6989
|
+
try {
|
|
6990
|
+
assertPublicationCurrent();
|
|
6991
|
+
// Bot API requires ascending source IDs. Preserve the source-UID association even if copy IDs reorder them.
|
|
6992
|
+
const order = source.messageIds.map((_, index) => index)
|
|
6993
|
+
.sort((a, b) => source.messageIds[a]! - source.messageIds[b]!);
|
|
6994
|
+
const messageIds = order.map((index) => source.messageIds[index]!);
|
|
6995
|
+
const body = { chat_id: target.chatId, message_thread_id: target.threadId,
|
|
6996
|
+
from_chat_id: source.chatId, disable_notification: true };
|
|
6997
|
+
const options = { maxAttempts: 1, retrySafety: "non-idempotent" as const, assertAuthority: assertPublicationCurrent };
|
|
6998
|
+
const copies = messageIds.length === 1
|
|
6999
|
+
? [await deps.callApi<{ message_id?: unknown }>("forwardMessage", { ...body, message_id: messageIds[0] }, options)]
|
|
7000
|
+
: await deps.callApi<Array<{ message_id?: unknown }>>("forwardMessages", { ...body, message_ids: messageIds }, options);
|
|
7001
|
+
assertPublicationCurrent();
|
|
7002
|
+
const copyIds = Array.isArray(copies) ? copies.map((copy) => copy?.message_id) : [];
|
|
7003
|
+
if (
|
|
7004
|
+
copyIds.length !== messageIds.length ||
|
|
7005
|
+
!copyIds.every((id): id is number => typeof id === "number" && Number.isSafeInteger(id) && id > 0) ||
|
|
7006
|
+
new Set(copyIds).size !== copyIds.length
|
|
7007
|
+
)
|
|
7008
|
+
return;
|
|
7009
|
+
pending.rerouteCopies = { target, bySourceUpdateId: new Map(order.map((index, position) =>
|
|
7010
|
+
[source.sourceUpdateIds[index]!, copyIds[position]!] as const)) };
|
|
7011
|
+
pending.phase = { kind: "selected" };
|
|
7012
|
+
return assertRecipientCurrent;
|
|
7013
|
+
} catch (error) {
|
|
7014
|
+
if (cap.isCurrent()) deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-destination-forward" });
|
|
7015
|
+
return;
|
|
7016
|
+
}
|
|
7017
|
+
};
|
|
6678
7018
|
const forwardPendingRerouteMessages = async (
|
|
6679
7019
|
pending: PendingUnboundReroute,
|
|
6680
7020
|
instanceId: string,
|
|
@@ -6700,12 +7040,19 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6700
7040
|
)
|
|
6701
7041
|
return false;
|
|
6702
7042
|
const ownership = structuredClone(liveOwnership);
|
|
6703
|
-
const
|
|
7043
|
+
const copies = pending.rerouteCopies;
|
|
7044
|
+
const published = !!copies && isDeepStrictEqual(copies.target, target);
|
|
7045
|
+
const messages = cloneTelegramMessagesForThread(
|
|
7046
|
+
pending.messages,
|
|
7047
|
+
threadId,
|
|
7048
|
+
published ? copies.bySourceUpdateId : undefined,
|
|
7049
|
+
);
|
|
6704
7050
|
// Issuance is durable before the RPC: an unacknowledged temporary-tab Forward never becomes another dispatch grant,
|
|
6705
7051
|
// even after restart. Any refusal or publication failure sends nothing and keeps the input held.
|
|
6706
7052
|
if (
|
|
6707
7053
|
!recordForwardAcceptance &&
|
|
6708
7054
|
isTemporaryReroute(pending) &&
|
|
7055
|
+
!published &&
|
|
6709
7056
|
!issueTemporaryThreadForward(pending, ctx)
|
|
6710
7057
|
) {
|
|
6711
7058
|
if (isTemporaryThreadForwardIssued(pending))
|
|
@@ -6720,6 +7067,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6720
7067
|
message,
|
|
6721
7068
|
ownership,
|
|
6722
7069
|
ctx,
|
|
7070
|
+
assertAuthority() {
|
|
7071
|
+
assertExecutionCurrent?.();
|
|
7072
|
+
Updates.assertTelegramUpdateExecutionCurrent(message);
|
|
7073
|
+
},
|
|
6723
7074
|
}),
|
|
6724
7075
|
),
|
|
6725
7076
|
);
|
|
@@ -6946,6 +7297,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6946
7297
|
temporaryCleanupTimers.delete(token);
|
|
6947
7298
|
if (retry) restoreSettlementTasks.add(task);
|
|
6948
7299
|
runTemporaryThreadCleanup(token, cap, reconcileExpiry)
|
|
7300
|
+
.then(() => {
|
|
7301
|
+
if (!cap.isCurrent()) return;
|
|
7302
|
+
const retained = cap.store.listTemporaryThreads().find(value => value.token === token);
|
|
7303
|
+
// A follower ACK can precede recipient receipt disposal. Reobserve a protected, unissued tab;
|
|
7304
|
+
// elapsed time never grants deletion, and an issued/unknown delete can never enter this path.
|
|
7305
|
+
if (retained?.phase === "created" && !retained.cleanupIssued &&
|
|
7306
|
+
Threads.isTelegramTemporaryThreadFullyResolved(retained) &&
|
|
7307
|
+
cap.store.inspectTemporaryThreadTarget(retained, cap.authority)?.kind === "temporary" && cap.isCurrent())
|
|
7308
|
+
queueTemporaryThreadCleanup(token, cap, 1000, reconcileExpiry, true);
|
|
7309
|
+
})
|
|
6949
7310
|
.catch((error) => {
|
|
6950
7311
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
6951
7312
|
phase: "temporary-thread-cleanup",
|
|
@@ -7672,7 +8033,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7672
8033
|
).includes(source.original.updateId)
|
|
7673
8034
|
)
|
|
7674
8035
|
continue;
|
|
7675
|
-
movedIntoTab ||= pending.allTabCopiesForwarded
|
|
8036
|
+
movedIntoTab ||= pending.allTabCopiesForwarded !== undefined;
|
|
7676
8037
|
removePendingReroute(id);
|
|
7677
8038
|
if (
|
|
7678
8039
|
signal.aborted ||
|
|
@@ -8695,10 +9056,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8695
9056
|
// Keep the captured plan even if the source owner's issued selection result is lost.
|
|
8696
9057
|
pending.liveRebind ??= selection;
|
|
8697
9058
|
if (!(await selectPendingDestination())) return true;
|
|
9059
|
+
const operationId = selection.coordinator.operationId;
|
|
8698
9060
|
if (current()) outcome = await selection.coordinator.advance();
|
|
8699
9061
|
// Cleanup readiness follows the canonical released row, not source disposal: a detached command ACK may
|
|
8700
9062
|
// still be pending while every attempt samples current work afresh.
|
|
8701
|
-
const operationId = selection.coordinator.operationId;
|
|
8702
9063
|
const row =
|
|
8703
9064
|
outcome === "protected"
|
|
8704
9065
|
? undefined
|
|
@@ -8706,12 +9067,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8706
9067
|
.getWorkspaceRestoreStore?.()
|
|
8707
9068
|
?.listLiveRebindings()
|
|
8708
9069
|
.find((value) => value.request.operationId === operationId);
|
|
8709
|
-
if (row?.phase === "released" && row.cleanup === undefined
|
|
9070
|
+
if (row?.phase === "released" && row.cleanup === undefined &&
|
|
9071
|
+
(row.inputRelease === undefined || row.inputRelease === "accepted")) {
|
|
8710
9072
|
scheduleLiveRebindCleanup(
|
|
8711
9073
|
operationId,
|
|
8712
9074
|
selection.record.owner?.kind === "leader" ? "leader" : "follower",
|
|
8713
9075
|
ctx,
|
|
8714
9076
|
);
|
|
9077
|
+
}
|
|
8715
9078
|
if (outcome === "released")
|
|
8716
9079
|
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8717
9080
|
} catch (error) {
|
|
@@ -8729,12 +9092,24 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8729
9092
|
);
|
|
8730
9093
|
return true;
|
|
8731
9094
|
}
|
|
9095
|
+
const cleanupRow = deps.getWorkspaceRestoreStore?.()?.listLiveRebindings().find(value => value.request.operationId === selection.coordinator?.operationId);
|
|
9096
|
+
const cleanupResult = cleanupRow?.inputRelease && cleanupRow.cleanup !== undefined
|
|
9097
|
+
? cleanupRow.cleanup === "confirmed" ? "old-Thread removal confirmed"
|
|
9098
|
+
: cleanupRow.cleanup === "unknown" || cleanupRow.cleanup === "issued" ? "old-Thread removal unconfirmed; deletion will not repeat"
|
|
9099
|
+
: cleanupRow.cleanup === "failed" ? "old-Thread removal failed" : "old-Thread removal was not issued"
|
|
9100
|
+
: undefined;
|
|
9101
|
+
const commandDisposed = !!(selection.extensionCommand && selection.extensionKind !== "generated-prompt" || selection.nameCommand ||
|
|
9102
|
+
selection.confirmationCommand || selection.helpCommand || selection.stopCommand || selection.nextCommand ||
|
|
9103
|
+
selection.menuCommand || selection.abortCommand || selection.coordinator?.selectedCommandReference());
|
|
8732
9104
|
// The tab now belongs to the rebound thread; only the one-shot chooser message goes.
|
|
8733
9105
|
await finalizePendingReroute(
|
|
8734
9106
|
parsed.rerouteId,
|
|
8735
9107
|
pending,
|
|
8736
9108
|
query,
|
|
8737
|
-
|
|
9109
|
+
cleanupResult
|
|
9110
|
+
? commandDisposed ? `Command source disposal confirmed; ${cleanupResult}; delivery and follow-on effects remain independent`
|
|
9111
|
+
: `Input queued in the rebound Thread; ${cleanupResult}`
|
|
9112
|
+
: selection.extensionKind === "generated-prompt"
|
|
8738
9113
|
? "Input queued in the rebound Thread; old-Thread cleanup is scheduled, not confirmed"
|
|
8739
9114
|
: selection.extensionCommand
|
|
8740
9115
|
? "Command source disposal confirmed; reply delivery and old-Thread cleanup are not confirmed"
|
|
@@ -8760,6 +9135,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8760
9135
|
);
|
|
8761
9136
|
return true;
|
|
8762
9137
|
}
|
|
9138
|
+
if (
|
|
9139
|
+
!parsed.useNewSlot &&
|
|
9140
|
+
isTemporaryReroute(pending) &&
|
|
9141
|
+
!resolvePendingRerouteForwardSource(pending)
|
|
9142
|
+
) {
|
|
9143
|
+
await deps.answerCallbackQuery(query.id, "Source copy unavailable; not routing");
|
|
9144
|
+
return true;
|
|
9145
|
+
}
|
|
8763
9146
|
if (!(await selectPendingDestination())) return true;
|
|
8764
9147
|
if (parsed.useNewSlot && sourceTarget && restoreWorkspace) {
|
|
8765
9148
|
const selection = (pending.workspaceRestore ??= {
|
|
@@ -8865,6 +9248,19 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8865
9248
|
);
|
|
8866
9249
|
return true;
|
|
8867
9250
|
}
|
|
9251
|
+
let assertRerouteCurrent = assertExecutionCurrent;
|
|
9252
|
+
if (isTemporaryReroute(pending)) {
|
|
9253
|
+
const confirmed = await publishPendingRerouteCopies(pending, record, ctx, assertExecutionCurrent,
|
|
9254
|
+
!!record.instanceId && record.instanceId !== currentInstanceId && !isCurrentLeaderRecord);
|
|
9255
|
+
if (!confirmed) {
|
|
9256
|
+
await deps.answerCallbackQuery(query.id, pending.phase.kind === "forward-unknown"
|
|
9257
|
+
? "Publication unconfirmed; not resending"
|
|
9258
|
+
: "Publication unavailable; input kept");
|
|
9259
|
+
return true;
|
|
9260
|
+
}
|
|
9261
|
+
assertRerouteCurrent = confirmed;
|
|
9262
|
+
}
|
|
9263
|
+
assertRerouteCurrent();
|
|
8868
9264
|
if (
|
|
8869
9265
|
record.instanceId &&
|
|
8870
9266
|
record.instanceId !== currentInstanceId &&
|
|
@@ -8875,7 +9271,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8875
9271
|
record.instanceId,
|
|
8876
9272
|
parsed.threadId,
|
|
8877
9273
|
ctx,
|
|
8878
|
-
|
|
9274
|
+
assertRerouteCurrent,
|
|
8879
9275
|
);
|
|
8880
9276
|
if (!allForwarded) {
|
|
8881
9277
|
await deps.answerCallbackQuery(
|
|
@@ -8891,6 +9287,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8891
9287
|
// Local handlers and queue admission consume the same one-time group grant as follower forwarding.
|
|
8892
9288
|
if (
|
|
8893
9289
|
isTemporaryReroute(pending) &&
|
|
9290
|
+
!pending.rerouteCopies &&
|
|
8894
9291
|
!issueTemporaryThreadForward(pending, ctx)
|
|
8895
9292
|
) {
|
|
8896
9293
|
await deps.answerCallbackQuery(
|
|
@@ -8899,7 +9296,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8899
9296
|
);
|
|
8900
9297
|
return true;
|
|
8901
9298
|
}
|
|
8902
|
-
|
|
9299
|
+
const localMessages = pending.rerouteCopies
|
|
9300
|
+
? cloneTelegramMessagesForThread(pending.messages, parsed.threadId, pending.rerouteCopies.bySourceUpdateId)
|
|
9301
|
+
: reroutedMessages;
|
|
9302
|
+
await dispatchPendingRerouteMessages(pending, localMessages, ctx);
|
|
9303
|
+
assertRerouteCurrent();
|
|
8903
9304
|
pending.messages = [];
|
|
8904
9305
|
return completeRoutedReroute();
|
|
8905
9306
|
};
|
|
@@ -10288,15 +10689,23 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10288
10689
|
* An All input gets one source-bound tab. The original stays deferred in All until explicit routing,
|
|
10289
10690
|
* and an existing entry is reused on replay, so restart never creates a second tab or retries an unknown one.
|
|
10290
10691
|
*/
|
|
10692
|
+
/**
|
|
10693
|
+
* Opens (or adopts) the routing tab for input sent from All. One text message may carry a command; anything else,
|
|
10694
|
+
* a voice note, a file or a coalesced album, is held untouched as one prompt.
|
|
10695
|
+
*/
|
|
10291
10696
|
const sendAllTabTemporaryInputChooser = async (
|
|
10292
10697
|
command: Commands.ParsedTelegramCommand | undefined,
|
|
10293
10698
|
commandText: string,
|
|
10294
|
-
|
|
10699
|
+
messages: readonly TMessage[],
|
|
10295
10700
|
ctx: TContext,
|
|
10296
10701
|
): Promise<boolean> => {
|
|
10297
|
-
const
|
|
10298
|
-
|
|
10299
|
-
|
|
10702
|
+
const message = messages[0];
|
|
10703
|
+
if (!message) return false;
|
|
10704
|
+
const updateIds = Updates.collectTelegramAdmissionSourceUpdateIds([
|
|
10705
|
+
...messages,
|
|
10706
|
+
]).sort((a, b) => a - b);
|
|
10707
|
+
const updateId = updateIds[0];
|
|
10708
|
+
const group = { journalBindingKey: "", updateIds };
|
|
10300
10709
|
const execution = Updates.getTelegramUpdateExecutionFence(message);
|
|
10301
10710
|
const cap = captureTemporaryThreadAuthority(
|
|
10302
10711
|
ctx,
|
|
@@ -10307,15 +10716,20 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10307
10716
|
!deps.runWorkspaceOperation ||
|
|
10308
10717
|
!deps.callApi ||
|
|
10309
10718
|
updateId === undefined ||
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10719
|
+
updateIds.length !== messages.length ||
|
|
10720
|
+
messages.some(
|
|
10721
|
+
(held) =>
|
|
10722
|
+
held.chat.type !== "private" ||
|
|
10723
|
+
held.chat.id !== cap.operatorUserId ||
|
|
10724
|
+
held.from?.id !== cap.operatorUserId,
|
|
10725
|
+
) ||
|
|
10313
10726
|
!cap.isCurrent()
|
|
10314
10727
|
)
|
|
10315
10728
|
return false;
|
|
10316
10729
|
const { store, operatorUserId, journalBindingKey, epoch, authority } = cap,
|
|
10317
10730
|
current = cap.isCurrent;
|
|
10318
10731
|
const source = { journalBindingKey, updateId };
|
|
10732
|
+
group.journalBindingKey = journalBindingKey;
|
|
10319
10733
|
const find = () =>
|
|
10320
10734
|
store
|
|
10321
10735
|
.listTemporaryThreads()
|
|
@@ -10352,7 +10766,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10352
10766
|
);
|
|
10353
10767
|
if (native) {
|
|
10354
10768
|
const adopted = store.registerImplicitTemporaryThread(
|
|
10355
|
-
|
|
10769
|
+
group,
|
|
10356
10770
|
native.implicit.target,
|
|
10357
10771
|
randomBytes(16).toString("hex"),
|
|
10358
10772
|
{
|
|
@@ -10385,6 +10799,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10385
10799
|
source,
|
|
10386
10800
|
randomBytes(16).toString("hex"),
|
|
10387
10801
|
authority,
|
|
10802
|
+
group,
|
|
10388
10803
|
);
|
|
10389
10804
|
entry = reservation?.entry;
|
|
10390
10805
|
if (!reservation?.reserved || !current()) return;
|
|
@@ -10425,19 +10840,20 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10425
10840
|
throw new Error(
|
|
10426
10841
|
"Temporary Thread authority changed; the input remains retryable.",
|
|
10427
10842
|
);
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
text
|
|
10431
|
-
|
|
10432
|
-
|
|
10843
|
+
// Only a lone text message is rewritten for command detection; media keeps its caption and payload.
|
|
10844
|
+
const held: TMessage[] =
|
|
10845
|
+
messages.length === 1 && typeof message.text === "string"
|
|
10846
|
+
? [
|
|
10847
|
+
Updates.carryTelegramUpdateExecutionFence(message, {
|
|
10848
|
+
...message,
|
|
10849
|
+
text: commandText,
|
|
10850
|
+
caption: undefined,
|
|
10851
|
+
} as TMessage),
|
|
10852
|
+
]
|
|
10853
|
+
: [...messages];
|
|
10433
10854
|
if (entry.phase !== "created" || !entry.target) {
|
|
10434
|
-
Updates.reportTelegramUpdateDeferred(
|
|
10435
|
-
|
|
10436
|
-
message.chat.id,
|
|
10437
|
-
message.message_id,
|
|
10438
|
-
"<b>⚠️ The routing tab for this command could not be confirmed.</b> It stays held and was not sent to Pi.",
|
|
10439
|
-
{ parseMode: "HTML" },
|
|
10440
|
-
);
|
|
10855
|
+
for (const input of held) Updates.reportTelegramUpdateDeferred(input);
|
|
10856
|
+
deps.recordRuntimeEvent?.("routing", new Error(TELEGRAM_THREADED_ROUTING_UNAVAILABLE_ERROR), { phase: "temporary-thread-unconfirmed" });
|
|
10441
10857
|
return true;
|
|
10442
10858
|
}
|
|
10443
10859
|
const target = entry.target;
|
|
@@ -10446,23 +10862,22 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10446
10862
|
deps.getLiveThreadTargets?.(),
|
|
10447
10863
|
);
|
|
10448
10864
|
const rerouteId = storePendingUnboundReroute(
|
|
10449
|
-
|
|
10865
|
+
held,
|
|
10450
10866
|
command ? "command" : "prompt",
|
|
10451
10867
|
target,
|
|
10452
10868
|
);
|
|
10453
10869
|
const pending = pendingUnboundReroutes.get(rerouteId)!;
|
|
10454
10870
|
pending.temporaryThread = entry;
|
|
10455
10871
|
pending.allTabCopies = getTelegramAllTabSourceMessageIds(
|
|
10456
|
-
|
|
10872
|
+
messages,
|
|
10457
10873
|
target.chatId,
|
|
10458
10874
|
);
|
|
10459
|
-
Updates.reportTelegramUpdateDeferred(
|
|
10875
|
+
for (const input of held) Updates.reportTelegramUpdateDeferred(input);
|
|
10460
10876
|
// Cancel is offered only when the exact source can still be abandoned; the tab removal needs that retention first.
|
|
10461
10877
|
if (
|
|
10462
10878
|
deps.editInteractiveMessage &&
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
journalBindingKey,
|
|
10879
|
+
held.every((input) =>
|
|
10880
|
+
Updates.supportsTelegramDeferredAbandonment(input, journalBindingKey),
|
|
10466
10881
|
)
|
|
10467
10882
|
) {
|
|
10468
10883
|
pending.abandonment = {
|
|
@@ -10477,6 +10892,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10477
10892
|
const tabCopyId = openedNow
|
|
10478
10893
|
? await moveAllTabSourceIntoTab(pending, target, current)
|
|
10479
10894
|
: undefined;
|
|
10895
|
+
if (!current()) {
|
|
10896
|
+
removePendingReroute(rerouteId);
|
|
10897
|
+
throw new Error("Temporary Thread source presentation authority changed.");
|
|
10898
|
+
}
|
|
10899
|
+
if (
|
|
10900
|
+
openedNow &&
|
|
10901
|
+
!pending.allTabCopiesForwarded &&
|
|
10902
|
+
pending.allTabCopies.length === held.length
|
|
10903
|
+
)
|
|
10904
|
+
pending.allTabSourceRetained = true;
|
|
10480
10905
|
try {
|
|
10481
10906
|
if (!deps.sendInteractiveMessage)
|
|
10482
10907
|
throw new Error("Temporary Thread chooser publication is unavailable.");
|
|
@@ -10499,96 +10924,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10499
10924
|
}
|
|
10500
10925
|
return true;
|
|
10501
10926
|
};
|
|
10502
|
-
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
10509
|
-
|
|
10510
|
-
): Promise<boolean> => {
|
|
10511
|
-
if (!deps.threadStore) return false;
|
|
10512
|
-
const records = deps.threadStore.list();
|
|
10513
|
-
const activeRecords = getTelegramRoutableThreadRecords(
|
|
10514
|
-
records,
|
|
10515
|
-
deps.getLiveThreadTargets?.(),
|
|
10516
|
-
);
|
|
10517
|
-
if (activeRecords.length === 0) return false;
|
|
10518
|
-
const commandMessage = Updates.carryTelegramUpdateExecutionFence(message, {
|
|
10519
|
-
...message,
|
|
10520
|
-
text: commandText,
|
|
10521
|
-
caption: undefined,
|
|
10522
|
-
} as TMessage);
|
|
10523
|
-
const rerouteId = storePendingUnboundReroute([commandMessage], "command");
|
|
10524
|
-
Updates.reportTelegramUpdateDeferred(commandMessage);
|
|
10525
|
-
const text = formatTelegramAllTabMenuChooserText(command.name);
|
|
10526
|
-
pendingUnboundReroutes.get(rerouteId)!.rootChooserText = text;
|
|
10527
|
-
const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
|
|
10528
|
-
rerouteId,
|
|
10529
|
-
activeRecords,
|
|
10530
|
-
{
|
|
10531
|
-
canRestore: typeof message.message_thread_id === "number",
|
|
10532
|
-
getDisplayTitle: deps.getDisplayTitle,
|
|
10533
|
-
},
|
|
10534
|
-
);
|
|
10535
|
-
let chooserId: number | undefined;
|
|
10536
|
-
try {
|
|
10537
|
-
if (deps.sendInteractiveMessage) {
|
|
10538
|
-
chooserId = await deps.sendInteractiveMessage(
|
|
10539
|
-
message.chat.id,
|
|
10540
|
-
text,
|
|
10541
|
-
"html",
|
|
10542
|
-
replyMarkup,
|
|
10543
|
-
options.target || options.replyToSource
|
|
10544
|
-
? {
|
|
10545
|
-
...(options.target ? { target: options.target } : {}),
|
|
10546
|
-
...(options.replyToSource
|
|
10547
|
-
? { replyToMessageId: message.message_id }
|
|
10548
|
-
: {}),
|
|
10549
|
-
}
|
|
10550
|
-
: undefined,
|
|
10551
|
-
);
|
|
10552
|
-
} else if (deps.callApi) {
|
|
10553
|
-
const chooser = await deps.callApi<{ message_id?: number }>(
|
|
10554
|
-
"sendMessage",
|
|
10555
|
-
{
|
|
10556
|
-
chat_id: message.chat.id,
|
|
10557
|
-
text,
|
|
10558
|
-
parse_mode: "HTML",
|
|
10559
|
-
reply_markup: replyMarkup,
|
|
10560
|
-
...(typeof options.target?.threadId === "number"
|
|
10561
|
-
? { message_thread_id: options.target.threadId }
|
|
10562
|
-
: {}),
|
|
10563
|
-
...(options.replyToSource
|
|
10564
|
-
? {
|
|
10565
|
-
reply_parameters: {
|
|
10566
|
-
message_id: message.message_id,
|
|
10567
|
-
allow_sending_without_reply: true,
|
|
10568
|
-
},
|
|
10569
|
-
}
|
|
10570
|
-
: {}),
|
|
10571
|
-
},
|
|
10572
|
-
);
|
|
10573
|
-
chooserId = chooser?.message_id;
|
|
10574
|
-
} else {
|
|
10575
|
-
chooserId = await deps.sendTextReply(
|
|
10576
|
-
message.chat.id,
|
|
10577
|
-
message.message_id,
|
|
10578
|
-
text,
|
|
10579
|
-
{
|
|
10580
|
-
parseMode: "HTML",
|
|
10581
|
-
target: options.target,
|
|
10582
|
-
},
|
|
10583
|
-
);
|
|
10584
|
-
}
|
|
10585
|
-
} catch (error) {
|
|
10586
|
-
removePendingReroute(rerouteId);
|
|
10587
|
-
throw error;
|
|
10588
|
-
}
|
|
10589
|
-
rememberRerouteChooser(rerouteId, chooserId);
|
|
10590
|
-
Updates.reportTelegramUpdateCompleted(commandMessage);
|
|
10591
|
-
return true;
|
|
10927
|
+
/** A coalesced album from All requires one routing tab; failure cannot answer in All or discard its source. */
|
|
10928
|
+
const routeAllTabAlbum = async (
|
|
10929
|
+
messages: TMessage[],
|
|
10930
|
+
ctx: TContext,
|
|
10931
|
+
): Promise<void> => {
|
|
10932
|
+
if (await sendAllTabTemporaryInputChooser(undefined, "", messages, ctx))
|
|
10933
|
+
return;
|
|
10934
|
+
throw new Error(TELEGRAM_THREADED_ROUTING_UNAVAILABLE_ERROR);
|
|
10592
10935
|
};
|
|
10593
10936
|
const dispatchCommandOrPrompt =
|
|
10594
10937
|
Commands.createTelegramCommandOrPromptDispatcher<TMessage, TContext>({
|
|
@@ -11164,6 +11507,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11164
11507
|
entry.source.journalBindingKey ===
|
|
11165
11508
|
deps.getAdmissionJournalBinding?.(),
|
|
11166
11509
|
) === true;
|
|
11510
|
+
const needsThread = deps.threadStore.getBotState().threadMode === "enabled" || bindings.length > 0 || presented;
|
|
11167
11511
|
// Returning before deferral lets the admission worker terminally settle expired replay; a presented tab is not expiry.
|
|
11168
11512
|
if (
|
|
11169
11513
|
command &&
|
|
@@ -11173,30 +11517,48 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11173
11517
|
)
|
|
11174
11518
|
return;
|
|
11175
11519
|
if (
|
|
11176
|
-
|
|
11177
|
-
(bindings.length > 0 || presented) &&
|
|
11520
|
+
needsThread &&
|
|
11178
11521
|
typeof message.text === "string" &&
|
|
11179
11522
|
message.text.trim() &&
|
|
11180
11523
|
(await sendAllTabTemporaryInputChooser(
|
|
11181
11524
|
command,
|
|
11182
11525
|
text,
|
|
11183
|
-
message as TMessage,
|
|
11526
|
+
[message as TMessage],
|
|
11184
11527
|
ctx,
|
|
11185
11528
|
))
|
|
11186
11529
|
)
|
|
11187
11530
|
return;
|
|
11188
|
-
|
|
11189
|
-
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11531
|
+
// Voice notes, files, stickers and albums from All get the same routing tab as text, whether or not the
|
|
11532
|
+
// client opened a tab for them; an album first gathers into one input so it opens one tab.
|
|
11533
|
+
if (
|
|
11534
|
+
!command &&
|
|
11535
|
+
needsThread &&
|
|
11536
|
+
typeof message.text !== "string" &&
|
|
11537
|
+
Media.hasTelegramMessagesPromptContent([message as TMessage])
|
|
11538
|
+
) {
|
|
11539
|
+
if (message.media_group_id) {
|
|
11540
|
+
deps.mediaGroupRuntime.queueMessage({
|
|
11541
|
+
message: message as TMessage,
|
|
11542
|
+
context: ctx,
|
|
11543
|
+
dispatchMessages: (messages, queuedCtx) =>
|
|
11544
|
+
routeAllTabAlbum(messages, queuedCtx ?? ctx),
|
|
11545
|
+
});
|
|
11546
|
+
Updates.reportTelegramUpdateDeferred(message as TMessage);
|
|
11194
11547
|
return;
|
|
11195
11548
|
}
|
|
11549
|
+
if (
|
|
11550
|
+
await sendAllTabTemporaryInputChooser(
|
|
11551
|
+
undefined,
|
|
11552
|
+
text,
|
|
11553
|
+
[message as TMessage],
|
|
11554
|
+
ctx,
|
|
11555
|
+
)
|
|
11556
|
+
)
|
|
11557
|
+
return;
|
|
11196
11558
|
}
|
|
11197
|
-
if (
|
|
11559
|
+
if (needsThread) {
|
|
11198
11560
|
const probeTarget = bindings[0]?.target;
|
|
11199
|
-
if (probeTarget?.threadId && deps.callApi) {
|
|
11561
|
+
if (!command && !text.startsWith("/") && probeTarget?.threadId && deps.callApi) {
|
|
11200
11562
|
try {
|
|
11201
11563
|
await deps.callApi("sendChatAction", {
|
|
11202
11564
|
chat_id: probeTarget.chatId,
|
|
@@ -11205,8 +11567,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11205
11567
|
});
|
|
11206
11568
|
} catch (error) {
|
|
11207
11569
|
if (
|
|
11208
|
-
Threads.isTelegramTopicModeUnavailableError(error)
|
|
11209
|
-
Threads.isTelegramTopicTargetStaleError(error)
|
|
11570
|
+
Threads.isTelegramTopicModeUnavailableError(error)
|
|
11210
11571
|
) {
|
|
11211
11572
|
deps.threadStore.setBotState({
|
|
11212
11573
|
threadMode: "disabled",
|
|
@@ -11226,12 +11587,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11226
11587
|
});
|
|
11227
11588
|
}
|
|
11228
11589
|
}
|
|
11229
|
-
|
|
11230
|
-
message.chat.id,
|
|
11231
|
-
message.message_id,
|
|
11232
|
-
"This bot is in threaded multi-instance mode. Send prompts in a bound Pi thread tab so they route to the right instance.",
|
|
11233
|
-
);
|
|
11234
|
-
return;
|
|
11590
|
+
throw new Error(TELEGRAM_THREADED_ROUTING_UNAVAILABLE_ERROR);
|
|
11235
11591
|
}
|
|
11236
11592
|
}
|
|
11237
11593
|
await textDispatch.handleMessage(message as TMessage, ctx);
|
|
@@ -11663,6 +12019,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11663
12019
|
prepareHeldCommand: commandHandler.prepareHeldCommand,
|
|
11664
12020
|
canPrepareHeldCommand: commandHandler.canPrepareHeldCommand,
|
|
11665
12021
|
onSessionSettled,
|
|
12022
|
+
onDeliverySettled,
|
|
11666
12023
|
observeLiveRebindLeaderWork,
|
|
11667
12024
|
prepareLiveRebindLeaderCleanup,
|
|
11668
12025
|
prepareLiveRebindFollowerCleanup,
|