@llblab/pi-telegram 0.56.0 → 0.56.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 +2 -2
- package/BACKLOG.md +2 -0
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +6 -5
- package/dist/lib/bus-leader.d.ts +2 -0
- package/dist/lib/bus-leader.js +26 -20
- package/dist/lib/bus.d.ts +5 -0
- package/dist/lib/bus.js +15 -7
- package/dist/lib/commands.d.ts +1 -1
- package/dist/lib/commands.js +33 -28
- package/dist/lib/extension.js +4 -1
- package/dist/lib/locks.d.ts +6 -2
- package/dist/lib/locks.js +3 -3
- package/dist/lib/polling.d.ts +6 -2
- package/dist/lib/polling.js +2 -2
- package/dist/lib/routing.d.ts +4 -0
- package/dist/lib/routing.js +62 -1
- package/dist/lib/sync.js +11 -1
- package/dist/lib/threads.d.ts +11 -0
- package/dist/lib/threads.js +57 -6
- package/dist/lib/updates.d.ts +4 -1
- package/dist/lib/updates.js +24 -5
- package/dist/package.json +1 -1
- package/docs/architecture.md +10 -0
- package/docs/multi-instance-bus.md +4 -2
- package/lib/bus-follower.ts +8 -5
- package/lib/bus-leader.ts +28 -23
- package/lib/bus.ts +16 -7
- package/lib/commands.ts +52 -47
- package/lib/extension.ts +4 -1
- package/lib/locks.ts +5 -2
- package/lib/polling.ts +4 -1
- package/lib/routing.ts +51 -1
- package/lib/sync.ts +11 -0
- package/lib/threads.ts +57 -5
- package/lib/updates.ts +22 -5
- package/package.json +1 -1
package/lib/bus-leader.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
24
24
|
TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
25
25
|
TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
26
|
+
TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
|
|
26
27
|
hasTelegramBusSteerQueueHandoffCapability,
|
|
27
28
|
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
28
29
|
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
@@ -1529,7 +1530,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1529
1530
|
deps: TelegramBusFollowerTargetProvisionerDeps,
|
|
1530
1531
|
): (
|
|
1531
1532
|
registration: TelegramBusInstanceRegistration,
|
|
1532
|
-
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
1533
|
+
options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
|
|
1533
1534
|
) => Promise<
|
|
1534
1535
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
1535
1536
|
> {
|
|
@@ -1548,6 +1549,16 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1548
1549
|
const chatId = deps.getAllowedUserId();
|
|
1549
1550
|
if (typeof chatId !== "number") return registration.target;
|
|
1550
1551
|
await deps.topicTargetStore.load();
|
|
1552
|
+
if (options?.recoverConnection && !options.existingWorkspaceBindingOnly) {
|
|
1553
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
1554
|
+
await Threads.resetTelegramConnectionProvisioning({
|
|
1555
|
+
store: deps.topicTargetStore, owner: "manual-follower", instanceId: registration.instanceId,
|
|
1556
|
+
profileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
|
|
1557
|
+
isCurrent: () => !deps.getCurrentLeaderEpoch ||
|
|
1558
|
+
(epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch),
|
|
1559
|
+
recordEvent: deps.recordRuntimeEvent,
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1551
1562
|
let restoredPredecessor: Threads.TelegramTopicTargetRecord | undefined;
|
|
1552
1563
|
if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
|
|
1553
1564
|
!registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
|
|
@@ -1580,6 +1591,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1580
1591
|
{
|
|
1581
1592
|
existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
|
|
1582
1593
|
sessionId: registration.sessionId,
|
|
1594
|
+
provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
|
|
1583
1595
|
onCapacityUnavailable() {
|
|
1584
1596
|
capacityUnavailable = true;
|
|
1585
1597
|
},
|
|
@@ -1842,12 +1854,6 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1842
1854
|
};
|
|
1843
1855
|
}
|
|
1844
1856
|
const probeRequiredRecord = reconnectRecord?.status === "probe-required";
|
|
1845
|
-
const exactSessionHandoff =
|
|
1846
|
-
crossSessionReuse &&
|
|
1847
|
-
!!requestedTarget &&
|
|
1848
|
-
registration.previousInstanceId === reconnectRecord?.instanceId &&
|
|
1849
|
-
requestedTarget.chatId === reconnectRecord.target.chatId &&
|
|
1850
|
-
requestedTarget.threadId === reconnectRecord.target.threadId;
|
|
1851
1857
|
const requiresVisibilityProbe =
|
|
1852
1858
|
crossSessionReuse ||
|
|
1853
1859
|
probeRequiredRecord ||
|
|
@@ -1869,19 +1875,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1869
1875
|
try {
|
|
1870
1876
|
assertRestoreRegistration();
|
|
1871
1877
|
await deps.callApi(
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
chat_id: connectedAnnouncement.target.chatId,
|
|
1881
|
-
message_thread_id: connectedAnnouncement.target.threadId,
|
|
1882
|
-
text: connectedAnnouncement.text,
|
|
1883
|
-
parse_mode: connectedAnnouncement.parseMode,
|
|
1884
|
-
},
|
|
1878
|
+
// Typing is presence, not evidence that a retained Thread still accepts messages.
|
|
1879
|
+
"sendMessage",
|
|
1880
|
+
{
|
|
1881
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
1882
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
1883
|
+
text: connectedAnnouncement.text,
|
|
1884
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
1885
|
+
},
|
|
1885
1886
|
);
|
|
1886
1887
|
assertRestoreRegistration();
|
|
1887
1888
|
if (recoverableTarget || probeRequiredRecord) {
|
|
@@ -2944,7 +2945,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
|
|
|
2944
2945
|
) => Promise<unknown> | unknown;
|
|
2945
2946
|
provisionFollowerTarget?: (
|
|
2946
2947
|
registration: TelegramBusInstanceRegistration,
|
|
2947
|
-
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
2948
|
+
options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
|
|
2948
2949
|
) =>
|
|
2949
2950
|
| Promise<
|
|
2950
2951
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
@@ -3453,6 +3454,9 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3453
3454
|
));
|
|
3454
3455
|
const restoringWorkspace =
|
|
3455
3456
|
envelope.kind === "follower.restoreWorkspace";
|
|
3457
|
+
const recoverConnection = envelope.kind === "follower.register" && envelope.recoverConnection === true;
|
|
3458
|
+
const supportsConnectionRecovery = [deps.protocolIdentity, envelope.registration.protocol]
|
|
3459
|
+
.every(identity => hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY));
|
|
3456
3460
|
const supportsWorkspaceAutoConnect =
|
|
3457
3461
|
hasTelegramBusCapability(
|
|
3458
3462
|
deps.protocolIdentity,
|
|
@@ -3468,7 +3472,8 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3468
3472
|
!readerCompatible() ||
|
|
3469
3473
|
(envelope.registration.cwd !== undefined &&
|
|
3470
3474
|
envelope.registration.sessionId === undefined) ||
|
|
3471
|
-
(restoringWorkspace && !supportsWorkspaceAutoConnect)
|
|
3475
|
+
(restoringWorkspace && !supportsWorkspaceAutoConnect) ||
|
|
3476
|
+
(recoverConnection && !supportsConnectionRecovery)
|
|
3472
3477
|
) {
|
|
3473
3478
|
return {
|
|
3474
3479
|
kind: "bus.ack" as const,
|
|
@@ -3526,7 +3531,7 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3526
3531
|
);
|
|
3527
3532
|
const target = await deps.provisionFollowerTarget?.(
|
|
3528
3533
|
envelope.registration,
|
|
3529
|
-
{ existingWorkspaceBindingOnly: restoringWorkspace },
|
|
3534
|
+
{ existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) },
|
|
3530
3535
|
);
|
|
3531
3536
|
if (restoringWorkspace && !target) {
|
|
3532
3537
|
return {
|
package/lib/bus.ts
CHANGED
|
@@ -144,6 +144,7 @@ export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
|
|
|
144
144
|
"queue-handoff-v1" as const;
|
|
145
145
|
/** Unspent Steer lane codec; independent from baseline receipt handoff support. */
|
|
146
146
|
export const TELEGRAM_BUS_CAPABILITY_QUEUE_STEER = "queue-steer-v1" as const;
|
|
147
|
+
export const TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY = "explicit-connect-recovery-v1" as const;
|
|
147
148
|
export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
|
|
148
149
|
"input-custody-reference-v1" as const;
|
|
149
150
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
|
|
@@ -184,6 +185,14 @@ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
|
|
|
184
185
|
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
185
186
|
] as const;
|
|
186
187
|
|
|
188
|
+
/** Start/help are restricted menu aliases, negotiated separately from the original held-command set. */
|
|
189
|
+
export const TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES = "held-menu-aliases-v1" as const;
|
|
190
|
+
export function getTelegramBusHeldCommandCapabilities(name: string): readonly string[] {
|
|
191
|
+
return name === "start" || name === "help"
|
|
192
|
+
? [...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES]
|
|
193
|
+
: TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES;
|
|
194
|
+
}
|
|
195
|
+
|
|
187
196
|
export interface TelegramBusSelectedCommandInput {
|
|
188
197
|
name: string;
|
|
189
198
|
target: TelegramTarget & { threadId: number };
|
|
@@ -1009,6 +1018,7 @@ export type TelegramBusEnvelope = (
|
|
|
1009
1018
|
| {
|
|
1010
1019
|
kind: "follower.register";
|
|
1011
1020
|
requestId: string;
|
|
1021
|
+
recoverConnection?: true;
|
|
1012
1022
|
registration: TelegramBusInstanceRegistration;
|
|
1013
1023
|
}
|
|
1014
1024
|
| {
|
|
@@ -2539,12 +2549,7 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2539
2549
|
: []),
|
|
2540
2550
|
...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
2541
2551
|
...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
|
|
2542
|
-
...(held
|
|
2543
|
-
? [
|
|
2544
|
-
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
2545
|
-
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2546
|
-
]
|
|
2547
|
-
: []),
|
|
2552
|
+
...(held ? getTelegramBusHeldCommandCapabilities(held.name) : []),
|
|
2548
2553
|
];
|
|
2549
2554
|
const follower = deps.getFollower(expected.instanceId),
|
|
2550
2555
|
secret = deps.getAuthSecret();
|
|
@@ -3545,8 +3550,12 @@ function parseRegisterEnvelope(
|
|
|
3545
3550
|
requestId: string,
|
|
3546
3551
|
kind: "follower.register" | "follower.restoreWorkspace",
|
|
3547
3552
|
): TelegramBusEnvelope | undefined {
|
|
3553
|
+
if (value.recoverConnection !== undefined &&
|
|
3554
|
+
(kind !== "follower.register" || value.recoverConnection !== true)) return undefined;
|
|
3548
3555
|
const registration = parseRegistration(value.registration);
|
|
3549
|
-
return registration ? { kind, requestId, registration
|
|
3556
|
+
return registration ? { kind, requestId, registration,
|
|
3557
|
+
...(kind === "follower.register" && value.recoverConnection === true ? { recoverConnection: true as const } : {}),
|
|
3558
|
+
} : undefined;
|
|
3550
3559
|
}
|
|
3551
3560
|
|
|
3552
3561
|
function parseFollowerPresenceEnvelope(
|
package/lib/commands.ts
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
} from "./updates.ts";
|
|
54
54
|
|
|
55
55
|
export type TelegramHeldCommandName =
|
|
56
|
-
"status" | "abort" | "stop" | "next" | "continue";
|
|
56
|
+
"status" | "start" | "help" | "abort" | "stop" | "next" | "continue";
|
|
57
57
|
|
|
58
58
|
/** Commands-owned no-fold admission over the ordinary recipient queue; source reporting stays separate from removal. */
|
|
59
59
|
export interface TelegramHeldTurnAdmission {
|
|
@@ -2773,53 +2773,58 @@ export function createTelegramCommandHandler<
|
|
|
2773
2773
|
},
|
|
2774
2774
|
};
|
|
2775
2775
|
};
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2776
|
+
// Held followers are already paired and bound. Start/help open the same restricted menu as status;
|
|
2777
|
+
// they cannot pair an operator, bootstrap another Thread or invoke unrestricted Bot API effects.
|
|
2778
|
+
const heldMenuPlan: HeldPlan = {
|
|
2779
|
+
completion: "before-effect",
|
|
2780
|
+
effect: "showStatus",
|
|
2781
|
+
prepare(ctx, effects) {
|
|
2782
|
+
const scopedStatus = effects?.showStatus;
|
|
2783
|
+
const captured = {
|
|
2784
|
+
...deps,
|
|
2785
|
+
showStatus: scopedStatus
|
|
2786
|
+
? (
|
|
2787
|
+
message: TMessage,
|
|
2788
|
+
context: TContext,
|
|
2789
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
2790
|
+
) => {
|
|
2791
|
+
const address = getTelegramCommandMessageTarget(message);
|
|
2792
|
+
return scopedStatus.call(
|
|
2793
|
+
effects,
|
|
2794
|
+
address.chatId,
|
|
2795
|
+
address.replyToMessageId,
|
|
2796
|
+
context,
|
|
2797
|
+
address.threadId,
|
|
2798
|
+
options,
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
: deps.showStatus,
|
|
2802
|
+
};
|
|
2803
|
+
return {
|
|
2804
|
+
captured,
|
|
2805
|
+
portsCurrent: () => !effects || effects.showStatus === scopedStatus,
|
|
2806
|
+
async run({ message, command, assertRecipient }) {
|
|
2807
|
+
const recipientOptions = { assertAuthority: assertRecipient };
|
|
2808
|
+
return handleTelegramCommandRuntime(
|
|
2809
|
+
"status",
|
|
2810
|
+
message,
|
|
2811
|
+
ctx,
|
|
2812
|
+
{
|
|
2813
|
+
...captured,
|
|
2814
|
+
assertExecutionCurrent: assertRecipient,
|
|
2815
|
+
showStatus: (value: TMessage, context: TContext) =>
|
|
2816
|
+
captured.showStatus(value, context, recipientOptions),
|
|
2817
|
+
},
|
|
2818
|
+
command.args,
|
|
2819
|
+
);
|
|
2820
|
+
},
|
|
2821
|
+
};
|
|
2822
2822
|
},
|
|
2823
|
+
};
|
|
2824
|
+
const heldPlans: Record<TelegramHeldCommandName, HeldPlan> = {
|
|
2825
|
+
status: heldMenuPlan,
|
|
2826
|
+
start: heldMenuPlan,
|
|
2827
|
+
help: heldMenuPlan,
|
|
2823
2828
|
abort: {
|
|
2824
2829
|
completion: "after-semantics",
|
|
2825
2830
|
effect: "sendTextReply",
|
package/lib/extension.ts
CHANGED
|
@@ -65,6 +65,8 @@ const telegramBusProtocolIdentity =
|
|
|
65
65
|
Bus.TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
66
66
|
Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
67
67
|
Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_STEER,
|
|
68
|
+
Bus.TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
|
|
69
|
+
Bus.TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES,
|
|
68
70
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
69
71
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
70
72
|
Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
@@ -1238,7 +1240,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1238
1240
|
follower.protocol,
|
|
1239
1241
|
[
|
|
1240
1242
|
...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
1241
|
-
...Bus.
|
|
1243
|
+
...Bus.getTelegramBusHeldCommandCapabilities(name),
|
|
1242
1244
|
],
|
|
1243
1245
|
) &&
|
|
1244
1246
|
typeof selectedMenuDelivery === "function" &&
|
|
@@ -1559,6 +1561,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1559
1561
|
threadStore,
|
|
1560
1562
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1561
1563
|
liveRebindCleanupSchedule: Routing.TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE,
|
|
1564
|
+
liveRebindCommandCompletionTimeoutMs: 8_000,
|
|
1562
1565
|
updateStatus,
|
|
1563
1566
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
1564
1567
|
dispatchNextQueuedTelegramTurn,
|
package/lib/locks.ts
CHANGED
|
@@ -1981,6 +1981,7 @@ export interface TelegramLockedPollingRuntime<
|
|
|
1981
1981
|
registerFollowerWithOwner?: (
|
|
1982
1982
|
ctx: TContext,
|
|
1983
1983
|
owner: TelegramLockEntry,
|
|
1984
|
+
options?: { recoverConnection?: boolean },
|
|
1984
1985
|
) => boolean | undefined | Promise<boolean | undefined>;
|
|
1985
1986
|
restoreFollowerWithOwner?: (
|
|
1986
1987
|
ctx: TContext,
|
|
@@ -2008,6 +2009,7 @@ interface TelegramLockedPollingRuntimeDeps<
|
|
|
2008
2009
|
registerFollowerWithOwner?: (
|
|
2009
2010
|
ctx: TContext,
|
|
2010
2011
|
owner: TelegramLockEntry,
|
|
2012
|
+
options?: { recoverConnection?: boolean },
|
|
2011
2013
|
) => boolean | undefined | Promise<boolean | undefined>;
|
|
2012
2014
|
restoreFollowerWithOwner?: (
|
|
2013
2015
|
ctx: TContext,
|
|
@@ -2294,6 +2296,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
2294
2296
|
const registered = await deps.registerFollowerWithOwner(
|
|
2295
2297
|
ctx,
|
|
2296
2298
|
acquired.lock,
|
|
2299
|
+
options.forceFreshLeaderThread ? { recoverConnection: true } : undefined,
|
|
2297
2300
|
);
|
|
2298
2301
|
if (!isCurrent()) return cancelled;
|
|
2299
2302
|
if (registered) {
|
|
@@ -2518,8 +2521,8 @@ export function createTelegramLockedPollingRuntime<
|
|
|
2518
2521
|
sessionAutoStartRun = run;
|
|
2519
2522
|
},
|
|
2520
2523
|
registerFollowerWithOwner: deps.registerFollowerWithOwner
|
|
2521
|
-
? async (ctx, owner) => {
|
|
2522
|
-
const registered = await deps.registerFollowerWithOwner?.(ctx, owner);
|
|
2524
|
+
? async (ctx, owner, options) => {
|
|
2525
|
+
const registered = await deps.registerFollowerWithOwner?.(ctx, owner, options);
|
|
2523
2526
|
if (registered) deps.updateStatus(ctx);
|
|
2524
2527
|
return registered === true;
|
|
2525
2528
|
}
|
package/lib/polling.ts
CHANGED
|
@@ -757,6 +757,7 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
|
|
|
757
757
|
registerFollowerWithOwner: (
|
|
758
758
|
ctx: TContext,
|
|
759
759
|
owner: TOwner,
|
|
760
|
+
options?: { recoverConnection?: boolean },
|
|
760
761
|
) => Promise<boolean | undefined>;
|
|
761
762
|
restoreFollowerWithOwner: (
|
|
762
763
|
ctx: TContext,
|
|
@@ -776,6 +777,7 @@ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
|
|
|
776
777
|
registerFollowerWithLeader: (
|
|
777
778
|
ctx: TContext,
|
|
778
779
|
owner: TOwner,
|
|
780
|
+
options?: { recoverConnection?: boolean },
|
|
779
781
|
) => Promise<boolean | undefined>;
|
|
780
782
|
restoreFollowerWithLeader?: (
|
|
781
783
|
ctx: TContext,
|
|
@@ -1180,9 +1182,10 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
|
1180
1182
|
const registerFollowerWithOwner = async (
|
|
1181
1183
|
ctx: TContext,
|
|
1182
1184
|
owner: TOwner,
|
|
1185
|
+
options?: { recoverConnection?: boolean },
|
|
1183
1186
|
): Promise<boolean | undefined> => {
|
|
1184
1187
|
if (!(await refreshFollowerState())) return undefined;
|
|
1185
|
-
return deps.registerFollowerWithLeader(ctx, owner);
|
|
1188
|
+
return deps.registerFollowerWithLeader(ctx, owner, options);
|
|
1186
1189
|
};
|
|
1187
1190
|
const restoreFollowerWithOwner = async (
|
|
1188
1191
|
ctx: TContext,
|
package/lib/routing.ts
CHANGED
|
@@ -916,6 +916,16 @@ export function createTelegramLiveRebindCoordinator(input: {
|
|
|
916
916
|
}
|
|
917
917
|
: undefined;
|
|
918
918
|
},
|
|
919
|
+
/** Observe only an already-issued leader command; never prepares, relocates or executes input. */
|
|
920
|
+
observeCommandCompletion(): "unknown" | "released" | undefined {
|
|
921
|
+
if (!continuationIssued || !leader?.complete) return undefined;
|
|
922
|
+
if (!current()) return "unknown";
|
|
923
|
+
try { return observeCommandCompletion(); }
|
|
924
|
+
catch (error) {
|
|
925
|
+
input.recordRuntimeEvent?.("telegram", error, { phase: "live-rebind-command-observation" });
|
|
926
|
+
return "unknown";
|
|
927
|
+
}
|
|
928
|
+
},
|
|
919
929
|
async advance(): Promise<"protected" | "unknown" | "released"> {
|
|
920
930
|
if (advancing || !current()) return "protected";
|
|
921
931
|
if (released) return "released";
|
|
@@ -1561,6 +1571,8 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
1561
1571
|
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
|
|
1562
1572
|
temporaryThreadCleanupDelayMs?: number;
|
|
1563
1573
|
/** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
|
|
1574
|
+
/** Bounded ACK observation inside the original chooser admission, independent from menu/reply delivery. */
|
|
1575
|
+
liveRebindCommandCompletionTimeoutMs?: number;
|
|
1564
1576
|
liveRebindCleanupSchedule?: {
|
|
1565
1577
|
delaysMs: readonly number[];
|
|
1566
1578
|
intervalMs: number;
|
|
@@ -1937,6 +1949,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1937
1949
|
extensionCommand?: Commands.ParsedTelegramCommand;
|
|
1938
1950
|
extensionKind?: Commands.PreparedSelectedCommand["kind"];
|
|
1939
1951
|
coordinator?: ReturnType<typeof createTelegramLiveRebindCoordinator>;
|
|
1952
|
+
commandCompletionWaited?: true;
|
|
1940
1953
|
isCurrent?: () => boolean;
|
|
1941
1954
|
};
|
|
1942
1955
|
/** The acknowledged tab presenting an All source; `sourceTarget` is this tab, not the original's location. */
|
|
@@ -4870,7 +4883,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
4870
4883
|
Bus.isSameTelegramBusFollowerRegistration(live, follower!) &&
|
|
4871
4884
|
[
|
|
4872
4885
|
...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
4873
|
-
...(heldSelected ? Bus.
|
|
4886
|
+
...(heldSelected ? Bus.getTelegramBusHeldCommandCapabilities(heldSelected.name) : []),
|
|
4874
4887
|
].every((cap) => Bus.hasTelegramBusCapability(live.protocol, cap)) &&
|
|
4875
4888
|
!!live.target &&
|
|
4876
4889
|
live.target.chatId === binding.target.chatId &&
|
|
@@ -5642,6 +5655,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
5642
5655
|
return {
|
|
5643
5656
|
operationId: request.operationId,
|
|
5644
5657
|
selectedCommandReference: () => coordinator?.selectedCommandReference(),
|
|
5658
|
+
observeCommandCompletion: () => coordinator?.observeCommandCompletion(),
|
|
5645
5659
|
async advance() {
|
|
5646
5660
|
if (!coordinator) {
|
|
5647
5661
|
if (!current() || !getRecipient()) return "protected";
|
|
@@ -9058,6 +9072,42 @@ export function createTelegramInboundRouteRuntime<
|
|
|
9058
9072
|
if (!(await selectPendingDestination())) return true;
|
|
9059
9073
|
const operationId = selection.coordinator.operationId;
|
|
9060
9074
|
if (current()) outcome = await selection.coordinator.advance();
|
|
9075
|
+
const completionTimeoutMs = deps.liveRebindCommandCompletionTimeoutMs ?? 0;
|
|
9076
|
+
if (outcome === "unknown" && !selection.commandCompletionWaited && selection.record.owner?.kind === "leader" &&
|
|
9077
|
+
(selection.helpCommand || selection.menuCommand || selection.abortCommand || selection.nextCommand || selection.stopCommand) &&
|
|
9078
|
+
Number.isFinite(completionTimeoutMs) && completionTimeoutMs > 0 &&
|
|
9079
|
+
selection.coordinator.observeCommandCompletion() !== undefined) {
|
|
9080
|
+
selection.commandCompletionWaited = true;
|
|
9081
|
+
const coordinator = selection.coordinator;
|
|
9082
|
+
const signal = Updates.getTelegramUpdateExecutionFence(query)?.signal;
|
|
9083
|
+
outcome = await new Promise<"unknown" | "released">((resolve) => {
|
|
9084
|
+
let settled = false;
|
|
9085
|
+
const subscriptions: (() => void)[] = [];
|
|
9086
|
+
const finish = (result: "unknown" | "released") => {
|
|
9087
|
+
if (settled) return;
|
|
9088
|
+
settled = true;
|
|
9089
|
+
clearTimeout(timer);
|
|
9090
|
+
for (const unsubscribe of subscriptions) unsubscribe();
|
|
9091
|
+
signal?.removeEventListener("abort", abort);
|
|
9092
|
+
resolve(result);
|
|
9093
|
+
};
|
|
9094
|
+
const abort = () => finish("unknown");
|
|
9095
|
+
const observe = () => {
|
|
9096
|
+
if (settled) return;
|
|
9097
|
+
if (!current()) { finish("unknown"); return; }
|
|
9098
|
+
if (coordinator.observeCommandCompletion() === "released") finish("released");
|
|
9099
|
+
};
|
|
9100
|
+
const timer = setTimeout(abort, completionTimeoutMs);
|
|
9101
|
+
timer.unref?.();
|
|
9102
|
+
for (const message of selection.messages) {
|
|
9103
|
+
const unsubscribe = Updates.subscribeTelegramDeferredSourceCompletion(message, observe);
|
|
9104
|
+
if (unsubscribe) subscriptions.push(unsubscribe);
|
|
9105
|
+
}
|
|
9106
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
9107
|
+
if (signal?.aborted || !current()) abort();
|
|
9108
|
+
else if (coordinator.observeCommandCompletion() === "released") finish("released");
|
|
9109
|
+
});
|
|
9110
|
+
}
|
|
9061
9111
|
// Cleanup readiness follows the canonical released row, not source disposal: a detached command ACK may
|
|
9062
9112
|
// still be pending while every attempt samples current work afresh.
|
|
9063
9113
|
const row =
|
package/lib/sync.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isSameTelegramProcessInstance,
|
|
19
19
|
isTelegramTopicTargetStaleError,
|
|
20
20
|
provisionOwnBusTopic,
|
|
21
|
+
resetTelegramConnectionProvisioning,
|
|
21
22
|
type TelegramOwnTopicProvisionResult,
|
|
22
23
|
type TelegramTopicTargetStore,
|
|
23
24
|
type TelegramWorkspaceDisplayBinding,
|
|
@@ -826,6 +827,15 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
826
827
|
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
827
828
|
};
|
|
828
829
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
830
|
+
if (deps.forceFreshUnnamed) {
|
|
831
|
+
await resetTelegramConnectionProvisioning({
|
|
832
|
+
store: deps.topicTargetStore, owner: "leader", instanceId: deps.instanceId,
|
|
833
|
+
profileKey: leaderProfileKey, isCurrent: () => {
|
|
834
|
+
assertLeaderEpoch("connect-recovery");
|
|
835
|
+
return true;
|
|
836
|
+
}, recordEvent: deps.recordEvent,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
829
839
|
const legacyLeaderRecord =
|
|
830
840
|
deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
831
841
|
let capacityUnavailable = false;
|
|
@@ -836,6 +846,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
836
846
|
legacyLeaderRecord?.instanceId,
|
|
837
847
|
{
|
|
838
848
|
sessionId: deps.sessionId,
|
|
849
|
+
provisionProfileKey: leaderProfileKey,
|
|
839
850
|
onCapacityUnavailable() {
|
|
840
851
|
capacityUnavailable = true;
|
|
841
852
|
},
|
package/lib/threads.ts
CHANGED
|
@@ -783,6 +783,7 @@ export interface TelegramTopicTargetStore {
|
|
|
783
783
|
) => { slot?: string; threadName?: string } | undefined;
|
|
784
784
|
listReservations: () => TelegramThreadReservation[];
|
|
785
785
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
786
|
+
resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
|
|
786
787
|
listPendingCleanups: () => TelegramThreadCleanupIntent[];
|
|
787
788
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
788
789
|
reserveThread: (reservation: TelegramThreadReservation) => void;
|
|
@@ -902,6 +903,7 @@ export interface TelegramTopicTargetStore {
|
|
|
902
903
|
options?: {
|
|
903
904
|
existingBindingOnly?: boolean;
|
|
904
905
|
sessionId?: string;
|
|
906
|
+
provisionProfileKey?: string;
|
|
905
907
|
onCapacityUnavailable?: () => void;
|
|
906
908
|
},
|
|
907
909
|
) => TelegramWorkspaceBindingIdentity | undefined;
|
|
@@ -4168,6 +4170,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4168
4170
|
}
|
|
4169
4171
|
),
|
|
4170
4172
|
isCurrentPublication?: () => boolean,
|
|
4173
|
+
resetProvisions?: readonly TelegramThreadPendingProvision[],
|
|
4171
4174
|
): Promise<boolean> => {
|
|
4172
4175
|
const submittedPath = getPath(),
|
|
4173
4176
|
submittedProfile = activeProfile();
|
|
@@ -4367,6 +4370,13 @@ export function createTelegramTopicTargetStore(
|
|
|
4367
4370
|
!!expectedRestore,
|
|
4368
4371
|
),
|
|
4369
4372
|
);
|
|
4373
|
+
if (resetProvisions) {
|
|
4374
|
+
const recoveries = readProvisionRecoveries(path, true);
|
|
4375
|
+
if (mutationRevision !== persistedRevision || resetProvisions.some(expected =>
|
|
4376
|
+
expected.target || Object.hasOwn(recoveries, expected.id) ||
|
|
4377
|
+
!isDeepStrictEqual(disk.pendingProvisions?.find(value => value.id === expected.id), expected)))
|
|
4378
|
+
throw new Error("Telegram connection recovery provisioning evidence changed.");
|
|
4379
|
+
}
|
|
4370
4380
|
if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
|
|
4371
4381
|
throw new Error(
|
|
4372
4382
|
"Workspace Restore evidence changed before publication.",
|
|
@@ -4429,7 +4439,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4429
4439
|
}
|
|
4430
4440
|
: {}),
|
|
4431
4441
|
reservations: reservations.map((reservation) => ({ ...reservation })),
|
|
4432
|
-
pendingProvisions: pendingProvisions.map((provision) => ({
|
|
4442
|
+
pendingProvisions: pendingProvisions.filter(provision => !resetProvisions?.some(expected => expected.id === provision.id)).map((provision) => ({
|
|
4433
4443
|
...provision,
|
|
4434
4444
|
...(provision.target ? { target: { ...provision.target } } : {}),
|
|
4435
4445
|
})),
|
|
@@ -4636,6 +4646,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4636
4646
|
loadedProfile = submittedProfile;
|
|
4637
4647
|
loaded = true;
|
|
4638
4648
|
observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
|
|
4649
|
+
if (resetProvisions) { pendingProvisions = file.pendingProvisions; mutationRevision += 1; }
|
|
4639
4650
|
if (transition) {
|
|
4640
4651
|
records = new Map(
|
|
4641
4652
|
file.threads.map((record) => {
|
|
@@ -4808,6 +4819,10 @@ export function createTelegramTopicTargetStore(
|
|
|
4808
4819
|
if (isCurrentPublication?.() === false) return false;
|
|
4809
4820
|
loadedPath = path;
|
|
4810
4821
|
loaded = true;
|
|
4822
|
+
if (resetProvisions) {
|
|
4823
|
+
if (mutationRevision !== persistedRevision) return false;
|
|
4824
|
+
pendingProvisions = file.pendingProvisions;
|
|
4825
|
+
}
|
|
4811
4826
|
if (mutationRevision === persistedRevision) dirty = false;
|
|
4812
4827
|
return true;
|
|
4813
4828
|
});
|
|
@@ -6814,6 +6829,11 @@ export function createTelegramTopicTargetStore(
|
|
|
6814
6829
|
current.status = "ambiguous";
|
|
6815
6830
|
return true;
|
|
6816
6831
|
},
|
|
6832
|
+
resetPendingProvisionForConnect(expected, isCurrent) {
|
|
6833
|
+
// Stage no in-memory deletion: refusal or a stale publication must keep its protective hold.
|
|
6834
|
+
if (expected.target || !isCurrent()) return Promise.resolve(false);
|
|
6835
|
+
return persistSnapshot(undefined, isCurrent, [structuredClone(expected)]);
|
|
6836
|
+
},
|
|
6817
6837
|
removePendingProvision(id) {
|
|
6818
6838
|
const before = pendingProvisions.length;
|
|
6819
6839
|
pendingProvisions = pendingProvisions.filter(
|
|
@@ -7608,8 +7628,14 @@ export function createTelegramTopicTargetStore(
|
|
|
7608
7628
|
) {
|
|
7609
7629
|
return undefined;
|
|
7610
7630
|
}
|
|
7611
|
-
const
|
|
7612
|
-
|
|
7631
|
+
const ownedProvisions = pendingProvisions.filter(value =>
|
|
7632
|
+
value.workspaceBindingKey === identity.bindingKey &&
|
|
7633
|
+
(value.instanceId === instanceId || value.instanceId === effectivePreviousInstanceId ||
|
|
7634
|
+
(options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
|
|
7635
|
+
if (ownedProvisions.length > 1) return undefined;
|
|
7636
|
+
const ownProvision = ownedProvisions[0];
|
|
7637
|
+
const retainedTarget = ownProvision?.target ?? binding?.target ?? legacyRecord?.target;
|
|
7638
|
+
const retainedSlot = ownProvision?.slot ?? binding?.slot ?? legacyRecord?.slot;
|
|
7613
7639
|
const otherBindings = Array.from(workspaceBindings.values())
|
|
7614
7640
|
.filter(
|
|
7615
7641
|
(other) => other.bindingKey !== identity.bindingKey && other.slot,
|
|
@@ -7641,7 +7667,7 @@ export function createTelegramTopicTargetStore(
|
|
|
7641
7667
|
)
|
|
7642
7668
|
.map((reservation) => reservation.slot),
|
|
7643
7669
|
...pendingProvisions
|
|
7644
|
-
.filter((provision) =>
|
|
7670
|
+
.filter((provision) => provision !== ownProvision &&
|
|
7645
7671
|
isPendingProvisionLiveOrTargeted(provision, nowMs),
|
|
7646
7672
|
)
|
|
7647
7673
|
.map((provision) => provision.slot),
|
|
@@ -9661,6 +9687,32 @@ export function createTelegramTopicTargetRenamer(
|
|
|
9661
9687
|
};
|
|
9662
9688
|
}
|
|
9663
9689
|
|
|
9690
|
+
/** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
|
|
9691
|
+
export async function resetTelegramConnectionProvisioning(input: {
|
|
9692
|
+
store: TelegramTopicTargetStore;
|
|
9693
|
+
owner: TelegramThreadPendingProvision["owner"];
|
|
9694
|
+
instanceId: string;
|
|
9695
|
+
profileKey: string;
|
|
9696
|
+
isCurrent: () => boolean;
|
|
9697
|
+
recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
9698
|
+
}): Promise<void> {
|
|
9699
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9700
|
+
const removed: TelegramThreadPendingProvision[] = [];
|
|
9701
|
+
for (const pending of input.store.listPendingProvisions()) {
|
|
9702
|
+
if (pending.owner !== input.owner || pending.target ||
|
|
9703
|
+
(pending.profileKey !== input.profileKey && pending.instanceId !== input.instanceId)) continue;
|
|
9704
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9705
|
+
if (!await input.store.resetPendingProvisionForConnect(pending, input.isCurrent))
|
|
9706
|
+
throw new Error("Telegram connection recovery lost publication authority.");
|
|
9707
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9708
|
+
removed.push(pending);
|
|
9709
|
+
}
|
|
9710
|
+
if (removed.length === 0) return;
|
|
9711
|
+
input.recordEvent("connection", "Explicit connect reset unfinished Thread creation; an unconfirmed Telegram Thread may remain.", {
|
|
9712
|
+
phase: "connect-provision-reset", count: removed.length, owner: input.owner,
|
|
9713
|
+
});
|
|
9714
|
+
}
|
|
9715
|
+
|
|
9664
9716
|
export function createTelegramTopicTargetProvisioner(
|
|
9665
9717
|
deps: TelegramTopicTargetProvisionerDeps,
|
|
9666
9718
|
): (
|
|
@@ -9923,7 +9975,7 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
9923
9975
|
slot,
|
|
9924
9976
|
);
|
|
9925
9977
|
}
|
|
9926
|
-
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
|
|
9978
|
+
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}:${randomUUID()}`;
|
|
9927
9979
|
const pendingOwner =
|
|
9928
9980
|
request.owner?.kind === "leader" ? "leader" : "manual-follower";
|
|
9929
9981
|
assertLeaderEpoch("before-pending-intent");
|