@llblab/pi-kit 0.33.0 → 0.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +4 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +11 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +261 -29
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +13 -3
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +299 -29
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +2 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
package/BACKLOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Backlog
|
|
2
2
|
|
|
3
|
-
The 0.33.
|
|
3
|
+
The 0.33.1 composition advances only Telegram to 0.55.1; package pins, resource order and bundled runtime ownership remain authoritative in `package.json`. Release outcomes belong in [CHANGELOG.md](./CHANGELOG.md).
|
|
4
4
|
|
|
5
5
|
## Carried checks
|
|
6
6
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.33.1: Telegram Reroute Visibility and Follower Cleanup
|
|
6
|
+
|
|
7
|
+
- `Telegram Reroute`: Advances only the exact Telegram pin to `0.55.1`. Temporary Reroute forwards prompts into the selected Thread before native admission and anchors answers to confirmed copies. Follower cleanup reobserves resolved temporary tabs after receipt protection clears, including retained completed membership after runtime replacement; issued or unknown deletion never repeats. Other pins, package membership, resource paths and load order are unchanged.
|
|
8
|
+
|
|
5
9
|
## 0.33.0: Telegram Native Restore and Lifecycle Fences
|
|
6
10
|
|
|
7
11
|
- `Telegram 0.55`: Advances the exact Telegram pin to `0.55.0`. Native Restore confirms its new binding and decides old-tab cleanup before retained input runs. All is input-only; preview, typing and reconnect reject stale lifecycle work. Exact Linux/macOS home displays as Home/home without changing routing identity. Compatible Restore capabilities are negotiated separately from bus protocol v3. Other pins, package membership, resources and load order are unchanged.
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
18
18
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.0` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
|
|
19
19
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
20
20
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
|
|
21
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.55.
|
|
21
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.55.1` | Telegram companion with visible Reroute, receipt-fenced temporary cleanup, native pre-dispatch Restore, input-only All, previews/typing/reconnect, follower Threads, Windows support, files, voice, and controls |
|
|
22
22
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
23
23
|
|
|
24
24
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -45,7 +45,7 @@ Prefer the kit instead of separately loading the same packages. If you already u
|
|
|
45
45
|
|
|
46
46
|
## Development
|
|
47
47
|
|
|
48
|
-
The `0.33.
|
|
48
|
+
The `0.33.1` composition advances only Telegram to `0.55.1`: temporary-tab Reroute forwards text, voice, files and albums into the selected Thread before native admission and anchors answers to acknowledged copies. Follower cleanup rechecks resolved temporary tabs after recipient receipt protection clears, including retained completed membership after runtime replacement; issued or unknown deletion never repeats. Native Restore, input-only All, lifecycle fences and Classic DM remain supported. Other pins, package membership, resource paths and load order are unchanged.
|
|
49
49
|
|
|
50
50
|
Telegram keeps bus protocol v3: update every Pi process that runs pi-telegram together; 0.53.x followers cannot register with current leaders. Pre-dispatch Restore additionally negotiates compatible reader and execution capabilities; older supported paths remain distinct. Runtime storage stays in `tmp/pi-telegram`. Drain the prompt queue or accept losing waiting work before reload/restart; damaged-state reset discards all profiles' runtime continuity. `npm run validate` checks exact installed pins, declared resources, dependency audit and bundled inventory. It does not certify installed-client rendering; carried checks remain in [Backlog](./BACKLOG.md).
|
|
51
51
|
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.55.1: Visible Reroute and follower cleanup
|
|
8
|
+
|
|
9
|
+
- `Reroute visibility`: Temporary Reroute forwards text, voice, files and albums into the selected Thread before local/follower admission, then anchors replies to confirmed copies. Source/recipient/generation fences reach HTTP and IPC write/ACK boundaries; one native grant prevents replay after partial/lost ACKs. Unknown cold All copies stay held with Cancel; real native-source recovery remains supported. Journal originals, receipt/group cleanup protections, Restore and cleanup timing are unchanged.
|
|
10
|
+
- `Reroute cleanup`: A follower ACK no longer strands a resolved Routing tab when recipient queue custody blocks its first cleanup check. Metadata-only rechecks and existing delivery-drain wakes wait for fresh source/receipt protection to clear, including retained completed membership after runtime replacement. New input, live bindings, authority loss and unverifiable evidence remain protective; issued or unknown deletion never repeats.
|
|
11
|
+
|
|
7
12
|
## 0.55.0: Native Restore and fenced lifecycles
|
|
8
13
|
|
|
9
14
|
- `Restore cleanup`: Live Restore binds the new tab and decides old-tab cleanup before native leader/follower inputs run. Protected work defers cleanup; session replacement carries no deletion grant, and unknown deletion never repeats. Follower execution targets the rebound tab without rewriting journal originals or quoting the former tab. Exact donor ACKs seal routing clocks and retire the menu. Reader/execution capabilities, source custody and detached replies stay separate; busy hints coalesce.
|
|
@@ -143,7 +143,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
|
|
|
143
143
|
| Generative Apps | Install or explicitly replace a reviewed `.mjs` application whose generated JSON button view may mix direct `app::method` actions with ordinary model prompts. | Repeated games, controls, tutors, and adapters compile routine interaction without losing selective model interpretation, explanation, or adaptation. |
|
|
144
144
|
| Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
|
|
145
145
|
| Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
|
|
146
|
-
| Reroute and restore | Give text, voice, files and albums sent from `All`, plus unknown threads, explicit reroute and replace/restore choices. |
|
|
146
|
+
| Reroute and restore | Give text, voice, files and albums sent from `All`, plus unknown threads, explicit reroute and replace/restore choices. | Reroute from a routing tab forwards the prompt into the selected Thread before agent admission and anchors replies there; cleanup still requires source ACKs. Restore rebinds the tab and may remove only the replaced old tab after positive cleanup proofs. Blocked cleanup preserves the relocation and protected work. |
|
|
147
147
|
| Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
|
|
148
148
|
| Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, negotiated bus protocol/build/capabilities, separate polling and inbound-worker progress, journal depth, local/foreign queue ownership, automatic retry waits, transport, and failures. | Compatible build skew, foreign semantic authority, a healthy poller, durable backoff and an infrastructure-blocked worker remain distinguishable without hidden logs. |
|
|
149
149
|
| Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
|
|
@@ -388,6 +388,7 @@ export declare function createTelegramBusFollowerClientRuntime<TContext, TReacti
|
|
|
388
388
|
message: TMessage;
|
|
389
389
|
ownership: TelegramBusForwardOwnership;
|
|
390
390
|
ctx: TContext;
|
|
391
|
+
assertAuthority?: () => void;
|
|
391
392
|
}) => Promise<import("./bus.ts").TelegramBusForeignUpdateSettlement>;
|
|
392
393
|
forwardEditedMessage: (input: {
|
|
393
394
|
message: TMessage;
|
|
@@ -670,6 +670,8 @@ export declare function createTelegramBusForeignOwnedUpdateForwarder<TContext, T
|
|
|
670
670
|
message: TMessage;
|
|
671
671
|
ownership: TelegramBusForwardOwnership;
|
|
672
672
|
ctx: TContext;
|
|
673
|
+
/** Process-local source/recipient lifetime; never part of the wire envelope. */
|
|
674
|
+
assertAuthority?: () => void;
|
|
673
675
|
}) => Promise<TelegramBusForeignUpdateSettlement>;
|
|
674
676
|
forwardEditedMessage: (input: {
|
|
675
677
|
message: TMessage;
|
|
@@ -971,7 +971,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
971
971
|
sourceReference,
|
|
972
972
|
};
|
|
973
973
|
};
|
|
974
|
-
const send = async (envelope, ownership) => {
|
|
974
|
+
const send = async (envelope, ownership, assertAuthority) => {
|
|
975
975
|
if (deps.validateForwardOwnership &&
|
|
976
976
|
!deps.validateForwardOwnership(ownership))
|
|
977
977
|
return reject({
|
|
@@ -991,10 +991,15 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
991
991
|
socketPath,
|
|
992
992
|
envelope,
|
|
993
993
|
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
994
|
-
retry: getTelegramBusTransportRetryPolicy({
|
|
994
|
+
retry: assertAuthority ? { attempts: 1, delayMs: 0 } : getTelegramBusTransportRetryPolicy({
|
|
995
995
|
endpoint: socketPath,
|
|
996
996
|
operation: "operation",
|
|
997
997
|
}),
|
|
998
|
+
...(assertAuthority || deps.validateForwardOwnership ? { assertAuthority() {
|
|
999
|
+
assertAuthority?.();
|
|
1000
|
+
if (deps.validateForwardOwnership && !deps.validateForwardOwnership(ownership))
|
|
1001
|
+
throw new Error("Telegram follower forwarding ownership changed.");
|
|
1002
|
+
} } : {}),
|
|
998
1003
|
});
|
|
999
1004
|
}
|
|
1000
1005
|
catch (error) {
|
|
@@ -1066,7 +1071,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
1066
1071
|
return { status: "accepted", delivery: envelope.delivery };
|
|
1067
1072
|
};
|
|
1068
1073
|
// A recipient already holding the source reference is only woken; it never receives a second payload copy.
|
|
1069
|
-
const forward = (kind, value, ownership, build) => {
|
|
1074
|
+
const forward = (kind, value, ownership, build, assertAuthority) => {
|
|
1070
1075
|
const prepared = prepare(kind, value, ownership);
|
|
1071
1076
|
if ("settlement" in prepared)
|
|
1072
1077
|
return Promise.resolve(prepared.settlement);
|
|
@@ -1078,7 +1083,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
1078
1083
|
};
|
|
1079
1084
|
return send(prepared.sourceReference
|
|
1080
1085
|
? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
|
|
1081
|
-
: build(base, getNowMs()), ownership);
|
|
1086
|
+
: build(base, getNowMs()), ownership, assertAuthority);
|
|
1082
1087
|
};
|
|
1083
1088
|
return {
|
|
1084
1089
|
forwardCallback: ({ query, ownership }) => forward("leader.forwardCallback", query, ownership, (base, sentAtMs) => ({
|
|
@@ -1093,7 +1098,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
1093
1098
|
reactionUpdate,
|
|
1094
1099
|
sentAtMs,
|
|
1095
1100
|
})),
|
|
1096
|
-
forwardMessage: ({ message, ownership }) => forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
|
|
1101
|
+
forwardMessage: ({ message, ownership, assertAuthority }) => forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
|
|
1097
1102
|
const forwardCommentBatchPosition = deps.getForwardCommentBatchPosition?.(message);
|
|
1098
1103
|
return {
|
|
1099
1104
|
kind: "leader.forwardMessage",
|
|
@@ -1104,7 +1109,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
1104
1109
|
: {}),
|
|
1105
1110
|
sentAtMs,
|
|
1106
1111
|
};
|
|
1107
|
-
}),
|
|
1112
|
+
}, assertAuthority),
|
|
1108
1113
|
forwardEditedMessage: ({ message, ownership }) => forward("leader.forwardEditedMessage", message, ownership, (base, sentAtMs) => ({
|
|
1109
1114
|
kind: "leader.forwardEditedMessage",
|
|
1110
1115
|
...base,
|
|
@@ -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?: {
|
|
@@ -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.");
|
|
@@ -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;
|
|
@@ -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)
|
|
@@ -141,6 +141,8 @@ export interface TelegramForeignOwnedUpdateForwarder<TContext, TReactionUpdate e
|
|
|
141
141
|
message: TMessage;
|
|
142
142
|
ownership: TelegramTargetOwnershipView;
|
|
143
143
|
ctx: TContext;
|
|
144
|
+
/** Local issuance/ACK fence; implementations must not serialize it into payloads. */
|
|
145
|
+
assertAuthority?: () => void;
|
|
144
146
|
}) => Promise<TelegramBusForeignUpdateSettlement> | TelegramBusForeignUpdateSettlement;
|
|
145
147
|
forwardEditedMessage?: (input: {
|
|
146
148
|
message: TMessage;
|