@llblab/pi-telegram 0.55.1 → 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 +5 -4
- package/BACKLOG.md +6 -1
- package/CHANGELOG.md +15 -0
- package/README.md +28 -17
- package/dist/lib/bindings.d.ts +8 -4
- package/dist/lib/bindings.js +41 -1
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +10 -5
- package/dist/lib/bus-leader.d.ts +2 -0
- package/dist/lib/bus-leader.js +34 -20
- package/dist/lib/bus.d.ts +8 -0
- package/dist/lib/bus.js +21 -7
- package/dist/lib/commands.d.ts +1 -1
- package/dist/lib/commands.js +33 -28
- package/dist/lib/extension.js +18 -3
- package/dist/lib/lifecycle.d.ts +2 -1
- package/dist/lib/lifecycle.js +28 -20
- package/dist/lib/locks.d.ts +6 -2
- package/dist/lib/locks.js +3 -3
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +102 -99
- package/dist/lib/pi.d.ts +3 -2
- package/dist/lib/pi.js +3 -0
- package/dist/lib/polling.d.ts +6 -2
- package/dist/lib/polling.js +5 -3
- package/dist/lib/queue.d.ts +57 -5
- package/dist/lib/queue.js +313 -25
- package/dist/lib/routing.d.ts +4 -0
- package/dist/lib/routing.js +62 -1
- package/dist/lib/status.d.ts +1 -2
- package/dist/lib/status.js +6 -7
- 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 +12 -7
- package/dist/lib/updates.js +44 -9
- package/dist/package.json +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +24 -13
- package/docs/callback-namespaces.md +4 -0
- package/docs/multi-instance-bus.md +6 -4
- package/docs/ui-style.md +14 -10
- package/lib/bindings.ts +52 -3
- package/lib/bus-follower.ts +13 -5
- package/lib/bus-leader.ts +37 -23
- package/lib/bus.ts +23 -7
- package/lib/commands.ts +52 -47
- package/lib/extension.ts +18 -2
- package/lib/lifecycle.ts +34 -28
- package/lib/locks.ts +5 -2
- package/lib/menu-queue.ts +116 -162
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +7 -2
- package/lib/queue.ts +328 -33
- package/lib/routing.ts +51 -1
- package/lib/status.ts +7 -7
- package/lib/sync.ts +11 -0
- package/lib/threads.ts +57 -5
- package/lib/updates.ts +44 -15
- package/package.json +1 -1
package/lib/bus-leader.ts
CHANGED
|
@@ -23,6 +23,8 @@ 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,
|
|
27
|
+
hasTelegramBusSteerQueueHandoffCapability,
|
|
26
28
|
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
27
29
|
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
28
30
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
@@ -1528,7 +1530,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1528
1530
|
deps: TelegramBusFollowerTargetProvisionerDeps,
|
|
1529
1531
|
): (
|
|
1530
1532
|
registration: TelegramBusInstanceRegistration,
|
|
1531
|
-
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
1533
|
+
options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
|
|
1532
1534
|
) => Promise<
|
|
1533
1535
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
1534
1536
|
> {
|
|
@@ -1547,6 +1549,16 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1547
1549
|
const chatId = deps.getAllowedUserId();
|
|
1548
1550
|
if (typeof chatId !== "number") return registration.target;
|
|
1549
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
|
+
}
|
|
1550
1562
|
let restoredPredecessor: Threads.TelegramTopicTargetRecord | undefined;
|
|
1551
1563
|
if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
|
|
1552
1564
|
!registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
|
|
@@ -1579,6 +1591,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1579
1591
|
{
|
|
1580
1592
|
existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
|
|
1581
1593
|
sessionId: registration.sessionId,
|
|
1594
|
+
provisionProfileKey: registration.profileKey ?? `manual:${registration.instanceId}`,
|
|
1582
1595
|
onCapacityUnavailable() {
|
|
1583
1596
|
capacityUnavailable = true;
|
|
1584
1597
|
},
|
|
@@ -1841,12 +1854,6 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1841
1854
|
};
|
|
1842
1855
|
}
|
|
1843
1856
|
const probeRequiredRecord = reconnectRecord?.status === "probe-required";
|
|
1844
|
-
const exactSessionHandoff =
|
|
1845
|
-
crossSessionReuse &&
|
|
1846
|
-
!!requestedTarget &&
|
|
1847
|
-
registration.previousInstanceId === reconnectRecord?.instanceId &&
|
|
1848
|
-
requestedTarget.chatId === reconnectRecord.target.chatId &&
|
|
1849
|
-
requestedTarget.threadId === reconnectRecord.target.threadId;
|
|
1850
1857
|
const requiresVisibilityProbe =
|
|
1851
1858
|
crossSessionReuse ||
|
|
1852
1859
|
probeRequiredRecord ||
|
|
@@ -1868,19 +1875,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1868
1875
|
try {
|
|
1869
1876
|
assertRestoreRegistration();
|
|
1870
1877
|
await deps.callApi(
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
chat_id: connectedAnnouncement.target.chatId,
|
|
1880
|
-
message_thread_id: connectedAnnouncement.target.threadId,
|
|
1881
|
-
text: connectedAnnouncement.text,
|
|
1882
|
-
parse_mode: connectedAnnouncement.parseMode,
|
|
1883
|
-
},
|
|
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
|
+
},
|
|
1884
1886
|
);
|
|
1885
1887
|
assertRestoreRegistration();
|
|
1886
1888
|
if (recoverableTarget || probeRequiredRecord) {
|
|
@@ -2943,7 +2945,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
|
|
|
2943
2945
|
) => Promise<unknown> | unknown;
|
|
2944
2946
|
provisionFollowerTarget?: (
|
|
2945
2947
|
registration: TelegramBusInstanceRegistration,
|
|
2946
|
-
options?: { existingWorkspaceBindingOnly?: boolean },
|
|
2948
|
+
options?: { existingWorkspaceBindingOnly?: boolean; recoverConnection?: boolean },
|
|
2947
2949
|
) =>
|
|
2948
2950
|
| Promise<
|
|
2949
2951
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
@@ -3273,6 +3275,10 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3273
3275
|
"Telegram queue handoff capability was not negotiated.",
|
|
3274
3276
|
);
|
|
3275
3277
|
}
|
|
3278
|
+
if (envelope.payload.queueLane === "steer" &&
|
|
3279
|
+
![deps.protocolIdentity, donor.protocol, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
|
|
3280
|
+
return rejectTelegramBusRequest(envelope.requestId, "Steer queue handoff capability was not negotiated.");
|
|
3281
|
+
}
|
|
3276
3282
|
if (
|
|
3277
3283
|
!recipient?.registrationGeneration ||
|
|
3278
3284
|
envelope.recipientRegistrationGeneration !==
|
|
@@ -3448,6 +3454,9 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3448
3454
|
));
|
|
3449
3455
|
const restoringWorkspace =
|
|
3450
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));
|
|
3451
3460
|
const supportsWorkspaceAutoConnect =
|
|
3452
3461
|
hasTelegramBusCapability(
|
|
3453
3462
|
deps.protocolIdentity,
|
|
@@ -3463,7 +3472,8 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3463
3472
|
!readerCompatible() ||
|
|
3464
3473
|
(envelope.registration.cwd !== undefined &&
|
|
3465
3474
|
envelope.registration.sessionId === undefined) ||
|
|
3466
|
-
(restoringWorkspace && !supportsWorkspaceAutoConnect)
|
|
3475
|
+
(restoringWorkspace && !supportsWorkspaceAutoConnect) ||
|
|
3476
|
+
(recoverConnection && !supportsConnectionRecovery)
|
|
3467
3477
|
) {
|
|
3468
3478
|
return {
|
|
3469
3479
|
kind: "bus.ack" as const,
|
|
@@ -3521,7 +3531,7 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3521
3531
|
);
|
|
3522
3532
|
const target = await deps.provisionFollowerTarget?.(
|
|
3523
3533
|
envelope.registration,
|
|
3524
|
-
{ existingWorkspaceBindingOnly: restoringWorkspace },
|
|
3534
|
+
{ existingWorkspaceBindingOnly: restoringWorkspace, ...(recoverConnection ? { recoverConnection: true } : {}) },
|
|
3525
3535
|
);
|
|
3526
3536
|
if (restoringWorkspace && !target) {
|
|
3527
3537
|
return {
|
|
@@ -4477,6 +4487,10 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
4477
4487
|
"Telegram queue handoff capability was not negotiated.",
|
|
4478
4488
|
);
|
|
4479
4489
|
}
|
|
4490
|
+
if (input.payload.queueLane === "steer" &&
|
|
4491
|
+
![deps.protocolIdentity, recipient?.protocol].every(hasTelegramBusSteerQueueHandoffCapability)) {
|
|
4492
|
+
return rejectTelegramBusRequest(input.requestId, "Steer queue handoff capability was not negotiated.");
|
|
4493
|
+
}
|
|
4480
4494
|
if (
|
|
4481
4495
|
!recipient?.registrationGeneration ||
|
|
4482
4496
|
input.recipientRegistrationGeneration !== recipient.registrationGeneration
|
package/lib/bus.ts
CHANGED
|
@@ -142,6 +142,9 @@ export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION =
|
|
|
142
142
|
"durable-follower-admission-v1" as const;
|
|
143
143
|
export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
|
|
144
144
|
"queue-handoff-v1" as const;
|
|
145
|
+
/** Unspent Steer lane codec; independent from baseline receipt handoff support. */
|
|
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;
|
|
145
148
|
export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
|
|
146
149
|
"input-custody-reference-v1" as const;
|
|
147
150
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
|
|
@@ -182,6 +185,14 @@ export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
|
|
|
182
185
|
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
183
186
|
] as const;
|
|
184
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
|
+
|
|
185
196
|
export interface TelegramBusSelectedCommandInput {
|
|
186
197
|
name: string;
|
|
187
198
|
target: TelegramTarget & { threadId: number };
|
|
@@ -461,6 +472,11 @@ export function hasTelegramBusCapability(
|
|
|
461
472
|
return identity?.capabilities.includes(capability) ?? false;
|
|
462
473
|
}
|
|
463
474
|
|
|
475
|
+
export function hasTelegramBusSteerQueueHandoffCapability(identity: TelegramBusProtocolIdentity | undefined): boolean {
|
|
476
|
+
return hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) &&
|
|
477
|
+
hasTelegramBusCapability(identity, TELEGRAM_BUS_CAPABILITY_QUEUE_STEER);
|
|
478
|
+
}
|
|
479
|
+
|
|
464
480
|
export function getTelegramInputCustodyPeerReadiness(
|
|
465
481
|
followers: readonly Pick<
|
|
466
482
|
TelegramBusFollowerView,
|
|
@@ -1002,6 +1018,7 @@ export type TelegramBusEnvelope = (
|
|
|
1002
1018
|
| {
|
|
1003
1019
|
kind: "follower.register";
|
|
1004
1020
|
requestId: string;
|
|
1021
|
+
recoverConnection?: true;
|
|
1005
1022
|
registration: TelegramBusInstanceRegistration;
|
|
1006
1023
|
}
|
|
1007
1024
|
| {
|
|
@@ -2532,12 +2549,7 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2532
2549
|
: []),
|
|
2533
2550
|
...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
2534
2551
|
...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
|
|
2535
|
-
...(held
|
|
2536
|
-
? [
|
|
2537
|
-
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
2538
|
-
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2539
|
-
]
|
|
2540
|
-
: []),
|
|
2552
|
+
...(held ? getTelegramBusHeldCommandCapabilities(held.name) : []),
|
|
2541
2553
|
];
|
|
2542
2554
|
const follower = deps.getFollower(expected.instanceId),
|
|
2543
2555
|
secret = deps.getAuthSecret();
|
|
@@ -3538,8 +3550,12 @@ function parseRegisterEnvelope(
|
|
|
3538
3550
|
requestId: string,
|
|
3539
3551
|
kind: "follower.register" | "follower.restoreWorkspace",
|
|
3540
3552
|
): TelegramBusEnvelope | undefined {
|
|
3553
|
+
if (value.recoverConnection !== undefined &&
|
|
3554
|
+
(kind !== "follower.register" || value.recoverConnection !== true)) return undefined;
|
|
3541
3555
|
const registration = parseRegistration(value.registration);
|
|
3542
|
-
return registration ? { kind, requestId, registration
|
|
3556
|
+
return registration ? { kind, requestId, registration,
|
|
3557
|
+
...(kind === "follower.register" && value.recoverConnection === true ? { recoverConnection: true as const } : {}),
|
|
3558
|
+
} : undefined;
|
|
3543
3559
|
}
|
|
3544
3560
|
|
|
3545
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
|
@@ -64,6 +64,9 @@ const telegramBusProtocolIdentity =
|
|
|
64
64
|
Bus.createTelegramCurrentBusProtocolIdentity([
|
|
65
65
|
Bus.TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
66
66
|
Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
|
|
67
|
+
Bus.TELEGRAM_BUS_CAPABILITY_QUEUE_STEER,
|
|
68
|
+
Bus.TELEGRAM_BUS_CAPABILITY_CONNECT_RECOVERY,
|
|
69
|
+
Bus.TELEGRAM_BUS_CAPABILITY_HELD_MENU_ALIASES,
|
|
67
70
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
68
71
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
69
72
|
Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
@@ -717,7 +720,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
717
720
|
},
|
|
718
721
|
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
719
722
|
getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
|
|
720
|
-
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
721
723
|
getRecentRuntimeEvents: runtimeEvents.getEvents,
|
|
722
724
|
getRuntimeLockState: lockRuntime.getStatusLabel,
|
|
723
725
|
...threadStatusProjectionRuntime,
|
|
@@ -1026,6 +1028,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1026
1028
|
requestNextDispatchAnnouncement,
|
|
1027
1029
|
cancelNextDispatchAnnouncement,
|
|
1028
1030
|
watchdog: queueDispatchWatchdogRuntime,
|
|
1031
|
+
onTurnEnd: onSteerTurnEnd,
|
|
1029
1032
|
} = Bindings.createTelegramQueueBindingRuntime({
|
|
1030
1033
|
store: telegramQueueStore,
|
|
1031
1034
|
queue,
|
|
@@ -1037,6 +1040,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1037
1040
|
promptDispatch: promptDispatchRuntime,
|
|
1038
1041
|
isIdle,
|
|
1039
1042
|
hasPendingMessages,
|
|
1043
|
+
isContextCurrent: telegramSessionContextStore.isCurrent,
|
|
1044
|
+
isContextAborted: Pi.isExtensionContextAbortRequested,
|
|
1040
1045
|
updateStatus,
|
|
1041
1046
|
sendTextReply,
|
|
1042
1047
|
sendUserMessage,
|
|
@@ -1123,6 +1128,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1123
1128
|
|
|
1124
1129
|
// --- Queue And Settings Menus ---
|
|
1125
1130
|
const queueMenuRuntime = MenuQueue.createTelegramQueueMenuRuntime({
|
|
1131
|
+
hasPendingDispatch: lifecycle.hasDispatchPending,
|
|
1126
1132
|
telegramQueueStore,
|
|
1127
1133
|
queueMutationRuntime,
|
|
1128
1134
|
sendInteractiveMessage,
|
|
@@ -1234,7 +1240,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1234
1240
|
follower.protocol,
|
|
1235
1241
|
[
|
|
1236
1242
|
...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
1237
|
-
...Bus.
|
|
1243
|
+
...Bus.getTelegramBusHeldCommandCapabilities(name),
|
|
1238
1244
|
],
|
|
1239
1245
|
) &&
|
|
1240
1246
|
typeof selectedMenuDelivery === "function" &&
|
|
@@ -1555,6 +1561,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1555
1561
|
threadStore,
|
|
1556
1562
|
runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
|
|
1557
1563
|
liveRebindCleanupSchedule: Routing.TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE,
|
|
1564
|
+
liveRebindCommandCompletionTimeoutMs: 8_000,
|
|
1558
1565
|
updateStatus,
|
|
1559
1566
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
1560
1567
|
dispatchNextQueuedTelegramTurn,
|
|
@@ -2331,6 +2338,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
2331
2338
|
});
|
|
2332
2339
|
queueHandoffReconciliationBinding.set(
|
|
2333
2340
|
Updates.createTelegramQueueHandoffReconciliationRuntimeAssembly({
|
|
2341
|
+
hasPendingDispatch: lifecycle.hasDispatchPending,
|
|
2342
|
+
canHandoffSteerWithRecipient(recipient) {
|
|
2343
|
+
const live = telegramBusFollowerRegistry.get(recipient.instanceId);
|
|
2344
|
+
return live?.registrationGeneration === recipient.registrationGeneration &&
|
|
2345
|
+
[telegramBusProtocolIdentity, live?.protocol,
|
|
2346
|
+
...(telegramBusFollowerRegistrationState.isRegistered() ? [telegramBusFollowerRegistrationState.getLeaderProtocol()] : [])]
|
|
2347
|
+
.every(Bus.hasTelegramBusSteerQueueHandoffCapability);
|
|
2348
|
+
},
|
|
2334
2349
|
ownsDirect: lockRuntime.owns,
|
|
2335
2350
|
isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
|
|
2336
2351
|
isBusEnabled: isTelegramBusRuntimeEnabled,
|
|
@@ -3046,6 +3061,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
3046
3061
|
sendMarkdownReply,
|
|
3047
3062
|
sendTextReply,
|
|
3048
3063
|
dispatchNextQueuedTelegramTurn,
|
|
3064
|
+
onSteerTurnEnd,
|
|
3049
3065
|
onPromptHandedOff(turn, ctx) {
|
|
3050
3066
|
updateAdmissionRuntimeBinding
|
|
3051
3067
|
.getSettlement()
|
package/lib/lifecycle.ts
CHANGED
|
@@ -26,6 +26,8 @@ import type {
|
|
|
26
26
|
ToolExecutionEndEvent,
|
|
27
27
|
ToolExecutionStartEvent,
|
|
28
28
|
ToolExecutionUpdateEvent,
|
|
29
|
+
TurnEndEvent,
|
|
30
|
+
TurnEndEventResult,
|
|
29
31
|
UIPromptEndEvent,
|
|
30
32
|
UIPromptStartEvent,
|
|
31
33
|
} from "./pi.ts";
|
|
@@ -100,6 +102,7 @@ export interface TelegramLifecycleRegistrationDeps {
|
|
|
100
102
|
event: { model: TelegramLifecycleModel },
|
|
101
103
|
ctx: ExtensionContext,
|
|
102
104
|
) => Promise<void> | void;
|
|
105
|
+
onTurnEnd?: (event: TurnEndEvent, ctx: ExtensionContext) => TurnEndEventResult | undefined;
|
|
103
106
|
onAgentStart: (
|
|
104
107
|
event: AgentStartEvent,
|
|
105
108
|
ctx: ExtensionContext,
|
|
@@ -575,7 +578,6 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
575
578
|
>(
|
|
576
579
|
deps: TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel>,
|
|
577
580
|
): TelegramSessionLifecycleHooks {
|
|
578
|
-
const isSessionActive = deps.contextStore.isCurrent;
|
|
579
581
|
const suspendForReplacement =
|
|
580
582
|
BusFollower.createTelegramBusFollowerSessionReplacementSuspender({
|
|
581
583
|
registrationState: deps.follower.registrationState,
|
|
@@ -583,32 +585,44 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
583
585
|
suspendPolling: deps.follower.suspendPolling,
|
|
584
586
|
recordRuntimeEvent: deps.follower.recordRuntimeEvent,
|
|
585
587
|
});
|
|
586
|
-
|
|
587
|
-
const queueLifecycle =
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
588
|
+
// A reused context can be current for a successor; inner awaits need this invocation's generation too.
|
|
589
|
+
const queueLifecycle = (ctx: ExtensionContext, generation: number, preserveTarget = true) =>
|
|
590
|
+
Queue.createTelegramSessionLifecycleRuntime({
|
|
591
|
+
...deps.queue,
|
|
592
|
+
isSessionActive: () => deps.contextStore.isCurrent(ctx, generation),
|
|
593
|
+
stopPolling: () => suspendForReplacement(preserveTarget),
|
|
594
|
+
clearPendingMediaGroups: deps.services.suspendGroupedInput,
|
|
595
|
+
});
|
|
593
596
|
const servicesLifecycle: TelegramSessionLifecycleHooks = {
|
|
594
597
|
async onSessionStart(event, ctx) {
|
|
598
|
+
const generation = deps.contextStore.getGeneration();
|
|
599
|
+
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
595
600
|
const resumeConnection = deps.services.connection?.prepare(event, ctx);
|
|
596
|
-
await queueLifecycle.onSessionStart(event, ctx);
|
|
597
|
-
if (!
|
|
601
|
+
await queueLifecycle(ctx, generation).onSessionStart(event, ctx);
|
|
602
|
+
if (!isCurrent()) return;
|
|
598
603
|
deps.services.resumeGroupedInput(ctx);
|
|
599
604
|
await deps.services.delivery.onSessionStart();
|
|
600
|
-
if (!
|
|
605
|
+
if (!isCurrent()) return;
|
|
601
606
|
if (resumeConnection) resumeConnection();
|
|
602
607
|
else await deps.services.polling.onSessionStart(event, ctx);
|
|
608
|
+
if (!isCurrent()) return;
|
|
603
609
|
deps.services.capabilityMonitor.start(ctx);
|
|
604
610
|
deps.services.queueWatchdog.start(ctx);
|
|
611
|
+
await BusFollower.createTelegramBusFollowerSessionRefreshHook({
|
|
612
|
+
registrationState: deps.follower.registrationState,
|
|
613
|
+
registrationRuntime: deps.follower.registrationRuntime,
|
|
614
|
+
getLeaderState: deps.follower.getLeaderState,
|
|
615
|
+
isSessionActive: isCurrent,
|
|
616
|
+
updateStatus: deps.follower.updateStatus,
|
|
617
|
+
recordRuntimeEvent: deps.follower.recordRuntimeEvent,
|
|
618
|
+
})(event, ctx);
|
|
605
619
|
},
|
|
606
620
|
async onSessionShutdown(event, ctx) {
|
|
607
621
|
const generation = deps.contextStore.getGeneration();
|
|
608
622
|
const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
|
|
609
623
|
if (!isCurrent()) return;
|
|
610
624
|
deps.services.connection?.onSessionShutdown(event, ctx);
|
|
611
|
-
preserveTarget = event.reason !== "resume";
|
|
625
|
+
const preserveTarget = event.reason !== "resume";
|
|
612
626
|
let preserveThread: (() => Promise<void>) | undefined;
|
|
613
627
|
if (event.reason === "quit") {
|
|
614
628
|
try {
|
|
@@ -625,7 +639,7 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
625
639
|
if (!isCurrent()) return;
|
|
626
640
|
deps.services.queueWatchdog.stop();
|
|
627
641
|
deps.services.capabilityMonitor.stop();
|
|
628
|
-
await queueLifecycle.onSessionShutdown(event, ctx);
|
|
642
|
+
await queueLifecycle(ctx, generation, preserveTarget).onSessionShutdown(event, ctx);
|
|
629
643
|
if (!isCurrent()) return;
|
|
630
644
|
await deps.services.inboundWorker.onSessionShutdown();
|
|
631
645
|
if (!isCurrent()) return;
|
|
@@ -638,23 +652,9 @@ export function createTelegramBridgeSessionLifecycleAssembly<
|
|
|
638
652
|
}
|
|
639
653
|
},
|
|
640
654
|
};
|
|
641
|
-
const followerLifecycle = appendTelegramLifecycleHooks(
|
|
642
|
-
servicesLifecycle,
|
|
643
|
-
{
|
|
644
|
-
onSessionStart: BusFollower.createTelegramBusFollowerSessionRefreshHook({
|
|
645
|
-
registrationState: deps.follower.registrationState,
|
|
646
|
-
registrationRuntime: deps.follower.registrationRuntime,
|
|
647
|
-
getLeaderState: deps.follower.getLeaderState,
|
|
648
|
-
isSessionActive,
|
|
649
|
-
updateStatus: deps.follower.updateStatus,
|
|
650
|
-
recordRuntimeEvent: deps.follower.recordRuntimeEvent,
|
|
651
|
-
}),
|
|
652
|
-
},
|
|
653
|
-
isSessionActive,
|
|
654
|
-
);
|
|
655
655
|
return createTelegramSessionGenerationFence(
|
|
656
656
|
deps.contextStore,
|
|
657
|
-
|
|
657
|
+
servicesLifecycle,
|
|
658
658
|
);
|
|
659
659
|
}
|
|
660
660
|
|
|
@@ -1042,6 +1042,12 @@ export function registerTelegramLifecycleHooks(
|
|
|
1042
1042
|
if (!isActive(ctx)) return;
|
|
1043
1043
|
await deps.onAgentStart(event, ctx);
|
|
1044
1044
|
});
|
|
1045
|
+
if (deps.onTurnEnd) {
|
|
1046
|
+
pi.on("turn_end", (event, ctx) => {
|
|
1047
|
+
if (!isActive(ctx)) return;
|
|
1048
|
+
return deps.onTurnEnd?.(event, ctx);
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1045
1051
|
pi.on("tool_execution_start", async (event, ctx) => {
|
|
1046
1052
|
if (!isActive(ctx)) return;
|
|
1047
1053
|
await deps.onToolExecutionStart(event, ctx);
|
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
|
}
|