@llblab/pi-telegram 0.54.2 → 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 +14 -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 +338 -63
- 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 +434 -78
- 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(
|
|
@@ -1330,6 +1348,8 @@ export const TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS = 60 * 60_000;
|
|
|
1330
1348
|
export const TELEGRAM_ROUTING_CHOICE_EXPIRED = "Routing choice expired";
|
|
1331
1349
|
/** Every temporary routing tab carries this name, whether the bot created it or adopted Telegram's native tab. */
|
|
1332
1350
|
const TELEGRAM_TEMPORARY_THREAD_NAME = "🚦 Routing";
|
|
1351
|
+
/** A mobile client's native tab creation precedes the All input it is named after by about a second. */
|
|
1352
|
+
const TELEGRAM_IMPLICIT_ALL_INPUT_WINDOW_SEC = 10;
|
|
1333
1353
|
|
|
1334
1354
|
export function isTelegramAllTabCommandExpired(
|
|
1335
1355
|
message: { date?: number; message_thread_id?: number },
|
|
@@ -1797,6 +1817,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1797
1817
|
TContext
|
|
1798
1818
|
>
|
|
1799
1819
|
>["canPrepareHeldCommand"];
|
|
1820
|
+
/** Agent-settled hint: wakes waiting leader cleanup retries early; every readiness gate is still sampled fresh. */
|
|
1821
|
+
onSessionSettled(ctx: TContext): void;
|
|
1800
1822
|
/** Fresh body-free recipient observation only; never reconstructs an attempt or grants cleanup. */
|
|
1801
1823
|
observeLiveRebindLeaderWork(
|
|
1802
1824
|
intent: Threads.TelegramWorkspaceLiveRebindIntent,
|
|
@@ -1886,6 +1908,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1886
1908
|
temporaryThread?: Threads.TelegramTemporaryThreadEntry;
|
|
1887
1909
|
/** Source copies left in All (a menu-picked command); deleted once, when the input is consumed. */
|
|
1888
1910
|
allTabCopies?: number[];
|
|
1911
|
+
/** The tab holds a forwarded copy of the threadless original, which then left All at once. */
|
|
1912
|
+
allTabCopiesForwarded?: true;
|
|
1889
1913
|
/** Acknowledged same-tab membership stays protective when its reader disappears; never a dispatch/cleanup grant. */
|
|
1890
1914
|
temporaryMembership?: true;
|
|
1891
1915
|
/** Re-entrancy lock for one command route in flight; orthogonal to `phase`. */
|
|
@@ -1916,7 +1940,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1916
1940
|
/** Some Forward outcome is unknown; process-local, so restart-safe issuance remains a release gate. */
|
|
1917
1941
|
| { kind: "forward-unknown" }
|
|
1918
1942
|
| { kind: "cleanup"; cleanup: PendingRerouteCleanup }
|
|
1919
|
-
|
|
1943
|
+
/** `retainRecord`: a live rebind's chooser-scoped command reference outlives its deleted menu. */
|
|
1944
|
+
| { kind: "finalizing"; message: string; retainRecord?: true };
|
|
1920
1945
|
/** The command a chooser routes, read from its own source exactly as the root heading named it. */
|
|
1921
1946
|
const getPendingRerouteCommandName = (
|
|
1922
1947
|
pending: PendingUnboundReroute,
|
|
@@ -1945,6 +1970,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1945
1970
|
executor: Threads.TelegramWorkspaceRestoreExecutor;
|
|
1946
1971
|
generation: number | undefined;
|
|
1947
1972
|
scope: string | undefined;
|
|
1973
|
+
target: { chatId: number; threadId: number };
|
|
1974
|
+
name: string;
|
|
1975
|
+
createdAtSec: number;
|
|
1948
1976
|
}
|
|
1949
1977
|
>();
|
|
1950
1978
|
const guidedUnboundTopicKeys = new Set<string>();
|
|
@@ -2776,6 +2804,52 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2776
2804
|
pending.sourceTarget.chatId,
|
|
2777
2805
|
pending.allTabCopies?.splice(0) ?? [],
|
|
2778
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
|
+
};
|
|
2779
2853
|
const dismissRerouteChooserMessage = async (
|
|
2780
2854
|
query: TCallbackQuery,
|
|
2781
2855
|
assertExecutionCurrent?: () => void,
|
|
@@ -2805,6 +2879,36 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2805
2879
|
return false;
|
|
2806
2880
|
}
|
|
2807
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
|
+
};
|
|
2808
2912
|
const closeReroutedUnboundTopic = async (
|
|
2809
2913
|
target: { chatId: number; threadId: number } | undefined,
|
|
2810
2914
|
messageId: number | undefined,
|
|
@@ -2970,8 +3074,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2970
3074
|
continue;
|
|
2971
3075
|
if (!recordCancelledTemporaryThreadInput(pending, ctx, current))
|
|
2972
3076
|
continue;
|
|
3077
|
+
// A sibling cancelled by Restore stays visible in the restored tab, like a desktop input typed there.
|
|
2973
3078
|
if (await retireCancellationChooser(id, pending, current))
|
|
2974
|
-
await
|
|
3079
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
2975
3080
|
} catch (error) {
|
|
2976
3081
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
2977
3082
|
phase: "temporary-thread-restore-sibling",
|
|
@@ -3701,6 +3806,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
3701
3806
|
);
|
|
3702
3807
|
if (!intent) continue;
|
|
3703
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
|
+
};
|
|
3704
3828
|
if (intent.routing?.cleanup === undefined) {
|
|
3705
3829
|
if (
|
|
3706
3830
|
!intent.request.source.updateIds.every((id) =>
|
|
@@ -3711,13 +3835,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
3711
3835
|
)
|
|
3712
3836
|
)
|
|
3713
3837
|
continue;
|
|
3714
|
-
// Positive settlement of every selected source is the success proof
|
|
3838
|
+
// Positive settlement of every selected source is the success proof.
|
|
3715
3839
|
await disposeTemporaryThreadSiblingsAfterRestore(
|
|
3716
3840
|
intent,
|
|
3717
3841
|
ctx,
|
|
3718
3842
|
settlementAuthority.isCurrent,
|
|
3719
3843
|
);
|
|
3720
3844
|
assertSettlementCurrent();
|
|
3845
|
+
await retireRestoreChoosers();
|
|
3721
3846
|
if (
|
|
3722
3847
|
isRerouteTargetProtected(
|
|
3723
3848
|
intent.request.binding.target,
|
|
@@ -3753,27 +3878,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
3753
3878
|
}
|
|
3754
3879
|
if (!store.retire(intent, settlementAuthority)) continue;
|
|
3755
3880
|
retired = true;
|
|
3756
|
-
|
|
3757
|
-
if (pending.workspaceRestore?.operationId !== operationId)
|
|
3758
|
-
continue;
|
|
3759
|
-
pending.phase = { kind: "finalizing", message: "Restored" };
|
|
3760
|
-
if (
|
|
3761
|
-
!deps.editInteractiveMessage ||
|
|
3762
|
-
pending.chooserMessageId === undefined
|
|
3763
|
-
)
|
|
3764
|
-
continue;
|
|
3765
|
-
assertSettlementCurrent();
|
|
3766
|
-
await deps.editInteractiveMessage(
|
|
3767
|
-
pending.sourceTarget.chatId,
|
|
3768
|
-
pending.chooserMessageId,
|
|
3769
|
-
"<b>✅ Message routed.</b>",
|
|
3770
|
-
"html",
|
|
3771
|
-
{ inline_keyboard: [] },
|
|
3772
|
-
);
|
|
3773
|
-
assertSettlementCurrent();
|
|
3774
|
-
if (pendingUnboundReroutes.get(id) === pending)
|
|
3775
|
-
removePendingReroute(id);
|
|
3776
|
-
}
|
|
3881
|
+
await retireRestoreChoosers();
|
|
3777
3882
|
}
|
|
3778
3883
|
},
|
|
3779
3884
|
),
|
|
@@ -5089,6 +5194,23 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5089
5194
|
messages: selection.messages,
|
|
5090
5195
|
restoreStore: store,
|
|
5091
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,
|
|
5092
5214
|
authority: {
|
|
5093
5215
|
executor: { instanceId, leaderEpoch: String(epoch) },
|
|
5094
5216
|
operatorUserId,
|
|
@@ -5783,6 +5905,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5783
5905
|
string,
|
|
5784
5906
|
ReturnType<typeof setTimeout> | "running"
|
|
5785
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>();
|
|
5786
5910
|
/** End an unissued attempt honestly under fresh admission and current operator authority; no executor grant is borrowed. */
|
|
5787
5911
|
const terminalizeUnissuedLiveRebindCleanup = async (
|
|
5788
5912
|
operationId: string,
|
|
@@ -5855,7 +5979,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5855
5979
|
liveRebindCleanupChains.delete(operationId);
|
|
5856
5980
|
return;
|
|
5857
5981
|
}
|
|
5858
|
-
const
|
|
5982
|
+
const attemptNow = () => {
|
|
5983
|
+
liveRebindCleanupWakers.delete(operationId);
|
|
5859
5984
|
if (liveRebindCleanupChains.get(operationId) !== timer) return;
|
|
5860
5985
|
liveRebindCleanupChains.set(operationId, "running");
|
|
5861
5986
|
let continued = false;
|
|
@@ -5901,9 +6026,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5901
6026
|
});
|
|
5902
6027
|
restoreSettlementTasks.add(task);
|
|
5903
6028
|
void task.finally(() => restoreSettlementTasks.delete(task));
|
|
5904
|
-
}
|
|
6029
|
+
};
|
|
6030
|
+
const timer = setTimeout(
|
|
6031
|
+
attemptNow,
|
|
6032
|
+
pacing.delaysMs[attempt] ?? pacing.intervalMs,
|
|
6033
|
+
);
|
|
5905
6034
|
timer.unref?.();
|
|
5906
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();
|
|
5907
6048
|
};
|
|
5908
6049
|
const restoreWorkspace = deps.workspaceRestoreRecipient
|
|
5909
6050
|
? async (input: {
|
|
@@ -6441,6 +6582,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6441
6582
|
assertExecutionCurrent = Updates.createTelegramUpdateExecutionFenceGuard(
|
|
6442
6583
|
query,
|
|
6443
6584
|
),
|
|
6585
|
+
retainRecord = false,
|
|
6444
6586
|
): Promise<void> => {
|
|
6445
6587
|
assertExecutionCurrent();
|
|
6446
6588
|
const dismissed = await dismissRerouteChooserMessage(
|
|
@@ -6449,12 +6591,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
6449
6591
|
);
|
|
6450
6592
|
assertExecutionCurrent();
|
|
6451
6593
|
if (dismissed) {
|
|
6452
|
-
|
|
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);
|
|
6453
6597
|
await deps.answerCallbackQuery(query.id, successMessage);
|
|
6454
6598
|
await deleteAllTabCopies(pending);
|
|
6455
6599
|
return;
|
|
6456
6600
|
}
|
|
6457
|
-
pending.phase = {
|
|
6601
|
+
pending.phase = {
|
|
6602
|
+
kind: "finalizing",
|
|
6603
|
+
message: successMessage,
|
|
6604
|
+
...(retainRecord ? { retainRecord: true as const } : {}),
|
|
6605
|
+
};
|
|
6458
6606
|
await deps.answerCallbackQuery(
|
|
6459
6607
|
query.id,
|
|
6460
6608
|
`${successMessage}; tap again to clear the menu`,
|
|
@@ -7012,16 +7160,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7012
7160
|
if (
|
|
7013
7161
|
!isPendingRerouteCancelled(pending) ||
|
|
7014
7162
|
!isCurrent() ||
|
|
7015
|
-
!deps.editInteractiveMessage ||
|
|
7163
|
+
(!deps.deleteMessage && !deps.editInteractiveMessage) ||
|
|
7016
7164
|
pending.chooserMessageId === undefined
|
|
7017
7165
|
)
|
|
7018
7166
|
return false;
|
|
7019
|
-
await
|
|
7167
|
+
await retireRerouteChooserMessage(
|
|
7020
7168
|
pending.sourceTarget.chatId,
|
|
7021
7169
|
pending.chooserMessageId,
|
|
7022
7170
|
"<b>⛔️ Routing cancelled.</b>",
|
|
7023
|
-
"html",
|
|
7024
|
-
{ inline_keyboard: [] },
|
|
7025
7171
|
);
|
|
7026
7172
|
if (!isCurrent() || pendingUnboundReroutes.get(id) !== pending)
|
|
7027
7173
|
return false;
|
|
@@ -7518,6 +7664,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7518
7664
|
const result = source.expire();
|
|
7519
7665
|
if (!result) return;
|
|
7520
7666
|
// Expiry revokes the donor carrier; old controls and late ACKs cannot issue another delivery. No body is archived.
|
|
7667
|
+
let movedIntoTab = false;
|
|
7521
7668
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
7522
7669
|
if (
|
|
7523
7670
|
!Updates.collectTelegramAdmissionSourceUpdateIds(
|
|
@@ -7525,6 +7672,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7525
7672
|
).includes(source.original.updateId)
|
|
7526
7673
|
)
|
|
7527
7674
|
continue;
|
|
7675
|
+
movedIntoTab ||= pending.allTabCopiesForwarded === true;
|
|
7528
7676
|
removePendingReroute(id);
|
|
7529
7677
|
if (
|
|
7530
7678
|
signal.aborted ||
|
|
@@ -7557,13 +7705,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
7557
7705
|
// never closes over this source or asks the worker to retain a terminal prompt while metadata publication fails.
|
|
7558
7706
|
if (temporary?.target && runtimeCurrent()) {
|
|
7559
7707
|
scheduleTemporaryThreadCleanup(temporary.target, ctx, true);
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
[message!],
|
|
7708
|
+
// A moved original already left All with its forward; only an unmoved one is deleted here.
|
|
7709
|
+
if (!movedIntoTab)
|
|
7710
|
+
await deleteAllTabMessages(
|
|
7564
7711
|
temporary.target.chatId,
|
|
7565
|
-
|
|
7566
|
-
|
|
7712
|
+
getTelegramAllTabSourceMessageIds(
|
|
7713
|
+
[message!],
|
|
7714
|
+
temporary.target.chatId,
|
|
7715
|
+
),
|
|
7716
|
+
);
|
|
7567
7717
|
}
|
|
7568
7718
|
},
|
|
7569
7719
|
);
|
|
@@ -8167,6 +8317,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8167
8317
|
query,
|
|
8168
8318
|
pending.phase.message,
|
|
8169
8319
|
assertExecutionCurrent,
|
|
8320
|
+
pending.phase.retainRecord === true,
|
|
8170
8321
|
);
|
|
8171
8322
|
return true;
|
|
8172
8323
|
}
|
|
@@ -8561,7 +8712,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8561
8712
|
selection.record.owner?.kind === "leader" ? "leader" : "follower",
|
|
8562
8713
|
ctx,
|
|
8563
8714
|
);
|
|
8564
|
-
if (outcome === "released")
|
|
8715
|
+
if (outcome === "released")
|
|
8716
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8565
8717
|
} catch (error) {
|
|
8566
8718
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
8567
8719
|
phase: "live-rebind-chooser",
|
|
@@ -8570,31 +8722,41 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8570
8722
|
selection.isCurrent = undefined;
|
|
8571
8723
|
}
|
|
8572
8724
|
assertExecutionCurrent();
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
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,
|
|
8598
8760
|
);
|
|
8599
8761
|
return true;
|
|
8600
8762
|
}
|
|
@@ -8606,6 +8768,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8606
8768
|
messages: [...pending.messages],
|
|
8607
8769
|
});
|
|
8608
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;
|
|
8609
8773
|
const ownerUserId = deps.configStore.getAllowedUserId();
|
|
8610
8774
|
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
8611
8775
|
const admissionScope = deps.getAdmissionScope?.();
|
|
@@ -8671,7 +8835,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8671
8835
|
: routed;
|
|
8672
8836
|
await dispatchPendingRerouteMessages(pending, messages, ctx);
|
|
8673
8837
|
assertRecipientCurrent();
|
|
8674
|
-
|
|
8838
|
+
delivered = true;
|
|
8839
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8675
8840
|
return true;
|
|
8676
8841
|
}
|
|
8677
8842
|
const forwarded = await forwardPendingRerouteMessages(
|
|
@@ -8682,7 +8847,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8682
8847
|
assertRecipientCurrent,
|
|
8683
8848
|
recordForwardAcceptance,
|
|
8684
8849
|
);
|
|
8685
|
-
if (forwarded)
|
|
8850
|
+
if (forwarded) {
|
|
8851
|
+
delivered = true;
|
|
8852
|
+
await releaseAllTabCopiesIntoSurvivingTab(pending);
|
|
8853
|
+
}
|
|
8686
8854
|
return forwarded;
|
|
8687
8855
|
},
|
|
8688
8856
|
});
|
|
@@ -8693,7 +8861,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
8693
8861
|
if (!selectionCurrent()) return true;
|
|
8694
8862
|
await deps.answerCallbackQuery(
|
|
8695
8863
|
query.id,
|
|
8696
|
-
"Restore unconfirmed; not resending",
|
|
8864
|
+
delivered ? "Restored" : "Restore unconfirmed; not resending",
|
|
8697
8865
|
);
|
|
8698
8866
|
return true;
|
|
8699
8867
|
}
|
|
@@ -10075,6 +10243,47 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10075
10243
|
}
|
|
10076
10244
|
return undefined;
|
|
10077
10245
|
};
|
|
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
|
+
*/
|
|
10250
|
+
const findImplicitCreationForAllInput = (
|
|
10251
|
+
message: TMessage,
|
|
10252
|
+
updateId: number,
|
|
10253
|
+
cap: NonNullable<ReturnType<typeof captureTemporaryThreadAuthority>>,
|
|
10254
|
+
ctx: TContext,
|
|
10255
|
+
) => {
|
|
10256
|
+
const text = (message.text ?? message.caption ?? "").trim();
|
|
10257
|
+
let match:
|
|
10258
|
+
| {
|
|
10259
|
+
key: string;
|
|
10260
|
+
implicit: typeof implicitThreadCreations extends Map<string, infer V>
|
|
10261
|
+
? V
|
|
10262
|
+
: never;
|
|
10263
|
+
}
|
|
10264
|
+
| undefined;
|
|
10265
|
+
for (const [key, implicit] of implicitThreadCreations) {
|
|
10266
|
+
if (
|
|
10267
|
+
((implicit.name && text.startsWith(implicit.name)) ||
|
|
10268
|
+
implicit.updateId === updateId - 1) &&
|
|
10269
|
+
implicit.updateId < updateId &&
|
|
10270
|
+
implicit.createdAtSec > 0 &&
|
|
10271
|
+
message.date !== undefined &&
|
|
10272
|
+
message.date >= implicit.createdAtSec &&
|
|
10273
|
+
message.date - implicit.createdAtSec <=
|
|
10274
|
+
TELEGRAM_IMPLICIT_ALL_INPUT_WINDOW_SEC &&
|
|
10275
|
+
implicit.ctx === ctx &&
|
|
10276
|
+
implicit.operatorUserId === cap.operatorUserId &&
|
|
10277
|
+
implicit.journalBindingKey === cap.journalBindingKey &&
|
|
10278
|
+
isDeepStrictEqual(implicit.executor, cap.authority.executor) &&
|
|
10279
|
+
implicit.generation === deps.getSessionGeneration?.() &&
|
|
10280
|
+
implicit.scope === deps.getAdmissionScope?.() &&
|
|
10281
|
+
(!match || implicit.updateId > match.implicit.updateId)
|
|
10282
|
+
)
|
|
10283
|
+
match = { key, implicit };
|
|
10284
|
+
}
|
|
10285
|
+
return match;
|
|
10286
|
+
};
|
|
10078
10287
|
/**
|
|
10079
10288
|
* An All input gets one source-bound tab. The original stays deferred in All until explicit routing,
|
|
10080
10289
|
* and an existing entry is reused on replay, so restart never creates a second tab or retries an unknown one.
|
|
@@ -10116,6 +10325,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10116
10325
|
entry.source.updateId === updateId,
|
|
10117
10326
|
);
|
|
10118
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;
|
|
10119
10331
|
await deps.runWorkspaceOperation(
|
|
10120
10332
|
{
|
|
10121
10333
|
operationId: `temporary-thread-${randomBytes(16).toString("hex")}`,
|
|
@@ -10130,6 +10342,45 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10130
10342
|
entry = found;
|
|
10131
10343
|
return;
|
|
10132
10344
|
}
|
|
10345
|
+
// Mobile clients sending from All first create a native tab named after the input, then deliver
|
|
10346
|
+
// the input itself to All: adopt that tab rather than leaving it behind next to a second one.
|
|
10347
|
+
const native = findImplicitCreationForAllInput(
|
|
10348
|
+
message,
|
|
10349
|
+
updateId,
|
|
10350
|
+
cap,
|
|
10351
|
+
ctx,
|
|
10352
|
+
);
|
|
10353
|
+
if (native) {
|
|
10354
|
+
const adopted = store.registerImplicitTemporaryThread(
|
|
10355
|
+
{ journalBindingKey, updateIds: [updateId] },
|
|
10356
|
+
native.implicit.target,
|
|
10357
|
+
randomBytes(16).toString("hex"),
|
|
10358
|
+
{
|
|
10359
|
+
...authority,
|
|
10360
|
+
isCurrent: () =>
|
|
10361
|
+
current() &&
|
|
10362
|
+
implicitThreadCreations.get(native.key) === native.implicit,
|
|
10363
|
+
},
|
|
10364
|
+
);
|
|
10365
|
+
if (adopted && current()) {
|
|
10366
|
+
implicitThreadCreations.delete(native.key);
|
|
10367
|
+
entry = adopted;
|
|
10368
|
+
openedNow = true;
|
|
10369
|
+
// Adopted native tabs share the routing name; a failed rename never blocks routing.
|
|
10370
|
+
try {
|
|
10371
|
+
await deps.callApi!("editForumTopic", {
|
|
10372
|
+
chat_id: native.implicit.target.chatId,
|
|
10373
|
+
message_thread_id: native.implicit.target.threadId,
|
|
10374
|
+
name: TELEGRAM_TEMPORARY_THREAD_NAME,
|
|
10375
|
+
});
|
|
10376
|
+
} catch (error) {
|
|
10377
|
+
deps.recordRuntimeEvent?.("routing", error, {
|
|
10378
|
+
phase: "temporary-thread-name",
|
|
10379
|
+
});
|
|
10380
|
+
}
|
|
10381
|
+
return;
|
|
10382
|
+
}
|
|
10383
|
+
}
|
|
10133
10384
|
const reservation = store.reserveTemporaryThread(
|
|
10134
10385
|
source,
|
|
10135
10386
|
randomBytes(16).toString("hex"),
|
|
@@ -10161,12 +10412,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10161
10412
|
threadId <= 0
|
|
10162
10413
|
)
|
|
10163
10414
|
return;
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10415
|
+
const acknowledged = store.acknowledgeTemporaryThread(
|
|
10416
|
+
reservation.entry,
|
|
10417
|
+
{ chatId: operatorUserId, threadId },
|
|
10418
|
+
authority,
|
|
10419
|
+
);
|
|
10420
|
+
openedNow = !!acknowledged;
|
|
10421
|
+
entry = acknowledged ?? entry;
|
|
10170
10422
|
},
|
|
10171
10423
|
);
|
|
10172
10424
|
if (!current() || !entry)
|
|
@@ -10222,6 +10474,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10222
10474
|
pending.rootChooserText = formatTelegramTemporaryThreadChooserText(
|
|
10223
10475
|
command?.name,
|
|
10224
10476
|
);
|
|
10477
|
+
const tabCopyId = openedNow
|
|
10478
|
+
? await moveAllTabSourceIntoTab(pending, target, current)
|
|
10479
|
+
: undefined;
|
|
10225
10480
|
try {
|
|
10226
10481
|
if (!deps.sendInteractiveMessage)
|
|
10227
10482
|
throw new Error("Temporary Thread chooser publication is unavailable.");
|
|
@@ -10233,7 +10488,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10233
10488
|
canCancel: !!pending.abandonment,
|
|
10234
10489
|
getDisplayTitle: deps.getDisplayTitle,
|
|
10235
10490
|
}),
|
|
10236
|
-
|
|
10491
|
+
tabCopyId === undefined
|
|
10492
|
+
? { target }
|
|
10493
|
+
: { target, replyToMessageId: tabCopyId },
|
|
10237
10494
|
);
|
|
10238
10495
|
rememberRerouteChooser(rerouteId, chooserId);
|
|
10239
10496
|
} catch (error) {
|
|
@@ -10625,6 +10882,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
10625
10882
|
executor: structuredClone(cap.authority.executor),
|
|
10626
10883
|
generation: deps.getSessionGeneration?.(),
|
|
10627
10884
|
scope: deps.getAdmissionScope?.(),
|
|
10885
|
+
target: {
|
|
10886
|
+
chatId: lifecycle.target.chatId,
|
|
10887
|
+
threadId: lifecycle.target.threadId!,
|
|
10888
|
+
},
|
|
10889
|
+
name:
|
|
10890
|
+
"name" in created && typeof created.name === "string"
|
|
10891
|
+
? created.name
|
|
10892
|
+
: "",
|
|
10893
|
+
createdAtSec: message.date ?? 0,
|
|
10628
10894
|
});
|
|
10629
10895
|
} else implicitThreadCreations.delete(key);
|
|
10630
10896
|
};
|
|
@@ -11015,6 +11281,33 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11015
11281
|
threadId: target.threadId,
|
|
11016
11282
|
};
|
|
11017
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
|
+
};
|
|
11018
11311
|
const reclaimUnboundTargetForLeader = async (
|
|
11019
11312
|
base: Partial<Threads.TelegramTopicTargetRecord> &
|
|
11020
11313
|
Pick<Threads.TelegramTopicTargetRecord, "createdAtMs">,
|
|
@@ -11023,7 +11316,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11023
11316
|
threadName: string,
|
|
11024
11317
|
event: string,
|
|
11025
11318
|
details: Record<string, unknown>,
|
|
11319
|
+
leaderBinding?: Threads.TelegramWorkspaceThreadBinding,
|
|
11026
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
|
+
}
|
|
11027
11338
|
deps.threadStore!.upsert({
|
|
11028
11339
|
...base,
|
|
11029
11340
|
profileKey,
|
|
@@ -11055,6 +11366,35 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11055
11366
|
slot,
|
|
11056
11367
|
profileKey,
|
|
11057
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
|
+
}
|
|
11058
11398
|
await textDispatch.handleMessage(message as TMessage, ctx);
|
|
11059
11399
|
};
|
|
11060
11400
|
const records = deps.threadStore.list();
|
|
@@ -11195,7 +11535,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11195
11535
|
if (!currentLeaderIsStale) throw error;
|
|
11196
11536
|
}
|
|
11197
11537
|
if (currentLeaderIsStale) {
|
|
11198
|
-
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!);
|
|
11199
11546
|
if (!slot) {
|
|
11200
11547
|
deps.threadStore.markStaleByTarget(
|
|
11201
11548
|
currentLeaderRecord.target,
|
|
@@ -11231,6 +11578,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11231
11578
|
phase: "leader-topic-unbound-stale-reclaim",
|
|
11232
11579
|
staleThreadId: currentLeaderRecord.target.threadId,
|
|
11233
11580
|
},
|
|
11581
|
+
leaderBinding?.slot === slot ? leaderBinding : undefined,
|
|
11234
11582
|
);
|
|
11235
11583
|
return;
|
|
11236
11584
|
}
|
|
@@ -11245,10 +11593,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11245
11593
|
deps.threadStore.getByProfileKey(leaderProfileKey);
|
|
11246
11594
|
const priorLeaderIdentity =
|
|
11247
11595
|
deps.threadStore.getIdentityByProfileKey(leaderProfileKey);
|
|
11596
|
+
const leaderBinding = claimLeaderWorkspaceBinding();
|
|
11248
11597
|
const slot = deps.threadStore.allocateSlot(
|
|
11249
11598
|
leaderProfileKey,
|
|
11250
|
-
|
|
11599
|
+
leaderBinding?.slot ??
|
|
11600
|
+
priorLeaderRecord?.slot ??
|
|
11601
|
+
priorLeaderIdentity?.slot,
|
|
11602
|
+
leaderBinding?.bindingKey,
|
|
11251
11603
|
);
|
|
11604
|
+
if (leaderBinding && leaderBinding.slot !== slot)
|
|
11605
|
+
deps.threadStore.releaseWorkspaceClaim(instanceId!);
|
|
11252
11606
|
if (!slot) {
|
|
11253
11607
|
await deps.sendTextReply(
|
|
11254
11608
|
target.chatId,
|
|
@@ -11278,6 +11632,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11278
11632
|
threadName,
|
|
11279
11633
|
"Bus leader reclaimed unbound thread",
|
|
11280
11634
|
{ phase: "leader-topic-reclaim" },
|
|
11635
|
+
leaderBinding?.slot === slot ? leaderBinding : undefined,
|
|
11281
11636
|
);
|
|
11282
11637
|
return;
|
|
11283
11638
|
}
|
|
@@ -11307,6 +11662,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
11307
11662
|
onUpdateCompleted,
|
|
11308
11663
|
prepareHeldCommand: commandHandler.prepareHeldCommand,
|
|
11309
11664
|
canPrepareHeldCommand: commandHandler.canPrepareHeldCommand,
|
|
11665
|
+
onSessionSettled,
|
|
11310
11666
|
observeLiveRebindLeaderWork,
|
|
11311
11667
|
prepareLiveRebindLeaderCleanup,
|
|
11312
11668
|
prepareLiveRebindFollowerCleanup,
|