@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/lib/updates.ts
CHANGED
|
@@ -698,10 +698,9 @@ export function buildTelegramUpdateFlowAction<
|
|
|
698
698
|
NonNullable<TUpdate["message"] | TUpdate["edited_message"]>,
|
|
699
699
|
NonNullable<TUpdate["guest_message"]>
|
|
700
700
|
> {
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
701
|
+
// Business chats are independent from bot chats even when chat/message IDs coincide.
|
|
702
|
+
// Raw handlers may own that namespace; the default DM runtime has no deletion authority.
|
|
703
|
+
if (update.deleted_business_messages !== undefined) return { kind: "ignore" };
|
|
705
704
|
if (update.message_reaction) {
|
|
706
705
|
return { kind: "reaction", reactionUpdate: update.message_reaction };
|
|
707
706
|
}
|
|
@@ -1739,6 +1738,11 @@ export interface TelegramUpdateWorkerJournalSnapshot {
|
|
|
1739
1738
|
|
|
1740
1739
|
export interface TelegramUpdateWorkerJournalPort {
|
|
1741
1740
|
read: () => TelegramUpdateWorkerJournalSnapshot;
|
|
1741
|
+
/** Optional strict observation; it must not recover, repair or grant new receipt authority. */
|
|
1742
|
+
isQueueReceiptCurrent?: (
|
|
1743
|
+
receipt: TelegramQueueAdmissionReceiptLike,
|
|
1744
|
+
owner: TelegramUpdateJournalQueueOwner,
|
|
1745
|
+
) => boolean;
|
|
1742
1746
|
markQueued: (receipt: {
|
|
1743
1747
|
queueKind: "prompt" | "control";
|
|
1744
1748
|
receiptId: string;
|
|
@@ -1858,7 +1862,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
1858
1862
|
receipts: readonly TelegramQueueAdmissionReceiptLike[];
|
|
1859
1863
|
ctx: TContext;
|
|
1860
1864
|
reason: TelegramQueueReceiptCompletionReason;
|
|
1861
|
-
}) =>
|
|
1865
|
+
}) => boolean;
|
|
1862
1866
|
stop: () => Promise<void>;
|
|
1863
1867
|
waitForDrain: () => Promise<void>;
|
|
1864
1868
|
getState: () => TelegramUpdateWorkerStateSnapshot;
|
|
@@ -2268,6 +2272,23 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
2268
2272
|
return true;
|
|
2269
2273
|
};
|
|
2270
2274
|
|
|
2275
|
+
const getCurrentQueueReceipt = (receipt: TelegramQueueAdmissionReceiptLike) => {
|
|
2276
|
+
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
2277
|
+
if (!committed || !areTelegramQueueAdmissionReceiptsEqual(
|
|
2278
|
+
committed.receipt, normalizeQueueReceipt(receipt),
|
|
2279
|
+
)) return undefined;
|
|
2280
|
+
try {
|
|
2281
|
+
if (deps.journal.isQueueReceiptCurrent &&
|
|
2282
|
+
deps.journal.isQueueReceiptCurrent(committed.receipt, committed.queueOwner) !== true) {
|
|
2283
|
+
return undefined;
|
|
2284
|
+
}
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
recordRuntimeEvent(error, { phase: "queue-receipt-observation" });
|
|
2287
|
+
return undefined;
|
|
2288
|
+
}
|
|
2289
|
+
return committed;
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2271
2292
|
const clearRetryTimer = (): void => {
|
|
2272
2293
|
if (retryTimer !== undefined) cancelRetry(retryTimer);
|
|
2273
2294
|
retryTimer = undefined;
|
|
@@ -3023,11 +3044,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3023
3044
|
!expectedOwner.controller.signal.aborted
|
|
3024
3045
|
) {
|
|
3025
3046
|
pendingSignal = false;
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
return;
|
|
3030
|
-
}
|
|
3047
|
+
// Keep newer wakes in this run so waitForDrain also covers them. A failure
|
|
3048
|
+
// observed after signal() still keeps its latch; this loop never clears it.
|
|
3049
|
+
await drain(expectedOwner);
|
|
3031
3050
|
}
|
|
3032
3051
|
};
|
|
3033
3052
|
const operation = run();
|
|
@@ -3265,25 +3284,10 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3265
3284
|
}
|
|
3266
3285
|
transition("queued", input.updateId);
|
|
3267
3286
|
},
|
|
3268
|
-
isQueueReceiptCommitted(receipt)
|
|
3269
|
-
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
3270
|
-
return (
|
|
3271
|
-
committed !== undefined &&
|
|
3272
|
-
areTelegramQueueAdmissionReceiptsEqual(
|
|
3273
|
-
committed.receipt,
|
|
3274
|
-
normalizeQueueReceipt(receipt),
|
|
3275
|
-
)
|
|
3276
|
-
);
|
|
3277
|
-
},
|
|
3287
|
+
isQueueReceiptCommitted: receipt => getCurrentQueueReceipt(receipt) !== undefined,
|
|
3278
3288
|
getQueueReceiptOwner(receipt) {
|
|
3279
|
-
const committed =
|
|
3280
|
-
return committed
|
|
3281
|
-
areTelegramQueueAdmissionReceiptsEqual(
|
|
3282
|
-
committed.receipt,
|
|
3283
|
-
normalizeQueueReceipt(receipt),
|
|
3284
|
-
)
|
|
3285
|
-
? { ...committed.queueOwner }
|
|
3286
|
-
: undefined;
|
|
3289
|
+
const committed = getCurrentQueueReceipt(receipt);
|
|
3290
|
+
return committed ? { ...committed.queueOwner } : undefined;
|
|
3287
3291
|
},
|
|
3288
3292
|
completeQueueReceipts(input) {
|
|
3289
3293
|
const expectedOwner = owner;
|
|
@@ -3292,9 +3296,9 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3292
3296
|
expectedOwner.controller.signal.aborted ||
|
|
3293
3297
|
!(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)
|
|
3294
3298
|
) {
|
|
3295
|
-
return;
|
|
3299
|
+
return false;
|
|
3296
3300
|
}
|
|
3297
|
-
if (input.receipts.length === 0) return;
|
|
3301
|
+
if (input.receipts.length === 0) return true;
|
|
3298
3302
|
const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
|
|
3299
3303
|
const receiptIds = new Set<string>();
|
|
3300
3304
|
const sourceUpdateIds = new Set<number>();
|
|
@@ -3306,7 +3310,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3306
3310
|
}> = [];
|
|
3307
3311
|
for (const receipt of normalizedReceipts) {
|
|
3308
3312
|
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
3309
|
-
if (!committed)
|
|
3313
|
+
if (!committed) return false;
|
|
3310
3314
|
if (
|
|
3311
3315
|
receiptIds.has(receipt.receiptId) ||
|
|
3312
3316
|
!areTelegramQueueAdmissionReceiptsEqual(
|
|
@@ -3328,7 +3332,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3328
3332
|
`Telegram ${input.reason} requested invalid queue receipt ${receipt.receiptId}.`,
|
|
3329
3333
|
),
|
|
3330
3334
|
);
|
|
3331
|
-
return;
|
|
3335
|
+
return false;
|
|
3332
3336
|
}
|
|
3333
3337
|
receiptIds.add(receipt.receiptId);
|
|
3334
3338
|
queuedCompletions.push({
|
|
@@ -3339,7 +3343,6 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3339
3343
|
sourceUpdateIds.add(updateId);
|
|
3340
3344
|
}
|
|
3341
3345
|
}
|
|
3342
|
-
if (receiptIds.size === 0) return;
|
|
3343
3346
|
let removedUpdateIds: readonly number[];
|
|
3344
3347
|
try {
|
|
3345
3348
|
removedUpdateIds = deps.journal.completeQueued(
|
|
@@ -3351,7 +3354,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3351
3354
|
"queue-receipt-completion",
|
|
3352
3355
|
error,
|
|
3353
3356
|
);
|
|
3354
|
-
return;
|
|
3357
|
+
return false;
|
|
3355
3358
|
}
|
|
3356
3359
|
const removed = new Set(removedUpdateIds);
|
|
3357
3360
|
if (
|
|
@@ -3365,8 +3368,11 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3365
3368
|
`Telegram ${input.reason} did not complete every receipt source.`,
|
|
3366
3369
|
),
|
|
3367
3370
|
);
|
|
3368
|
-
return;
|
|
3371
|
+
return false;
|
|
3369
3372
|
}
|
|
3373
|
+
input.receipts.forEach((receipt, index) => {
|
|
3374
|
+
acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]!);
|
|
3375
|
+
});
|
|
3370
3376
|
for (const updateId of sourceUpdateIds) claims.delete(updateId);
|
|
3371
3377
|
for (const receiptId of receiptIds) {
|
|
3372
3378
|
committedQueueReceipts.delete(receiptId);
|
|
@@ -3385,6 +3391,7 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
|
|
|
3385
3391
|
notifyStateChange();
|
|
3386
3392
|
pendingSignal = true;
|
|
3387
3393
|
launchDrain();
|
|
3394
|
+
return true;
|
|
3388
3395
|
},
|
|
3389
3396
|
async stop() {
|
|
3390
3397
|
const expectedOwner = owner;
|
|
@@ -3676,9 +3683,17 @@ export function createTelegramInputCustodyWorkerJournalPort(
|
|
|
3676
3683
|
};
|
|
3677
3684
|
return {
|
|
3678
3685
|
read: () => store.read() as unknown as TelegramUpdateWorkerJournalSnapshot,
|
|
3686
|
+
isQueueReceiptCurrent(receipt, owner) {
|
|
3687
|
+
const entries = store.read().entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
|
|
3688
|
+
return receipt.sourceUpdateIds.length > 0 && entries.length === receipt.sourceUpdateIds.length &&
|
|
3689
|
+
entries.every((entry, index) => entry.updateId === receipt.sourceUpdateIds[index] &&
|
|
3690
|
+
entry.state === "queued" && entry.queueKind === receipt.queueKind && !entry.queueHandoff &&
|
|
3691
|
+
entry.queueOwner !== undefined && areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner));
|
|
3692
|
+
},
|
|
3679
3693
|
markQueued: legacyMutation,
|
|
3680
3694
|
markExecutionFailure: legacyMutation,
|
|
3681
|
-
|
|
3695
|
+
// The common drain batches excluded IDs here; the store rejects all non-vetoed input atomically.
|
|
3696
|
+
removeCompleted: updateIds => store.removeExcluded(updateIds),
|
|
3682
3697
|
completeQueued: receipts => store.completeQueued(receipts),
|
|
3683
3698
|
inputCustody: {
|
|
3684
3699
|
acquireInput: store.acquireInput,
|
|
@@ -4511,13 +4526,20 @@ export function createTelegramInputCustodyLifecycleBindingResolver(deps: {
|
|
|
4511
4526
|
journal: TelegramInputJournalStore } | undefined;
|
|
4512
4527
|
getRecipientBindingKey(): string | undefined;
|
|
4513
4528
|
}): () => TelegramUpdateAdmissionLifecycleJournalBinding | undefined {
|
|
4529
|
+
// Stable per store, not per descriptor: a renewed source has new captured worker ports.
|
|
4530
|
+
const ports = new WeakMap<TelegramInputJournalStore,
|
|
4531
|
+
ReturnType<typeof createTelegramInputCustodyWorkerJournalPort>>();
|
|
4514
4532
|
return () => {
|
|
4515
4533
|
if (!deps.isEnabled()) return undefined;
|
|
4516
4534
|
const source = deps.resolveInputJournal();
|
|
4517
4535
|
const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
|
|
4518
4536
|
if (!source || !source.runtimeKey || !source.recoveryKey || !recipientBindingKey)
|
|
4519
4537
|
return undefined;
|
|
4520
|
-
|
|
4538
|
+
let port = ports.get(source.journal);
|
|
4539
|
+
if (!port) {
|
|
4540
|
+
port = createTelegramInputCustodyWorkerJournalPort(source.journal);
|
|
4541
|
+
ports.set(source.journal, port);
|
|
4542
|
+
}
|
|
4521
4543
|
return { runtimeKey: JSON.stringify({ source: source.runtimeKey, recipientBindingKey }),
|
|
4522
4544
|
recoveryKey: source.recoveryKey,
|
|
4523
4545
|
recipientBindingKey, journal: { ...port, appendBatch: source.journal.appendBatch,
|
|
@@ -4681,6 +4703,7 @@ export function createTelegramUpdateAdmissionHandle<
|
|
|
4681
4703
|
try {
|
|
4682
4704
|
execution.assertCurrent();
|
|
4683
4705
|
await deps.defaultHandle(boundUpdate, ctx, execution);
|
|
4706
|
+
execution.assertCurrent();
|
|
4684
4707
|
} finally {
|
|
4685
4708
|
immediate = false;
|
|
4686
4709
|
}
|
|
@@ -4720,6 +4743,16 @@ export interface TelegramQueueAdmissionItemLike {
|
|
|
4720
4743
|
admissionReceipts?: readonly TelegramQueueAdmissionReceiptLike[];
|
|
4721
4744
|
}
|
|
4722
4745
|
|
|
4746
|
+
// A dispatched prompt can remain in memory until agent_start. Remember only acknowledged
|
|
4747
|
+
// removal of these exact receipt objects for later local discard, never for dispatch/replay.
|
|
4748
|
+
const acknowledgedQueueReceiptCompletions = new WeakMap<
|
|
4749
|
+
TelegramQueueAdmissionReceiptLike, TelegramQueueAdmissionReceiptLike
|
|
4750
|
+
>();
|
|
4751
|
+
const isQueueReceiptCompletionAcknowledged = (receipt: TelegramQueueAdmissionReceiptLike): boolean => {
|
|
4752
|
+
const acknowledged = acknowledgedQueueReceiptCompletions.get(receipt);
|
|
4753
|
+
return acknowledged !== undefined && areTelegramQueueAdmissionReceiptsEqual(acknowledged, receipt);
|
|
4754
|
+
};
|
|
4755
|
+
|
|
4723
4756
|
export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
|
|
4724
4757
|
isItemReady: (item: TelegramQueueAdmissionItemLike) => boolean;
|
|
4725
4758
|
getQueueReceiptOwner: (
|
|
@@ -4728,26 +4761,51 @@ export interface TelegramQueueAdmissionSettlementRuntime<TContext> {
|
|
|
4728
4761
|
onPromptHandedOff: (
|
|
4729
4762
|
item: TelegramQueueAdmissionItemLike,
|
|
4730
4763
|
ctx: TContext,
|
|
4731
|
-
) =>
|
|
4764
|
+
) => boolean;
|
|
4732
4765
|
onControlSettled: (
|
|
4733
4766
|
item: TelegramQueueAdmissionItemLike,
|
|
4734
4767
|
ctx: TContext,
|
|
4735
|
-
) =>
|
|
4768
|
+
) => boolean;
|
|
4736
4769
|
onItemsDiscarded: (
|
|
4737
4770
|
items: readonly TelegramQueueAdmissionItemLike[],
|
|
4738
4771
|
ctx: TContext,
|
|
4739
|
-
) =>
|
|
4772
|
+
) => boolean;
|
|
4740
4773
|
}
|
|
4741
4774
|
|
|
4742
4775
|
export function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(
|
|
4743
4776
|
runtimes: readonly TelegramQueueAdmissionSettlementRuntime<TContext>[],
|
|
4744
4777
|
): TelegramQueueAdmissionSettlementRuntime<TContext> {
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4778
|
+
// Plan the whole request before mutation; unready is not an acknowledgement of completion.
|
|
4779
|
+
const complete = (
|
|
4780
|
+
items: readonly TelegramQueueAdmissionItemLike[],
|
|
4781
|
+
ctx: TContext,
|
|
4782
|
+
kind: "prompt" | "control" | "discard",
|
|
4783
|
+
): boolean => {
|
|
4784
|
+
const plan = new Map<TelegramQueueAdmissionSettlementRuntime<TContext>, TelegramQueueAdmissionReceiptLike[]>();
|
|
4785
|
+
for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
|
|
4786
|
+
if (kind === "discard" && isQueueReceiptCompletionAcknowledged(receipt)) continue;
|
|
4787
|
+
const candidates = runtimes.filter(runtime => runtime.isItemReady({ admissionReceipts: [receipt] }));
|
|
4788
|
+
const selected = candidates[0];
|
|
4789
|
+
if (!selected) return false;
|
|
4790
|
+
if (candidates.length > 1) {
|
|
4791
|
+
const owner = selected.getQueueReceiptOwner(receipt);
|
|
4792
|
+
if (!owner || candidates.some(runtime => {
|
|
4793
|
+
const candidate = runtime.getQueueReceiptOwner(receipt);
|
|
4794
|
+
return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
|
|
4795
|
+
})) return false;
|
|
4796
|
+
}
|
|
4797
|
+
const receipts = plan.get(selected) ?? [];
|
|
4798
|
+
receipts.push(receipt);
|
|
4799
|
+
plan.set(selected, receipts);
|
|
4800
|
+
}
|
|
4801
|
+
for (const [runtime, receipts] of plan) {
|
|
4802
|
+
const item = { admissionReceipts: receipts };
|
|
4803
|
+
const completed = kind === "discard" ? runtime.onItemsDiscarded([item], ctx)
|
|
4804
|
+
: kind === "prompt" ? runtime.onPromptHandedOff(item, ctx)
|
|
4805
|
+
: runtime.onControlSettled(item, ctx);
|
|
4806
|
+
if (completed !== true) return false;
|
|
4807
|
+
}
|
|
4808
|
+
return true;
|
|
4751
4809
|
};
|
|
4752
4810
|
return {
|
|
4753
4811
|
isItemReady: (item) =>
|
|
@@ -4773,12 +4831,9 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime<TContext>(
|
|
|
4773
4831
|
}
|
|
4774
4832
|
return owner ? { ...owner } : undefined;
|
|
4775
4833
|
},
|
|
4776
|
-
onPromptHandedOff: (item, ctx) =>
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
settle((runtime) => runtime.onControlSettled(item, ctx)),
|
|
4780
|
-
onItemsDiscarded: (items, ctx) =>
|
|
4781
|
-
settle((runtime) => runtime.onItemsDiscarded(items, ctx)),
|
|
4834
|
+
onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt"),
|
|
4835
|
+
onControlSettled: (item, ctx) => complete([item], ctx, "control"),
|
|
4836
|
+
onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
|
|
4782
4837
|
};
|
|
4783
4838
|
}
|
|
4784
4839
|
|
|
@@ -4789,12 +4844,14 @@ export function createTelegramQueueAdmissionSettlementRuntime<TContext>(
|
|
|
4789
4844
|
items: readonly TelegramQueueAdmissionItemLike[],
|
|
4790
4845
|
ctx: TContext,
|
|
4791
4846
|
reason: TelegramQueueReceiptCompletionReason,
|
|
4792
|
-
):
|
|
4847
|
+
): boolean => {
|
|
4793
4848
|
const receipts: TelegramQueueAdmissionReceiptLike[] = [];
|
|
4794
4849
|
for (const item of items) {
|
|
4795
|
-
|
|
4850
|
+
for (const receipt of item.admissionReceipts ?? []) {
|
|
4851
|
+
if (reason !== "discard" || !isQueueReceiptCompletionAcknowledged(receipt)) receipts.push(receipt);
|
|
4852
|
+
}
|
|
4796
4853
|
}
|
|
4797
|
-
worker.completeQueueReceipts({ receipts, ctx, reason });
|
|
4854
|
+
return receipts.length === 0 || worker.completeQueueReceipts({ receipts, ctx, reason });
|
|
4798
4855
|
};
|
|
4799
4856
|
return {
|
|
4800
4857
|
isItemReady: (item) =>
|
|
@@ -5322,10 +5379,6 @@ export function createTelegramQueueHandoffReconciler<TContext>(
|
|
|
5322
5379
|
const recipientJournalBindingKey =
|
|
5323
5380
|
deps.createRecipientJournalBindingKey(recipient);
|
|
5324
5381
|
if (!recipientJournalBindingKey) continue;
|
|
5325
|
-
const recipientItem = structuredClone(item);
|
|
5326
|
-
recipientItem.admissionReceipts = [
|
|
5327
|
-
{ ...receipt, journalBindingKey: recipientJournalBindingKey },
|
|
5328
|
-
];
|
|
5329
5382
|
const expectedOwner = deps.getReceiptOwner(receipt);
|
|
5330
5383
|
const lifecycle = deps.getLifecycleForReceipt(receipt);
|
|
5331
5384
|
if (
|
|
@@ -5350,11 +5403,11 @@ export function createTelegramQueueHandoffReconciler<TContext>(
|
|
|
5350
5403
|
},
|
|
5351
5404
|
handoffToken,
|
|
5352
5405
|
lifecycle,
|
|
5353
|
-
stageRemote: async () => {
|
|
5354
|
-
const payload =
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
}
|
|
5406
|
+
stageRemote: async ({ payload: sourcePayload }) => {
|
|
5407
|
+
const payload = {
|
|
5408
|
+
...sourcePayload,
|
|
5409
|
+
admissionReceipts: [{ ...sourcePayload.admissionReceipts[0]!, journalBindingKey: recipientJournalBindingKey }],
|
|
5410
|
+
};
|
|
5358
5411
|
if (followerRegistered) {
|
|
5359
5412
|
return deps.stageThroughFollower({
|
|
5360
5413
|
recipient,
|
|
@@ -5637,11 +5690,25 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5637
5690
|
let activeRuntimeKey: string | undefined;
|
|
5638
5691
|
let journal: TelegramUpdateAdmissionLifecycleJournalBinding["journal"] | undefined;
|
|
5639
5692
|
let worker: TelegramUpdateWorkerRuntime<TContext> | undefined;
|
|
5693
|
+
let workerInputCustody: TelegramCustodyExecutionJournal | undefined;
|
|
5640
5694
|
let settlement: TelegramQueueAdmissionSettlementRuntime<TContext> | undefined;
|
|
5641
5695
|
let journalBindingKey: string | undefined;
|
|
5642
5696
|
let operation: Promise<void> = Promise.resolve();
|
|
5643
5697
|
let foreignQueueOwnerLiveness: TelegramProcessLiveness | undefined;
|
|
5644
5698
|
let releaseSourceReference: (() => void) | undefined;
|
|
5699
|
+
let sourceReferenceBinding: TelegramUpdateAdmissionLifecycleJournalBinding | undefined;
|
|
5700
|
+
|
|
5701
|
+
const withRetainedSourceReference = <T>(
|
|
5702
|
+
operation: (source: TelegramUpdateAdmissionLifecycleJournalBinding["journal"]) => T,
|
|
5703
|
+
): T => {
|
|
5704
|
+
if (!journal || !worker) throw new Error("Telegram update admission worker is not active.");
|
|
5705
|
+
// Synchronous retained-source access needs a reference, not renewed execution authority.
|
|
5706
|
+
const release = !releaseSourceReference && sourceReferenceBinding
|
|
5707
|
+
? deps.acquireSourceReference?.(sourceReferenceBinding) : undefined;
|
|
5708
|
+
try { return operation(journal); } finally { release?.(); }
|
|
5709
|
+
};
|
|
5710
|
+
const readObservedJournal = (): TelegramUpdateWorkerJournalSnapshot =>
|
|
5711
|
+
withRetainedSourceReference(source => source.read());
|
|
5645
5712
|
|
|
5646
5713
|
const stopCurrent = async (forget: boolean): Promise<void> => {
|
|
5647
5714
|
await worker?.stop();
|
|
@@ -5652,8 +5719,10 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5652
5719
|
{ phase: "source-reference-release" });
|
|
5653
5720
|
}
|
|
5654
5721
|
if (!forget) return;
|
|
5722
|
+
sourceReferenceBinding = undefined;
|
|
5655
5723
|
journal = undefined;
|
|
5656
5724
|
worker = undefined;
|
|
5725
|
+
workerInputCustody = undefined;
|
|
5657
5726
|
settlement = undefined;
|
|
5658
5727
|
journalBindingKey = undefined;
|
|
5659
5728
|
activeRuntimeKey = undefined;
|
|
@@ -5665,7 +5734,8 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5665
5734
|
await stopCurrent(true);
|
|
5666
5735
|
return;
|
|
5667
5736
|
}
|
|
5668
|
-
if (replace || !worker || activeRuntimeKey !== binding.runtimeKey
|
|
5737
|
+
if (replace || !worker || activeRuntimeKey !== binding.runtimeKey ||
|
|
5738
|
+
workerInputCustody !== binding.journal.inputCustody) {
|
|
5669
5739
|
const { runtimeKey, recoveryKey } = binding;
|
|
5670
5740
|
await stopCurrent(true);
|
|
5671
5741
|
binding = deps.resolveBinding();
|
|
@@ -5674,10 +5744,13 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5674
5744
|
throw new Error("Telegram update source binding changed during startup.");
|
|
5675
5745
|
}
|
|
5676
5746
|
const release = deps.acquireSourceReference?.(binding);
|
|
5747
|
+
const inputCustody = binding.journal.inputCustody;
|
|
5677
5748
|
try {
|
|
5678
5749
|
journal = binding.journal;
|
|
5679
5750
|
worker = deps.createWorker(binding.journal, binding);
|
|
5751
|
+
workerInputCustody = inputCustody;
|
|
5680
5752
|
settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
|
|
5753
|
+
sourceReferenceBinding = { ...binding, journal };
|
|
5681
5754
|
releaseSourceReference = release;
|
|
5682
5755
|
} catch (error) {
|
|
5683
5756
|
release?.();
|
|
@@ -5843,7 +5916,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5843
5916
|
"Telegram update journal queue handoff cancellation is not available.",
|
|
5844
5917
|
);
|
|
5845
5918
|
}
|
|
5846
|
-
const result =
|
|
5919
|
+
const result = withRetainedSourceReference(source => source.cancelQueuedHandoff!(input));
|
|
5847
5920
|
worker.signal();
|
|
5848
5921
|
return result;
|
|
5849
5922
|
},
|
|
@@ -5897,21 +5970,15 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5897
5970
|
ownsJournalBinding: (candidate) =>
|
|
5898
5971
|
journalBindingKey !== undefined && journalBindingKey === candidate,
|
|
5899
5972
|
getForeignQueueOwnerLiveness: () => foreignQueueOwnerLiveness,
|
|
5900
|
-
getJournalEntryCount()
|
|
5901
|
-
if (!journal || !worker) {
|
|
5902
|
-
throw new Error("Telegram update admission worker is not active.");
|
|
5903
|
-
}
|
|
5904
|
-
return journal.read().entries.length;
|
|
5905
|
-
},
|
|
5973
|
+
getJournalEntryCount: () => readObservedJournal().entries.length,
|
|
5906
5974
|
hasPendingQueueMutationForItem(item) {
|
|
5907
5975
|
return Boolean(
|
|
5908
5976
|
journal &&
|
|
5909
5977
|
worker &&
|
|
5910
|
-
|
|
5911
|
-
.read()
|
|
5912
|
-
.entries.some(
|
|
5978
|
+
readObservedJournal().entries.some(
|
|
5913
5979
|
(entry) =>
|
|
5914
5980
|
entry.state !== "queued" &&
|
|
5981
|
+
entry.preApprovalExcluded !== true &&
|
|
5915
5982
|
isTelegramReactionDependencyForQueueItem(
|
|
5916
5983
|
entry.update,
|
|
5917
5984
|
item,
|
|
@@ -5934,11 +6001,11 @@ export function createTelegramUpdateAdmissionLifecycleRuntime<TContext>(
|
|
|
5934
6001
|
settlement?.isItemReady(item) ??
|
|
5935
6002
|
(item.admissionReceipts?.length ?? 0) === 0,
|
|
5936
6003
|
onPromptHandedOff: (item, ctx) =>
|
|
5937
|
-
settlement?.onPromptHandedOff(item, ctx),
|
|
6004
|
+
settlement?.onPromptHandedOff(item, ctx) ?? false,
|
|
5938
6005
|
onControlSettled: (item, ctx) =>
|
|
5939
|
-
settlement?.onControlSettled(item, ctx),
|
|
6006
|
+
settlement?.onControlSettled(item, ctx) ?? false,
|
|
5940
6007
|
onItemsDiscarded: (items, ctx) =>
|
|
5941
|
-
settlement?.onItemsDiscarded(items, ctx),
|
|
6008
|
+
settlement?.onItemsDiscarded(items, ctx) ?? false,
|
|
5942
6009
|
};
|
|
5943
6010
|
}
|
|
5944
6011
|
|
|
@@ -141,6 +141,12 @@ export type TelegramWorkspaceRetirementFence =
|
|
|
141
141
|
phase: "deletion-issued";
|
|
142
142
|
deletionIssuedAtMs: number;
|
|
143
143
|
})
|
|
144
|
+
| (TelegramWorkspaceRetirementFenceBase & {
|
|
145
|
+
destructiveKind?: "pressure-retirement";
|
|
146
|
+
phase: "deletion-rejected";
|
|
147
|
+
deletionIssuedAtMs: number;
|
|
148
|
+
rejectionConfirmedAtMs: number;
|
|
149
|
+
})
|
|
144
150
|
| (TelegramWorkspaceRetirementFenceBase & {
|
|
145
151
|
phase: "commit-ready";
|
|
146
152
|
absenceConfirmedAtMs: number;
|
|
@@ -318,6 +324,14 @@ export interface TelegramWorkspaceAdmissionLedger {
|
|
|
318
324
|
expected: TelegramWorkspaceRetirementFence,
|
|
319
325
|
) => TelegramWorkspaceRetirementFence;
|
|
320
326
|
confirmThreadCleanupAbsence: TelegramWorkspaceAdmissionLedger["confirmRetirementAbsence"];
|
|
327
|
+
/** Caller must have exact transport proof of rejection, never merely target presence. */
|
|
328
|
+
confirmRetirementRejection: (
|
|
329
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
330
|
+
) => TelegramWorkspaceRetirementFence;
|
|
331
|
+
/** Caller must first durably withdraw the exact rejected intent, retaining its binding. */
|
|
332
|
+
completeRejectedRetirementFence: (
|
|
333
|
+
expected: TelegramWorkspaceRetirementFence,
|
|
334
|
+
) => boolean;
|
|
321
335
|
releaseUnissuedRetirementFence: (
|
|
322
336
|
expected: TelegramWorkspaceRetirementFence,
|
|
323
337
|
) => boolean;
|
|
@@ -491,6 +505,12 @@ function normalizeFence(
|
|
|
491
505
|
deletionIssuedAtMs: value.deletionIssuedAtMs,
|
|
492
506
|
};
|
|
493
507
|
}
|
|
508
|
+
if (value.phase === "deletion-rejected" &&
|
|
509
|
+
(value.destructiveKind === undefined || value.destructiveKind === "pressure-retirement") &&
|
|
510
|
+
isSafeTimestamp(value.deletionIssuedAtMs) && isSafeTimestamp(value.rejectionConfirmedAtMs)) {
|
|
511
|
+
return { ...base, destructiveKind: value.destructiveKind, phase: "deletion-rejected",
|
|
512
|
+
deletionIssuedAtMs: value.deletionIssuedAtMs, rejectionConfirmedAtMs: value.rejectionConfirmedAtMs };
|
|
513
|
+
}
|
|
494
514
|
if (
|
|
495
515
|
value.phase === "commit-ready" &&
|
|
496
516
|
isSafeTimestamp(value.absenceConfirmedAtMs) &&
|
|
@@ -768,6 +788,10 @@ function areFencesEqual(
|
|
|
768
788
|
) {
|
|
769
789
|
return left.deletionIssuedAtMs === right.deletionIssuedAtMs;
|
|
770
790
|
}
|
|
791
|
+
if (left.phase === "deletion-rejected" && right.phase === "deletion-rejected") {
|
|
792
|
+
return left.deletionIssuedAtMs === right.deletionIssuedAtMs &&
|
|
793
|
+
left.rejectionConfirmedAtMs === right.rejectionConfirmedAtMs;
|
|
794
|
+
}
|
|
771
795
|
return (
|
|
772
796
|
left.phase === "commit-ready" &&
|
|
773
797
|
right.phase === "commit-ready" &&
|
|
@@ -1519,6 +1543,9 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1519
1543
|
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1520
1544
|
authorityChanged("Telegram Workspace absence confirmation requires fence owner authority.");
|
|
1521
1545
|
}
|
|
1546
|
+
if (expected.phase === "deletion-rejected") {
|
|
1547
|
+
authorityChanged("Rejected Telegram Workspace deletion cannot confirm absence.");
|
|
1548
|
+
}
|
|
1522
1549
|
return transact((state) => {
|
|
1523
1550
|
if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1524
1551
|
authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
|
|
@@ -1571,9 +1598,10 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1571
1598
|
});
|
|
1572
1599
|
}
|
|
1573
1600
|
|
|
1574
|
-
function
|
|
1601
|
+
function completeDestructiveFence(
|
|
1575
1602
|
expected: TelegramWorkspaceRetirementFence,
|
|
1576
|
-
expectedKind: TelegramWorkspaceDeletionFenceKind
|
|
1603
|
+
expectedKind: TelegramWorkspaceDeletionFenceKind,
|
|
1604
|
+
requiredPhase: "commit-ready" | "deletion-rejected",
|
|
1577
1605
|
): boolean {
|
|
1578
1606
|
validateExpectedFence(expected, options.profileKey);
|
|
1579
1607
|
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
@@ -1588,14 +1616,47 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1588
1616
|
if (!areFencesEqual(state.fence, expected)) {
|
|
1589
1617
|
authorityChanged("Telegram Workspace retirement fence authority changed.");
|
|
1590
1618
|
}
|
|
1591
|
-
if (state.fence.phase !==
|
|
1592
|
-
authorityChanged(
|
|
1619
|
+
if (state.fence.phase !== requiredPhase) {
|
|
1620
|
+
authorityChanged(requiredPhase === "commit-ready"
|
|
1621
|
+
? "Telegram Workspace retirement commit is not ready."
|
|
1622
|
+
: "Telegram Workspace retirement rejection is not confirmed.");
|
|
1593
1623
|
}
|
|
1594
1624
|
delete state.fence;
|
|
1595
1625
|
return { result: true, changed: true };
|
|
1596
1626
|
});
|
|
1597
1627
|
}
|
|
1598
1628
|
|
|
1629
|
+
function confirmRetirementRejection(expected: TelegramWorkspaceRetirementFence): TelegramWorkspaceRetirementFence {
|
|
1630
|
+
validateExpectedFence(expected, options.profileKey);
|
|
1631
|
+
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== "pressure-retirement" ||
|
|
1632
|
+
!areOwnersEqual(expected.owner, options.owner) ||
|
|
1633
|
+
(expected.phase !== "deletion-issued" && expected.phase !== "deletion-rejected")) {
|
|
1634
|
+
authorityChanged("Telegram Workspace rejection requires exact issued retirement authority.");
|
|
1635
|
+
}
|
|
1636
|
+
return transact((state) => {
|
|
1637
|
+
const current = state.fence;
|
|
1638
|
+
if (!current || !isTelegramWorkspaceRetirementFence(current) || !areFenceBasesEqual(current, expected) ||
|
|
1639
|
+
(current.phase !== "deletion-issued" && current.phase !== "deletion-rejected") ||
|
|
1640
|
+
current.deletionIssuedAtMs !== expected.deletionIssuedAtMs ||
|
|
1641
|
+
(expected.phase === "deletion-rejected" && !areFencesEqual(current, expected))) {
|
|
1642
|
+
authorityChanged("Telegram Workspace retirement fence authority changed.");
|
|
1643
|
+
}
|
|
1644
|
+
if (current.phase === "deletion-rejected") return { result: cloneFence(current), changed: false };
|
|
1645
|
+
const rejected: TelegramWorkspaceRetirementFence = { ...current,
|
|
1646
|
+
destructiveKind: "pressure-retirement", phase: "deletion-rejected", rejectionConfirmedAtMs: getNowMs() };
|
|
1647
|
+
state.fence = rejected;
|
|
1648
|
+
return { result: cloneFence(rejected), changed: true };
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function completeRetirementFence(expected: TelegramWorkspaceRetirementFence,
|
|
1653
|
+
expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement") {
|
|
1654
|
+
return completeDestructiveFence(expected, expectedKind, "commit-ready");
|
|
1655
|
+
}
|
|
1656
|
+
function completeRejectedRetirementFence(expected: TelegramWorkspaceRetirementFence) {
|
|
1657
|
+
return completeDestructiveFence(expected, "pressure-retirement", "deletion-rejected");
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1599
1660
|
function adoptThreadCleanupFence(expected: TelegramWorkspaceRetirementFence,
|
|
1600
1661
|
replacement: { owner: TelegramWorkspaceAdmissionOwner; leaderEpoch: number | string }) {
|
|
1601
1662
|
return adoptRetirementFence(expected, replacement, "manual-thread-cleanup");
|
|
@@ -1635,6 +1696,8 @@ export function createTelegramWorkspaceAdmissionLedger(
|
|
|
1635
1696
|
issueThreadCleanupDeletionPermit,
|
|
1636
1697
|
confirmRetirementAbsence,
|
|
1637
1698
|
confirmThreadCleanupAbsence,
|
|
1699
|
+
confirmRetirementRejection,
|
|
1700
|
+
completeRejectedRetirementFence,
|
|
1638
1701
|
releaseUnissuedRetirementFence,
|
|
1639
1702
|
releaseUnissuedThreadCleanupFence,
|
|
1640
1703
|
completeRetirementFence,
|