@llblab/pi-telegram 0.50.0 → 0.51.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 +9 -7
- package/BACKLOG.md +13 -32
- package/CHANGELOG.md +18 -0
- package/README.md +5 -5
- package/dist/lib/bindings.d.ts +3 -5
- package/dist/lib/bindings.js +4 -11
- package/dist/lib/bus-follower.d.ts +1 -1
- package/dist/lib/bus-follower.js +93 -9
- package/dist/lib/bus-leader.d.ts +9 -3
- package/dist/lib/bus-leader.js +231 -71
- package/dist/lib/bus.d.ts +4 -0
- package/dist/lib/bus.js +34 -7
- package/dist/lib/commands.d.ts +1 -4
- package/dist/lib/commands.js +13 -33
- package/dist/lib/extension.js +64 -6
- package/dist/lib/journal.d.ts +3 -0
- package/dist/lib/journal.js +13 -0
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +25 -4
- package/dist/lib/locks.d.ts +1 -0
- package/dist/lib/locks.js +32 -12
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +72 -1
- package/dist/lib/paths.d.ts +10 -0
- package/dist/lib/paths.js +22 -1
- package/dist/lib/polling.js +2 -2
- package/dist/lib/queue.d.ts +8 -1
- package/dist/lib/queue.js +28 -2
- package/dist/lib/replies.d.ts +3 -0
- package/dist/lib/replies.js +17 -0
- package/dist/lib/sync.d.ts +9 -0
- package/dist/lib/sync.js +49 -2
- package/dist/lib/telegram-api.d.ts +9 -1
- package/dist/lib/telegram-api.js +23 -3
- package/dist/lib/thread-cleanup-manager.js +7 -2
- package/dist/lib/threads.d.ts +2 -0
- package/dist/lib/threads.js +68 -37
- package/dist/lib/updates.d.ts +6 -4
- package/dist/lib/updates.js +134 -61
- package/dist/lib/workspace-admission.d.ts +9 -0
- package/dist/lib/workspace-admission.js +49 -3
- package/dist/lib/workspace-retirement.d.ts +77 -6
- package/dist/lib/workspace-retirement.js +384 -8
- package/dist/lib/workspace-slots.d.ts +3 -0
- package/dist/lib/workspace-slots.js +6 -0
- package/dist/package.json +1 -1
- package/docs/architecture.md +23 -14
- package/docs/multi-instance-bus.md +32 -11
- package/docs/public-api.md +1 -1
- package/docs/updates.md +2 -0
- package/lib/bindings.ts +5 -17
- package/lib/bus-follower.ts +69 -12
- package/lib/bus-leader.ts +236 -94
- package/lib/bus.ts +39 -9
- package/lib/commands.ts +18 -41
- package/lib/extension.ts +70 -10
- package/lib/journal.ts +14 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/locks.ts +32 -13
- package/lib/menu-queue.ts +126 -1
- package/lib/paths.ts +29 -1
- package/lib/polling.ts +2 -2
- package/lib/queue.ts +48 -3
- package/lib/replies.ts +19 -0
- package/lib/sync.ts +53 -2
- package/lib/telegram-api.ts +30 -2
- package/lib/thread-cleanup-manager.ts +11 -3
- package/lib/threads.ts +73 -36
- package/lib/updates.ts +145 -78
- package/lib/workspace-admission.ts +67 -4
- package/lib/workspace-retirement.ts +465 -7
- package/lib/workspace-slots.ts +7 -0
- package/package.json +1 -1
package/dist/lib/updates.d.ts
CHANGED
|
@@ -441,6 +441,8 @@ export interface TelegramUpdateWorkerJournalSnapshot {
|
|
|
441
441
|
}
|
|
442
442
|
export interface TelegramUpdateWorkerJournalPort {
|
|
443
443
|
read: () => TelegramUpdateWorkerJournalSnapshot;
|
|
444
|
+
/** Optional strict observation; it must not recover, repair or grant new receipt authority. */
|
|
445
|
+
isQueueReceiptCurrent?: (receipt: TelegramQueueAdmissionReceiptLike, owner: TelegramUpdateJournalQueueOwner) => boolean;
|
|
444
446
|
markQueued: (receipt: {
|
|
445
447
|
queueKind: "prompt" | "control";
|
|
446
448
|
receiptId: string;
|
|
@@ -527,7 +529,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
527
529
|
receipts: readonly TelegramQueueAdmissionReceiptLike[];
|
|
528
530
|
ctx: TContext;
|
|
529
531
|
reason: TelegramQueueReceiptCompletionReason;
|
|
530
|
-
}) =>
|
|
532
|
+
}) => boolean;
|
|
531
533
|
stop: () => Promise<void>;
|
|
532
534
|
waitForDrain: () => Promise<void>;
|
|
533
535
|
getState: () => TelegramUpdateWorkerStateSnapshot;
|
|
@@ -1000,9 +1002,9 @@ export interface TelegramQueueAdmissionItemLike {
|
|
|
1000
1002
|
export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
|
|
1001
1003
|
isItemReady: (item: TelegramQueueAdmissionItemLike) => boolean;
|
|
1002
1004
|
getQueueReceiptOwner: (receipt: TelegramQueueAdmissionReceiptLike) => TelegramUpdateJournalQueueOwner | undefined;
|
|
1003
|
-
onPromptHandedOff: (item: TelegramQueueAdmissionItemLike, ctx: TContext) =>
|
|
1004
|
-
onControlSettled: (item: TelegramQueueAdmissionItemLike, ctx: TContext) =>
|
|
1005
|
-
onItemsDiscarded: (items: readonly TelegramQueueAdmissionItemLike[], ctx: TContext) =>
|
|
1005
|
+
onPromptHandedOff: (item: TelegramQueueAdmissionItemLike, ctx: TContext) => boolean;
|
|
1006
|
+
onControlSettled: (item: TelegramQueueAdmissionItemLike, ctx: TContext) => boolean;
|
|
1007
|
+
onItemsDiscarded: (items: readonly TelegramQueueAdmissionItemLike[], ctx: TContext) => boolean;
|
|
1006
1008
|
}
|
|
1007
1009
|
export declare function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(runtimes: readonly TelegramQueueAdmissionSettlementRuntime<TContext>[]): TelegramQueueAdmissionSettlementRuntime<TContext>;
|
|
1008
1010
|
export declare function createTelegramQueueAdmissionSettlementRuntime<TContext>(worker: TelegramUpdateWorkerRuntime<TContext>): TelegramQueueAdmissionSettlementRuntime<TContext>;
|
package/dist/lib/updates.js
CHANGED
|
@@ -303,10 +303,10 @@ export function reportTelegramQueueAdmission(values, receipts) {
|
|
|
303
303
|
return true;
|
|
304
304
|
}
|
|
305
305
|
export function buildTelegramUpdateFlowAction(update, allowedUserId) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
306
|
+
// Business chats are independent from bot chats even when chat/message IDs coincide.
|
|
307
|
+
// Raw handlers may own that namespace; the default DM runtime has no deletion authority.
|
|
308
|
+
if (update.deleted_business_messages !== undefined)
|
|
309
|
+
return { kind: "ignore" };
|
|
310
310
|
if (update.message_reaction) {
|
|
311
311
|
return { kind: "reaction", reactionUpdate: update.message_reaction };
|
|
312
312
|
}
|
|
@@ -1017,6 +1017,22 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1017
1017
|
}
|
|
1018
1018
|
return true;
|
|
1019
1019
|
};
|
|
1020
|
+
const getCurrentQueueReceipt = (receipt) => {
|
|
1021
|
+
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1022
|
+
if (!committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)))
|
|
1023
|
+
return undefined;
|
|
1024
|
+
try {
|
|
1025
|
+
if (deps.journal.isQueueReceiptCurrent &&
|
|
1026
|
+
deps.journal.isQueueReceiptCurrent(committed.receipt, committed.queueOwner) !== true) {
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
catch (error) {
|
|
1031
|
+
recordRuntimeEvent(error, { phase: "queue-receipt-observation" });
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
return committed;
|
|
1035
|
+
};
|
|
1020
1036
|
const clearRetryTimer = () => {
|
|
1021
1037
|
if (retryTimer !== undefined)
|
|
1022
1038
|
cancelRetry(retryTimer);
|
|
@@ -1581,11 +1597,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1581
1597
|
owner === expectedOwner &&
|
|
1582
1598
|
!expectedOwner.controller.signal.aborted) {
|
|
1583
1599
|
pendingSignal = false;
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1600
|
+
// Keep newer wakes in this run so waitForDrain also covers them. A failure
|
|
1601
|
+
// observed after signal() still keeps its latch; this loop never clears it.
|
|
1602
|
+
await drain(expectedOwner);
|
|
1589
1603
|
}
|
|
1590
1604
|
};
|
|
1591
1605
|
const operation = run();
|
|
@@ -1785,27 +1799,20 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1785
1799
|
}
|
|
1786
1800
|
transition("queued", input.updateId);
|
|
1787
1801
|
},
|
|
1788
|
-
isQueueReceiptCommitted(receipt)
|
|
1789
|
-
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1790
|
-
return (committed !== undefined &&
|
|
1791
|
-
areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)));
|
|
1792
|
-
},
|
|
1802
|
+
isQueueReceiptCommitted: receipt => getCurrentQueueReceipt(receipt) !== undefined,
|
|
1793
1803
|
getQueueReceiptOwner(receipt) {
|
|
1794
|
-
const committed =
|
|
1795
|
-
return committed
|
|
1796
|
-
areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt))
|
|
1797
|
-
? { ...committed.queueOwner }
|
|
1798
|
-
: undefined;
|
|
1804
|
+
const committed = getCurrentQueueReceipt(receipt);
|
|
1805
|
+
return committed ? { ...committed.queueOwner } : undefined;
|
|
1799
1806
|
},
|
|
1800
1807
|
completeQueueReceipts(input) {
|
|
1801
1808
|
const expectedOwner = owner;
|
|
1802
1809
|
if (!expectedOwner ||
|
|
1803
1810
|
expectedOwner.controller.signal.aborted ||
|
|
1804
1811
|
!(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)) {
|
|
1805
|
-
return;
|
|
1812
|
+
return false;
|
|
1806
1813
|
}
|
|
1807
1814
|
if (input.receipts.length === 0)
|
|
1808
|
-
return;
|
|
1815
|
+
return true;
|
|
1809
1816
|
const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
|
|
1810
1817
|
const receiptIds = new Set();
|
|
1811
1818
|
const sourceUpdateIds = new Set();
|
|
@@ -1813,13 +1820,13 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1813
1820
|
for (const receipt of normalizedReceipts) {
|
|
1814
1821
|
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1815
1822
|
if (!committed)
|
|
1816
|
-
|
|
1823
|
+
return false;
|
|
1817
1824
|
if (receiptIds.has(receipt.receiptId) ||
|
|
1818
1825
|
!areTelegramQueueAdmissionReceiptsEqual(committed.receipt, receipt) ||
|
|
1819
1826
|
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity) ||
|
|
1820
1827
|
receipt.sourceUpdateIds.some((updateId) => sourceUpdateIds.has(updateId))) {
|
|
1821
1828
|
blockWithFailure("invalid-outcome", "queue-receipt-completion-invalid", new TelegramUpdateAdmissionOutcomeError(`Telegram ${input.reason} requested invalid queue receipt ${receipt.receiptId}.`));
|
|
1822
|
-
return;
|
|
1829
|
+
return false;
|
|
1823
1830
|
}
|
|
1824
1831
|
receiptIds.add(receipt.receiptId);
|
|
1825
1832
|
queuedCompletions.push({
|
|
@@ -1830,22 +1837,23 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1830
1837
|
sourceUpdateIds.add(updateId);
|
|
1831
1838
|
}
|
|
1832
1839
|
}
|
|
1833
|
-
if (receiptIds.size === 0)
|
|
1834
|
-
return;
|
|
1835
1840
|
let removedUpdateIds;
|
|
1836
1841
|
try {
|
|
1837
1842
|
removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
|
|
1838
1843
|
}
|
|
1839
1844
|
catch (error) {
|
|
1840
1845
|
blockWithFailure("journal-write", "queue-receipt-completion", error);
|
|
1841
|
-
return;
|
|
1846
|
+
return false;
|
|
1842
1847
|
}
|
|
1843
1848
|
const removed = new Set(removedUpdateIds);
|
|
1844
1849
|
if (removed.size !== sourceUpdateIds.size ||
|
|
1845
1850
|
[...sourceUpdateIds].some((updateId) => !removed.has(updateId))) {
|
|
1846
1851
|
blockWithFailure("journal-write", "queue-receipt-completion", new Error(`Telegram ${input.reason} did not complete every receipt source.`));
|
|
1847
|
-
return;
|
|
1852
|
+
return false;
|
|
1848
1853
|
}
|
|
1854
|
+
input.receipts.forEach((receipt, index) => {
|
|
1855
|
+
acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]);
|
|
1856
|
+
});
|
|
1849
1857
|
for (const updateId of sourceUpdateIds)
|
|
1850
1858
|
claims.delete(updateId);
|
|
1851
1859
|
for (const receiptId of receiptIds) {
|
|
@@ -1859,6 +1867,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1859
1867
|
notifyStateChange();
|
|
1860
1868
|
pendingSignal = true;
|
|
1861
1869
|
launchDrain();
|
|
1870
|
+
return true;
|
|
1862
1871
|
},
|
|
1863
1872
|
async stop() {
|
|
1864
1873
|
const expectedOwner = owner;
|
|
@@ -2023,9 +2032,17 @@ export function createTelegramInputCustodyWorkerJournalPort(store) {
|
|
|
2023
2032
|
};
|
|
2024
2033
|
return {
|
|
2025
2034
|
read: () => store.read(),
|
|
2035
|
+
isQueueReceiptCurrent(receipt, owner) {
|
|
2036
|
+
const entries = store.read().entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
|
|
2037
|
+
return receipt.sourceUpdateIds.length > 0 && entries.length === receipt.sourceUpdateIds.length &&
|
|
2038
|
+
entries.every((entry, index) => entry.updateId === receipt.sourceUpdateIds[index] &&
|
|
2039
|
+
entry.state === "queued" && entry.queueKind === receipt.queueKind && !entry.queueHandoff &&
|
|
2040
|
+
entry.queueOwner !== undefined && areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner));
|
|
2041
|
+
},
|
|
2026
2042
|
markQueued: legacyMutation,
|
|
2027
2043
|
markExecutionFailure: legacyMutation,
|
|
2028
|
-
|
|
2044
|
+
// The common drain batches excluded IDs here; the store rejects all non-vetoed input atomically.
|
|
2045
|
+
removeCompleted: updateIds => store.removeExcluded(updateIds),
|
|
2029
2046
|
completeQueued: receipts => store.completeQueued(receipts),
|
|
2030
2047
|
inputCustody: {
|
|
2031
2048
|
acquireInput: store.acquireInput,
|
|
@@ -2620,6 +2637,8 @@ export function createTelegramInputCustodyActivationReadinessResolver(deps) {
|
|
|
2620
2637
|
};
|
|
2621
2638
|
}
|
|
2622
2639
|
export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
2640
|
+
// Stable per store, not per descriptor: a renewed source has new captured worker ports.
|
|
2641
|
+
const ports = new WeakMap();
|
|
2623
2642
|
return () => {
|
|
2624
2643
|
if (!deps.isEnabled())
|
|
2625
2644
|
return undefined;
|
|
@@ -2627,7 +2646,11 @@ export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
|
2627
2646
|
const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
|
|
2628
2647
|
if (!source || !source.runtimeKey || !source.recoveryKey || !recipientBindingKey)
|
|
2629
2648
|
return undefined;
|
|
2630
|
-
|
|
2649
|
+
let port = ports.get(source.journal);
|
|
2650
|
+
if (!port) {
|
|
2651
|
+
port = createTelegramInputCustodyWorkerJournalPort(source.journal);
|
|
2652
|
+
ports.set(source.journal, port);
|
|
2653
|
+
}
|
|
2631
2654
|
return { runtimeKey: JSON.stringify({ source: source.runtimeKey, recipientBindingKey }),
|
|
2632
2655
|
recoveryKey: source.recoveryKey,
|
|
2633
2656
|
recipientBindingKey, journal: { ...port, appendBatch: source.journal.appendBatch,
|
|
@@ -2757,6 +2780,7 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
2757
2780
|
try {
|
|
2758
2781
|
execution.assertCurrent();
|
|
2759
2782
|
await deps.defaultHandle(boundUpdate, ctx, execution);
|
|
2783
|
+
execution.assertCurrent();
|
|
2760
2784
|
}
|
|
2761
2785
|
finally {
|
|
2762
2786
|
immediate = false;
|
|
@@ -2778,10 +2802,45 @@ export function createTelegramCustodiedUpdateAdmissionHandle(deps) {
|
|
|
2778
2802
|
});
|
|
2779
2803
|
return (update, ctx, signal) => session.execute(update, started => admission(started, ctx, signal));
|
|
2780
2804
|
}
|
|
2805
|
+
// A dispatched prompt can remain in memory until agent_start. Remember only acknowledged
|
|
2806
|
+
// removal of these exact receipt objects for later local discard, never for dispatch/replay.
|
|
2807
|
+
const acknowledgedQueueReceiptCompletions = new WeakMap();
|
|
2808
|
+
const isQueueReceiptCompletionAcknowledged = (receipt) => {
|
|
2809
|
+
const acknowledged = acknowledgedQueueReceiptCompletions.get(receipt);
|
|
2810
|
+
return acknowledged !== undefined && areTelegramQueueAdmissionReceiptsEqual(acknowledged, receipt);
|
|
2811
|
+
};
|
|
2781
2812
|
export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2813
|
+
// Plan the whole request before mutation; unready is not an acknowledgement of completion.
|
|
2814
|
+
const complete = (items, ctx, kind) => {
|
|
2815
|
+
const plan = new Map();
|
|
2816
|
+
for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
|
|
2817
|
+
if (kind === "discard" && isQueueReceiptCompletionAcknowledged(receipt))
|
|
2818
|
+
continue;
|
|
2819
|
+
const candidates = runtimes.filter(runtime => runtime.isItemReady({ admissionReceipts: [receipt] }));
|
|
2820
|
+
const selected = candidates[0];
|
|
2821
|
+
if (!selected)
|
|
2822
|
+
return false;
|
|
2823
|
+
if (candidates.length > 1) {
|
|
2824
|
+
const owner = selected.getQueueReceiptOwner(receipt);
|
|
2825
|
+
if (!owner || candidates.some(runtime => {
|
|
2826
|
+
const candidate = runtime.getQueueReceiptOwner(receipt);
|
|
2827
|
+
return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
|
|
2828
|
+
}))
|
|
2829
|
+
return false;
|
|
2830
|
+
}
|
|
2831
|
+
const receipts = plan.get(selected) ?? [];
|
|
2832
|
+
receipts.push(receipt);
|
|
2833
|
+
plan.set(selected, receipts);
|
|
2834
|
+
}
|
|
2835
|
+
for (const [runtime, receipts] of plan) {
|
|
2836
|
+
const item = { admissionReceipts: receipts };
|
|
2837
|
+
const completed = kind === "discard" ? runtime.onItemsDiscarded([item], ctx)
|
|
2838
|
+
: kind === "prompt" ? runtime.onPromptHandedOff(item, ctx)
|
|
2839
|
+
: runtime.onControlSettled(item, ctx);
|
|
2840
|
+
if (completed !== true)
|
|
2841
|
+
return false;
|
|
2842
|
+
}
|
|
2843
|
+
return true;
|
|
2785
2844
|
};
|
|
2786
2845
|
return {
|
|
2787
2846
|
isItemReady: (item) => (item.admissionReceipts ?? []).every((receipt) => runtimes.some((runtime) => runtime.isItemReady({ admissionReceipts: [receipt] }))),
|
|
@@ -2799,19 +2858,21 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
|
2799
2858
|
}
|
|
2800
2859
|
return owner ? { ...owner } : undefined;
|
|
2801
2860
|
},
|
|
2802
|
-
onPromptHandedOff: (item, ctx) =>
|
|
2803
|
-
onControlSettled: (item, ctx) =>
|
|
2804
|
-
onItemsDiscarded: (items, ctx) =>
|
|
2861
|
+
onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt"),
|
|
2862
|
+
onControlSettled: (item, ctx) => complete([item], ctx, "control"),
|
|
2863
|
+
onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
|
|
2805
2864
|
};
|
|
2806
2865
|
}
|
|
2807
2866
|
export function createTelegramQueueAdmissionSettlementRuntime(worker) {
|
|
2808
2867
|
const complete = (items, ctx, reason) => {
|
|
2809
2868
|
const receipts = [];
|
|
2810
2869
|
for (const item of items) {
|
|
2811
|
-
|
|
2812
|
-
|
|
2870
|
+
for (const receipt of item.admissionReceipts ?? []) {
|
|
2871
|
+
if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt))
|
|
2872
|
+
receipts.push(receipt);
|
|
2873
|
+
}
|
|
2813
2874
|
}
|
|
2814
|
-
worker.completeQueueReceipts({ receipts, ctx, reason });
|
|
2875
|
+
return receipts.length === 0 || worker.completeQueueReceipts({ receipts, ctx, reason });
|
|
2815
2876
|
};
|
|
2816
2877
|
return {
|
|
2817
2878
|
isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
|
|
@@ -3065,10 +3126,6 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
3065
3126
|
const recipientJournalBindingKey = deps.createRecipientJournalBindingKey(recipient);
|
|
3066
3127
|
if (!recipientJournalBindingKey)
|
|
3067
3128
|
continue;
|
|
3068
|
-
const recipientItem = structuredClone(item);
|
|
3069
|
-
recipientItem.admissionReceipts = [
|
|
3070
|
-
{ ...receipt, journalBindingKey: recipientJournalBindingKey },
|
|
3071
|
-
];
|
|
3072
3129
|
const expectedOwner = deps.getReceiptOwner(receipt);
|
|
3073
3130
|
const lifecycle = deps.getLifecycleForReceipt(receipt);
|
|
3074
3131
|
if (!expectedOwner ||
|
|
@@ -3091,11 +3148,11 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
3091
3148
|
},
|
|
3092
3149
|
handoffToken,
|
|
3093
3150
|
lifecycle,
|
|
3094
|
-
stageRemote: async () => {
|
|
3095
|
-
const payload =
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
}
|
|
3151
|
+
stageRemote: async ({ payload: sourcePayload }) => {
|
|
3152
|
+
const payload = {
|
|
3153
|
+
...sourcePayload,
|
|
3154
|
+
admissionReceipts: [{ ...sourcePayload.admissionReceipts[0], journalBindingKey: recipientJournalBindingKey }],
|
|
3155
|
+
};
|
|
3099
3156
|
if (followerRegistered) {
|
|
3100
3157
|
return deps.stageThroughFollower({
|
|
3101
3158
|
recipient,
|
|
@@ -3244,11 +3301,27 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3244
3301
|
let activeRuntimeKey;
|
|
3245
3302
|
let journal;
|
|
3246
3303
|
let worker;
|
|
3304
|
+
let workerInputCustody;
|
|
3247
3305
|
let settlement;
|
|
3248
3306
|
let journalBindingKey;
|
|
3249
3307
|
let operation = Promise.resolve();
|
|
3250
3308
|
let foreignQueueOwnerLiveness;
|
|
3251
3309
|
let releaseSourceReference;
|
|
3310
|
+
let sourceReferenceBinding;
|
|
3311
|
+
const withRetainedSourceReference = (operation) => {
|
|
3312
|
+
if (!journal || !worker)
|
|
3313
|
+
throw new Error("Telegram update admission worker is not active.");
|
|
3314
|
+
// Synchronous retained-source access needs a reference, not renewed execution authority.
|
|
3315
|
+
const release = !releaseSourceReference && sourceReferenceBinding
|
|
3316
|
+
? deps.acquireSourceReference?.(sourceReferenceBinding) : undefined;
|
|
3317
|
+
try {
|
|
3318
|
+
return operation(journal);
|
|
3319
|
+
}
|
|
3320
|
+
finally {
|
|
3321
|
+
release?.();
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
const readObservedJournal = () => withRetainedSourceReference(source => source.read());
|
|
3252
3325
|
const stopCurrent = async (forget) => {
|
|
3253
3326
|
await worker?.stop();
|
|
3254
3327
|
const release = releaseSourceReference;
|
|
@@ -3261,8 +3334,10 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3261
3334
|
}
|
|
3262
3335
|
if (!forget)
|
|
3263
3336
|
return;
|
|
3337
|
+
sourceReferenceBinding = undefined;
|
|
3264
3338
|
journal = undefined;
|
|
3265
3339
|
worker = undefined;
|
|
3340
|
+
workerInputCustody = undefined;
|
|
3266
3341
|
settlement = undefined;
|
|
3267
3342
|
journalBindingKey = undefined;
|
|
3268
3343
|
activeRuntimeKey = undefined;
|
|
@@ -3274,7 +3349,8 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3274
3349
|
await stopCurrent(true);
|
|
3275
3350
|
return;
|
|
3276
3351
|
}
|
|
3277
|
-
if (replace || !worker || activeRuntimeKey !== binding.runtimeKey
|
|
3352
|
+
if (replace || !worker || activeRuntimeKey !== binding.runtimeKey ||
|
|
3353
|
+
workerInputCustody !== binding.journal.inputCustody) {
|
|
3278
3354
|
const { runtimeKey, recoveryKey } = binding;
|
|
3279
3355
|
await stopCurrent(true);
|
|
3280
3356
|
binding = deps.resolveBinding();
|
|
@@ -3283,10 +3359,13 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3283
3359
|
throw new Error("Telegram update source binding changed during startup.");
|
|
3284
3360
|
}
|
|
3285
3361
|
const release = deps.acquireSourceReference?.(binding);
|
|
3362
|
+
const inputCustody = binding.journal.inputCustody;
|
|
3286
3363
|
try {
|
|
3287
3364
|
journal = binding.journal;
|
|
3288
3365
|
worker = deps.createWorker(binding.journal, binding);
|
|
3366
|
+
workerInputCustody = inputCustody;
|
|
3289
3367
|
settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
|
|
3368
|
+
sourceReferenceBinding = { ...binding, journal };
|
|
3290
3369
|
releaseSourceReference = release;
|
|
3291
3370
|
}
|
|
3292
3371
|
catch (error) {
|
|
@@ -3430,7 +3509,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3430
3509
|
if (!journal || !worker || !journal.cancelQueuedHandoff) {
|
|
3431
3510
|
throw new Error("Telegram update journal queue handoff cancellation is not available.");
|
|
3432
3511
|
}
|
|
3433
|
-
const result =
|
|
3512
|
+
const result = withRetainedSourceReference(source => source.cancelQueuedHandoff(input));
|
|
3434
3513
|
worker.signal();
|
|
3435
3514
|
return result;
|
|
3436
3515
|
},
|
|
@@ -3468,18 +3547,12 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3468
3547
|
: undefined,
|
|
3469
3548
|
ownsJournalBinding: (candidate) => journalBindingKey !== undefined && journalBindingKey === candidate,
|
|
3470
3549
|
getForeignQueueOwnerLiveness: () => foreignQueueOwnerLiveness,
|
|
3471
|
-
getJournalEntryCount()
|
|
3472
|
-
if (!journal || !worker) {
|
|
3473
|
-
throw new Error("Telegram update admission worker is not active.");
|
|
3474
|
-
}
|
|
3475
|
-
return journal.read().entries.length;
|
|
3476
|
-
},
|
|
3550
|
+
getJournalEntryCount: () => readObservedJournal().entries.length,
|
|
3477
3551
|
hasPendingQueueMutationForItem(item) {
|
|
3478
3552
|
return Boolean(journal &&
|
|
3479
3553
|
worker &&
|
|
3480
|
-
|
|
3481
|
-
.
|
|
3482
|
-
.entries.some((entry) => entry.state !== "queued" &&
|
|
3554
|
+
readObservedJournal().entries.some((entry) => entry.state !== "queued" &&
|
|
3555
|
+
entry.preApprovalExcluded !== true &&
|
|
3483
3556
|
isTelegramReactionDependencyForQueueItem(entry.update, item)));
|
|
3484
3557
|
},
|
|
3485
3558
|
signal: () => worker?.signal(),
|
|
@@ -3496,9 +3569,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3496
3569
|
},
|
|
3497
3570
|
isItemReady: (item) => settlement?.isItemReady(item) ??
|
|
3498
3571
|
(item.admissionReceipts?.length ?? 0) === 0,
|
|
3499
|
-
onPromptHandedOff: (item, ctx) => settlement?.onPromptHandedOff(item, ctx),
|
|
3500
|
-
onControlSettled: (item, ctx) => settlement?.onControlSettled(item, ctx),
|
|
3501
|
-
onItemsDiscarded: (items, ctx) => settlement?.onItemsDiscarded(items, ctx),
|
|
3572
|
+
onPromptHandedOff: (item, ctx) => settlement?.onPromptHandedOff(item, ctx) ?? false,
|
|
3573
|
+
onControlSettled: (item, ctx) => settlement?.onControlSettled(item, ctx) ?? false,
|
|
3574
|
+
onItemsDiscarded: (items, ctx) => settlement?.onItemsDiscarded(items, ctx) ?? false,
|
|
3502
3575
|
};
|
|
3503
3576
|
}
|
|
3504
3577
|
/** Compose source-bound routing and late grouped settlement under one worker. */
|
|
@@ -75,6 +75,11 @@ export type TelegramWorkspaceRetirementFence = (TelegramWorkspaceRetirementFence
|
|
|
75
75
|
}) | (TelegramWorkspaceRetirementFenceBase & {
|
|
76
76
|
phase: "deletion-issued";
|
|
77
77
|
deletionIssuedAtMs: number;
|
|
78
|
+
}) | (TelegramWorkspaceRetirementFenceBase & {
|
|
79
|
+
destructiveKind?: "pressure-retirement";
|
|
80
|
+
phase: "deletion-rejected";
|
|
81
|
+
deletionIssuedAtMs: number;
|
|
82
|
+
rejectionConfirmedAtMs: number;
|
|
78
83
|
}) | (TelegramWorkspaceRetirementFenceBase & {
|
|
79
84
|
phase: "commit-ready";
|
|
80
85
|
absenceConfirmedAtMs: number;
|
|
@@ -215,6 +220,10 @@ export interface TelegramWorkspaceAdmissionLedger {
|
|
|
215
220
|
issueThreadCleanupDeletionPermit: TelegramWorkspaceAdmissionLedger["issueDeletionPermit"];
|
|
216
221
|
confirmRetirementAbsence: (expected: TelegramWorkspaceRetirementFence) => TelegramWorkspaceRetirementFence;
|
|
217
222
|
confirmThreadCleanupAbsence: TelegramWorkspaceAdmissionLedger["confirmRetirementAbsence"];
|
|
223
|
+
/** Caller must have exact transport proof of rejection, never merely target presence. */
|
|
224
|
+
confirmRetirementRejection: (expected: TelegramWorkspaceRetirementFence) => TelegramWorkspaceRetirementFence;
|
|
225
|
+
/** Caller must first durably withdraw the exact rejected intent, retaining its binding. */
|
|
226
|
+
completeRejectedRetirementFence: (expected: TelegramWorkspaceRetirementFence) => boolean;
|
|
218
227
|
releaseUnissuedRetirementFence: (expected: TelegramWorkspaceRetirementFence) => boolean;
|
|
219
228
|
releaseUnissuedThreadCleanupFence: TelegramWorkspaceAdmissionLedger["releaseUnissuedRetirementFence"];
|
|
220
229
|
completeRetirementFence: (expected: TelegramWorkspaceRetirementFence) => boolean;
|
|
@@ -195,6 +195,12 @@ function normalizeFence(value, profileKey) {
|
|
|
195
195
|
deletionIssuedAtMs: value.deletionIssuedAtMs,
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
|
+
if (value.phase === "deletion-rejected" &&
|
|
199
|
+
(value.destructiveKind === undefined || value.destructiveKind === "pressure-retirement") &&
|
|
200
|
+
isSafeTimestamp(value.deletionIssuedAtMs) && isSafeTimestamp(value.rejectionConfirmedAtMs)) {
|
|
201
|
+
return { ...base, destructiveKind: value.destructiveKind, phase: "deletion-rejected",
|
|
202
|
+
deletionIssuedAtMs: value.deletionIssuedAtMs, rejectionConfirmedAtMs: value.rejectionConfirmedAtMs };
|
|
203
|
+
}
|
|
198
204
|
if (value.phase === "commit-ready" &&
|
|
199
205
|
isSafeTimestamp(value.absenceConfirmedAtMs) &&
|
|
200
206
|
(value.deletionIssuedAtMs === undefined ||
|
|
@@ -395,6 +401,10 @@ function areFencesEqual(left, right) {
|
|
|
395
401
|
right.phase === "deletion-issued") {
|
|
396
402
|
return left.deletionIssuedAtMs === right.deletionIssuedAtMs;
|
|
397
403
|
}
|
|
404
|
+
if (left.phase === "deletion-rejected" && right.phase === "deletion-rejected") {
|
|
405
|
+
return left.deletionIssuedAtMs === right.deletionIssuedAtMs &&
|
|
406
|
+
left.rejectionConfirmedAtMs === right.rejectionConfirmedAtMs;
|
|
407
|
+
}
|
|
398
408
|
return (left.phase === "commit-ready" &&
|
|
399
409
|
right.phase === "commit-ready" &&
|
|
400
410
|
left.absenceConfirmedAtMs === right.absenceConfirmedAtMs &&
|
|
@@ -1023,6 +1033,9 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1023
1033
|
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1024
1034
|
authorityChanged("Telegram Workspace absence confirmation requires fence owner authority.");
|
|
1025
1035
|
}
|
|
1036
|
+
if (expected.phase === "deletion-rejected") {
|
|
1037
|
+
authorityChanged("Rejected Telegram Workspace deletion cannot confirm absence.");
|
|
1038
|
+
}
|
|
1026
1039
|
return transact((state) => {
|
|
1027
1040
|
if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1028
1041
|
authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
|
|
@@ -1069,7 +1082,7 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1069
1082
|
return { result: true, changed: true };
|
|
1070
1083
|
});
|
|
1071
1084
|
}
|
|
1072
|
-
function
|
|
1085
|
+
function completeDestructiveFence(expected, expectedKind, requiredPhase) {
|
|
1073
1086
|
validateExpectedFence(expected, options.profileKey);
|
|
1074
1087
|
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1075
1088
|
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
@@ -1084,13 +1097,44 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1084
1097
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1085
1098
|
authorityChanged("Telegram Workspace retirement fence authority changed.");
|
|
1086
1099
|
}
|
|
1087
|
-
if (state.fence.phase !==
|
|
1088
|
-
authorityChanged(
|
|
1100
|
+
if (state.fence.phase !== requiredPhase) {
|
|
1101
|
+
authorityChanged(requiredPhase === "commit-ready"
|
|
1102
|
+
? "Telegram Workspace retirement commit is not ready."
|
|
1103
|
+
: "Telegram Workspace retirement rejection is not confirmed.");
|
|
1089
1104
|
}
|
|
1090
1105
|
delete state.fence;
|
|
1091
1106
|
return { result: true, changed: true };
|
|
1092
1107
|
});
|
|
1093
1108
|
}
|
|
1109
|
+
function confirmRetirementRejection(expected) {
|
|
1110
|
+
validateExpectedFence(expected, options.profileKey);
|
|
1111
|
+
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== "pressure-retirement" ||
|
|
1112
|
+
!areOwnersEqual(expected.owner, options.owner) ||
|
|
1113
|
+
(expected.phase !== "deletion-issued" && expected.phase !== "deletion-rejected")) {
|
|
1114
|
+
authorityChanged("Telegram Workspace rejection requires exact issued retirement authority.");
|
|
1115
|
+
}
|
|
1116
|
+
return transact((state) => {
|
|
1117
|
+
const current = state.fence;
|
|
1118
|
+
if (!current || !isTelegramWorkspaceRetirementFence(current) || !areFenceBasesEqual(current, expected) ||
|
|
1119
|
+
(current.phase !== "deletion-issued" && current.phase !== "deletion-rejected") ||
|
|
1120
|
+
current.deletionIssuedAtMs !== expected.deletionIssuedAtMs ||
|
|
1121
|
+
(expected.phase === "deletion-rejected" && !areFencesEqual(current, expected))) {
|
|
1122
|
+
authorityChanged("Telegram Workspace retirement fence authority changed.");
|
|
1123
|
+
}
|
|
1124
|
+
if (current.phase === "deletion-rejected")
|
|
1125
|
+
return { result: cloneFence(current), changed: false };
|
|
1126
|
+
const rejected = { ...current,
|
|
1127
|
+
destructiveKind: "pressure-retirement", phase: "deletion-rejected", rejectionConfirmedAtMs: getNowMs() };
|
|
1128
|
+
state.fence = rejected;
|
|
1129
|
+
return { result: cloneFence(rejected), changed: true };
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function completeRetirementFence(expected, expectedKind = "pressure-retirement") {
|
|
1133
|
+
return completeDestructiveFence(expected, expectedKind, "commit-ready");
|
|
1134
|
+
}
|
|
1135
|
+
function completeRejectedRetirementFence(expected) {
|
|
1136
|
+
return completeDestructiveFence(expected, "pressure-retirement", "deletion-rejected");
|
|
1137
|
+
}
|
|
1094
1138
|
function adoptThreadCleanupFence(expected, replacement) {
|
|
1095
1139
|
return adoptRetirementFence(expected, replacement, "manual-thread-cleanup");
|
|
1096
1140
|
}
|
|
@@ -1128,6 +1172,8 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1128
1172
|
issueThreadCleanupDeletionPermit,
|
|
1129
1173
|
confirmRetirementAbsence,
|
|
1130
1174
|
confirmThreadCleanupAbsence,
|
|
1175
|
+
confirmRetirementRejection,
|
|
1176
|
+
completeRejectedRetirementFence,
|
|
1131
1177
|
releaseUnissuedRetirementFence,
|
|
1132
1178
|
releaseUnissuedThreadCleanupFence,
|
|
1133
1179
|
completeRetirementFence,
|