@llblab/pi-telegram 0.55.0 → 0.56.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 +3 -2
- package/BACKLOG.md +4 -1
- package/CHANGELOG.md +14 -0
- package/README.md +27 -18
- 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 +5 -1
- package/dist/lib/bus-leader.js +9 -1
- package/dist/lib/bus.d.ts +5 -0
- package/dist/lib/bus.js +17 -6
- package/dist/lib/extension.js +14 -2
- package/dist/lib/lifecycle.d.ts +2 -1
- package/dist/lib/lifecycle.js +28 -20
- 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.js +3 -1
- package/dist/lib/queue.d.ts +57 -5
- package/dist/lib/queue.js +313 -25
- package/dist/lib/routing.d.ts +1 -1
- package/dist/lib/routing.js +261 -29
- package/dist/lib/status.d.ts +1 -2
- package/dist/lib/status.js +6 -7
- package/dist/lib/turns.d.ts +4 -0
- package/dist/lib/turns.js +6 -0
- package/dist/lib/updates.d.ts +10 -6
- package/dist/lib/updates.js +20 -4
- package/dist/package.json +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +17 -14
- package/docs/callback-namespaces.md +4 -0
- package/docs/multi-instance-bus.md +4 -2
- package/docs/ui-style.md +14 -10
- package/lib/bindings.ts +52 -3
- package/lib/bus-follower.ts +5 -0
- package/lib/bus-leader.ts +9 -0
- package/lib/bus.ts +20 -3
- package/lib/extension.ts +14 -1
- package/lib/lifecycle.ts +34 -28
- package/lib/menu-queue.ts +116 -162
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +3 -1
- package/lib/queue.ts +328 -33
- package/lib/routing.ts +299 -29
- package/lib/status.ts +7 -7
- package/lib/turns.ts +7 -0
- package/lib/updates.ts +24 -10
- package/package.json +1 -1
package/dist/lib/routing.d.ts
CHANGED
|
@@ -209,7 +209,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
209
209
|
canUsePreDispatchRebind?: (recipient?: Threads.TelegramWorkspaceRestoreRecipient) => boolean;
|
|
210
210
|
/** Strict complete namespace plus exact current/historical references; only an empty result clears journal protection. */
|
|
211
211
|
inspectTemporaryThreadSources?: (target: Queue.TelegramQueueTarget, requiredJournalBindingKeys: readonly string[], ownInputs?: readonly Threads.TelegramTemporaryThreadInput[]) => readonly number[] | undefined;
|
|
212
|
-
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to
|
|
212
|
+
/** Quiet period after the last cancelled input before one cleanup attempt; defaults to 0 ms. */
|
|
213
213
|
temporaryThreadCleanupDelayMs?: number;
|
|
214
214
|
/** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
|
|
215
215
|
liveRebindCleanupSchedule?: {
|
package/dist/lib/routing.js
CHANGED
|
@@ -1331,13 +1331,20 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1331
1331
|
requestDispatchNextQueuedTelegramTurn(ctx);
|
|
1332
1332
|
},
|
|
1333
1333
|
});
|
|
1334
|
-
const cloneTelegramMessagesForThread = (messages, threadId) => {
|
|
1335
|
-
return messages.map((message) =>
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1334
|
+
const cloneTelegramMessagesForThread = (messages, threadId, copies) => {
|
|
1335
|
+
return messages.map((message) => {
|
|
1336
|
+
const ids = Updates.collectTelegramAdmissionSourceUpdateIds([message]);
|
|
1337
|
+
const copyId = copies && ids.length === 1 ? copies.get(ids[0]) : undefined;
|
|
1338
|
+
if (copies && (!Number.isSafeInteger(copyId) || copyId <= 0))
|
|
1339
|
+
throw new Error("Reroute destination copy identity is unavailable.");
|
|
1340
|
+
const routed = {
|
|
1341
|
+
...message,
|
|
1342
|
+
message_id: copyId ?? 0,
|
|
1343
|
+
message_thread_id: threadId,
|
|
1344
|
+
reply_to_message: undefined,
|
|
1345
|
+
};
|
|
1346
|
+
return Updates.carryTelegramUpdateExecutionFence(message, copies ? Turns.withTelegramTurnReplyAnchor(routed) : routed);
|
|
1347
|
+
});
|
|
1341
1348
|
};
|
|
1342
1349
|
/** Whether a chooser is still pending in this Thread, ignoring the ones the caller itself owns. */
|
|
1343
1350
|
const hasPendingRerouteForTarget = (target, own) => [...pendingUnboundReroutes.values()].some((pending) => pending.sourceTarget.chatId === target.chatId &&
|
|
@@ -1603,10 +1610,12 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1603
1610
|
return !acceptedWorkClear;
|
|
1604
1611
|
};
|
|
1605
1612
|
/** Best-effort, once consumed: All keeps no copy of an input whose routing tab carried it; failure is only recorded. */
|
|
1606
|
-
const deleteAllTabMessages = async (chatId, messageIds) => {
|
|
1613
|
+
const deleteAllTabMessages = async (chatId, messageIds, isCurrent) => {
|
|
1607
1614
|
if (!deps.deleteMessage)
|
|
1608
1615
|
return;
|
|
1609
1616
|
for (const messageId of messageIds) {
|
|
1617
|
+
if (isCurrent?.() === false)
|
|
1618
|
+
return;
|
|
1610
1619
|
try {
|
|
1611
1620
|
await deps.deleteMessage(chatId, messageId);
|
|
1612
1621
|
}
|
|
@@ -1620,13 +1629,14 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1620
1629
|
}
|
|
1621
1630
|
};
|
|
1622
1631
|
/** One-shot per chooser: the copies are taken before deletion, so a later consuming path finds none. */
|
|
1623
|
-
const deleteAllTabCopies = (pending) => deleteAllTabMessages(pending.sourceTarget.chatId, pending.allTabCopies?.splice(0) ?? []);
|
|
1632
|
+
const deleteAllTabCopies = (pending, isCurrent) => deleteAllTabMessages(pending.sourceTarget.chatId, pending.allTabCopies?.splice(0) ?? [], isCurrent);
|
|
1624
1633
|
/**
|
|
1625
1634
|
* Restore keeps the tab as the Thread, so the input must stay visible there: the All original goes only when the tab
|
|
1626
1635
|
* holds its forwarded copy. Without one it stays where the operator can still read it.
|
|
1627
1636
|
*/
|
|
1628
1637
|
const releaseAllTabCopiesIntoSurvivingTab = (pending) => {
|
|
1629
|
-
if (pending.allTabCopiesForwarded
|
|
1638
|
+
if (pending.allTabCopiesForwarded &&
|
|
1639
|
+
isDeepStrictEqual(pending.allTabCopiesForwarded.target, pending.sourceTarget))
|
|
1630
1640
|
return deleteAllTabCopies(pending);
|
|
1631
1641
|
pending.allTabCopies?.splice(0);
|
|
1632
1642
|
return Promise.resolve();
|
|
@@ -1640,39 +1650,109 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1640
1650
|
const messageIds = [...(pending.allTabCopies ?? [])].sort((a, b) => a - b);
|
|
1641
1651
|
if (messageIds.length === 0 || !deps.callApi || !isCurrent())
|
|
1642
1652
|
return;
|
|
1653
|
+
const copyTarget = { ...target };
|
|
1643
1654
|
const route = {
|
|
1644
|
-
chat_id:
|
|
1645
|
-
message_thread_id:
|
|
1655
|
+
chat_id: copyTarget.chatId,
|
|
1656
|
+
message_thread_id: copyTarget.threadId,
|
|
1646
1657
|
from_chat_id: pending.sourceTarget.chatId,
|
|
1647
1658
|
disable_notification: true,
|
|
1648
1659
|
};
|
|
1660
|
+
const options = {
|
|
1661
|
+
maxAttempts: 1,
|
|
1662
|
+
retrySafety: "non-idempotent",
|
|
1663
|
+
assertAuthority() {
|
|
1664
|
+
if (!isCurrent())
|
|
1665
|
+
throw new Error("Temporary Thread source forwarding authority changed.");
|
|
1666
|
+
},
|
|
1667
|
+
};
|
|
1649
1668
|
try {
|
|
1650
1669
|
// An album moves as one album; forwardMessages skips what it cannot forward, so a short reply is not a move.
|
|
1651
1670
|
const copies = messageIds.length === 1
|
|
1652
1671
|
? [
|
|
1653
|
-
await deps.callApi("forwardMessage", {
|
|
1654
|
-
...route,
|
|
1655
|
-
message_id: messageIds[0],
|
|
1656
|
-
}),
|
|
1672
|
+
await deps.callApi("forwardMessage", { ...route, message_id: messageIds[0] }, options),
|
|
1657
1673
|
]
|
|
1658
|
-
: await deps.callApi("forwardMessages", { ...route, message_ids: messageIds });
|
|
1674
|
+
: await deps.callApi("forwardMessages", { ...route, message_ids: messageIds }, options);
|
|
1675
|
+
if (!isCurrent())
|
|
1676
|
+
return;
|
|
1659
1677
|
const copyIds = Array.isArray(copies)
|
|
1660
1678
|
? copies.map((copy) => copy?.message_id)
|
|
1661
1679
|
: [];
|
|
1662
1680
|
if (copyIds.length !== messageIds.length ||
|
|
1663
|
-
!copyIds.every((id) => typeof id === "number" && Number.isSafeInteger(id))
|
|
1681
|
+
!copyIds.every((id) => typeof id === "number" && Number.isSafeInteger(id) && id > 0) ||
|
|
1682
|
+
new Set(copyIds).size !== copyIds.length)
|
|
1664
1683
|
return;
|
|
1665
|
-
pending.allTabCopiesForwarded =
|
|
1666
|
-
|
|
1667
|
-
|
|
1684
|
+
pending.allTabCopiesForwarded = {
|
|
1685
|
+
target: copyTarget,
|
|
1686
|
+
sourceMessageIds: messageIds,
|
|
1687
|
+
messageIds: copyIds,
|
|
1688
|
+
};
|
|
1689
|
+
await deleteAllTabCopies(pending, isCurrent);
|
|
1690
|
+
if (!isCurrent())
|
|
1691
|
+
return;
|
|
1692
|
+
return pending.allTabCopiesForwarded.messageIds[0];
|
|
1668
1693
|
}
|
|
1669
1694
|
catch (error) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1695
|
+
if (isCurrent())
|
|
1696
|
+
deps.recordRuntimeEvent?.("routing", error, {
|
|
1697
|
+
phase: "temporary-thread-source-forward",
|
|
1698
|
+
});
|
|
1673
1699
|
return;
|
|
1674
1700
|
}
|
|
1675
1701
|
};
|
|
1702
|
+
/** Read original source addresses, not warm target rewrites; unknown copy provenance never grants a Reroute. */
|
|
1703
|
+
const resolvePendingRerouteForwardSource = (pending) => {
|
|
1704
|
+
try {
|
|
1705
|
+
const originals = pending.messages.map((message) => {
|
|
1706
|
+
const snapshot = Updates.inspectTelegramDeferredSourceSnapshot(message);
|
|
1707
|
+
const ids = Updates.collectTelegramAdmissionSourceUpdateIds([message]);
|
|
1708
|
+
const original = snapshot?.update.message;
|
|
1709
|
+
if (!snapshot ||
|
|
1710
|
+
ids.length !== 1 ||
|
|
1711
|
+
snapshot.source.updateId !== ids[0] ||
|
|
1712
|
+
snapshot.update.update_id !== ids[0] ||
|
|
1713
|
+
Object.keys(snapshot.update).some((key) => key !== "update_id" && key !== "message") ||
|
|
1714
|
+
snapshot.source.journalBindingKey !== deps.getAdmissionJournalBinding?.() ||
|
|
1715
|
+
!original ||
|
|
1716
|
+
original.chat?.type !== "private" ||
|
|
1717
|
+
original.chat.id !== pending.sourceTarget.chatId ||
|
|
1718
|
+
original.from?.id !== deps.configStore.getAllowedUserId() ||
|
|
1719
|
+
original.from?.is_bot !== false ||
|
|
1720
|
+
!Number.isSafeInteger(original.message_id) ||
|
|
1721
|
+
original.message_id <= 0)
|
|
1722
|
+
return undefined;
|
|
1723
|
+
return { message: original, updateId: ids[0] };
|
|
1724
|
+
});
|
|
1725
|
+
if (originals.length === 0 || originals.some((source) => !source))
|
|
1726
|
+
return;
|
|
1727
|
+
originals.sort((a, b) => a.message.message_id - b.message.message_id);
|
|
1728
|
+
const sourceMessageIds = originals.map((source) => source.message.message_id);
|
|
1729
|
+
const sourceUpdateIds = originals.map((source) => source.updateId);
|
|
1730
|
+
if (new Set(sourceMessageIds).size !== sourceMessageIds.length ||
|
|
1731
|
+
new Set(sourceUpdateIds).size !== sourceUpdateIds.length)
|
|
1732
|
+
return;
|
|
1733
|
+
const copied = pending.allTabCopiesForwarded;
|
|
1734
|
+
if (copied) {
|
|
1735
|
+
if (!isDeepStrictEqual(copied.target, pending.sourceTarget) ||
|
|
1736
|
+
!isDeepStrictEqual(copied.sourceMessageIds, sourceMessageIds) ||
|
|
1737
|
+
copied.messageIds.length !== sourceMessageIds.length ||
|
|
1738
|
+
!copied.messageIds.every((id) => Number.isSafeInteger(id) && id > 0) ||
|
|
1739
|
+
new Set(copied.messageIds).size !== copied.messageIds.length)
|
|
1740
|
+
return;
|
|
1741
|
+
return { chatId: copied.target.chatId, sourceUpdateIds, sourceMessageIds, messageIds: [...copied.messageIds] };
|
|
1742
|
+
}
|
|
1743
|
+
const nativeOriginals = originals.every((source) => typeof pending.sourceTarget.threadId === "number" &&
|
|
1744
|
+
source.message.message_thread_id === pending.sourceTarget.threadId);
|
|
1745
|
+
const retainedAllOriginals = pending.allTabSourceRetained === true &&
|
|
1746
|
+
originals.every((source) => source.message.message_thread_id === undefined) &&
|
|
1747
|
+
isDeepStrictEqual([...(pending.allTabCopies ?? [])].sort((a, b) => a - b), sourceMessageIds);
|
|
1748
|
+
if (nativeOriginals || retainedAllOriginals)
|
|
1749
|
+
return { chatId: pending.sourceTarget.chatId, sourceUpdateIds, sourceMessageIds, messageIds: [...sourceMessageIds] };
|
|
1750
|
+
}
|
|
1751
|
+
catch (error) {
|
|
1752
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-source-proof" });
|
|
1753
|
+
}
|
|
1754
|
+
return undefined;
|
|
1755
|
+
};
|
|
1676
1756
|
const dismissRerouteChooserMessage = async (query, assertExecutionCurrent) => {
|
|
1677
1757
|
const chatId = query.message?.chat?.id;
|
|
1678
1758
|
const messageId = query.message?.message_id;
|
|
@@ -4076,7 +4156,24 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4076
4156
|
wake(reason);
|
|
4077
4157
|
};
|
|
4078
4158
|
const onSessionSettled = (ctx) => wakeLiveRebindCleanup(ctx, "session-settled");
|
|
4079
|
-
const onDeliverySettled = (ctx) =>
|
|
4159
|
+
const onDeliverySettled = (ctx) => {
|
|
4160
|
+
wakeLiveRebindCleanup(ctx, "delivery-drained");
|
|
4161
|
+
const cap = captureTemporaryThreadAuthority(ctx);
|
|
4162
|
+
if (!cap?.isCurrent())
|
|
4163
|
+
return;
|
|
4164
|
+
try {
|
|
4165
|
+
for (const entry of cap.store.listTemporaryThreads()) {
|
|
4166
|
+
if (!cap.isCurrent())
|
|
4167
|
+
return;
|
|
4168
|
+
if (entry.phase === "created" && entry.target && !entry.cleanupIssued &&
|
|
4169
|
+
Threads.isTelegramTemporaryThreadFullyResolved(entry))
|
|
4170
|
+
scheduleTemporaryThreadCleanup(entry.target, ctx);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
catch (error) {
|
|
4174
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-drain" });
|
|
4175
|
+
}
|
|
4176
|
+
};
|
|
4080
4177
|
const restoreWorkspace = deps.workspaceRestoreRecipient
|
|
4081
4178
|
? async (input) => {
|
|
4082
4179
|
const ports = deps.workspaceRestoreRecipient;
|
|
@@ -4532,6 +4629,92 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4532
4629
|
return false;
|
|
4533
4630
|
}
|
|
4534
4631
|
};
|
|
4632
|
+
/** Publish display copies under the native one-shot grant; positive output is not input/source settlement. */
|
|
4633
|
+
const publishPendingRerouteCopies = async (pending, record, ctx, assertExecutionCurrent, foreign) => {
|
|
4634
|
+
const source = resolvePendingRerouteForwardSource(pending);
|
|
4635
|
+
const target = { ...record.target };
|
|
4636
|
+
const recordIdentity = (value) => value && ({
|
|
4637
|
+
target: value.target, profileKey: value.profileKey, owner: value.owner,
|
|
4638
|
+
instanceId: value.instanceId, slot: value.slot, createdAtMs: value.createdAtMs,
|
|
4639
|
+
});
|
|
4640
|
+
const bindingIdentity = () => deps.threadStore.listWorkspaceBindings()
|
|
4641
|
+
.filter((value) => isDeepStrictEqual(value.target, target))
|
|
4642
|
+
.map((value) => ({ bindingKey: value.bindingKey, sessionId: value.sessionId, sessionKey: value.sessionKey,
|
|
4643
|
+
cwd: value.cwd, workspaceKey: value.workspaceKey, instanceSlot: value.instanceSlot, slot: value.slot,
|
|
4644
|
+
target: value.target }));
|
|
4645
|
+
const expectedRecord = structuredClone(recordIdentity(record));
|
|
4646
|
+
const expectedBindings = structuredClone(bindingIdentity());
|
|
4647
|
+
const ownership = deps.getTargetOwnership?.(target);
|
|
4648
|
+
if (!source || !deps.callApi || expectedBindings.length > 1 ||
|
|
4649
|
+
(foreign && (!ownership || ownership.instanceId !== record.instanceId ||
|
|
4650
|
+
!ownership.ownerGeneration || !ownership.recipientBindingKey)))
|
|
4651
|
+
return;
|
|
4652
|
+
const expectedOwnership = ownership && structuredClone(ownership);
|
|
4653
|
+
const cap = captureTemporaryThreadAuthority(ctx, () => {
|
|
4654
|
+
try {
|
|
4655
|
+
assertExecutionCurrent();
|
|
4656
|
+
const current = getTelegramRoutableThreadRecords(deps.threadStore.list(), deps.getLiveThreadTargets?.())
|
|
4657
|
+
.find((value) => isDeepStrictEqual(value.target, target));
|
|
4658
|
+
return isDeepStrictEqual(recordIdentity(current), expectedRecord) &&
|
|
4659
|
+
isDeepStrictEqual(bindingIdentity(), expectedBindings) &&
|
|
4660
|
+
(!foreign || Bus.isTelegramBusForwardOwnershipCurrent(expectedOwnership, deps.getTargetOwnership?.(target)));
|
|
4661
|
+
}
|
|
4662
|
+
catch {
|
|
4663
|
+
return false;
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4666
|
+
if (!cap?.isCurrent())
|
|
4667
|
+
return;
|
|
4668
|
+
const temporary = pending.temporaryThread ?? findCreatedTemporaryThread(pending.sourceTarget);
|
|
4669
|
+
if (!temporary || !issueTemporaryThreadForward(pending, ctx))
|
|
4670
|
+
return;
|
|
4671
|
+
pending.phase = { kind: "forward-unknown" };
|
|
4672
|
+
const assertRecipientCurrent = () => {
|
|
4673
|
+
assertExecutionCurrent();
|
|
4674
|
+
if (!cap.isCurrent())
|
|
4675
|
+
throw new Error("Reroute publication recipient authority changed.");
|
|
4676
|
+
};
|
|
4677
|
+
const assertPublicationCurrent = () => {
|
|
4678
|
+
assertRecipientCurrent();
|
|
4679
|
+
const entry = cap.store.listTemporaryThreads().find((value) => value.token === temporary.token);
|
|
4680
|
+
const ids = [...source.sourceUpdateIds].sort((a, b) => a - b);
|
|
4681
|
+
if (!entry || entry.phase !== "created" || entry.cleanupIssued ||
|
|
4682
|
+
entry.operatorUserId !== cap.operatorUserId ||
|
|
4683
|
+
!isDeepStrictEqual(entry.executor, cap.authority.executor) ||
|
|
4684
|
+
!isDeepStrictEqual(entry.target, pending.sourceTarget) ||
|
|
4685
|
+
!entry.forwardedInputs?.some((input) => input.journalBindingKey === cap.journalBindingKey &&
|
|
4686
|
+
isDeepStrictEqual(input.updateIds, ids)) ||
|
|
4687
|
+
!isDeepStrictEqual(resolvePendingRerouteForwardSource(pending), source))
|
|
4688
|
+
throw new Error("Reroute publication source authority changed.");
|
|
4689
|
+
};
|
|
4690
|
+
try {
|
|
4691
|
+
assertPublicationCurrent();
|
|
4692
|
+
// Bot API requires ascending source IDs. Preserve the source-UID association even if copy IDs reorder them.
|
|
4693
|
+
const order = source.messageIds.map((_, index) => index)
|
|
4694
|
+
.sort((a, b) => source.messageIds[a] - source.messageIds[b]);
|
|
4695
|
+
const messageIds = order.map((index) => source.messageIds[index]);
|
|
4696
|
+
const body = { chat_id: target.chatId, message_thread_id: target.threadId,
|
|
4697
|
+
from_chat_id: source.chatId, disable_notification: true };
|
|
4698
|
+
const options = { maxAttempts: 1, retrySafety: "non-idempotent", assertAuthority: assertPublicationCurrent };
|
|
4699
|
+
const copies = messageIds.length === 1
|
|
4700
|
+
? [await deps.callApi("forwardMessage", { ...body, message_id: messageIds[0] }, options)]
|
|
4701
|
+
: await deps.callApi("forwardMessages", { ...body, message_ids: messageIds }, options);
|
|
4702
|
+
assertPublicationCurrent();
|
|
4703
|
+
const copyIds = Array.isArray(copies) ? copies.map((copy) => copy?.message_id) : [];
|
|
4704
|
+
if (copyIds.length !== messageIds.length ||
|
|
4705
|
+
!copyIds.every((id) => typeof id === "number" && Number.isSafeInteger(id) && id > 0) ||
|
|
4706
|
+
new Set(copyIds).size !== copyIds.length)
|
|
4707
|
+
return;
|
|
4708
|
+
pending.rerouteCopies = { target, bySourceUpdateId: new Map(order.map((index, position) => [source.sourceUpdateIds[index], copyIds[position]])) };
|
|
4709
|
+
pending.phase = { kind: "selected" };
|
|
4710
|
+
return assertRecipientCurrent;
|
|
4711
|
+
}
|
|
4712
|
+
catch (error) {
|
|
4713
|
+
if (cap.isCurrent())
|
|
4714
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-destination-forward" });
|
|
4715
|
+
return;
|
|
4716
|
+
}
|
|
4717
|
+
};
|
|
4535
4718
|
const forwardPendingRerouteMessages = async (pending, instanceId, threadId, ctx, assertExecutionCurrent, recordForwardAcceptance) => {
|
|
4536
4719
|
assertExecutionCurrent?.();
|
|
4537
4720
|
const forwardMessage = deps.foreignOwnedUpdateForwarder?.forwardMessage;
|
|
@@ -4545,11 +4728,14 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4545
4728
|
!liveOwnership.recipientBindingKey)
|
|
4546
4729
|
return false;
|
|
4547
4730
|
const ownership = structuredClone(liveOwnership);
|
|
4548
|
-
const
|
|
4731
|
+
const copies = pending.rerouteCopies;
|
|
4732
|
+
const published = !!copies && isDeepStrictEqual(copies.target, target);
|
|
4733
|
+
const messages = cloneTelegramMessagesForThread(pending.messages, threadId, published ? copies.bySourceUpdateId : undefined);
|
|
4549
4734
|
// Issuance is durable before the RPC: an unacknowledged temporary-tab Forward never becomes another dispatch grant,
|
|
4550
4735
|
// even after restart. Any refusal or publication failure sends nothing and keeps the input held.
|
|
4551
4736
|
if (!recordForwardAcceptance &&
|
|
4552
4737
|
isTemporaryReroute(pending) &&
|
|
4738
|
+
!published &&
|
|
4553
4739
|
!issueTemporaryThreadForward(pending, ctx)) {
|
|
4554
4740
|
if (isTemporaryThreadForwardIssued(pending))
|
|
4555
4741
|
pending.phase = { kind: "forward-unknown" };
|
|
@@ -4561,6 +4747,10 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4561
4747
|
message,
|
|
4562
4748
|
ownership,
|
|
4563
4749
|
ctx,
|
|
4750
|
+
assertAuthority() {
|
|
4751
|
+
assertExecutionCurrent?.();
|
|
4752
|
+
Updates.assertTelegramUpdateExecutionCurrent(message);
|
|
4753
|
+
},
|
|
4564
4754
|
})));
|
|
4565
4755
|
assertExecutionCurrent?.();
|
|
4566
4756
|
if (!isDeepStrictEqual(deps.getTargetOwnership?.(target), ownership))
|
|
@@ -4719,6 +4909,17 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4719
4909
|
if (retry)
|
|
4720
4910
|
restoreSettlementTasks.add(task);
|
|
4721
4911
|
runTemporaryThreadCleanup(token, cap, reconcileExpiry)
|
|
4912
|
+
.then(() => {
|
|
4913
|
+
if (!cap.isCurrent())
|
|
4914
|
+
return;
|
|
4915
|
+
const retained = cap.store.listTemporaryThreads().find(value => value.token === token);
|
|
4916
|
+
// A follower ACK can precede recipient receipt disposal. Reobserve a protected, unissued tab;
|
|
4917
|
+
// elapsed time never grants deletion, and an issued/unknown delete can never enter this path.
|
|
4918
|
+
if (retained?.phase === "created" && !retained.cleanupIssued &&
|
|
4919
|
+
Threads.isTelegramTemporaryThreadFullyResolved(retained) &&
|
|
4920
|
+
cap.store.inspectTemporaryThreadTarget(retained, cap.authority)?.kind === "temporary" && cap.isCurrent())
|
|
4921
|
+
queueTemporaryThreadCleanup(token, cap, 1000, reconcileExpiry, true);
|
|
4922
|
+
})
|
|
4722
4923
|
.catch((error) => {
|
|
4723
4924
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
4724
4925
|
phase: "temporary-thread-cleanup",
|
|
@@ -5244,7 +5445,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5244
5445
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
5245
5446
|
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId))
|
|
5246
5447
|
continue;
|
|
5247
|
-
movedIntoTab ||= pending.allTabCopiesForwarded
|
|
5448
|
+
movedIntoTab ||= pending.allTabCopiesForwarded !== undefined;
|
|
5248
5449
|
removePendingReroute(id);
|
|
5249
5450
|
if (signal.aborted ||
|
|
5250
5451
|
deps.isContextActive?.(ctx) !== true ||
|
|
@@ -6047,6 +6248,12 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6047
6248
|
: "Input queued in the rebound Thread; old-Thread cleanup is scheduled, not confirmed", assertExecutionCurrent, true);
|
|
6048
6249
|
return true;
|
|
6049
6250
|
}
|
|
6251
|
+
if (!parsed.useNewSlot &&
|
|
6252
|
+
isTemporaryReroute(pending) &&
|
|
6253
|
+
!resolvePendingRerouteForwardSource(pending)) {
|
|
6254
|
+
await deps.answerCallbackQuery(query.id, "Source copy unavailable; not routing");
|
|
6255
|
+
return true;
|
|
6256
|
+
}
|
|
6050
6257
|
if (!(await selectPendingDestination()))
|
|
6051
6258
|
return true;
|
|
6052
6259
|
if (parsed.useNewSlot && sourceTarget && restoreWorkspace) {
|
|
@@ -6130,10 +6337,22 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6130
6337
|
await deps.answerCallbackQuery(query.id, delivered ? "Restored" : "Restore unconfirmed; not resending");
|
|
6131
6338
|
return true;
|
|
6132
6339
|
}
|
|
6340
|
+
let assertRerouteCurrent = assertExecutionCurrent;
|
|
6341
|
+
if (isTemporaryReroute(pending)) {
|
|
6342
|
+
const confirmed = await publishPendingRerouteCopies(pending, record, ctx, assertExecutionCurrent, !!record.instanceId && record.instanceId !== currentInstanceId && !isCurrentLeaderRecord);
|
|
6343
|
+
if (!confirmed) {
|
|
6344
|
+
await deps.answerCallbackQuery(query.id, pending.phase.kind === "forward-unknown"
|
|
6345
|
+
? "Publication unconfirmed; not resending"
|
|
6346
|
+
: "Publication unavailable; input kept");
|
|
6347
|
+
return true;
|
|
6348
|
+
}
|
|
6349
|
+
assertRerouteCurrent = confirmed;
|
|
6350
|
+
}
|
|
6351
|
+
assertRerouteCurrent();
|
|
6133
6352
|
if (record.instanceId &&
|
|
6134
6353
|
record.instanceId !== currentInstanceId &&
|
|
6135
6354
|
!isCurrentLeaderRecord) {
|
|
6136
|
-
const allForwarded = await forwardPendingRerouteMessages(pending, record.instanceId, parsed.threadId, ctx,
|
|
6355
|
+
const allForwarded = await forwardPendingRerouteMessages(pending, record.instanceId, parsed.threadId, ctx, assertRerouteCurrent);
|
|
6137
6356
|
if (!allForwarded) {
|
|
6138
6357
|
await deps.answerCallbackQuery(query.id, pending.phase.kind === "forward-unknown"
|
|
6139
6358
|
? "Delivery unconfirmed; not resending"
|
|
@@ -6144,11 +6363,16 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6144
6363
|
}
|
|
6145
6364
|
// Local handlers and queue admission consume the same one-time group grant as follower forwarding.
|
|
6146
6365
|
if (isTemporaryReroute(pending) &&
|
|
6366
|
+
!pending.rerouteCopies &&
|
|
6147
6367
|
!issueTemporaryThreadForward(pending, ctx)) {
|
|
6148
6368
|
await deps.answerCallbackQuery(query.id, "Delivery unconfirmed; not resending");
|
|
6149
6369
|
return true;
|
|
6150
6370
|
}
|
|
6151
|
-
|
|
6371
|
+
const localMessages = pending.rerouteCopies
|
|
6372
|
+
? cloneTelegramMessagesForThread(pending.messages, parsed.threadId, pending.rerouteCopies.bySourceUpdateId)
|
|
6373
|
+
: reroutedMessages;
|
|
6374
|
+
await dispatchPendingRerouteMessages(pending, localMessages, ctx);
|
|
6375
|
+
assertRerouteCurrent();
|
|
6152
6376
|
pending.messages = [];
|
|
6153
6377
|
return completeRoutedReroute();
|
|
6154
6378
|
};
|
|
@@ -7325,6 +7549,14 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
7325
7549
|
const tabCopyId = openedNow
|
|
7326
7550
|
? await moveAllTabSourceIntoTab(pending, target, current)
|
|
7327
7551
|
: undefined;
|
|
7552
|
+
if (!current()) {
|
|
7553
|
+
removePendingReroute(rerouteId);
|
|
7554
|
+
throw new Error("Temporary Thread source presentation authority changed.");
|
|
7555
|
+
}
|
|
7556
|
+
if (openedNow &&
|
|
7557
|
+
!pending.allTabCopiesForwarded &&
|
|
7558
|
+
pending.allTabCopies.length === held.length)
|
|
7559
|
+
pending.allTabSourceRetained = true;
|
|
7328
7560
|
try {
|
|
7329
7561
|
if (!deps.sendInteractiveMessage)
|
|
7330
7562
|
throw new Error("Temporary Thread chooser publication is unavailable.");
|
package/dist/lib/status.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
|
|
8
8
|
export declare function formatTelegramConnectionFailure(error: unknown): string;
|
|
9
|
-
export type TelegramStatusQueueLane = "control" | "priority" | "default";
|
|
9
|
+
export type TelegramStatusQueueLane = "control" | "steer" | "priority" | "default";
|
|
10
10
|
export interface TelegramUsageStats {
|
|
11
11
|
totalInput: number;
|
|
12
12
|
totalOutput: number;
|
|
@@ -345,7 +345,6 @@ export interface TelegramBridgeStatusRuntimeDeps<TQueueItem extends {
|
|
|
345
345
|
hasPendingModelSwitch: () => boolean;
|
|
346
346
|
getQueuedItems: () => TQueueItem[];
|
|
347
347
|
getQueuedItemCount?: (items: TQueueItem[]) => number;
|
|
348
|
-
formatQueuedStatus: (items: TQueueItem[]) => string;
|
|
349
348
|
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
|
|
350
349
|
getRuntimeLockState?: () => string;
|
|
351
350
|
getBusRole?: () => TelegramBridgeBusRole | undefined;
|
package/dist/lib/status.js
CHANGED
|
@@ -605,9 +605,7 @@ function getTelegramStatusBarLabel(state) {
|
|
|
605
605
|
}
|
|
606
606
|
export function buildTelegramStatusBarText(theme, state) {
|
|
607
607
|
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
|
|
608
|
-
const queued = state.queuedStatus
|
|
609
|
-
? theme.fg("warning", state.queuedStatus)
|
|
610
|
-
: "";
|
|
608
|
+
const queued = state.queuedStatus ? theme.fg("warning", state.queuedStatus) : "";
|
|
611
609
|
if (!state.hasBotToken)
|
|
612
610
|
return `${label} ${theme.fg("muted", "not configured")}${queued}`;
|
|
613
611
|
if (state.pollingStopReason === "persistent-conflict" &&
|
|
@@ -949,6 +947,7 @@ function buildTelegramInboundWorkerDiagnosticLines(worker) {
|
|
|
949
947
|
function countTelegramQueueLanes(items) {
|
|
950
948
|
const counts = {
|
|
951
949
|
control: 0,
|
|
950
|
+
steer: 0,
|
|
952
951
|
priority: 0,
|
|
953
952
|
default: 0,
|
|
954
953
|
};
|
|
@@ -957,9 +956,9 @@ function countTelegramQueueLanes(items) {
|
|
|
957
956
|
return counts;
|
|
958
957
|
}
|
|
959
958
|
function buildTelegramBridgeCompactStatusLines(state) {
|
|
960
|
-
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
959
|
+
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, steer: steerQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
961
960
|
const queueLine = `- queued turns: ${state.queuedItems.length}${state.queuedItems.length > 0
|
|
962
|
-
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
961
|
+
? ` (control=${controlQueueCount}${steerQueueCount > 0 ? `, steer=${steerQueueCount}` : ""}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
963
962
|
: ""}`;
|
|
964
963
|
const executionState = state.pendingDispatch
|
|
965
964
|
? "pending dispatch"
|
|
@@ -1017,7 +1016,7 @@ export function buildTelegramBridgeStatusLines(state, options = {}) {
|
|
|
1017
1016
|
return buildTelegramBridgeCompactStatusLines(state);
|
|
1018
1017
|
}
|
|
1019
1018
|
function buildTelegramBridgeDiagnosticStatusLines(state) {
|
|
1020
|
-
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
1019
|
+
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, steer: steerQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
1021
1020
|
return [
|
|
1022
1021
|
"connection:",
|
|
1023
1022
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
@@ -1063,7 +1062,7 @@ function buildTelegramBridgeDiagnosticStatusLines(state) {
|
|
|
1063
1062
|
"",
|
|
1064
1063
|
"queue:",
|
|
1065
1064
|
`- queued turns: ${state.queuedItems.length}`,
|
|
1066
|
-
`- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1065
|
+
`- lanes: control=${controlQueueCount}${steerQueueCount > 0 ? `, steer=${steerQueueCount}` : ""}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1067
1066
|
...buildTelegramBusFollowerLines(state),
|
|
1068
1067
|
...buildTelegramLocalBusLines(state, { verbose: true }),
|
|
1069
1068
|
...buildTelegramTopicTargetDiagnosticLines(state),
|
package/dist/lib/turns.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ import { type TelegramVoiceReplyMode } from "./voice.ts";
|
|
|
9
9
|
export declare const TELEGRAM_PREFIX = "[telegram]";
|
|
10
10
|
/** A routed source keeps its unique message/file/group identity, but cannot quote an anchor in its former tab. */
|
|
11
11
|
export declare function withoutTelegramTurnReplyAnchor<TMessage extends object>(message: TMessage): TMessage;
|
|
12
|
+
/** A confirmed same-target copy may replace the former-tab suppression on a warm input projection. */
|
|
13
|
+
export declare function withTelegramTurnReplyAnchor<TMessage extends {
|
|
14
|
+
message_id: number;
|
|
15
|
+
}>(message: TMessage): TMessage;
|
|
12
16
|
export declare const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
|
|
13
17
|
export interface TelegramTurnTarget {
|
|
14
18
|
chatId: number;
|
package/dist/lib/turns.js
CHANGED
|
@@ -14,6 +14,12 @@ const REPLY_ANCHOR_SUPPRESSED = Symbol.for("@llblab/pi-telegram:reply-anchor-sup
|
|
|
14
14
|
export function withoutTelegramTurnReplyAnchor(message) {
|
|
15
15
|
return { ...message, [REPLY_ANCHOR_SUPPRESSED]: true };
|
|
16
16
|
}
|
|
17
|
+
/** A confirmed same-target copy may replace the former-tab suppression on a warm input projection. */
|
|
18
|
+
export function withTelegramTurnReplyAnchor(message) {
|
|
19
|
+
if (!Number.isSafeInteger(message.message_id) || message.message_id <= 0)
|
|
20
|
+
throw new Error("Telegram reply anchor requires a positive message ID.");
|
|
21
|
+
return { ...message, [REPLY_ANCHOR_SUPPRESSED]: false };
|
|
22
|
+
}
|
|
17
23
|
export const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
|
|
18
24
|
function getTelegramTurnTarget(message) {
|
|
19
25
|
return Number.isInteger(message.message_thread_id)
|
package/dist/lib/updates.d.ts
CHANGED
|
@@ -22,10 +22,6 @@ export declare const TELEGRAM_PRIORITY_REACTIONS: readonly [{
|
|
|
22
22
|
readonly id: 10;
|
|
23
23
|
readonly name: "like";
|
|
24
24
|
readonly emoji: "👍";
|
|
25
|
-
}, {
|
|
26
|
-
readonly id: 11;
|
|
27
|
-
readonly name: "lightning";
|
|
28
|
-
readonly emoji: "⚡";
|
|
29
25
|
}, {
|
|
30
26
|
readonly id: 12;
|
|
31
27
|
readonly name: "heart";
|
|
@@ -34,7 +30,8 @@ export declare const TELEGRAM_PRIORITY_REACTIONS: readonly [{
|
|
|
34
30
|
readonly id: 13;
|
|
35
31
|
readonly name: "dove";
|
|
36
32
|
readonly emoji: "🕊";
|
|
37
|
-
}
|
|
33
|
+
}];
|
|
34
|
+
export declare const TELEGRAM_STEER_REACTIONS: readonly [{
|
|
38
35
|
readonly id: 14;
|
|
39
36
|
readonly name: "fire";
|
|
40
37
|
readonly emoji: "🔥";
|
|
@@ -60,7 +57,7 @@ export declare const TELEGRAM_REMOVAL_REACTIONS: readonly [{
|
|
|
60
57
|
readonly name: "wastebasket";
|
|
61
58
|
readonly emoji: "🗑";
|
|
62
59
|
}];
|
|
63
|
-
export declare const TELEGRAM_PRIORITY_REACTION_EMOJIS: ("👍" | "
|
|
60
|
+
export declare const TELEGRAM_PRIORITY_REACTION_EMOJIS: ("👍" | "❤" | "🕊")[];
|
|
64
61
|
export declare const TELEGRAM_REMOVAL_REACTION_EMOJIS: ("👎" | "👻" | "💔" | "💩" | "🗑")[];
|
|
65
62
|
export interface TelegramUpdateDeletion {
|
|
66
63
|
deleted_business_messages?: {
|
|
@@ -141,6 +138,8 @@ export interface TelegramForeignOwnedUpdateForwarder<TContext, TReactionUpdate e
|
|
|
141
138
|
message: TMessage;
|
|
142
139
|
ownership: TelegramTargetOwnershipView;
|
|
143
140
|
ctx: TContext;
|
|
141
|
+
/** Local issuance/ACK fence; implementations must not serialize it into payloads. */
|
|
142
|
+
assertAuthority?: () => void;
|
|
144
143
|
}) => Promise<TelegramBusForeignUpdateSettlement> | TelegramBusForeignUpdateSettlement;
|
|
145
144
|
forwardEditedMessage?: (input: {
|
|
146
145
|
message: TMessage;
|
|
@@ -1317,6 +1316,7 @@ export interface TelegramQueueHandoffControlExecutionDeps<TContext> {
|
|
|
1317
1316
|
export declare function createTelegramQueueHandoffControlExecutionFactory<TContext>(deps: TelegramQueueHandoffControlExecutionDeps<TContext>): (payload: TelegramControlQueueHandoffPayload) => PendingTelegramControlItem<TContext>["execute"];
|
|
1318
1317
|
export interface TelegramQueueHandoffCoordinatorInput<TContext> {
|
|
1319
1318
|
item: TelegramQueueItem<TContext>;
|
|
1319
|
+
steerSupported?: boolean;
|
|
1320
1320
|
expectedOwner: TelegramUpdateJournalQueueOwner;
|
|
1321
1321
|
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
1322
1322
|
handoffToken: string;
|
|
@@ -1356,6 +1356,8 @@ export declare function createTelegramQueueHandoffRecipientRuntime<TContext>(dep
|
|
|
1356
1356
|
kind: "leader.offerQueueHandoff";
|
|
1357
1357
|
}>, ctx: TContext) => Promise<TelegramQueueHandoffStageResult>;
|
|
1358
1358
|
export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
1359
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
1360
|
+
hasPendingDispatch?: () => boolean;
|
|
1359
1361
|
ownsDirect: () => boolean;
|
|
1360
1362
|
isFollowerRegistered: () => boolean;
|
|
1361
1363
|
isBusEnabled: () => boolean;
|
|
@@ -1380,6 +1382,8 @@ export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
|
1380
1382
|
recordFailure?: (error: unknown, details: Record<string, unknown>) => void;
|
|
1381
1383
|
}
|
|
1382
1384
|
export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<TContext> {
|
|
1385
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
1386
|
+
hasPendingDispatch?: () => boolean;
|
|
1383
1387
|
ownsDirect: () => boolean;
|
|
1384
1388
|
isFollowerRegistered: () => boolean;
|
|
1385
1389
|
isBusEnabled: () => boolean;
|