@llblab/pi-kit 0.19.2 → 0.21.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/CHANGELOG.md +13 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +6 -6
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
- package/node_modules/@llblab/pi-state-flow/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/index.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +1 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +41 -11
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +94 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +2 -5
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +11 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +8 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +19 -6
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +5 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +27 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +10 -8
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +4 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +5 -5
- package/node_modules/@llblab/pi-state-flow/index.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +8 -3
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +41 -9
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +83 -1
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +2 -4
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +11 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +10 -4
- package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +23 -6
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +29 -3
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +13 -32
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +3 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +93 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +9 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +231 -71
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +34 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +63 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +25 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +32 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +22 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +9 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +23 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +7 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +68 -37
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +6 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +134 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +49 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +77 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +384 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +21 -14
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +31 -10
- package/node_modules/@llblab/pi-telegram/docs/updates.md +2 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +5 -7
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +69 -12
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +236 -94
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +39 -9
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +69 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +14 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +23 -4
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +32 -13
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -1
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +2 -3
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +45 -1
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -2
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +11 -3
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +73 -36
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +145 -78
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +67 -4
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +465 -7
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +7 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -11,7 +11,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
11
11
|
import { isTelegramApiCommitUnknownError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
|
|
12
12
|
import { withTelegramFileTransaction } from "./locks.js";
|
|
13
13
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
14
|
-
import { planTelegramWorkspaceSlotAllocation, TELEGRAM_WORKSPACE_SLOTS, } from "./workspace-slots.js";
|
|
14
|
+
import { planTelegramWorkspaceSlotAllocation, TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
|
|
15
15
|
import { resolveAgentDir, resolveTelegramProfileTempFilePath, } from "./paths.js";
|
|
16
16
|
const TELEGRAM_WORKSPACE_KEY_MAX_LENGTH = 180;
|
|
17
17
|
const TELEGRAM_SESSION_ID_MAX_LENGTH = 256;
|
|
@@ -1301,19 +1301,31 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1301
1301
|
dirty = true;
|
|
1302
1302
|
mutationRevision += 1;
|
|
1303
1303
|
};
|
|
1304
|
-
const
|
|
1304
|
+
const markWorkspaceBindingInactiveByTarget = (target, inactiveSinceMs = getNowMs()) => {
|
|
1305
|
+
if (!Number.isFinite(inactiveSinceMs) || inactiveSinceMs < 0)
|
|
1306
|
+
return false;
|
|
1307
|
+
for (const [key, binding] of workspaceBindings) {
|
|
1308
|
+
if (!targetMatches(binding.target, target) || binding.inactiveSinceMs !== undefined)
|
|
1309
|
+
continue;
|
|
1310
|
+
workspaceBindings.set(key, { ...binding, inactiveSinceMs });
|
|
1311
|
+
markDirty();
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
return false;
|
|
1315
|
+
};
|
|
1316
|
+
const persistSnapshot = (transition) => {
|
|
1305
1317
|
const persist = persistQueue.then(async () => {
|
|
1306
1318
|
const path = getPath();
|
|
1307
1319
|
if (loadedPath !== path && !dirty)
|
|
1308
1320
|
resetForPath(path);
|
|
1309
1321
|
if (options.canPersist && !options.canPersist()) {
|
|
1310
|
-
if (!
|
|
1322
|
+
if (!transition)
|
|
1311
1323
|
await loadFromDisk();
|
|
1312
1324
|
return false;
|
|
1313
1325
|
}
|
|
1314
1326
|
if (!dirty || !loaded)
|
|
1315
1327
|
await loadFromDisk();
|
|
1316
|
-
if (
|
|
1328
|
+
if (transition && !transition.isCurrent())
|
|
1317
1329
|
return false;
|
|
1318
1330
|
const nowMs = getNowMs();
|
|
1319
1331
|
reservations = reservations.filter((reservation) => reservation.expiresAtMs === undefined ||
|
|
@@ -1322,6 +1334,11 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1322
1334
|
const currentRecords = Array.from(records.values())
|
|
1323
1335
|
.filter(isPersistedThreadRecord)
|
|
1324
1336
|
.map(cloneRecord);
|
|
1337
|
+
if (transition?.kind === "detach") {
|
|
1338
|
+
const owners = currentRecords.filter((record) => targetMatches(record.target, transition.target));
|
|
1339
|
+
if (owners.length !== 1 || !isDeepStrictEqual(normalizeRecord(owners[0]), transition.owner))
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1325
1342
|
records = new Map(currentRecords.map((record) => [
|
|
1326
1343
|
getRecordOwnerKey(record),
|
|
1327
1344
|
cloneRecord(record),
|
|
@@ -1358,21 +1375,34 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1358
1375
|
return serialized;
|
|
1359
1376
|
}),
|
|
1360
1377
|
};
|
|
1361
|
-
if (
|
|
1362
|
-
const record = file.threads.find((record) => targetMatches(record.target,
|
|
1378
|
+
if (transition) {
|
|
1379
|
+
const record = file.threads.find((record) => targetMatches(record.target, transition.target));
|
|
1363
1380
|
if (!record)
|
|
1364
1381
|
return false;
|
|
1382
|
+
if (transition.kind === "detach") {
|
|
1383
|
+
const bindings = file.workspaceBindings.filter((binding) => targetMatches(binding.target, record.target));
|
|
1384
|
+
if (bindings.length !== 1 || !/^[A-Z]$/.test(record.slot ?? "") ||
|
|
1385
|
+
bindings[0].slot !== record.slot)
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1365
1388
|
file.threads = file.threads.filter((candidate) => candidate !== record);
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1389
|
+
for (const binding of file.workspaceBindings) {
|
|
1390
|
+
if (targetMatches(binding.target, transition.target) && binding.inactiveSinceMs === undefined) {
|
|
1391
|
+
binding.inactiveSinceMs = nowMs;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (transition.kind === "invalidate") {
|
|
1395
|
+
file.syncObservations = file.syncObservations.filter((observation) => !targetMatches(observation.target, record.target));
|
|
1396
|
+
file.syncObservations.push({
|
|
1397
|
+
target: { ...record.target },
|
|
1398
|
+
syncStatus: "deleted",
|
|
1399
|
+
observedAtMs: nowMs,
|
|
1400
|
+
...(record.instanceId ? { instanceId: record.instanceId } : {}),
|
|
1401
|
+
...(record.slot ? { slot: record.slot } : {}),
|
|
1402
|
+
lastSyncError: transition.lastSyncError,
|
|
1403
|
+
lastReconcileAction: "mark-stale",
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1376
1406
|
}
|
|
1377
1407
|
let persistedSemanticSnapshot;
|
|
1378
1408
|
try {
|
|
@@ -1382,7 +1412,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1382
1412
|
/* missing or invalid snapshots must be replaced */
|
|
1383
1413
|
}
|
|
1384
1414
|
// Normalize optional fields to wire JSON; object key order is not a state change.
|
|
1385
|
-
if (isDeepStrictEqual(persistedSemanticSnapshot, getTelegramStateSemanticSnapshot(JSON.parse(JSON.stringify(file)))) &&
|
|
1415
|
+
if (!transition && isDeepStrictEqual(persistedSemanticSnapshot, getTelegramStateSemanticSnapshot(JSON.parse(JSON.stringify(file)))) &&
|
|
1386
1416
|
mutationRevision === persistedRevision &&
|
|
1387
1417
|
statusRevision === persistedStatusRevision) {
|
|
1388
1418
|
loadedPath = path;
|
|
@@ -1398,18 +1428,21 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1398
1428
|
});
|
|
1399
1429
|
await chmod(tempPath, 0o600);
|
|
1400
1430
|
try {
|
|
1401
|
-
if (
|
|
1431
|
+
if (transition) {
|
|
1402
1432
|
let applied = false;
|
|
1403
1433
|
const commit = () => {
|
|
1404
1434
|
if (getPath() !== path || mutationRevision !== persistedRevision ||
|
|
1405
|
-
statusRevision !== persistedStatusRevision || !
|
|
1435
|
+
statusRevision !== persistedStatusRevision || !transition.isCurrent())
|
|
1406
1436
|
return;
|
|
1407
1437
|
// Fence and rename share one synchronous commit boundary. No stale
|
|
1408
|
-
//
|
|
1438
|
+
// transition enters the live projection before durable commit.
|
|
1409
1439
|
renameSync(tempPath, path);
|
|
1410
1440
|
loadedPath = path;
|
|
1411
|
-
records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target,
|
|
1441
|
+
records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target, transition.target)));
|
|
1412
1442
|
syncObservations = file.syncObservations;
|
|
1443
|
+
workspaceBindings = new Map(file.workspaceBindings.map((binding) => [
|
|
1444
|
+
getWorkspaceBindingMapKey(binding), cloneWorkspaceBinding(binding),
|
|
1445
|
+
]));
|
|
1413
1446
|
mutationRevision += 1;
|
|
1414
1447
|
dirty = false;
|
|
1415
1448
|
applied = true;
|
|
@@ -1465,7 +1498,13 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1465
1498
|
await persistSnapshot();
|
|
1466
1499
|
},
|
|
1467
1500
|
invalidateTarget(target, isCurrent, lastSyncError) {
|
|
1468
|
-
return persistSnapshot({ target, isCurrent, lastSyncError });
|
|
1501
|
+
return persistSnapshot({ kind: "invalidate", target, isCurrent, lastSyncError });
|
|
1502
|
+
},
|
|
1503
|
+
detachTargetOwner(expected, isCurrent) {
|
|
1504
|
+
const owner = normalizeRecord(expected);
|
|
1505
|
+
if (!owner)
|
|
1506
|
+
return Promise.resolve(false);
|
|
1507
|
+
return persistSnapshot({ kind: "detach", owner, target: owner.target, isCurrent });
|
|
1469
1508
|
},
|
|
1470
1509
|
list() {
|
|
1471
1510
|
return Array.from(records.values()).map(cloneRecord);
|
|
@@ -1994,18 +2033,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1994
2033
|
markDirty();
|
|
1995
2034
|
return true;
|
|
1996
2035
|
},
|
|
1997
|
-
markWorkspaceBindingInactiveByTarget
|
|
1998
|
-
if (!Number.isFinite(inactiveSinceMs) || inactiveSinceMs < 0)
|
|
1999
|
-
return false;
|
|
2000
|
-
for (const [key, binding] of workspaceBindings) {
|
|
2001
|
-
if (!targetMatches(binding.target, target) || binding.inactiveSinceMs !== undefined)
|
|
2002
|
-
continue;
|
|
2003
|
-
workspaceBindings.set(key, { ...binding, inactiveSinceMs });
|
|
2004
|
-
markDirty();
|
|
2005
|
-
return true;
|
|
2006
|
-
}
|
|
2007
|
-
return false;
|
|
2008
|
-
},
|
|
2036
|
+
markWorkspaceBindingInactiveByTarget,
|
|
2009
2037
|
markWorkspaceBindingActiveByTarget(target) {
|
|
2010
2038
|
if (hasWorkspaceRetirementConflict({ target }))
|
|
2011
2039
|
return false;
|
|
@@ -2419,8 +2447,11 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2419
2447
|
const record = Array.from(records.values()).find((entry) => targetMatches(entry.target, target));
|
|
2420
2448
|
const pending = syncStatus === "deleted" ? pendingProvisions.find((entry) => entry.target && targetMatches(entry.target, target)) : undefined;
|
|
2421
2449
|
const source = record ?? pending;
|
|
2450
|
+
// Confirmed absence ends this binding's active target lifetime, even after
|
|
2451
|
+
// an earlier stale observation already removed its live record.
|
|
2452
|
+
const inactive = syncStatus === "deleted" && markWorkspaceBindingInactiveByTarget(target);
|
|
2422
2453
|
if (!source?.target)
|
|
2423
|
-
return
|
|
2454
|
+
return inactive;
|
|
2424
2455
|
syncObservations = syncObservations.filter((entry) => !targetMatches(entry.target, target));
|
|
2425
2456
|
syncObservations.push({
|
|
2426
2457
|
target: { ...source.target }, syncStatus, observedAtMs: getNowMs(),
|
|
@@ -3576,7 +3607,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3576
3607
|
if (existing && isCurrentThreadRecord(existing)) {
|
|
3577
3608
|
const slot = existing.slot ?? deps.store.allocateSlot(request.profileKey);
|
|
3578
3609
|
if (!slot) {
|
|
3579
|
-
throw new
|
|
3610
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
3580
3611
|
}
|
|
3581
3612
|
const occupied = listOccupiedTelegramThreadIdentities({
|
|
3582
3613
|
records: deps.store.list(),
|
|
@@ -3691,7 +3722,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3691
3722
|
(candidateThreadName ? undefined : identity?.slot) ??
|
|
3692
3723
|
preferredNameSlot), request.workspaceBindingKey);
|
|
3693
3724
|
if (!slot) {
|
|
3694
|
-
throw new
|
|
3725
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
3695
3726
|
}
|
|
3696
3727
|
const uniqueCandidate = candidateThreadName &&
|
|
3697
3728
|
isTelegramTopicThreadNameValidForSlot(candidateThreadName, slot) &&
|
|
@@ -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>;
|
|
@@ -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;
|