@llblab/pi-kit 0.33.0 → 0.34.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/BACKLOG.md +2 -1
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-telegram/AGENTS.md +5 -4
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
- package/node_modules/@llblab/pi-telegram/README.md +29 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +8 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +10 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +34 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +32 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +33 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +18 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +28 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +102 -99
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +57 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +313 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +323 -30
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +6 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +11 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +57 -6
- 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 +14 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -9
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +27 -14
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -4
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +14 -10
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +36 -10
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +116 -162
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +350 -30
- package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +46 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -584,6 +584,20 @@ export function createTelegramLiveRebindCoordinator(input) {
|
|
|
584
584
|
}
|
|
585
585
|
: undefined;
|
|
586
586
|
},
|
|
587
|
+
/** Observe only an already-issued leader command; never prepares, relocates or executes input. */
|
|
588
|
+
observeCommandCompletion() {
|
|
589
|
+
if (!continuationIssued || !leader?.complete)
|
|
590
|
+
return undefined;
|
|
591
|
+
if (!current())
|
|
592
|
+
return "unknown";
|
|
593
|
+
try {
|
|
594
|
+
return observeCommandCompletion();
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
input.recordRuntimeEvent?.("telegram", error, { phase: "live-rebind-command-observation" });
|
|
598
|
+
return "unknown";
|
|
599
|
+
}
|
|
600
|
+
},
|
|
587
601
|
async advance() {
|
|
588
602
|
if (advancing || !current())
|
|
589
603
|
return "protected";
|
|
@@ -1331,13 +1345,20 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1331
1345
|
requestDispatchNextQueuedTelegramTurn(ctx);
|
|
1332
1346
|
},
|
|
1333
1347
|
});
|
|
1334
|
-
const cloneTelegramMessagesForThread = (messages, threadId) => {
|
|
1335
|
-
return messages.map((message) =>
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1348
|
+
const cloneTelegramMessagesForThread = (messages, threadId, copies) => {
|
|
1349
|
+
return messages.map((message) => {
|
|
1350
|
+
const ids = Updates.collectTelegramAdmissionSourceUpdateIds([message]);
|
|
1351
|
+
const copyId = copies && ids.length === 1 ? copies.get(ids[0]) : undefined;
|
|
1352
|
+
if (copies && (!Number.isSafeInteger(copyId) || copyId <= 0))
|
|
1353
|
+
throw new Error("Reroute destination copy identity is unavailable.");
|
|
1354
|
+
const routed = {
|
|
1355
|
+
...message,
|
|
1356
|
+
message_id: copyId ?? 0,
|
|
1357
|
+
message_thread_id: threadId,
|
|
1358
|
+
reply_to_message: undefined,
|
|
1359
|
+
};
|
|
1360
|
+
return Updates.carryTelegramUpdateExecutionFence(message, copies ? Turns.withTelegramTurnReplyAnchor(routed) : routed);
|
|
1361
|
+
});
|
|
1341
1362
|
};
|
|
1342
1363
|
/** Whether a chooser is still pending in this Thread, ignoring the ones the caller itself owns. */
|
|
1343
1364
|
const hasPendingRerouteForTarget = (target, own) => [...pendingUnboundReroutes.values()].some((pending) => pending.sourceTarget.chatId === target.chatId &&
|
|
@@ -1603,10 +1624,12 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1603
1624
|
return !acceptedWorkClear;
|
|
1604
1625
|
};
|
|
1605
1626
|
/** 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) => {
|
|
1627
|
+
const deleteAllTabMessages = async (chatId, messageIds, isCurrent) => {
|
|
1607
1628
|
if (!deps.deleteMessage)
|
|
1608
1629
|
return;
|
|
1609
1630
|
for (const messageId of messageIds) {
|
|
1631
|
+
if (isCurrent?.() === false)
|
|
1632
|
+
return;
|
|
1610
1633
|
try {
|
|
1611
1634
|
await deps.deleteMessage(chatId, messageId);
|
|
1612
1635
|
}
|
|
@@ -1620,13 +1643,14 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1620
1643
|
}
|
|
1621
1644
|
};
|
|
1622
1645
|
/** 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) ?? []);
|
|
1646
|
+
const deleteAllTabCopies = (pending, isCurrent) => deleteAllTabMessages(pending.sourceTarget.chatId, pending.allTabCopies?.splice(0) ?? [], isCurrent);
|
|
1624
1647
|
/**
|
|
1625
1648
|
* Restore keeps the tab as the Thread, so the input must stay visible there: the All original goes only when the tab
|
|
1626
1649
|
* holds its forwarded copy. Without one it stays where the operator can still read it.
|
|
1627
1650
|
*/
|
|
1628
1651
|
const releaseAllTabCopiesIntoSurvivingTab = (pending) => {
|
|
1629
|
-
if (pending.allTabCopiesForwarded
|
|
1652
|
+
if (pending.allTabCopiesForwarded &&
|
|
1653
|
+
isDeepStrictEqual(pending.allTabCopiesForwarded.target, pending.sourceTarget))
|
|
1630
1654
|
return deleteAllTabCopies(pending);
|
|
1631
1655
|
pending.allTabCopies?.splice(0);
|
|
1632
1656
|
return Promise.resolve();
|
|
@@ -1640,39 +1664,109 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1640
1664
|
const messageIds = [...(pending.allTabCopies ?? [])].sort((a, b) => a - b);
|
|
1641
1665
|
if (messageIds.length === 0 || !deps.callApi || !isCurrent())
|
|
1642
1666
|
return;
|
|
1667
|
+
const copyTarget = { ...target };
|
|
1643
1668
|
const route = {
|
|
1644
|
-
chat_id:
|
|
1645
|
-
message_thread_id:
|
|
1669
|
+
chat_id: copyTarget.chatId,
|
|
1670
|
+
message_thread_id: copyTarget.threadId,
|
|
1646
1671
|
from_chat_id: pending.sourceTarget.chatId,
|
|
1647
1672
|
disable_notification: true,
|
|
1648
1673
|
};
|
|
1674
|
+
const options = {
|
|
1675
|
+
maxAttempts: 1,
|
|
1676
|
+
retrySafety: "non-idempotent",
|
|
1677
|
+
assertAuthority() {
|
|
1678
|
+
if (!isCurrent())
|
|
1679
|
+
throw new Error("Temporary Thread source forwarding authority changed.");
|
|
1680
|
+
},
|
|
1681
|
+
};
|
|
1649
1682
|
try {
|
|
1650
1683
|
// An album moves as one album; forwardMessages skips what it cannot forward, so a short reply is not a move.
|
|
1651
1684
|
const copies = messageIds.length === 1
|
|
1652
1685
|
? [
|
|
1653
|
-
await deps.callApi("forwardMessage", {
|
|
1654
|
-
...route,
|
|
1655
|
-
message_id: messageIds[0],
|
|
1656
|
-
}),
|
|
1686
|
+
await deps.callApi("forwardMessage", { ...route, message_id: messageIds[0] }, options),
|
|
1657
1687
|
]
|
|
1658
|
-
: await deps.callApi("forwardMessages", { ...route, message_ids: messageIds });
|
|
1688
|
+
: await deps.callApi("forwardMessages", { ...route, message_ids: messageIds }, options);
|
|
1689
|
+
if (!isCurrent())
|
|
1690
|
+
return;
|
|
1659
1691
|
const copyIds = Array.isArray(copies)
|
|
1660
1692
|
? copies.map((copy) => copy?.message_id)
|
|
1661
1693
|
: [];
|
|
1662
1694
|
if (copyIds.length !== messageIds.length ||
|
|
1663
|
-
!copyIds.every((id) => typeof id === "number" && Number.isSafeInteger(id))
|
|
1695
|
+
!copyIds.every((id) => typeof id === "number" && Number.isSafeInteger(id) && id > 0) ||
|
|
1696
|
+
new Set(copyIds).size !== copyIds.length)
|
|
1664
1697
|
return;
|
|
1665
|
-
pending.allTabCopiesForwarded =
|
|
1666
|
-
|
|
1667
|
-
|
|
1698
|
+
pending.allTabCopiesForwarded = {
|
|
1699
|
+
target: copyTarget,
|
|
1700
|
+
sourceMessageIds: messageIds,
|
|
1701
|
+
messageIds: copyIds,
|
|
1702
|
+
};
|
|
1703
|
+
await deleteAllTabCopies(pending, isCurrent);
|
|
1704
|
+
if (!isCurrent())
|
|
1705
|
+
return;
|
|
1706
|
+
return pending.allTabCopiesForwarded.messageIds[0];
|
|
1668
1707
|
}
|
|
1669
1708
|
catch (error) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1709
|
+
if (isCurrent())
|
|
1710
|
+
deps.recordRuntimeEvent?.("routing", error, {
|
|
1711
|
+
phase: "temporary-thread-source-forward",
|
|
1712
|
+
});
|
|
1673
1713
|
return;
|
|
1674
1714
|
}
|
|
1675
1715
|
};
|
|
1716
|
+
/** Read original source addresses, not warm target rewrites; unknown copy provenance never grants a Reroute. */
|
|
1717
|
+
const resolvePendingRerouteForwardSource = (pending) => {
|
|
1718
|
+
try {
|
|
1719
|
+
const originals = pending.messages.map((message) => {
|
|
1720
|
+
const snapshot = Updates.inspectTelegramDeferredSourceSnapshot(message);
|
|
1721
|
+
const ids = Updates.collectTelegramAdmissionSourceUpdateIds([message]);
|
|
1722
|
+
const original = snapshot?.update.message;
|
|
1723
|
+
if (!snapshot ||
|
|
1724
|
+
ids.length !== 1 ||
|
|
1725
|
+
snapshot.source.updateId !== ids[0] ||
|
|
1726
|
+
snapshot.update.update_id !== ids[0] ||
|
|
1727
|
+
Object.keys(snapshot.update).some((key) => key !== "update_id" && key !== "message") ||
|
|
1728
|
+
snapshot.source.journalBindingKey !== deps.getAdmissionJournalBinding?.() ||
|
|
1729
|
+
!original ||
|
|
1730
|
+
original.chat?.type !== "private" ||
|
|
1731
|
+
original.chat.id !== pending.sourceTarget.chatId ||
|
|
1732
|
+
original.from?.id !== deps.configStore.getAllowedUserId() ||
|
|
1733
|
+
original.from?.is_bot !== false ||
|
|
1734
|
+
!Number.isSafeInteger(original.message_id) ||
|
|
1735
|
+
original.message_id <= 0)
|
|
1736
|
+
return undefined;
|
|
1737
|
+
return { message: original, updateId: ids[0] };
|
|
1738
|
+
});
|
|
1739
|
+
if (originals.length === 0 || originals.some((source) => !source))
|
|
1740
|
+
return;
|
|
1741
|
+
originals.sort((a, b) => a.message.message_id - b.message.message_id);
|
|
1742
|
+
const sourceMessageIds = originals.map((source) => source.message.message_id);
|
|
1743
|
+
const sourceUpdateIds = originals.map((source) => source.updateId);
|
|
1744
|
+
if (new Set(sourceMessageIds).size !== sourceMessageIds.length ||
|
|
1745
|
+
new Set(sourceUpdateIds).size !== sourceUpdateIds.length)
|
|
1746
|
+
return;
|
|
1747
|
+
const copied = pending.allTabCopiesForwarded;
|
|
1748
|
+
if (copied) {
|
|
1749
|
+
if (!isDeepStrictEqual(copied.target, pending.sourceTarget) ||
|
|
1750
|
+
!isDeepStrictEqual(copied.sourceMessageIds, sourceMessageIds) ||
|
|
1751
|
+
copied.messageIds.length !== sourceMessageIds.length ||
|
|
1752
|
+
!copied.messageIds.every((id) => Number.isSafeInteger(id) && id > 0) ||
|
|
1753
|
+
new Set(copied.messageIds).size !== copied.messageIds.length)
|
|
1754
|
+
return;
|
|
1755
|
+
return { chatId: copied.target.chatId, sourceUpdateIds, sourceMessageIds, messageIds: [...copied.messageIds] };
|
|
1756
|
+
}
|
|
1757
|
+
const nativeOriginals = originals.every((source) => typeof pending.sourceTarget.threadId === "number" &&
|
|
1758
|
+
source.message.message_thread_id === pending.sourceTarget.threadId);
|
|
1759
|
+
const retainedAllOriginals = pending.allTabSourceRetained === true &&
|
|
1760
|
+
originals.every((source) => source.message.message_thread_id === undefined) &&
|
|
1761
|
+
isDeepStrictEqual([...(pending.allTabCopies ?? [])].sort((a, b) => a - b), sourceMessageIds);
|
|
1762
|
+
if (nativeOriginals || retainedAllOriginals)
|
|
1763
|
+
return { chatId: pending.sourceTarget.chatId, sourceUpdateIds, sourceMessageIds, messageIds: [...sourceMessageIds] };
|
|
1764
|
+
}
|
|
1765
|
+
catch (error) {
|
|
1766
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-source-proof" });
|
|
1767
|
+
}
|
|
1768
|
+
return undefined;
|
|
1769
|
+
};
|
|
1676
1770
|
const dismissRerouteChooserMessage = async (query, assertExecutionCurrent) => {
|
|
1677
1771
|
const chatId = query.message?.chat?.id;
|
|
1678
1772
|
const messageId = query.message?.message_id;
|
|
@@ -3020,7 +3114,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
3020
3114
|
Bus.isSameTelegramBusFollowerRegistration(live, follower) &&
|
|
3021
3115
|
[
|
|
3022
3116
|
...Bus.TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
3023
|
-
...(heldSelected ? Bus.
|
|
3117
|
+
...(heldSelected ? Bus.getTelegramBusHeldCommandCapabilities(heldSelected.name) : []),
|
|
3024
3118
|
].every((cap) => Bus.hasTelegramBusCapability(live.protocol, cap)) &&
|
|
3025
3119
|
!!live.target &&
|
|
3026
3120
|
live.target.chatId === binding.target.chatId &&
|
|
@@ -3573,6 +3667,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
3573
3667
|
return {
|
|
3574
3668
|
operationId: request.operationId,
|
|
3575
3669
|
selectedCommandReference: () => coordinator?.selectedCommandReference(),
|
|
3670
|
+
observeCommandCompletion: () => coordinator?.observeCommandCompletion(),
|
|
3576
3671
|
async advance() {
|
|
3577
3672
|
if (!coordinator) {
|
|
3578
3673
|
if (!current() || !getRecipient())
|
|
@@ -4076,7 +4171,24 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4076
4171
|
wake(reason);
|
|
4077
4172
|
};
|
|
4078
4173
|
const onSessionSettled = (ctx) => wakeLiveRebindCleanup(ctx, "session-settled");
|
|
4079
|
-
const onDeliverySettled = (ctx) =>
|
|
4174
|
+
const onDeliverySettled = (ctx) => {
|
|
4175
|
+
wakeLiveRebindCleanup(ctx, "delivery-drained");
|
|
4176
|
+
const cap = captureTemporaryThreadAuthority(ctx);
|
|
4177
|
+
if (!cap?.isCurrent())
|
|
4178
|
+
return;
|
|
4179
|
+
try {
|
|
4180
|
+
for (const entry of cap.store.listTemporaryThreads()) {
|
|
4181
|
+
if (!cap.isCurrent())
|
|
4182
|
+
return;
|
|
4183
|
+
if (entry.phase === "created" && entry.target && !entry.cleanupIssued &&
|
|
4184
|
+
Threads.isTelegramTemporaryThreadFullyResolved(entry))
|
|
4185
|
+
scheduleTemporaryThreadCleanup(entry.target, ctx);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
catch (error) {
|
|
4189
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "temporary-thread-cleanup-drain" });
|
|
4190
|
+
}
|
|
4191
|
+
};
|
|
4080
4192
|
const restoreWorkspace = deps.workspaceRestoreRecipient
|
|
4081
4193
|
? async (input) => {
|
|
4082
4194
|
const ports = deps.workspaceRestoreRecipient;
|
|
@@ -4532,6 +4644,92 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4532
4644
|
return false;
|
|
4533
4645
|
}
|
|
4534
4646
|
};
|
|
4647
|
+
/** Publish display copies under the native one-shot grant; positive output is not input/source settlement. */
|
|
4648
|
+
const publishPendingRerouteCopies = async (pending, record, ctx, assertExecutionCurrent, foreign) => {
|
|
4649
|
+
const source = resolvePendingRerouteForwardSource(pending);
|
|
4650
|
+
const target = { ...record.target };
|
|
4651
|
+
const recordIdentity = (value) => value && ({
|
|
4652
|
+
target: value.target, profileKey: value.profileKey, owner: value.owner,
|
|
4653
|
+
instanceId: value.instanceId, slot: value.slot, createdAtMs: value.createdAtMs,
|
|
4654
|
+
});
|
|
4655
|
+
const bindingIdentity = () => deps.threadStore.listWorkspaceBindings()
|
|
4656
|
+
.filter((value) => isDeepStrictEqual(value.target, target))
|
|
4657
|
+
.map((value) => ({ bindingKey: value.bindingKey, sessionId: value.sessionId, sessionKey: value.sessionKey,
|
|
4658
|
+
cwd: value.cwd, workspaceKey: value.workspaceKey, instanceSlot: value.instanceSlot, slot: value.slot,
|
|
4659
|
+
target: value.target }));
|
|
4660
|
+
const expectedRecord = structuredClone(recordIdentity(record));
|
|
4661
|
+
const expectedBindings = structuredClone(bindingIdentity());
|
|
4662
|
+
const ownership = deps.getTargetOwnership?.(target);
|
|
4663
|
+
if (!source || !deps.callApi || expectedBindings.length > 1 ||
|
|
4664
|
+
(foreign && (!ownership || ownership.instanceId !== record.instanceId ||
|
|
4665
|
+
!ownership.ownerGeneration || !ownership.recipientBindingKey)))
|
|
4666
|
+
return;
|
|
4667
|
+
const expectedOwnership = ownership && structuredClone(ownership);
|
|
4668
|
+
const cap = captureTemporaryThreadAuthority(ctx, () => {
|
|
4669
|
+
try {
|
|
4670
|
+
assertExecutionCurrent();
|
|
4671
|
+
const current = getTelegramRoutableThreadRecords(deps.threadStore.list(), deps.getLiveThreadTargets?.())
|
|
4672
|
+
.find((value) => isDeepStrictEqual(value.target, target));
|
|
4673
|
+
return isDeepStrictEqual(recordIdentity(current), expectedRecord) &&
|
|
4674
|
+
isDeepStrictEqual(bindingIdentity(), expectedBindings) &&
|
|
4675
|
+
(!foreign || Bus.isTelegramBusForwardOwnershipCurrent(expectedOwnership, deps.getTargetOwnership?.(target)));
|
|
4676
|
+
}
|
|
4677
|
+
catch {
|
|
4678
|
+
return false;
|
|
4679
|
+
}
|
|
4680
|
+
});
|
|
4681
|
+
if (!cap?.isCurrent())
|
|
4682
|
+
return;
|
|
4683
|
+
const temporary = pending.temporaryThread ?? findCreatedTemporaryThread(pending.sourceTarget);
|
|
4684
|
+
if (!temporary || !issueTemporaryThreadForward(pending, ctx))
|
|
4685
|
+
return;
|
|
4686
|
+
pending.phase = { kind: "forward-unknown" };
|
|
4687
|
+
const assertRecipientCurrent = () => {
|
|
4688
|
+
assertExecutionCurrent();
|
|
4689
|
+
if (!cap.isCurrent())
|
|
4690
|
+
throw new Error("Reroute publication recipient authority changed.");
|
|
4691
|
+
};
|
|
4692
|
+
const assertPublicationCurrent = () => {
|
|
4693
|
+
assertRecipientCurrent();
|
|
4694
|
+
const entry = cap.store.listTemporaryThreads().find((value) => value.token === temporary.token);
|
|
4695
|
+
const ids = [...source.sourceUpdateIds].sort((a, b) => a - b);
|
|
4696
|
+
if (!entry || entry.phase !== "created" || entry.cleanupIssued ||
|
|
4697
|
+
entry.operatorUserId !== cap.operatorUserId ||
|
|
4698
|
+
!isDeepStrictEqual(entry.executor, cap.authority.executor) ||
|
|
4699
|
+
!isDeepStrictEqual(entry.target, pending.sourceTarget) ||
|
|
4700
|
+
!entry.forwardedInputs?.some((input) => input.journalBindingKey === cap.journalBindingKey &&
|
|
4701
|
+
isDeepStrictEqual(input.updateIds, ids)) ||
|
|
4702
|
+
!isDeepStrictEqual(resolvePendingRerouteForwardSource(pending), source))
|
|
4703
|
+
throw new Error("Reroute publication source authority changed.");
|
|
4704
|
+
};
|
|
4705
|
+
try {
|
|
4706
|
+
assertPublicationCurrent();
|
|
4707
|
+
// Bot API requires ascending source IDs. Preserve the source-UID association even if copy IDs reorder them.
|
|
4708
|
+
const order = source.messageIds.map((_, index) => index)
|
|
4709
|
+
.sort((a, b) => source.messageIds[a] - source.messageIds[b]);
|
|
4710
|
+
const messageIds = order.map((index) => source.messageIds[index]);
|
|
4711
|
+
const body = { chat_id: target.chatId, message_thread_id: target.threadId,
|
|
4712
|
+
from_chat_id: source.chatId, disable_notification: true };
|
|
4713
|
+
const options = { maxAttempts: 1, retrySafety: "non-idempotent", assertAuthority: assertPublicationCurrent };
|
|
4714
|
+
const copies = messageIds.length === 1
|
|
4715
|
+
? [await deps.callApi("forwardMessage", { ...body, message_id: messageIds[0] }, options)]
|
|
4716
|
+
: await deps.callApi("forwardMessages", { ...body, message_ids: messageIds }, options);
|
|
4717
|
+
assertPublicationCurrent();
|
|
4718
|
+
const copyIds = Array.isArray(copies) ? copies.map((copy) => copy?.message_id) : [];
|
|
4719
|
+
if (copyIds.length !== messageIds.length ||
|
|
4720
|
+
!copyIds.every((id) => typeof id === "number" && Number.isSafeInteger(id) && id > 0) ||
|
|
4721
|
+
new Set(copyIds).size !== copyIds.length)
|
|
4722
|
+
return;
|
|
4723
|
+
pending.rerouteCopies = { target, bySourceUpdateId: new Map(order.map((index, position) => [source.sourceUpdateIds[index], copyIds[position]])) };
|
|
4724
|
+
pending.phase = { kind: "selected" };
|
|
4725
|
+
return assertRecipientCurrent;
|
|
4726
|
+
}
|
|
4727
|
+
catch (error) {
|
|
4728
|
+
if (cap.isCurrent())
|
|
4729
|
+
deps.recordRuntimeEvent?.("routing", error, { phase: "reroute-destination-forward" });
|
|
4730
|
+
return;
|
|
4731
|
+
}
|
|
4732
|
+
};
|
|
4535
4733
|
const forwardPendingRerouteMessages = async (pending, instanceId, threadId, ctx, assertExecutionCurrent, recordForwardAcceptance) => {
|
|
4536
4734
|
assertExecutionCurrent?.();
|
|
4537
4735
|
const forwardMessage = deps.foreignOwnedUpdateForwarder?.forwardMessage;
|
|
@@ -4545,11 +4743,14 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4545
4743
|
!liveOwnership.recipientBindingKey)
|
|
4546
4744
|
return false;
|
|
4547
4745
|
const ownership = structuredClone(liveOwnership);
|
|
4548
|
-
const
|
|
4746
|
+
const copies = pending.rerouteCopies;
|
|
4747
|
+
const published = !!copies && isDeepStrictEqual(copies.target, target);
|
|
4748
|
+
const messages = cloneTelegramMessagesForThread(pending.messages, threadId, published ? copies.bySourceUpdateId : undefined);
|
|
4549
4749
|
// Issuance is durable before the RPC: an unacknowledged temporary-tab Forward never becomes another dispatch grant,
|
|
4550
4750
|
// even after restart. Any refusal or publication failure sends nothing and keeps the input held.
|
|
4551
4751
|
if (!recordForwardAcceptance &&
|
|
4552
4752
|
isTemporaryReroute(pending) &&
|
|
4753
|
+
!published &&
|
|
4553
4754
|
!issueTemporaryThreadForward(pending, ctx)) {
|
|
4554
4755
|
if (isTemporaryThreadForwardIssued(pending))
|
|
4555
4756
|
pending.phase = { kind: "forward-unknown" };
|
|
@@ -4561,6 +4762,10 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4561
4762
|
message,
|
|
4562
4763
|
ownership,
|
|
4563
4764
|
ctx,
|
|
4765
|
+
assertAuthority() {
|
|
4766
|
+
assertExecutionCurrent?.();
|
|
4767
|
+
Updates.assertTelegramUpdateExecutionCurrent(message);
|
|
4768
|
+
},
|
|
4564
4769
|
})));
|
|
4565
4770
|
assertExecutionCurrent?.();
|
|
4566
4771
|
if (!isDeepStrictEqual(deps.getTargetOwnership?.(target), ownership))
|
|
@@ -4719,6 +4924,17 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
4719
4924
|
if (retry)
|
|
4720
4925
|
restoreSettlementTasks.add(task);
|
|
4721
4926
|
runTemporaryThreadCleanup(token, cap, reconcileExpiry)
|
|
4927
|
+
.then(() => {
|
|
4928
|
+
if (!cap.isCurrent())
|
|
4929
|
+
return;
|
|
4930
|
+
const retained = cap.store.listTemporaryThreads().find(value => value.token === token);
|
|
4931
|
+
// A follower ACK can precede recipient receipt disposal. Reobserve a protected, unissued tab;
|
|
4932
|
+
// elapsed time never grants deletion, and an issued/unknown delete can never enter this path.
|
|
4933
|
+
if (retained?.phase === "created" && !retained.cleanupIssued &&
|
|
4934
|
+
Threads.isTelegramTemporaryThreadFullyResolved(retained) &&
|
|
4935
|
+
cap.store.inspectTemporaryThreadTarget(retained, cap.authority)?.kind === "temporary" && cap.isCurrent())
|
|
4936
|
+
queueTemporaryThreadCleanup(token, cap, 1000, reconcileExpiry, true);
|
|
4937
|
+
})
|
|
4722
4938
|
.catch((error) => {
|
|
4723
4939
|
deps.recordRuntimeEvent?.("routing", error, {
|
|
4724
4940
|
phase: "temporary-thread-cleanup",
|
|
@@ -5244,7 +5460,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5244
5460
|
for (const [id, pending] of pendingUnboundReroutes) {
|
|
5245
5461
|
if (!Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages).includes(source.original.updateId))
|
|
5246
5462
|
continue;
|
|
5247
|
-
movedIntoTab ||= pending.allTabCopiesForwarded
|
|
5463
|
+
movedIntoTab ||= pending.allTabCopiesForwarded !== undefined;
|
|
5248
5464
|
removePendingReroute(id);
|
|
5249
5465
|
if (signal.aborted ||
|
|
5250
5466
|
deps.isContextActive?.(ctx) !== true ||
|
|
@@ -5983,6 +6199,52 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
5983
6199
|
const operationId = selection.coordinator.operationId;
|
|
5984
6200
|
if (current())
|
|
5985
6201
|
outcome = await selection.coordinator.advance();
|
|
6202
|
+
const completionTimeoutMs = deps.liveRebindCommandCompletionTimeoutMs ?? 0;
|
|
6203
|
+
if (outcome === "unknown" && !selection.commandCompletionWaited && selection.record.owner?.kind === "leader" &&
|
|
6204
|
+
(selection.helpCommand || selection.menuCommand || selection.abortCommand || selection.nextCommand || selection.stopCommand) &&
|
|
6205
|
+
Number.isFinite(completionTimeoutMs) && completionTimeoutMs > 0 &&
|
|
6206
|
+
selection.coordinator.observeCommandCompletion() !== undefined) {
|
|
6207
|
+
selection.commandCompletionWaited = true;
|
|
6208
|
+
const coordinator = selection.coordinator;
|
|
6209
|
+
const signal = Updates.getTelegramUpdateExecutionFence(query)?.signal;
|
|
6210
|
+
outcome = await new Promise((resolve) => {
|
|
6211
|
+
let settled = false;
|
|
6212
|
+
const subscriptions = [];
|
|
6213
|
+
const finish = (result) => {
|
|
6214
|
+
if (settled)
|
|
6215
|
+
return;
|
|
6216
|
+
settled = true;
|
|
6217
|
+
clearTimeout(timer);
|
|
6218
|
+
for (const unsubscribe of subscriptions)
|
|
6219
|
+
unsubscribe();
|
|
6220
|
+
signal?.removeEventListener("abort", abort);
|
|
6221
|
+
resolve(result);
|
|
6222
|
+
};
|
|
6223
|
+
const abort = () => finish("unknown");
|
|
6224
|
+
const observe = () => {
|
|
6225
|
+
if (settled)
|
|
6226
|
+
return;
|
|
6227
|
+
if (!current()) {
|
|
6228
|
+
finish("unknown");
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6231
|
+
if (coordinator.observeCommandCompletion() === "released")
|
|
6232
|
+
finish("released");
|
|
6233
|
+
};
|
|
6234
|
+
const timer = setTimeout(abort, completionTimeoutMs);
|
|
6235
|
+
timer.unref?.();
|
|
6236
|
+
for (const message of selection.messages) {
|
|
6237
|
+
const unsubscribe = Updates.subscribeTelegramDeferredSourceCompletion(message, observe);
|
|
6238
|
+
if (unsubscribe)
|
|
6239
|
+
subscriptions.push(unsubscribe);
|
|
6240
|
+
}
|
|
6241
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
6242
|
+
if (signal?.aborted || !current())
|
|
6243
|
+
abort();
|
|
6244
|
+
else if (coordinator.observeCommandCompletion() === "released")
|
|
6245
|
+
finish("released");
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
5986
6248
|
// Cleanup readiness follows the canonical released row, not source disposal: a detached command ACK may
|
|
5987
6249
|
// still be pending while every attempt samples current work afresh.
|
|
5988
6250
|
const row = outcome === "protected"
|
|
@@ -6047,6 +6309,12 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6047
6309
|
: "Input queued in the rebound Thread; old-Thread cleanup is scheduled, not confirmed", assertExecutionCurrent, true);
|
|
6048
6310
|
return true;
|
|
6049
6311
|
}
|
|
6312
|
+
if (!parsed.useNewSlot &&
|
|
6313
|
+
isTemporaryReroute(pending) &&
|
|
6314
|
+
!resolvePendingRerouteForwardSource(pending)) {
|
|
6315
|
+
await deps.answerCallbackQuery(query.id, "Source copy unavailable; not routing");
|
|
6316
|
+
return true;
|
|
6317
|
+
}
|
|
6050
6318
|
if (!(await selectPendingDestination()))
|
|
6051
6319
|
return true;
|
|
6052
6320
|
if (parsed.useNewSlot && sourceTarget && restoreWorkspace) {
|
|
@@ -6130,10 +6398,22 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6130
6398
|
await deps.answerCallbackQuery(query.id, delivered ? "Restored" : "Restore unconfirmed; not resending");
|
|
6131
6399
|
return true;
|
|
6132
6400
|
}
|
|
6401
|
+
let assertRerouteCurrent = assertExecutionCurrent;
|
|
6402
|
+
if (isTemporaryReroute(pending)) {
|
|
6403
|
+
const confirmed = await publishPendingRerouteCopies(pending, record, ctx, assertExecutionCurrent, !!record.instanceId && record.instanceId !== currentInstanceId && !isCurrentLeaderRecord);
|
|
6404
|
+
if (!confirmed) {
|
|
6405
|
+
await deps.answerCallbackQuery(query.id, pending.phase.kind === "forward-unknown"
|
|
6406
|
+
? "Publication unconfirmed; not resending"
|
|
6407
|
+
: "Publication unavailable; input kept");
|
|
6408
|
+
return true;
|
|
6409
|
+
}
|
|
6410
|
+
assertRerouteCurrent = confirmed;
|
|
6411
|
+
}
|
|
6412
|
+
assertRerouteCurrent();
|
|
6133
6413
|
if (record.instanceId &&
|
|
6134
6414
|
record.instanceId !== currentInstanceId &&
|
|
6135
6415
|
!isCurrentLeaderRecord) {
|
|
6136
|
-
const allForwarded = await forwardPendingRerouteMessages(pending, record.instanceId, parsed.threadId, ctx,
|
|
6416
|
+
const allForwarded = await forwardPendingRerouteMessages(pending, record.instanceId, parsed.threadId, ctx, assertRerouteCurrent);
|
|
6137
6417
|
if (!allForwarded) {
|
|
6138
6418
|
await deps.answerCallbackQuery(query.id, pending.phase.kind === "forward-unknown"
|
|
6139
6419
|
? "Delivery unconfirmed; not resending"
|
|
@@ -6144,11 +6424,16 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
6144
6424
|
}
|
|
6145
6425
|
// Local handlers and queue admission consume the same one-time group grant as follower forwarding.
|
|
6146
6426
|
if (isTemporaryReroute(pending) &&
|
|
6427
|
+
!pending.rerouteCopies &&
|
|
6147
6428
|
!issueTemporaryThreadForward(pending, ctx)) {
|
|
6148
6429
|
await deps.answerCallbackQuery(query.id, "Delivery unconfirmed; not resending");
|
|
6149
6430
|
return true;
|
|
6150
6431
|
}
|
|
6151
|
-
|
|
6432
|
+
const localMessages = pending.rerouteCopies
|
|
6433
|
+
? cloneTelegramMessagesForThread(pending.messages, parsed.threadId, pending.rerouteCopies.bySourceUpdateId)
|
|
6434
|
+
: reroutedMessages;
|
|
6435
|
+
await dispatchPendingRerouteMessages(pending, localMessages, ctx);
|
|
6436
|
+
assertRerouteCurrent();
|
|
6152
6437
|
pending.messages = [];
|
|
6153
6438
|
return completeRoutedReroute();
|
|
6154
6439
|
};
|
|
@@ -7325,6 +7610,14 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
7325
7610
|
const tabCopyId = openedNow
|
|
7326
7611
|
? await moveAllTabSourceIntoTab(pending, target, current)
|
|
7327
7612
|
: undefined;
|
|
7613
|
+
if (!current()) {
|
|
7614
|
+
removePendingReroute(rerouteId);
|
|
7615
|
+
throw new Error("Temporary Thread source presentation authority changed.");
|
|
7616
|
+
}
|
|
7617
|
+
if (openedNow &&
|
|
7618
|
+
!pending.allTabCopiesForwarded &&
|
|
7619
|
+
pending.allTabCopies.length === held.length)
|
|
7620
|
+
pending.allTabSourceRetained = true;
|
|
7328
7621
|
try {
|
|
7329
7622
|
if (!deps.sendInteractiveMessage)
|
|
7330
7623
|
throw new Error("Temporary Thread chooser publication is unavailable.");
|
|
@@ -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;
|
|
@@ -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),
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { getTelegramTargetKey } from "./target.js";
|
|
7
7
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError, } from "./telegram-api.js";
|
|
8
8
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
9
|
-
import { commitTelegramWorkspaceProvisionBinding, createTelegramCleanupTargetProtection, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, } from "./threads.js";
|
|
9
|
+
import { commitTelegramWorkspaceProvisionBinding, createTelegramCleanupTargetProtection, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, resetTelegramConnectionProvisioning, } from "./threads.js";
|
|
10
10
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
11
11
|
import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
|
|
12
12
|
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
|
|
@@ -463,11 +463,21 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
463
463
|
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
464
464
|
};
|
|
465
465
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
466
|
+
if (deps.forceFreshUnnamed) {
|
|
467
|
+
await resetTelegramConnectionProvisioning({
|
|
468
|
+
store: deps.topicTargetStore, owner: "leader", instanceId: deps.instanceId,
|
|
469
|
+
profileKey: leaderProfileKey, isCurrent: () => {
|
|
470
|
+
assertLeaderEpoch("connect-recovery");
|
|
471
|
+
return true;
|
|
472
|
+
}, recordEvent: deps.recordEvent,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
466
475
|
const legacyLeaderRecord = deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
467
476
|
let capacityUnavailable = false;
|
|
468
477
|
const workspaceIdentity = normalizedLeaderCwd
|
|
469
478
|
? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, {
|
|
470
479
|
sessionId: deps.sessionId,
|
|
480
|
+
provisionProfileKey: leaderProfileKey,
|
|
471
481
|
onCapacityUnavailable() {
|
|
472
482
|
capacityUnavailable = true;
|
|
473
483
|
},
|
|
@@ -495,6 +495,7 @@ export interface TelegramTopicTargetStore {
|
|
|
495
495
|
} | undefined;
|
|
496
496
|
listReservations: () => TelegramThreadReservation[];
|
|
497
497
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
498
|
+
resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
|
|
498
499
|
listPendingCleanups: () => TelegramThreadCleanupIntent[];
|
|
499
500
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
500
501
|
reserveThread: (reservation: TelegramThreadReservation) => void;
|
|
@@ -559,6 +560,7 @@ export interface TelegramTopicTargetStore {
|
|
|
559
560
|
claimWorkspaceIdentity: (cwd: string, instanceId: string, previousInstanceId?: string, options?: {
|
|
560
561
|
existingBindingOnly?: boolean;
|
|
561
562
|
sessionId?: string;
|
|
563
|
+
provisionProfileKey?: string;
|
|
562
564
|
onCapacityUnavailable?: () => void;
|
|
563
565
|
}) => TelegramWorkspaceBindingIdentity | undefined;
|
|
564
566
|
releaseWorkspaceClaim: (instanceId: string) => boolean;
|
|
@@ -957,4 +959,13 @@ export declare function getTelegramTargetFromApiBody(body: unknown): (TelegramTa
|
|
|
957
959
|
export declare function isTelegramTopicTargetStaleError(error: unknown): boolean;
|
|
958
960
|
export declare function isTelegramTopicModeUnavailableError(error: unknown): boolean;
|
|
959
961
|
export declare function createTelegramTopicTargetRenamer(deps: TelegramTopicTargetRenamerDeps): (request: TelegramTopicTargetRenameRequest) => Promise<TelegramTopicTargetRecord | undefined>;
|
|
962
|
+
/** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
|
|
963
|
+
export declare function resetTelegramConnectionProvisioning(input: {
|
|
964
|
+
store: TelegramTopicTargetStore;
|
|
965
|
+
owner: TelegramThreadPendingProvision["owner"];
|
|
966
|
+
instanceId: string;
|
|
967
|
+
profileKey: string;
|
|
968
|
+
isCurrent: () => boolean;
|
|
969
|
+
recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
970
|
+
}): Promise<void>;
|
|
960
971
|
export declare function createTelegramTopicTargetProvisioner(deps: TelegramTopicTargetProvisionerDeps): (request: TelegramTopicTargetProvisionRequest) => Promise<TelegramTopicTargetProvisionResult>;
|