@llblab/pi-telegram 0.56.0 → 0.56.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/BACKLOG.md +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +4 -2
- 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/menu-queue.js +5 -7
- 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 +6 -27
- package/dist/lib/updates.js +25 -12
- package/dist/package.json +1 -1
- package/docs/architecture.md +12 -2
- package/docs/multi-instance-bus.md +6 -4
- package/docs/ui-style.md +4 -8
- 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/menu-queue.ts +6 -8
- 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 +23 -12
- package/package.json +1 -1
package/docs/ui-style.md
CHANGED
|
@@ -56,7 +56,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
|
|
|
56
56
|
| `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
|
|
57
57
|
| `⛔️` | Cancel source routing | `⛔️ Cancel routing` chooser action and the `Routing cancelled` toast | Not an abort of active Pi work. Private retention remains mandatory; disposable-tab removal stays bound to exact eligible target authority and follows only once every input in the tab is resolved. |
|
|
58
58
|
| `❌` | No / cancel / terminal failure | Confirmation cancel buttons and terminal failure notices | Do not use for a recoverable operation failure that leaves session state intact. |
|
|
59
|
-
| `🗑` | Delete
|
|
59
|
+
| `🗑` | Delete | Destructive confirmations | Not a queue reaction shortcut; queue menus use reversible Keep/Skip selectors. |
|
|
60
60
|
|
|
61
61
|
### Informational Feedback
|
|
62
62
|
|
|
@@ -96,14 +96,10 @@ The menu may clear internal Skip but cannot remove a reaction created by the use
|
|
|
96
96
|
| --- | --- | --- | --- |
|
|
97
97
|
| `👍` | Promote to priority | Queue reaction shortcut | Normalized from variants like `👍️`. |
|
|
98
98
|
| `⚡` | Producer-owned Control marker | Control queue rows | Not a positive-reaction alias or command classifier; old stored Priority badges project `👍` without changing lane authority. |
|
|
99
|
-
|
|
|
100
|
-
| `🕊` / `🕊️` | Promote to priority | Queue reaction shortcut | Soft/peaceful promotion gesture. |
|
|
101
|
-
| `🔥` | Select Steer | Queue reaction shortcut and Steer badge | Dominates `👍`, heart and dove; not immediate issuance or model-use proof. |
|
|
99
|
+
| `🔥` | Select Steer | Queue reaction shortcut and Steer badge | Dominates `👍`; not immediate issuance or model-use proof. |
|
|
102
100
|
| `👎` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible until the marked turn reaches dispatch; not negative feedback to the agent. |
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
| `💩` | Defer removal of waiting turn | Queue reaction shortcut and queue emoji marker | Reversible reject metaphor. |
|
|
106
|
-
| `🗑` | Defer removal | Queue reaction shortcut and queue emoji marker | Like Skip, the reaction remains reversible until dispatch reaches the marked turn. |
|
|
101
|
+
|
|
102
|
+
Only `👍`, `👎`, and `🔥` are queue reaction shortcuts. Every other reaction is decorative, including the retired aliases. Retained queue state uses canonical badges without changing lane, disposition or receipt authority.
|
|
107
103
|
|
|
108
104
|
### Decorative Or Local-Example Emoji
|
|
109
105
|
|
package/lib/bus-follower.ts
CHANGED
|
@@ -42,7 +42,6 @@ import {
|
|
|
42
42
|
sendTelegramBusLocalEnvelope,
|
|
43
43
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
44
44
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
45
|
-
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
46
45
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
47
46
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
48
47
|
TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
|
|
@@ -52,6 +51,7 @@ import {
|
|
|
52
51
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
53
52
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
54
53
|
TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
|
|
54
|
+
getTelegramBusHeldCommandCapabilities,
|
|
55
55
|
TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
56
56
|
TelegramBusLocalAuthorityError,
|
|
57
57
|
type TelegramBusAgentMessage,
|
|
@@ -178,6 +178,7 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
178
178
|
target?: TelegramTarget;
|
|
179
179
|
previousInstanceId?: string;
|
|
180
180
|
restoreWorkspace?: boolean;
|
|
181
|
+
recoverConnection?: boolean;
|
|
181
182
|
},
|
|
182
183
|
) => Promise<boolean>;
|
|
183
184
|
setContext: (ctx: TContext) => void | Promise<void>;
|
|
@@ -2930,7 +2931,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2930
2931
|
deps.setActiveAuthSecret?.(undefined);
|
|
2931
2932
|
await deps.stopReceiving?.();
|
|
2932
2933
|
};
|
|
2933
|
-
const pendingHandoff = options
|
|
2934
|
+
const pendingHandoff = options && !options.recoverConnection
|
|
2934
2935
|
? undefined
|
|
2935
2936
|
: getTelegramFollowerSessionHandoff();
|
|
2936
2937
|
const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(
|
|
@@ -2946,8 +2947,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2946
2947
|
target?: TelegramTarget;
|
|
2947
2948
|
previousInstanceId?: string;
|
|
2948
2949
|
restoreWorkspace?: boolean;
|
|
2950
|
+
recoverConnection?: boolean;
|
|
2949
2951
|
}
|
|
2950
|
-
| undefined = options
|
|
2952
|
+
| undefined = options ? { ...pendingHandoffOptions, ...options } : pendingHandoffOptions;
|
|
2951
2953
|
const leaderSocketPath =
|
|
2952
2954
|
leader.busSocketPath ??
|
|
2953
2955
|
deps.getLeaderSocketPath?.() ??
|
|
@@ -2968,6 +2970,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2968
2970
|
: "follower.register",
|
|
2969
2971
|
requestId: registrationGeneration,
|
|
2970
2972
|
auth: activeAuthSecret,
|
|
2973
|
+
...(registrationOptions?.recoverConnection && !registrationOptions.restoreWorkspace
|
|
2974
|
+
? { recoverConnection: true as const } : {}),
|
|
2971
2975
|
registration: {
|
|
2972
2976
|
instanceId: deps.instanceId,
|
|
2973
2977
|
...(registrationOptions?.previousInstanceId
|
|
@@ -4546,8 +4550,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
4546
4550
|
? [
|
|
4547
4551
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
4548
4552
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
4549
|
-
|
|
4550
|
-
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
4553
|
+
...getTelegramBusHeldCommandCapabilities(commandInput.name),
|
|
4551
4554
|
]
|
|
4552
4555
|
: []),
|
|
4553
4556
|
];
|
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/menu-queue.ts
CHANGED
|
@@ -115,7 +115,7 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
115
115
|
if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
|
|
116
116
|
const rows = items.map((item) => {
|
|
117
117
|
const badge = getTelegramQueueMenuLaneBadge(item);
|
|
118
|
-
const prefix = item.reactionSuppressionEmoji ?
|
|
118
|
+
const prefix = item.reactionSuppressionEmoji ? "👎 " :
|
|
119
119
|
badge ? `${badge} ` : item.hasAttachments ? "📎 " : "";
|
|
120
120
|
const position = item.reactionSuppressionEmoji
|
|
121
121
|
? formatSkippedTelegramQueuePosition(item.queuePosition)
|
|
@@ -179,16 +179,15 @@ function escapeTelegramQueueMenuHtmlPreview(text: string): string {
|
|
|
179
179
|
return truncated ? escaped + suffix : escaped;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function getTelegramQueueMenuLaneBadge(item: Pick<TelegramQueueMenuItem, "queueLane"
|
|
182
|
+
function getTelegramQueueMenuLaneBadge(item: Pick<TelegramQueueMenuItem, "queueLane">): string {
|
|
183
183
|
if (item.queueLane === "control") return "⚡";
|
|
184
184
|
if (item.queueLane === "steer") return "🔥";
|
|
185
185
|
if (item.queueLane !== "priority") return "";
|
|
186
|
-
|
|
187
|
-
return emoji === "❤" || emoji === "🕊" ? emoji : "👍";
|
|
186
|
+
return "👍";
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
|
|
191
|
-
const marker = item.reactionSuppressionEmoji
|
|
190
|
+
const marker = item.reactionSuppressionEmoji ? "👎" : getTelegramQueueMenuLaneBadge(item);
|
|
192
191
|
const badge = marker ? ` ${marker}` : "";
|
|
193
192
|
const position = item.reactionSuppressionEmoji
|
|
194
193
|
? `<s>${item.queuePosition}</s>.`
|
|
@@ -920,10 +919,9 @@ function applyQueuedTelegramPromptMenuFlags<Context>(
|
|
|
920
919
|
const disposition: Extract<Queue.TelegramQueueReactionDisposition, { kind: "reaction-transition" }> = { kind: "reaction-transition" };
|
|
921
920
|
if (flags.lane !== undefined) {
|
|
922
921
|
disposition.promptLane = flags.lane;
|
|
923
|
-
disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ?
|
|
924
|
-
getTelegramQueueMenuLaneBadge({ queueLane: "priority", priorityEmoji: item.queueLane === "priority" ? item.priorityEmoji : undefined }) : null;
|
|
922
|
+
disposition.priorityEmoji = flags.lane === "steer" ? "🔥" : flags.lane === "priority" ? "👍" : null;
|
|
925
923
|
}
|
|
926
|
-
if (flags.skipped !== undefined) disposition.suppressionEmoji = flags.skipped ?
|
|
924
|
+
if (flags.skipped !== undefined) disposition.suppressionEmoji = flags.skipped ? "👎" : null;
|
|
927
925
|
return deps.queueMutationRuntime.applyReactionByMessageId(replyToMessageId, disposition, ctx) ||
|
|
928
926
|
((flags.lane === undefined || flags.lane === item.queueLane) &&
|
|
929
927
|
(flags.skipped === undefined || flags.skipped === (item.reactionSuppressionEmoji !== undefined)));
|
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
|
},
|