@llblab/pi-telegram 0.54.4 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +3 -1
- package/dist/lib/bindings.js +8 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +49 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +5 -3
- package/dist/lib/bus.js +13 -10
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +48 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +6 -1
- package/dist/lib/routing.js +334 -210
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +18 -5
- package/dist/lib/threads.js +107 -30
- package/dist/lib/turns.d.ts +2 -0
- package/dist/lib/turns.js +6 -1
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +12 -11
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +18 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +2 -2
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +9 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +17 -15
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +51 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +325 -238
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +120 -36
- package/lib/turns.ts +8 -1
- package/lib/updates.ts +58 -21
- package/package.json +1 -1
package/lib/bus-leader.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
27
27
|
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
28
28
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
29
|
+
TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND,
|
|
29
30
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
|
|
30
31
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
31
32
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
@@ -158,6 +159,8 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
|
|
|
158
159
|
|
|
159
160
|
export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
160
161
|
getAllowedUserId: () => number | undefined;
|
|
162
|
+
getLiveFollowers?: () => readonly TelegramBusFollowerView[];
|
|
163
|
+
isFollowerProcessAlive?: (pid: number) => boolean;
|
|
161
164
|
topicTargetStore: Threads.TelegramTopicTargetStore;
|
|
162
165
|
callApi: <TResponse>(
|
|
163
166
|
method: string,
|
|
@@ -569,12 +572,15 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
569
572
|
});
|
|
570
573
|
});
|
|
571
574
|
};
|
|
572
|
-
|
|
575
|
+
/** Returns the stabilization this startup owns; a stop or a newer startup meanwhile makes it inert. */
|
|
576
|
+
const beginStartupDisplayStabilization = (): number => {
|
|
573
577
|
cancelStartupDisplayStabilization();
|
|
574
578
|
startupDisplaySettled = false;
|
|
579
|
+
return startupDisplayGeneration;
|
|
575
580
|
};
|
|
576
|
-
const armStartupDisplayReconciliation = () => {
|
|
577
|
-
|
|
581
|
+
const armStartupDisplayReconciliation = (generation: number) => {
|
|
582
|
+
if (startupDisplayGeneration !== generation) return;
|
|
583
|
+
if (startupDisplayTimer) clearTimeout(startupDisplayTimer);
|
|
578
584
|
const delayMs =
|
|
579
585
|
deps.runtime.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
580
586
|
startupDisplayTimer = setTimeout(() => {
|
|
@@ -673,6 +679,8 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
673
679
|
});
|
|
674
680
|
const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
|
|
675
681
|
...provisionerPorts,
|
|
682
|
+
getLiveFollowers: deps.runtime.followerRegistry.list,
|
|
683
|
+
isFollowerProcessAlive: deps.runtime.isFollowerProcessAlive,
|
|
676
684
|
});
|
|
677
685
|
const renameLeaderThreadAdmitted = async (
|
|
678
686
|
threadName: string,
|
|
@@ -1388,14 +1396,18 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
1388
1396
|
resetThreadNameAdmitted,
|
|
1389
1397
|
captureWorkspaceExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
1390
1398
|
async startPolling(ctx: TContext) {
|
|
1391
|
-
|
|
1399
|
+
const stabilization = display
|
|
1400
|
+
? beginStartupDisplayStabilization()
|
|
1401
|
+
: undefined;
|
|
1392
1402
|
try {
|
|
1393
1403
|
await runtime.startPolling(ctx);
|
|
1394
1404
|
} catch (error) {
|
|
1395
|
-
|
|
1405
|
+
if (stabilization === startupDisplayGeneration)
|
|
1406
|
+
cancelStartupDisplayStabilization();
|
|
1396
1407
|
throw error;
|
|
1397
1408
|
}
|
|
1398
|
-
if (
|
|
1409
|
+
if (stabilization !== undefined)
|
|
1410
|
+
armStartupDisplayReconciliation(stabilization);
|
|
1399
1411
|
},
|
|
1400
1412
|
async stopPolling() {
|
|
1401
1413
|
cancelStartupDisplayStabilization();
|
|
@@ -1535,12 +1547,35 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1535
1547
|
const chatId = deps.getAllowedUserId();
|
|
1536
1548
|
if (typeof chatId !== "number") return registration.target;
|
|
1537
1549
|
await deps.topicTargetStore.load();
|
|
1550
|
+
let restoredPredecessor: Threads.TelegramTopicTargetRecord | undefined;
|
|
1551
|
+
if (options?.existingWorkspaceBindingOnly && registration.cwd && registration.sessionId &&
|
|
1552
|
+
!registration.previousInstanceId && deps.getLiveFollowers && deps.isFollowerProcessAlive) {
|
|
1553
|
+
const identity = Threads.createTelegramWorkspaceBindingIdentity(registration.cwd, 0, registration.sessionId);
|
|
1554
|
+
const records = deps.topicTargetStore.list(), liveFollowers = deps.getLiveFollowers();
|
|
1555
|
+
const candidates = deps.topicTargetStore.listWorkspaceBindings()
|
|
1556
|
+
.filter(binding => identity && binding.cwd === identity.cwd && binding.sessionId === identity.sessionId &&
|
|
1557
|
+
(!registration.target || (binding.target.chatId === registration.target.chatId && binding.target.threadId === registration.target.threadId)))
|
|
1558
|
+
.flatMap(binding => {
|
|
1559
|
+
const matches = records.filter(record => record.status === "active" && record.owner?.kind === "manual-follower" &&
|
|
1560
|
+
record.target.chatId === binding.target.chatId && record.target.threadId === binding.target.threadId);
|
|
1561
|
+
return matches.length === 1 ? matches : [];
|
|
1562
|
+
})
|
|
1563
|
+
.filter(record => {
|
|
1564
|
+
if (liveFollowers.some(peer => peer.instanceId === record.instanceId ||
|
|
1565
|
+
(peer.target?.chatId === record.target.chatId && peer.target.threadId === record.target.threadId))) return false;
|
|
1566
|
+
// A retained status is not current execution proof. Only an absent actual runtime PID grants this claim handoff.
|
|
1567
|
+
const match = /^([1-9][0-9]*):[0-9]+$/.exec(record.instanceId ?? ""), pid = match ? Number(match[1]) : undefined;
|
|
1568
|
+
if (pid === undefined || !Number.isSafeInteger(pid)) return false;
|
|
1569
|
+
try { return deps.isFollowerProcessAlive!(pid) === false; } catch { return false; }
|
|
1570
|
+
});
|
|
1571
|
+
if (candidates.length === 1) restoredPredecessor = candidates[0];
|
|
1572
|
+
}
|
|
1538
1573
|
let capacityUnavailable = false;
|
|
1539
1574
|
const workspaceIdentity = registration.cwd
|
|
1540
1575
|
? deps.topicTargetStore.claimWorkspaceIdentity(
|
|
1541
1576
|
registration.cwd,
|
|
1542
1577
|
registration.instanceId,
|
|
1543
|
-
registration.previousInstanceId,
|
|
1578
|
+
registration.previousInstanceId ?? restoredPredecessor?.instanceId,
|
|
1544
1579
|
{
|
|
1545
1580
|
existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
|
|
1546
1581
|
sessionId: registration.sessionId,
|
|
@@ -1603,6 +1638,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1603
1638
|
record.owner?.kind === "manual-follower" &&
|
|
1604
1639
|
((matchesCurrentIdentity && matchesRequestedTarget) ||
|
|
1605
1640
|
matchesSessionHandoff ||
|
|
1641
|
+
(record.instanceId === restoredPredecessor?.instanceId && matchesRequestedTarget) ||
|
|
1606
1642
|
matchesWorkspaceRecovery)
|
|
1607
1643
|
);
|
|
1608
1644
|
});
|
|
@@ -1815,16 +1851,14 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1815
1851
|
crossSessionReuse ||
|
|
1816
1852
|
probeRequiredRecord ||
|
|
1817
1853
|
recoverableTarget !== undefined;
|
|
1854
|
+
const retainedDisplayBinding = deps.topicTargetStore.getWorkspaceBindingByTarget(result.target, registration.sessionId);
|
|
1818
1855
|
let connectedAnnouncement =
|
|
1819
1856
|
!result.reused || requiresVisibilityProbe
|
|
1820
1857
|
? createTelegramBusInstanceLifecycleAnnouncement({
|
|
1821
1858
|
target: result.target,
|
|
1822
1859
|
threadName:
|
|
1823
1860
|
result.displayTitle ??
|
|
1824
|
-
(
|
|
1825
|
-
workspaceBinding.target.threadId === result.target.threadId
|
|
1826
|
-
? workspaceBinding.displayTitle
|
|
1827
|
-
: undefined) ??
|
|
1861
|
+
(retainedDisplayBinding?.cwd === workspaceIdentity?.cwd ? retainedDisplayBinding?.displayTitle : undefined) ??
|
|
1828
1862
|
result.record.threadName,
|
|
1829
1863
|
slot: result.record.slot,
|
|
1830
1864
|
state: "connected",
|
|
@@ -1972,6 +2006,16 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
1972
2006
|
},
|
|
1973
2007
|
},
|
|
1974
2008
|
);
|
|
2009
|
+
// Same-target reuse can retain a different slot than the initial claim.
|
|
2010
|
+
if (result.record.slot !== workspaceCommit.slot) {
|
|
2011
|
+
result = {
|
|
2012
|
+
...result,
|
|
2013
|
+
record: deps.topicTargetStore.upsert({
|
|
2014
|
+
...result.record,
|
|
2015
|
+
slot: workspaceCommit.slot,
|
|
2016
|
+
}),
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
1975
2019
|
if (connectedAnnouncement && workspaceCommit.displayTitle) {
|
|
1976
2020
|
connectedAnnouncement =
|
|
1977
2021
|
createTelegramBusInstanceLifecycleAnnouncement({
|
|
@@ -2604,8 +2648,7 @@ export function createTelegramBusSelectedMenuDeliveryHandler(deps: {
|
|
|
2604
2648
|
.find((value) => value.request.operationId === expected.operationId);
|
|
2605
2649
|
if (
|
|
2606
2650
|
!retained ||
|
|
2607
|
-
retained
|
|
2608
|
-
retained.cleanup ||
|
|
2651
|
+
!Threads.isTelegramLiveRebindInputAuthorized(retained) ||
|
|
2609
2652
|
retained.request.source.updateIds.length !== 1 ||
|
|
2610
2653
|
retained.recipient.kind !== "follower" ||
|
|
2611
2654
|
!isDeepStrictEqual(retained.executor, executor) ||
|
|
@@ -2695,8 +2738,13 @@ export function createTelegramBusSelectedMenuDeliveryHandler(deps: {
|
|
|
2695
2738
|
resolveJournal.call(workspace, live) !== recipient.journalBindingKey
|
|
2696
2739
|
)
|
|
2697
2740
|
throw new Error("Selected-menu authority changed.");
|
|
2741
|
+
const latest = store.listLiveRebindings().find(value => value.request.operationId === intent.request.operationId);
|
|
2742
|
+
if (!latest || !Threads.isTelegramLiveRebindInputAuthorized(latest) ||
|
|
2743
|
+
!isDeepStrictEqual({ ...latest, ...(intent.inputRelease ? { inputRelease: intent.inputRelease } : {}), revision: intent.revision, updatedAtMs: intent.updatedAtMs }, intent) ||
|
|
2744
|
+
(latest.inputRelease !== intent.inputRelease && !(intent.inputRelease === "issued" && latest.inputRelease === "accepted")))
|
|
2745
|
+
throw new Error("Selected-menu input grant changed.");
|
|
2698
2746
|
let confirmed = false;
|
|
2699
|
-
snapshot.call(threads,
|
|
2747
|
+
snapshot.call(threads, latest, (view) => {
|
|
2700
2748
|
const bindings = (view.workspaceBindings ?? []).filter(
|
|
2701
2749
|
(value) => value.bindingKey === intent.request.binding.bindingKey,
|
|
2702
2750
|
);
|
|
@@ -2936,6 +2984,7 @@ export interface TelegramBusLeaderEnvelopeHandlerDeps {
|
|
|
2936
2984
|
isCurrent: () => boolean,
|
|
2937
2985
|
) => Promise<void>;
|
|
2938
2986
|
getThreadDisplayMode?: () => TelegramThreadDisplayMode;
|
|
2987
|
+
requiresPreDispatchRebindReader?: () => boolean;
|
|
2939
2988
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
2940
2989
|
getTelegramProfile?: () => string | undefined;
|
|
2941
2990
|
getAllowedUserId?: () => number | undefined;
|
|
@@ -3385,6 +3434,8 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3385
3434
|
local: deps.protocolIdentity,
|
|
3386
3435
|
remote: envelope.registration.protocol,
|
|
3387
3436
|
});
|
|
3437
|
+
const readerCompatible = () => deps.requiresPreDispatchRebindReader?.() !== true ||
|
|
3438
|
+
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND);
|
|
3388
3439
|
const displayCompatible = () =>
|
|
3389
3440
|
(deps.getThreadDisplayMode?.() ?? "names") === "names" ||
|
|
3390
3441
|
(hasTelegramBusCapability(
|
|
@@ -3409,6 +3460,7 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3409
3460
|
if (
|
|
3410
3461
|
!compatibility.compatible ||
|
|
3411
3462
|
!displayCompatible() ||
|
|
3463
|
+
!readerCompatible() ||
|
|
3412
3464
|
(envelope.registration.cwd !== undefined &&
|
|
3413
3465
|
envelope.registration.sessionId === undefined) ||
|
|
3414
3466
|
(restoringWorkspace && !supportsWorkspaceAutoConnect)
|
|
@@ -3445,6 +3497,7 @@ export function createTelegramBusLeaderEnvelopeHandler(
|
|
|
3445
3497
|
const telegramProfile = deps.getTelegramProfile?.();
|
|
3446
3498
|
const operatorUserId = deps.getAllowedUserId?.();
|
|
3447
3499
|
const registrationCurrent = (): boolean =>
|
|
3500
|
+
readerCompatible() &&
|
|
3448
3501
|
(!deps.getCurrentLeaderEpoch ||
|
|
3449
3502
|
deps.getCurrentLeaderEpoch() === leaderEpoch) &&
|
|
3450
3503
|
(!deps.getTelegramProfile ||
|
|
@@ -4395,6 +4448,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
4395
4448
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
4396
4449
|
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
|
4397
4450
|
getThreadDisplayMode: deps.getThreadDisplayMode,
|
|
4451
|
+
requiresPreDispatchRebindReader: deps.requiresPreDispatchRebindReader,
|
|
4398
4452
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
4399
4453
|
getTelegramProfile: deps.getTelegramProfile,
|
|
4400
4454
|
getAllowedUserId: deps.getAllowedUserId,
|
|
@@ -4489,23 +4543,43 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
4489
4543
|
},
|
|
4490
4544
|
handleEnvelope,
|
|
4491
4545
|
});
|
|
4546
|
+
// The start that owns the endpoint and pruning. A stop clears it; a start that loses it mid-await undoes only what
|
|
4547
|
+
// nobody newer owns, so a stop during startup never leaves a stopped leader's endpoint listening.
|
|
4548
|
+
let activeStart: object | undefined;
|
|
4549
|
+
const releaseSupersededStart = async (): Promise<void> => {
|
|
4550
|
+
if (activeStart !== undefined) return;
|
|
4551
|
+
stopPruning();
|
|
4552
|
+
await localServer
|
|
4553
|
+
.stop()
|
|
4554
|
+
.catch((error) =>
|
|
4555
|
+
deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }),
|
|
4556
|
+
);
|
|
4557
|
+
};
|
|
4492
4558
|
return {
|
|
4493
4559
|
routeQueueHandoff: (envelope) => routeQueueHandoffEnvelope(envelope),
|
|
4494
4560
|
startPolling: async (ctx) => {
|
|
4561
|
+
const start = {};
|
|
4562
|
+
activeStart = start;
|
|
4495
4563
|
// Replay durable cleanup before publishing the follower endpoint so a
|
|
4496
4564
|
// replacement registration cannot reclaim a target while it is deleted.
|
|
4497
4565
|
await deps.provisionLeaderTarget?.(ctx);
|
|
4566
|
+
if (activeStart !== start) return;
|
|
4498
4567
|
await localServer.start();
|
|
4568
|
+
if (activeStart !== start) return releaseSupersededStart();
|
|
4499
4569
|
startPruning();
|
|
4500
4570
|
try {
|
|
4501
4571
|
await deps.startPolling(ctx);
|
|
4502
4572
|
} catch (error) {
|
|
4573
|
+
if (activeStart !== start) return releaseSupersededStart();
|
|
4574
|
+
activeStart = undefined;
|
|
4503
4575
|
stopPruning();
|
|
4504
4576
|
await localServer.stop();
|
|
4505
4577
|
throw error;
|
|
4506
4578
|
}
|
|
4579
|
+
if (activeStart !== start) return releaseSupersededStart();
|
|
4507
4580
|
},
|
|
4508
4581
|
stopPolling: async () => {
|
|
4582
|
+
activeStart = undefined;
|
|
4509
4583
|
stopPruning();
|
|
4510
4584
|
try {
|
|
4511
4585
|
await deps.stopPolling();
|
package/lib/bus.ts
CHANGED
|
@@ -156,6 +156,9 @@ export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT =
|
|
|
156
156
|
"session-replacement-intent-v1" as const;
|
|
157
157
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE =
|
|
158
158
|
"workspace-restore-v1" as const;
|
|
159
|
+
/** Shared Workspace readers understand independent pre-dispatch input/cleanup state. */
|
|
160
|
+
export const TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND = "live-rebind-pre-dispatch-reader-v2" as const;
|
|
161
|
+
export const TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION = "live-rebind-pre-dispatch-v1" as const;
|
|
159
162
|
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE =
|
|
160
163
|
"live-thread-rebind-save-v1" as const;
|
|
161
164
|
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY =
|
|
@@ -665,12 +668,6 @@ function hasTelegramBusDeliveryMarker(
|
|
|
665
668
|
);
|
|
666
669
|
}
|
|
667
670
|
|
|
668
|
-
export function markTelegramBusAggregateDelivery<
|
|
669
|
-
T extends Record<string, unknown>,
|
|
670
|
-
>(body: T): T {
|
|
671
|
-
return { ...body, [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true };
|
|
672
|
-
}
|
|
673
|
-
|
|
674
671
|
export function markTelegramBusCrossTargetDelivery<
|
|
675
672
|
T extends Record<string, unknown>,
|
|
676
673
|
>(body: T): T {
|
|
@@ -787,7 +784,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
787
784
|
if (apiMethod === "setMyCommands")
|
|
788
785
|
return isBotCommandRegistration(input.args[1]);
|
|
789
786
|
if (apiMethod === "sendChatAction")
|
|
790
|
-
return
|
|
787
|
+
return isTargetScoped(input.args[1]);
|
|
791
788
|
if (
|
|
792
789
|
apiMethod === "sendMessage" &&
|
|
793
790
|
hasTelegramBusDeliveryMarker(
|
|
@@ -795,8 +792,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
795
792
|
TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD,
|
|
796
793
|
)
|
|
797
794
|
) {
|
|
798
|
-
|
|
799
|
-
return body.message_thread_id === undefined && isTargetChatScoped(body);
|
|
795
|
+
return false;
|
|
800
796
|
}
|
|
801
797
|
if (
|
|
802
798
|
(apiMethod === "sendMessage" || apiMethod === "sendRichMessage") &&
|
|
@@ -1156,7 +1152,7 @@ export type TelegramBusEnvelope = (
|
|
|
1156
1152
|
recipientBindingKey: string;
|
|
1157
1153
|
operationId: string;
|
|
1158
1154
|
sourceUpdateIds: number[];
|
|
1159
|
-
mode: "release" | "discard" | "observe" | "observe-command";
|
|
1155
|
+
mode: "release" | "discard" | "observe" | "observe-held" | "observe-command";
|
|
1160
1156
|
oldTarget?: TelegramTarget & { threadId: number };
|
|
1161
1157
|
preparedSource?: TelegramBusPreparedCommandSource;
|
|
1162
1158
|
selectedCommand?: TelegramBusSelectedCommandInput;
|
|
@@ -1492,7 +1488,8 @@ export function parseTelegramBusEnvelope(
|
|
|
1492
1488
|
]) ||
|
|
1493
1489
|
(Object.hasOwn(value, "preparedSource") && !selectedCommand) ||
|
|
1494
1490
|
(value.mode === "observe-command" &&
|
|
1495
|
-
(!selectedCommand || Object.hasOwn(value, "oldTarget")))
|
|
1491
|
+
(!selectedCommand || Object.hasOwn(value, "oldTarget"))) ||
|
|
1492
|
+
(value.mode === "observe-held" && (parseThreadTarget(value.oldTarget)?.threadId ?? 0) <= 0)
|
|
1496
1493
|
)
|
|
1497
1494
|
break;
|
|
1498
1495
|
if (
|
|
@@ -1513,7 +1510,7 @@ export function parseTelegramBusEnvelope(
|
|
|
1513
1510
|
: value.mode === "release" ||
|
|
1514
1511
|
value.mode === "discard" ||
|
|
1515
1512
|
value.mode === "observe-command" ||
|
|
1516
|
-
(value.mode === "observe" &&
|
|
1513
|
+
((value.mode === "observe" || value.mode === "observe-held") &&
|
|
1517
1514
|
!!parseThreadTarget(value.oldTarget))) &&
|
|
1518
1515
|
isUniqueSourceUpdateIds(value.sourceUpdateIds) &&
|
|
1519
1516
|
typeof value.sentAtMs === "number" &&
|
|
@@ -1530,9 +1527,10 @@ export function parseTelegramBusEnvelope(
|
|
|
1530
1527
|
| "release"
|
|
1531
1528
|
| "discard"
|
|
1532
1529
|
| "observe"
|
|
1530
|
+
| "observe-held"
|
|
1533
1531
|
| "observe-command",
|
|
1534
1532
|
sentAtMs: value.sentAtMs,
|
|
1535
|
-
...(value.mode === "observe"
|
|
1533
|
+
...((value.mode === "observe" || value.mode === "observe-held")
|
|
1536
1534
|
? { oldTarget: parseThreadTarget(value.oldTarget)! }
|
|
1537
1535
|
: {}),
|
|
1538
1536
|
...(selectedCommand ? { selectedCommand, preparedSource } : {}),
|
|
@@ -2417,6 +2415,7 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2417
2415
|
| "release"
|
|
2418
2416
|
| "discard"
|
|
2419
2417
|
| "observe"
|
|
2418
|
+
| "observe-held"
|
|
2420
2419
|
| "observe-command";
|
|
2421
2420
|
sourceUpdateIds: number[];
|
|
2422
2421
|
slot: string;
|
|
@@ -2460,6 +2459,7 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2460
2459
|
"release",
|
|
2461
2460
|
"discard",
|
|
2462
2461
|
"observe",
|
|
2462
|
+
"observe-held",
|
|
2463
2463
|
"observe-command",
|
|
2464
2464
|
].includes(input.mode) ||
|
|
2465
2465
|
(input.mode === "observe-command" && !selectedCommand)))
|
|
@@ -2505,7 +2505,8 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2505
2505
|
}
|
|
2506
2506
|
: { updates: input.updates }),
|
|
2507
2507
|
});
|
|
2508
|
-
const observing = "mode" in expected && expected.mode === "observe";
|
|
2508
|
+
const observing = "mode" in expected && (expected.mode === "observe" || expected.mode === "observe-held");
|
|
2509
|
+
const observingHeld = "mode" in expected && expected.mode === "observe-held";
|
|
2509
2510
|
const commandObserving =
|
|
2510
2511
|
"mode" in expected && expected.mode === "observe-command";
|
|
2511
2512
|
const settling =
|
|
@@ -2520,6 +2521,7 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2520
2521
|
? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY]
|
|
2521
2522
|
: []),
|
|
2522
2523
|
...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
2524
|
+
...(observingHeld ? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
|
|
2523
2525
|
...(held
|
|
2524
2526
|
? [
|
|
2525
2527
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
@@ -2635,7 +2637,7 @@ export function createTelegramBusLiveRebindController(deps: {
|
|
|
2635
2637
|
? {
|
|
2636
2638
|
kind: "leader.settleLiveRebind" as const,
|
|
2637
2639
|
mode: expected.mode as
|
|
2638
|
-
"release" | "discard" | "observe" | "observe-command",
|
|
2640
|
+
"release" | "discard" | "observe" | "observe-held" | "observe-command",
|
|
2639
2641
|
sourceUpdateIds: expected.sourceUpdateIds!,
|
|
2640
2642
|
...(observing ? { oldTarget: expected.oldTarget } : {}),
|
|
2641
2643
|
}
|
package/lib/delivery.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Owns the public extension delivery contract, authorized scope resolution, operational rendering adapter, per-target serialization, chunk reconciliation, generation-fenced logical handles, and process-local runtime membrane; composes the established reply renderer with bus-aware Telegram API ports and excludes bot clients, Pi contexts, and consumer-extension policy
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { markTelegramBusAggregateDelivery } from "./bus.ts";
|
|
8
7
|
import {
|
|
9
8
|
assertTelegramInlineKeyboardCallbackData,
|
|
10
9
|
type TelegramInlineKeyboardMarkup,
|
|
@@ -134,6 +133,7 @@ export interface TelegramDeliveryTransportOptions {
|
|
|
134
133
|
/** @internal */
|
|
135
134
|
export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResolverDeps {
|
|
136
135
|
generation: string;
|
|
136
|
+
captureDrain?: () => (() => void) | undefined;
|
|
137
137
|
renderView: (
|
|
138
138
|
view: TelegramDeliveryView,
|
|
139
139
|
) => readonly TelegramDeliveryRenderedChunk[];
|
|
@@ -166,6 +166,7 @@ export interface TelegramDeliveryRuntimeDeps extends TelegramDeliveryTargetResol
|
|
|
166
166
|
/** @internal */
|
|
167
167
|
export interface TelegramBridgeDeliveryRuntimeDeps {
|
|
168
168
|
generation: string;
|
|
169
|
+
captureDrain?: () => (() => void) | undefined;
|
|
169
170
|
getTargetPolicyView: () => TelegramDeliveryTargetPolicyView;
|
|
170
171
|
getActiveTurnTarget: () => TelegramDeliveryTarget | undefined;
|
|
171
172
|
isTransportActive?: () => boolean;
|
|
@@ -295,6 +296,7 @@ function classifyTelegramDeliveryTransportError(error: unknown): {
|
|
|
295
296
|
export interface TelegramDeliveryTargetPolicyView {
|
|
296
297
|
canDeliver: boolean;
|
|
297
298
|
ownsDirect: boolean;
|
|
299
|
+
threaded?: boolean;
|
|
298
300
|
allowedChatId?: number;
|
|
299
301
|
followerTarget?: TelegramDeliveryTarget;
|
|
300
302
|
leaderTarget?: TelegramDeliveryTarget;
|
|
@@ -310,6 +312,7 @@ export interface TelegramDeliveryTargetPolicyRuntime {
|
|
|
310
312
|
/** @internal */
|
|
311
313
|
export function createTelegramDeliveryTargetPolicyRuntime(deps: {
|
|
312
314
|
ownsDirect(): boolean;
|
|
315
|
+
isThreadedMode?(): boolean;
|
|
313
316
|
isFollowerRegistered(): boolean;
|
|
314
317
|
getAllowedChatId(): number | undefined;
|
|
315
318
|
getFollowerTarget(): TelegramDeliveryTarget | undefined;
|
|
@@ -324,6 +327,7 @@ export function createTelegramDeliveryTargetPolicyRuntime(deps: {
|
|
|
324
327
|
return {
|
|
325
328
|
canDeliver: ownsDirect || deps.isFollowerRegistered(),
|
|
326
329
|
ownsDirect,
|
|
330
|
+
threaded: deps.isThreadedMode?.() === true,
|
|
327
331
|
allowedChatId: deps.getAllowedChatId(),
|
|
328
332
|
followerTarget: deps.getFollowerTarget(),
|
|
329
333
|
leaderTarget: deps.getLeaderTarget(),
|
|
@@ -338,25 +342,25 @@ export function createTelegramDeliveryTargetPolicyRuntime(deps: {
|
|
|
338
342
|
};
|
|
339
343
|
}
|
|
340
344
|
|
|
345
|
+
function isThreadedDelivery(view: TelegramDeliveryTargetPolicyView): boolean {
|
|
346
|
+
return view.threaded === true || view.followerTarget?.threadId !== undefined || view.leaderTarget?.threadId !== undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
341
349
|
/** @internal */
|
|
342
350
|
export function resolveTelegramDeliveryInstanceTarget(
|
|
343
351
|
view: TelegramDeliveryTargetPolicyView,
|
|
344
352
|
): TelegramDeliveryTarget | undefined {
|
|
345
353
|
if (!view.canDeliver) return undefined;
|
|
346
|
-
|
|
347
|
-
view.
|
|
348
|
-
|
|
349
|
-
(view.allowedChatId === undefined
|
|
350
|
-
? undefined
|
|
351
|
-
: { chatId: view.allowedChatId })
|
|
352
|
-
);
|
|
354
|
+
const target = view.followerTarget ?? view.leaderTarget ??
|
|
355
|
+
(view.allowedChatId === undefined ? undefined : { chatId: view.allowedChatId });
|
|
356
|
+
return target && (!isThreadedDelivery(view) || target.threadId !== undefined) ? target : undefined;
|
|
353
357
|
}
|
|
354
358
|
|
|
355
359
|
/** @internal */
|
|
356
360
|
export function resolveTelegramDeliveryAggregateTarget(
|
|
357
361
|
view: TelegramDeliveryTargetPolicyView,
|
|
358
362
|
): TelegramDeliveryTarget | undefined {
|
|
359
|
-
return !view.canDeliver || view.allowedChatId === undefined
|
|
363
|
+
return !view.canDeliver || view.allowedChatId === undefined || isThreadedDelivery(view)
|
|
360
364
|
? undefined
|
|
361
365
|
: { chatId: view.allowedChatId };
|
|
362
366
|
}
|
|
@@ -373,7 +377,7 @@ export function isTelegramDeliveryExplicitTargetAuthorized(
|
|
|
373
377
|
) {
|
|
374
378
|
return false;
|
|
375
379
|
}
|
|
376
|
-
if (candidate.threadId === undefined) return
|
|
380
|
+
if (candidate.threadId === undefined) return !isThreadedDelivery(view);
|
|
377
381
|
if (view.followerTarget) {
|
|
378
382
|
return areDeliveryTargetsEqual(candidate, view.followerTarget);
|
|
379
383
|
}
|
|
@@ -427,13 +431,27 @@ function resolveTelegramDeliveryTarget(
|
|
|
427
431
|
return { ok: true, value: cloneTarget(target) };
|
|
428
432
|
}
|
|
429
433
|
|
|
430
|
-
|
|
434
|
+
/** Observation cannot turn failed delivery into success or borrow a newer queue lifetime. */
|
|
435
|
+
function captureDeliveryDrainObserver(capture?: () => (() => void) | undefined): () => void {
|
|
436
|
+
try {
|
|
437
|
+
const notify = capture?.();
|
|
438
|
+
return () => {
|
|
439
|
+
try {
|
|
440
|
+
const emitted: unknown = notify?.();
|
|
441
|
+
if (emitted) void Promise.resolve(emitted).catch(() => undefined);
|
|
442
|
+
} catch { /* Observer failure is inert. */ }
|
|
443
|
+
};
|
|
444
|
+
} catch { return () => {}; }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function createTelegramDeliveryTargetQueue(captureDrain?: () => (() => void) | undefined) {
|
|
431
448
|
const queues = new Map<string, Promise<void>>();
|
|
432
449
|
async function run<T>(
|
|
433
450
|
target: TelegramDeliveryTarget,
|
|
434
451
|
operation: () => Promise<T>,
|
|
435
452
|
): Promise<T> {
|
|
436
453
|
const key = targetKey(target);
|
|
454
|
+
const notifyDrained = captureDeliveryDrainObserver(captureDrain);
|
|
437
455
|
const previous = queues.get(key) ?? Promise.resolve();
|
|
438
456
|
const current = previous.then(operation, operation);
|
|
439
457
|
const settled = current.then(
|
|
@@ -444,7 +462,7 @@ function createTelegramDeliveryTargetQueue() {
|
|
|
444
462
|
try {
|
|
445
463
|
return await current;
|
|
446
464
|
} finally {
|
|
447
|
-
if (queues.get(key) === settled) queues.delete(key);
|
|
465
|
+
if (queues.get(key) === settled) { queues.delete(key); notifyDrained(); }
|
|
448
466
|
}
|
|
449
467
|
}
|
|
450
468
|
return {
|
|
@@ -482,7 +500,10 @@ export function createTelegramDeliveryRuntime(
|
|
|
482
500
|
TelegramDeliveryHandle,
|
|
483
501
|
{ target: TelegramDeliveryTarget; messageIds: readonly number[] }
|
|
484
502
|
>();
|
|
485
|
-
const targetQueue = createTelegramDeliveryTargetQueue()
|
|
503
|
+
const targetQueue = createTelegramDeliveryTargetQueue(() => {
|
|
504
|
+
const notify = deps.captureDrain?.();
|
|
505
|
+
return () => active ? notify?.() : undefined;
|
|
506
|
+
});
|
|
486
507
|
const runForTarget = targetQueue.run;
|
|
487
508
|
const render = (
|
|
488
509
|
view: TelegramDeliveryView,
|
|
@@ -735,9 +756,17 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
735
756
|
throw new TelegramDeliveryTransportGenerationError();
|
|
736
757
|
}
|
|
737
758
|
};
|
|
759
|
+
const assertOutputTarget = (target: TelegramDeliveryTarget): void => {
|
|
760
|
+
if (isThreadedDelivery(getPolicyView()) && target.threadId === undefined)
|
|
761
|
+
throw new Error("Telegram All is input-only in Threaded Mode.");
|
|
762
|
+
};
|
|
738
763
|
return createTelegramDeliveryRuntime({
|
|
739
764
|
generation: deps.generation,
|
|
740
|
-
|
|
765
|
+
captureDrain: deps.captureDrain,
|
|
766
|
+
getActiveTurnTarget() {
|
|
767
|
+
const target = deps.getActiveTurnTarget();
|
|
768
|
+
return target && (!isThreadedDelivery(getPolicyView()) || target.threadId !== undefined) ? target : undefined;
|
|
769
|
+
},
|
|
741
770
|
getInstanceTarget() {
|
|
742
771
|
return resolveTelegramDeliveryInstanceTarget(getPolicyView());
|
|
743
772
|
},
|
|
@@ -763,6 +792,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
763
792
|
},
|
|
764
793
|
async sendChunk(target, chunk, options) {
|
|
765
794
|
assertTransportActive();
|
|
795
|
+
assertOutputTarget(target);
|
|
766
796
|
const sent = await withTelegramReplyParameters(
|
|
767
797
|
target.chatId,
|
|
768
798
|
options.replyToMessageId,
|
|
@@ -780,11 +810,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
780
810
|
? { reply_markup: options.replyMarkup }
|
|
781
811
|
: {}),
|
|
782
812
|
};
|
|
783
|
-
return deps.api.sendMessage(
|
|
784
|
-
target.threadId === undefined
|
|
785
|
-
? markTelegramBusAggregateDelivery(body)
|
|
786
|
-
: body,
|
|
787
|
-
);
|
|
813
|
+
return deps.api.sendMessage(body);
|
|
788
814
|
},
|
|
789
815
|
);
|
|
790
816
|
assertTransportActive();
|
|
@@ -797,6 +823,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
797
823
|
},
|
|
798
824
|
async editChunk(target, messageId, chunk, options) {
|
|
799
825
|
assertTransportActive();
|
|
826
|
+
assertOutputTarget(target);
|
|
800
827
|
await deps.api.editMessageText({
|
|
801
828
|
chat_id: target.chatId,
|
|
802
829
|
message_id: messageId,
|
|
@@ -814,6 +841,7 @@ export function createTelegramBridgeDeliveryRuntime(
|
|
|
814
841
|
},
|
|
815
842
|
async sendChatAction(target, action) {
|
|
816
843
|
assertTransportActive();
|
|
844
|
+
assertOutputTarget(target);
|
|
817
845
|
await deps.api.sendChatAction(target.chatId, action, {
|
|
818
846
|
message_thread_id: target.threadId,
|
|
819
847
|
});
|