@llblab/pi-telegram 0.54.4 → 0.55.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 +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +3 -1
- package/dist/lib/bindings.js +8 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +49 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +5 -3
- package/dist/lib/bus.js +13 -10
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +48 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +6 -1
- package/dist/lib/routing.js +334 -210
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +18 -5
- package/dist/lib/threads.js +107 -30
- package/dist/lib/turns.d.ts +2 -0
- package/dist/lib/turns.js +6 -1
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +12 -11
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +18 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +2 -2
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +9 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +17 -15
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +51 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +325 -238
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +120 -36
- package/lib/turns.ts +8 -1
- package/lib/updates.ts +58 -21
- package/package.json +1 -1
package/dist/lib/threads.js
CHANGED
|
@@ -73,6 +73,13 @@ export function createTelegramCleanupTargetProtection(store, departingRecord) {
|
|
|
73
73
|
// optional fields; neither changes the authority represented by a snapshot.
|
|
74
74
|
const sameSnapshot = (left, right) => isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
|
|
75
75
|
return (target, action) => {
|
|
76
|
+
try {
|
|
77
|
+
if (store.listProtectedLiveRebindTargets?.().some(candidate => targetMatches(candidate, target)))
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
76
83
|
// A source-bound temporary tab is removed only by its own Forward/Cancel, never by generic reconciliation.
|
|
77
84
|
if (store.listTemporaryThreadTargets) {
|
|
78
85
|
try {
|
|
@@ -1069,6 +1076,8 @@ function isWorkspaceLiveRebindIntent(value) {
|
|
|
1069
1076
|
"recipient",
|
|
1070
1077
|
"phase",
|
|
1071
1078
|
"cleanup",
|
|
1079
|
+
"inputRelease",
|
|
1080
|
+
"oldTargetProtected",
|
|
1072
1081
|
]) ||
|
|
1073
1082
|
value.kind !== "live-rebind" ||
|
|
1074
1083
|
!isTelegramWorkspaceRestoreRequest(value.request) ||
|
|
@@ -1085,6 +1094,13 @@ function isWorkspaceLiveRebindIntent(value) {
|
|
|
1085
1094
|
value.recipient.kind !==
|
|
1086
1095
|
(value.request.owner.owner?.kind === "leader" ? "leader" : "follower"))
|
|
1087
1096
|
return false;
|
|
1097
|
+
if (value.inputRelease !== undefined &&
|
|
1098
|
+
(!["held", "issued", "accepted"].includes(value.inputRelease) ||
|
|
1099
|
+
(value.inputRelease !== "held" && !["released", "finished"].includes(value.phase))))
|
|
1100
|
+
return false;
|
|
1101
|
+
if (value.oldTargetProtected !== undefined &&
|
|
1102
|
+
(value.oldTargetProtected !== true || value.inputRelease === undefined || value.phase !== "finished" || value.cleanup !== "not-issued"))
|
|
1103
|
+
return false;
|
|
1088
1104
|
if (value.phase === "rebound")
|
|
1089
1105
|
return value.cleanup === undefined;
|
|
1090
1106
|
if (value.phase === "released")
|
|
@@ -1272,11 +1288,33 @@ function conflictsWithWorkspaceRestoreProvision(request, provision) {
|
|
|
1272
1288
|
(targetMatches(provision.target, request.binding.target) ||
|
|
1273
1289
|
targetMatches(provision.target, request.target))));
|
|
1274
1290
|
}
|
|
1291
|
+
/** Canonical issuance is distinct from cleanup completion, source disposition and peer dispatch readiness. */
|
|
1292
|
+
export function isTelegramLiveRebindInputAuthorized(value) {
|
|
1293
|
+
return value.inputRelease === undefined
|
|
1294
|
+
? value.phase === "released" && value.cleanup === undefined
|
|
1295
|
+
: ["issued", "accepted"].includes(value.inputRelease) &&
|
|
1296
|
+
["released", "finished"].includes(value.phase) && value.cleanup !== "issued";
|
|
1297
|
+
}
|
|
1298
|
+
function liveRebindInputPending(value) {
|
|
1299
|
+
return value.inputRelease !== undefined && value.inputRelease !== "accepted";
|
|
1300
|
+
}
|
|
1301
|
+
function liveRebindActive(value) {
|
|
1302
|
+
return value.phase !== "finished" || liveRebindInputPending(value);
|
|
1303
|
+
}
|
|
1304
|
+
function liveRebindOldTargetUnknown(value) {
|
|
1305
|
+
return value.phase === "finished" && value.cleanup === "unknown" && value.inputRelease !== undefined;
|
|
1306
|
+
}
|
|
1307
|
+
function retainLiveRebind(value) {
|
|
1308
|
+
return liveRebindActive(value) || liveRebindOldTargetUnknown(value) || value.oldTargetProtected === true;
|
|
1309
|
+
}
|
|
1310
|
+
function protectedLiveRebindOldTargets(snapshot) {
|
|
1311
|
+
return (snapshot?.liveRebindings ?? []).filter(value => value.oldTargetProtected === true || liveRebindOldTargetUnknown(value) || value.inputRelease !== undefined && value.cleanup === "issued").map(value => ({ ...value.request.binding.target }));
|
|
1312
|
+
}
|
|
1275
1313
|
function pendingWorkspaceRelocationRequests(snapshot) {
|
|
1276
1314
|
return [
|
|
1277
1315
|
...(snapshot?.operations ?? []).map((value) => value.request),
|
|
1278
1316
|
...(snapshot?.liveRebindings ?? [])
|
|
1279
|
-
.filter(
|
|
1317
|
+
.filter(liveRebindActive)
|
|
1280
1318
|
.map((value) => value.request),
|
|
1281
1319
|
];
|
|
1282
1320
|
}
|
|
@@ -1311,7 +1349,7 @@ function assertWorkspaceRestoreBindingProtection(file) {
|
|
|
1311
1349
|
const owners = (file.threads ?? []).filter((value) => value.slot === current.slot ||
|
|
1312
1350
|
targetMatches(value.target, request.binding.target) ||
|
|
1313
1351
|
targetMatches(value.target, request.target));
|
|
1314
|
-
const live = file.workspaceRestore?.liveRebindings?.some((value) => value
|
|
1352
|
+
const live = file.workspaceRestore?.liveRebindings?.some((value) => liveRebindActive(value) &&
|
|
1315
1353
|
value.request.operationId === request.operationId);
|
|
1316
1354
|
if (owners.length > 1 ||
|
|
1317
1355
|
owners.some((value) => value.slot !== current.slot ||
|
|
@@ -1324,6 +1362,13 @@ function assertWorkspaceRestoreBindingProtection(file) {
|
|
|
1324
1362
|
throw new Error("Protected Workspace Restore owner target changed.");
|
|
1325
1363
|
}
|
|
1326
1364
|
}
|
|
1365
|
+
for (const old of protectedLiveRebindOldTargets(file.workspaceRestore)) {
|
|
1366
|
+
if (file.workspaceBindings?.some(value => targetMatches(value.target, old)) ||
|
|
1367
|
+
file.threads?.some(value => targetMatches(value.target, old) && ["active", "starting", "pending", "probe-required"].includes(value.status)) ||
|
|
1368
|
+
file.pendingProvisions?.some(value => !value.target || targetMatches(value.target, old)) ||
|
|
1369
|
+
file.reservations?.some(value => targetMatches(value.target, old)))
|
|
1370
|
+
throw new Error("Unknown live cleanup target remains protected.");
|
|
1371
|
+
}
|
|
1327
1372
|
const operations = [
|
|
1328
1373
|
...(file.workspaceRestore?.operations ?? []),
|
|
1329
1374
|
...(file.workspaceRestore?.liveRebindings ?? []),
|
|
@@ -1405,11 +1450,11 @@ function parseWorkspaceRestore(value) {
|
|
|
1405
1450
|
if (liveOperations.some((operation, index) => liveOperations
|
|
1406
1451
|
.slice(index + 1)
|
|
1407
1452
|
.some((other) => operation.request.operationId === other.request.operationId ||
|
|
1408
|
-
(operation
|
|
1409
|
-
other
|
|
1453
|
+
(liveRebindActive(operation) &&
|
|
1454
|
+
liveRebindActive(other) &&
|
|
1410
1455
|
workspaceRestoresConflict(operation.request, other.request))) ||
|
|
1411
1456
|
snapshot.operations.some((other) => operation.request.operationId === other.request.operationId ||
|
|
1412
|
-
(operation
|
|
1457
|
+
(liveRebindActive(operation) &&
|
|
1413
1458
|
workspaceRestoresConflict(operation.request, other.request)))))
|
|
1414
1459
|
throw new Error("Conflicting Workspace live rebinding evidence.");
|
|
1415
1460
|
const entries = snapshot.temporaryThreads ?? [];
|
|
@@ -1424,7 +1469,7 @@ function parseWorkspaceRestore(value) {
|
|
|
1424
1469
|
entries.some((entry) => !!entry.target &&
|
|
1425
1470
|
[
|
|
1426
1471
|
...snapshot.operations,
|
|
1427
|
-
...liveOperations.filter(
|
|
1472
|
+
...liveOperations.filter(liveRebindActive),
|
|
1428
1473
|
].some(({ request }) => targetMatches(request.binding.target, entry.target) ||
|
|
1429
1474
|
(targetMatches(request.target, entry.target) &&
|
|
1430
1475
|
!isTemporaryThreadRestore(entry, [{ request }])))))
|
|
@@ -2461,7 +2506,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2461
2506
|
(transition.kind === "relocate"
|
|
2462
2507
|
? !isDeepStrictEqual(transition.restore.operations.slice(0, -1), workspaceRestore?.operations ?? [])
|
|
2463
2508
|
: !isDeepStrictEqual(transition.restore.operations, workspaceRestore?.operations ?? []) ||
|
|
2464
|
-
!isDeepStrictEqual(transition.restore.liveRebindings?.slice(0, -1), (workspaceRestore?.liveRebindings ?? []).filter(
|
|
2509
|
+
!isDeepStrictEqual(transition.restore.liveRebindings?.slice(0, -1), (workspaceRestore?.liveRebindings ?? []).filter(retainLiveRebind))))
|
|
2465
2510
|
return false;
|
|
2466
2511
|
file.workspaceRestore = structuredClone(transition.restore);
|
|
2467
2512
|
parseWorkspaceRestore(file.workspaceRestore);
|
|
@@ -2891,6 +2936,14 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2891
2936
|
!isDeepStrictEqual(parseTopicTargetFile(readStoreSnapshot(path, false)), file))
|
|
2892
2937
|
throw new Error("Workspace live-rebind observation changed.");
|
|
2893
2938
|
},
|
|
2939
|
+
hasPreDispatchRebindEvidence() {
|
|
2940
|
+
const file = parseTopicTargetFile(readStoreSnapshot(getPath(), false) ?? { version: 1, records: [] });
|
|
2941
|
+
return file.workspaceRestore?.liveRebindings?.some(value => value.inputRelease !== undefined) === true;
|
|
2942
|
+
},
|
|
2943
|
+
listProtectedLiveRebindTargets() {
|
|
2944
|
+
const file = parseTopicTargetFile(readStoreSnapshot(getPath(), false) ?? { version: 1, records: [] });
|
|
2945
|
+
return protectedLiveRebindOldTargets(file.workspaceRestore);
|
|
2946
|
+
},
|
|
2894
2947
|
isWorkspaceLiveRebindCleanupTargetProtected(input) {
|
|
2895
2948
|
try {
|
|
2896
2949
|
const expected = structuredClone(input), path = getPath(), profile = activeProfile(), revision = mutationRevision;
|
|
@@ -2951,8 +3004,8 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2951
3004
|
targetMatches(other.request.target, old);
|
|
2952
3005
|
if (file.workspaceRestore.operations.some(touchesOld) ||
|
|
2953
3006
|
file.workspaceRestore.liveRebindings?.some((other) => other.request.operationId !== request.operationId &&
|
|
2954
|
-
other
|
|
2955
|
-
|
|
3007
|
+
(liveRebindActive(other) && touchesOld(other) ||
|
|
3008
|
+
(liveRebindOldTargetUnknown(other) || other.oldTargetProtected) && targetMatches(other.request.binding.target, old))))
|
|
2956
3009
|
return true;
|
|
2957
3010
|
for (const claim of workspaceClaims.values()) {
|
|
2958
3011
|
const bindings = canonical.filter((binding) => binding.bindingKey === claim.identity.bindingKey);
|
|
@@ -3113,12 +3166,14 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3113
3166
|
intent.routing !== undefined))
|
|
3114
3167
|
return false;
|
|
3115
3168
|
const state = read().state;
|
|
3169
|
+
if (protectedLiveRebindOldTargets(state).some(old => targetMatches(old, intent.request.binding.target) || targetMatches(old, intent.request.target)))
|
|
3170
|
+
return false;
|
|
3116
3171
|
if (pendingWorkspaceRelocationRequests(state).some((request) => workspaceRestoresConflict(request, intent.request)) ||
|
|
3117
3172
|
state.liveRebindings?.some((value) => value.request.operationId === intent.request.operationId))
|
|
3118
3173
|
return false;
|
|
3119
3174
|
if (state.revision === Number.MAX_SAFE_INTEGER)
|
|
3120
3175
|
throw new Error("Workspace Restore revision exhausted.");
|
|
3121
|
-
const pendingLive = (state.liveRebindings ?? []).filter(
|
|
3176
|
+
const pendingLive = (state.liveRebindings ?? []).filter(retainLiveRebind);
|
|
3122
3177
|
if ((live ? pendingLive.length : state.operations.length) >= 26)
|
|
3123
3178
|
throw new Error("Workspace Restore operation capacity reached.");
|
|
3124
3179
|
const restore = {
|
|
@@ -3352,7 +3407,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3352
3407
|
return {
|
|
3353
3408
|
list: () => structuredClone(storage.read().operations),
|
|
3354
3409
|
listLiveRebindings: () => structuredClone(storage.read().liveRebindings ?? []),
|
|
3355
|
-
async commitLiveRebind(requestInput, recipientInput, authority) {
|
|
3410
|
+
async commitLiveRebind(requestInput, recipientInput, authority, options) {
|
|
3356
3411
|
const request = structuredClone(requestInput), recipient = structuredClone(recipientInput);
|
|
3357
3412
|
const executor = structuredClone(authority.executor), operatorUserId = authority.operatorUserId;
|
|
3358
3413
|
const current = () => currentScope() &&
|
|
@@ -3370,6 +3425,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3370
3425
|
createdAtMs: at,
|
|
3371
3426
|
updatedAtMs: at,
|
|
3372
3427
|
phase: "rebound",
|
|
3428
|
+
...(options?.holdInput ? { inputRelease: "held" } : {}),
|
|
3373
3429
|
};
|
|
3374
3430
|
if (!current() || !isWorkspaceLiveRebindIntent(intent))
|
|
3375
3431
|
return undefined;
|
|
@@ -3383,7 +3439,8 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3383
3439
|
isDeepStrictEqual(value.request, request) &&
|
|
3384
3440
|
isDeepStrictEqual(value.recipient, recipient) &&
|
|
3385
3441
|
isDeepStrictEqual(value.executor, executor) &&
|
|
3386
|
-
value.operatorUserId === operatorUserId
|
|
3442
|
+
value.operatorUserId === operatorUserId &&
|
|
3443
|
+
(value.inputRelease !== undefined) === (options?.holdInput === true)
|
|
3387
3444
|
? structuredClone(value)
|
|
3388
3445
|
: undefined;
|
|
3389
3446
|
};
|
|
@@ -3411,14 +3468,24 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3411
3468
|
if (!entry ||
|
|
3412
3469
|
!isDeepStrictEqual(entry, expected) ||
|
|
3413
3470
|
entry.operatorUserId !== operator ||
|
|
3414
|
-
entry.phase === "finished")
|
|
3471
|
+
(entry.phase === "finished" && step !== "issue-input" && step !== "accept-input"))
|
|
3415
3472
|
return undefined;
|
|
3416
|
-
const issuing = step === "release" || step === "issue-cleanup";
|
|
3473
|
+
const issuing = step === "release" || step === "issue-cleanup" || step === "issue-input" || step === "accept-input" || step === "protect-old-target";
|
|
3417
3474
|
if ((issuing || step === "confirmed") &&
|
|
3418
3475
|
!isDeepStrictEqual(entry.executor, executor))
|
|
3419
3476
|
return undefined;
|
|
3420
3477
|
let next;
|
|
3421
|
-
if (step === "
|
|
3478
|
+
if (step === "issue-input" || step === "accept-input") {
|
|
3479
|
+
if (!["released", "finished"].includes(entry.phase) || entry.cleanup === "issued" || entry.inputRelease !== (step === "issue-input" ? "held" : "issued"))
|
|
3480
|
+
return undefined;
|
|
3481
|
+
next = { ...entry, inputRelease: step === "issue-input" ? "issued" : "accepted" };
|
|
3482
|
+
}
|
|
3483
|
+
else if (step === "protect-old-target") {
|
|
3484
|
+
if (entry.phase !== "released" || entry.cleanup !== undefined || entry.inputRelease !== "held")
|
|
3485
|
+
return undefined;
|
|
3486
|
+
next = { ...entry, phase: "finished", cleanup: "not-issued", oldTargetProtected: true };
|
|
3487
|
+
}
|
|
3488
|
+
else if (step === "release") {
|
|
3422
3489
|
if (entry.phase !== "rebound")
|
|
3423
3490
|
return undefined;
|
|
3424
3491
|
next = { ...entry, phase: "released" };
|
|
@@ -3673,8 +3740,18 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3673
3740
|
});
|
|
3674
3741
|
},
|
|
3675
3742
|
listTemporaryThreads: () => structuredClone(storage.read().temporaryThreads ?? []),
|
|
3676
|
-
reserveTemporaryThread(sourceInput, token, authority) {
|
|
3743
|
+
reserveTemporaryThread(sourceInput, token, authority, groupInput) {
|
|
3677
3744
|
const source = structuredClone(sourceInput);
|
|
3745
|
+
const group = groupInput
|
|
3746
|
+
? structuredClone(groupInput)
|
|
3747
|
+
: {
|
|
3748
|
+
journalBindingKey: source.journalBindingKey,
|
|
3749
|
+
updateIds: [source.updateId],
|
|
3750
|
+
};
|
|
3751
|
+
if (!isTemporaryThreadInput(group) ||
|
|
3752
|
+
group.journalBindingKey !== source.journalBindingKey ||
|
|
3753
|
+
Math.min(...group.updateIds) !== source.updateId)
|
|
3754
|
+
return undefined;
|
|
3678
3755
|
let reserved = false;
|
|
3679
3756
|
const entry = mutate(authority, (file, executor, operator) => {
|
|
3680
3757
|
const entries = file.temporaryThreads ?? [];
|
|
@@ -3690,12 +3767,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3690
3767
|
throw new Error("Invalid Workspace Restore clock.");
|
|
3691
3768
|
const next = {
|
|
3692
3769
|
source,
|
|
3693
|
-
inputs: [
|
|
3694
|
-
{
|
|
3695
|
-
journalBindingKey: source.journalBindingKey,
|
|
3696
|
-
updateIds: [source.updateId],
|
|
3697
|
-
},
|
|
3698
|
-
],
|
|
3770
|
+
inputs: [group],
|
|
3699
3771
|
operatorUserId: operator,
|
|
3700
3772
|
executor,
|
|
3701
3773
|
token,
|
|
@@ -3708,7 +3780,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3708
3780
|
if (!isTemporaryThreadEntry(next) ||
|
|
3709
3781
|
entries.some((value) => value.token === token ||
|
|
3710
3782
|
getTelegramTemporaryThreadInputs(value).some((input) => input.journalBindingKey === source.journalBindingKey &&
|
|
3711
|
-
input.updateIds.includes(
|
|
3783
|
+
input.updateIds.some((id) => group.updateIds.includes(id)))))
|
|
3712
3784
|
return undefined;
|
|
3713
3785
|
if (entries.length >= TEMPORARY_THREAD_CAPACITY)
|
|
3714
3786
|
throw new Error("Temporary Thread capacity reached.");
|
|
@@ -4075,7 +4147,8 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4075
4147
|
},
|
|
4076
4148
|
reserveThread(reservation) {
|
|
4077
4149
|
if (pendingWorkspaceRelocationRequests(workspaceRestore).some((request) => conflictsWithWorkspaceRestoreProvision(request, reservation)) ||
|
|
4078
|
-
conflictsWithTemporaryThread(workspaceRestore, reservation.target)
|
|
4150
|
+
conflictsWithTemporaryThread(workspaceRestore, reservation.target) ||
|
|
4151
|
+
protectedLiveRebindOldTargets(workspaceRestore).some(old => targetMatches(old, reservation.target))) {
|
|
4079
4152
|
throw new Error("Protected Workspace Restore provisioning conflict.");
|
|
4080
4153
|
}
|
|
4081
4154
|
const next = { ...reservation };
|
|
@@ -4086,7 +4159,8 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4086
4159
|
},
|
|
4087
4160
|
upsertPendingProvision(provision) {
|
|
4088
4161
|
if (pendingWorkspaceRelocationRequests(workspaceRestore).some((request) => conflictsWithWorkspaceRestoreProvision(request, provision)) ||
|
|
4089
|
-
conflictsWithTemporaryThread(workspaceRestore, provision.target)
|
|
4162
|
+
conflictsWithTemporaryThread(workspaceRestore, provision.target) ||
|
|
4163
|
+
protectedLiveRebindOldTargets(workspaceRestore).some(old => !provision.target || targetMatches(old, provision.target))) {
|
|
4090
4164
|
throw new Error("Protected Workspace Restore provisioning conflict.");
|
|
4091
4165
|
}
|
|
4092
4166
|
const next = {
|
|
@@ -4937,7 +5011,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4937
5011
|
const next = normalizeWorkspaceBindingRecord(binding);
|
|
4938
5012
|
if (next && hasWorkspaceRetirementConflict(next))
|
|
4939
5013
|
return undefined;
|
|
4940
|
-
if (!next)
|
|
5014
|
+
if (!next || protectedLiveRebindOldTargets(workspaceRestore).some(old => targetMatches(old, next.target)))
|
|
4941
5015
|
return undefined;
|
|
4942
5016
|
const nextMapKey = getWorkspaceBindingMapKey(next);
|
|
4943
5017
|
let claim = workspaceClaims.get(nextMapKey);
|
|
@@ -4994,16 +5068,19 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4994
5068
|
if (previous?.manualThreadName && !next.manualThreadName) {
|
|
4995
5069
|
next.manualThreadName = previous.manualThreadName;
|
|
4996
5070
|
}
|
|
4997
|
-
if (previous) {
|
|
5071
|
+
if (previous || replacedTargetBinding) {
|
|
4998
5072
|
const journalBindingKeys = Array.from(new Set([
|
|
4999
|
-
...(previous
|
|
5073
|
+
...(previous?.journalBindingKeys ?? []),
|
|
5074
|
+
...(replacedTargetBinding?.journalBindingKeys ?? []),
|
|
5000
5075
|
...(next.journalBindingKeys ?? []),
|
|
5001
5076
|
]));
|
|
5002
|
-
|
|
5077
|
+
const retainedComplete = (!previous || previous.journalBindingsComplete === true) &&
|
|
5078
|
+
(!replacedTargetBinding || replacedTargetBinding.journalBindingsComplete === true);
|
|
5079
|
+
if (journalBindingKeys.length || retainedComplete)
|
|
5003
5080
|
next.journalBindingKeys = journalBindingKeys;
|
|
5004
5081
|
else
|
|
5005
5082
|
delete next.journalBindingKeys;
|
|
5006
|
-
if (
|
|
5083
|
+
if (retainedComplete)
|
|
5007
5084
|
next.journalBindingsComplete = true;
|
|
5008
5085
|
else
|
|
5009
5086
|
delete next.journalBindingsComplete;
|
package/dist/lib/turns.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { type DownloadedTelegramMessageFile, type DownloadTelegramMessageFilesDe
|
|
|
7
7
|
import { truncateTelegramQueueSummary, type PendingTelegramTurn, type TelegramPreparedPromptTurn, type TelegramQueueItem, type TelegramQueueStore } from "./queue.ts";
|
|
8
8
|
import { type TelegramVoiceReplyMode } from "./voice.ts";
|
|
9
9
|
export declare const TELEGRAM_PREFIX = "[telegram]";
|
|
10
|
+
/** A routed source keeps its unique message/file/group identity, but cannot quote an anchor in its former tab. */
|
|
11
|
+
export declare function withoutTelegramTurnReplyAnchor<TMessage extends object>(message: TMessage): TMessage;
|
|
10
12
|
export declare const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
|
|
11
13
|
export interface TelegramTurnTarget {
|
|
12
14
|
chatId: number;
|
package/dist/lib/turns.js
CHANGED
|
@@ -9,6 +9,11 @@ import { appendTelegramAttachmentSection, appendTelegramListSection, buildTelegr
|
|
|
9
9
|
import { createTelegramQueueAdmissionReceipt, truncateTelegramQueueSummary, } from "./queue.js";
|
|
10
10
|
import { computeVoicePromptContribution, computeVoiceTurnFlags, getTelegramVoiceReplyMode, } from "./voice.js";
|
|
11
11
|
export const TELEGRAM_PREFIX = "[telegram]";
|
|
12
|
+
const REPLY_ANCHOR_SUPPRESSED = Symbol.for("@llblab/pi-telegram:reply-anchor-suppressed");
|
|
13
|
+
/** A routed source keeps its unique message/file/group identity, but cannot quote an anchor in its former tab. */
|
|
14
|
+
export function withoutTelegramTurnReplyAnchor(message) {
|
|
15
|
+
return { ...message, [REPLY_ANCHOR_SUPPRESSED]: true };
|
|
16
|
+
}
|
|
12
17
|
export const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
|
|
13
18
|
function getTelegramTurnTarget(message) {
|
|
14
19
|
return Number.isInteger(message.message_thread_id)
|
|
@@ -456,7 +461,7 @@ function buildPreparedTelegramPromptTurn(options, images) {
|
|
|
456
461
|
kind: "prompt",
|
|
457
462
|
chatId: firstMessage.chat.id,
|
|
458
463
|
target: getTelegramTurnTarget(firstMessage),
|
|
459
|
-
replyToMessageId: firstMessage.message_id,
|
|
464
|
+
replyToMessageId: Reflect.get(firstMessage, REPLY_ANCHOR_SUPPRESSED) === true ? 0 : firstMessage.message_id,
|
|
460
465
|
sourceMessageIds: collectTelegramMessageIds(options.messages),
|
|
461
466
|
queueOrder: options.queueOrder,
|
|
462
467
|
queueLane: "default",
|
package/dist/lib/updates.d.ts
CHANGED
|
@@ -229,7 +229,9 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
229
229
|
inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
|
|
230
230
|
inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
|
|
231
231
|
isSourceUnsettled?: () => boolean;
|
|
232
|
-
prepareLiveInput?: (sourceUpdateIds: readonly number[], isCurrent: () => boolean) => TelegramLiveDeferredInputPreparation | undefined;
|
|
232
|
+
prepareLiveInput?: (sourceUpdateIds: readonly number[], isCurrent: () => boolean, onTransferred?: () => void) => TelegramLiveDeferredInputPreparation | undefined;
|
|
233
|
+
/** Internal notification after the worker's exact donor-removal ACK; never issues completion itself. */
|
|
234
|
+
acknowledgeTransferred?: () => void;
|
|
233
235
|
prepareQueueAdmission?: () => TelegramDeferredQueueAdmissionPreparation | undefined;
|
|
234
236
|
prepareSourceCompletion?: () => Pick<TelegramDeferredSourceCompletionPreparation, "source" | "isCurrent"> | undefined;
|
|
235
237
|
supportsAbandonment?: (journalBindingKey: string) => boolean;
|
|
@@ -241,7 +243,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
241
243
|
export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
|
|
242
244
|
export declare function bindTelegramUpdateAdmissionSource<TUpdate extends TelegramUpdateFlow & {
|
|
243
245
|
update_id: number;
|
|
244
|
-
}>(update: TUpdate, report: TelegramUpdateAdmissionBinding["report"], controls?: Pick<TelegramUpdateAdmissionBinding, "abandon" | "armRoutingInput" | "getRoutingInput" | "acquireRouting" | "inspectSource" | "inspectSourceSnapshot" | "inspectCompletion" | "isSourceUnsettled" | "prepareLiveInput" | "prepareQueueAdmission" | "prepareSourceCompletion" | "supportsAbandonment" | "inspectAbandoning" | "inspectHistorical" | "isHistorical" | "isHistoricalReviewHeld">): TUpdate;
|
|
246
|
+
}>(update: TUpdate, report: TelegramUpdateAdmissionBinding["report"], controls?: Pick<TelegramUpdateAdmissionBinding, "abandon" | "armRoutingInput" | "getRoutingInput" | "acquireRouting" | "inspectSource" | "inspectSourceSnapshot" | "inspectCompletion" | "isSourceUnsettled" | "acknowledgeTransferred" | "prepareLiveInput" | "prepareQueueAdmission" | "prepareSourceCompletion" | "supportsAbandonment" | "inspectAbandoning" | "inspectHistorical" | "isHistorical" | "isHistoricalReviewHeld">): TUpdate;
|
|
245
247
|
/**
|
|
246
248
|
* Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
|
|
247
249
|
* is unresolved custody, except a lone button tap, which carries no input (the in-flight Cancel itself).
|
|
@@ -596,7 +598,9 @@ export interface TelegramUpdateExecutionFailureClassification {
|
|
|
596
598
|
}
|
|
597
599
|
export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
|
|
598
600
|
journal: TelegramUpdateWorkerJournalPort;
|
|
599
|
-
executeUpdate: (update: TelegramJournaledUpdate, ctx: TContext, signal: AbortSignal
|
|
601
|
+
executeUpdate: (update: TelegramJournaledUpdate, ctx: TContext, signal: AbortSignal, target?: TelegramTarget & {
|
|
602
|
+
threadId: number;
|
|
603
|
+
}) => Promise<TelegramUpdateAdmissionOutcome> | TelegramUpdateAdmissionOutcome;
|
|
600
604
|
/** Native source-only binding, installed by the admission-worker assembly rather than ordinary dispatch. */
|
|
601
605
|
admitPreparedLiveInput?: (update: TelegramJournaledUpdate, ctx: TContext, signal: AbortSignal) => Promise<{
|
|
602
606
|
outcome: TelegramUpdateAdmissionOutcome;
|
|
@@ -660,7 +664,9 @@ export interface TelegramLiveInputPreparation {
|
|
|
660
664
|
prepareStatusConsumption?(input: TelegramLiveStatusConsumption): boolean;
|
|
661
665
|
/** Native owner availability before recipient append; not admission or future target activation. */
|
|
662
666
|
canPrepareStatusConsumption?(): boolean;
|
|
663
|
-
release(canRelease: () => boolean
|
|
667
|
+
release(canRelease: () => boolean, target?: TelegramTarget & {
|
|
668
|
+
threadId: number;
|
|
669
|
+
}): boolean;
|
|
664
670
|
/** Ends a failed pre-append attempt only when no selected input was saved. */
|
|
665
671
|
cancelEmpty(): boolean;
|
|
666
672
|
/** One fenced removal of this never-dispatched recipient copy; unknown issuance cannot be retried. */
|
|
@@ -704,6 +710,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
|
|
|
704
710
|
sourceUpdateIds: readonly number[];
|
|
705
711
|
signal: AbortSignal;
|
|
706
712
|
isCurrent(): boolean;
|
|
713
|
+
onTransferred?(): void;
|
|
707
714
|
}) => TelegramLiveDeferredInputPreparation | undefined;
|
|
708
715
|
prepareDeferredQueueAdmission?: (input: {
|
|
709
716
|
updateId: number;
|
package/dist/lib/updates.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { withoutTelegramTurnReplyAnchor } from "./turns.js";
|
|
8
9
|
import { createTelegramUserPairingRuntime, getTelegramAuthorizationState, } from "./config.js";
|
|
9
10
|
import { TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_VERSION, areTelegramUpdateJournalQueueOwnersEqual, createTelegramUpdateJournalEntryDigest, doesTelegramJournalUpdateNameThread, isTelegramJournalLoneCallbackUpdate, getTelegramUpdateJournalBindingPath, isTelegramUpdateJournalLegacyFamilyVersion, isTelegramUpdateJournalQueueOwnerProcess, parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
10
11
|
import { isPiStaleContextError } from "./pi.js";
|
|
@@ -262,19 +263,26 @@ export function prepareTelegramLiveDeferredInput(values, isCurrent) {
|
|
|
262
263
|
carriers.some((value) => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal ||
|
|
263
264
|
getTelegramUpdateExecutionFence(value)?.isCurrent() !== true))
|
|
264
265
|
return undefined;
|
|
265
|
-
const
|
|
266
|
-
|
|
266
|
+
const bindings = carriers.map(getTelegramUpdateAdmissionBinding);
|
|
267
|
+
const acknowledgers = bindings.map(binding => binding?.acknowledgeTransferred);
|
|
268
|
+
const bindingsCurrent = () => carriers.every((value, index) => getTelegramUpdateAdmissionBinding(value) === bindings[index] && bindings[index]?.acknowledgeTransferred === acknowledgers[index]);
|
|
269
|
+
const unsettled = () => bindingsCurrent() && bindings.every(binding => binding?.isSourceUnsettled?.() === true);
|
|
267
270
|
if (!unsettled())
|
|
268
271
|
return undefined;
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
let transferred = false;
|
|
273
|
+
const prepared = getTelegramUpdateAdmissionBinding(first)?.prepareLiveInput?.(ids, () => bindingsCurrent() && isCurrent() &&
|
|
274
|
+
carriers.every((value) => getTelegramUpdateExecutionFence(value)?.isCurrent() === true), () => {
|
|
275
|
+
transferred = true;
|
|
276
|
+
for (let index = 0; index < bindings.length; index++)
|
|
277
|
+
acknowledgers[index]?.call(bindings[index]);
|
|
278
|
+
});
|
|
271
279
|
return (prepared && {
|
|
272
280
|
...prepared,
|
|
273
281
|
isCurrent: () => unsettled() && prepared.isCurrent(),
|
|
274
282
|
confirmSaved: () => unsettled() && prepared.confirmSaved(),
|
|
275
283
|
beginRelease: (canRelease) => unsettled() && prepared.beginRelease(() => unsettled() && canRelease()),
|
|
276
284
|
settleTransferred: (canSettle) => unsettled()
|
|
277
|
-
? prepared.settleTransferred(() => unsettled() && canSettle())
|
|
285
|
+
? prepared.settleTransferred(() => (transferred || unsettled()) && canSettle())
|
|
278
286
|
: "protected",
|
|
279
287
|
cancel: () => unsettled() && prepared.cancel(),
|
|
280
288
|
});
|
|
@@ -1714,7 +1722,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1714
1722
|
transition("blocked", currentUpdateId, "authority-lost");
|
|
1715
1723
|
return "blocked";
|
|
1716
1724
|
};
|
|
1717
|
-
const executeWithinOwner = async (expectedOwner, update, preparedExecute) => {
|
|
1725
|
+
const executeWithinOwner = async (expectedOwner, update, preparedExecute, target) => {
|
|
1718
1726
|
if (expectedOwner.controller.signal.aborted) {
|
|
1719
1727
|
return TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED;
|
|
1720
1728
|
}
|
|
@@ -1723,7 +1731,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1723
1731
|
? preparedExecute(update, expectedOwner.ctx, expectedOwner.controller.signal)
|
|
1724
1732
|
: deps.executeCustodiedUpdate
|
|
1725
1733
|
? deps.executeCustodiedUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal)
|
|
1726
|
-
: deps.executeUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal));
|
|
1734
|
+
: deps.executeUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal, target));
|
|
1727
1735
|
const settlement = execution.then((outcome) => custodied
|
|
1728
1736
|
? {
|
|
1729
1737
|
ok: true,
|
|
@@ -2434,7 +2442,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2434
2442
|
throw new Error("Prepared status admission is unconfirmed.");
|
|
2435
2443
|
return admitted.outcome;
|
|
2436
2444
|
}
|
|
2437
|
-
: undefined);
|
|
2445
|
+
: undefined, prepared?.target);
|
|
2438
2446
|
if (execution === TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED) {
|
|
2439
2447
|
return "aborted";
|
|
2440
2448
|
}
|
|
@@ -2490,6 +2498,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2490
2498
|
snapshotInvalidated = true;
|
|
2491
2499
|
break;
|
|
2492
2500
|
}
|
|
2501
|
+
if (prepared?.target) {
|
|
2502
|
+
prepared.ids = new Set([...prepared.ids].filter(id => id !== entry.updateId));
|
|
2503
|
+
if (!prepared.ids.size && livePreparation === prepared)
|
|
2504
|
+
livePreparation = undefined;
|
|
2505
|
+
updateClaimCounts();
|
|
2506
|
+
}
|
|
2493
2507
|
const postExecutionAuthority = checkAuthority(expectedOwner, entry.updateId);
|
|
2494
2508
|
if (postExecutionAuthority)
|
|
2495
2509
|
return postExecutionAuthority;
|
|
@@ -2856,7 +2870,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2856
2870
|
launchDrain();
|
|
2857
2871
|
},
|
|
2858
2872
|
prepareDeferredLiveInput(input) {
|
|
2859
|
-
const expectedOwner = owner, binding = deps.getJournalBindingKey?.(), ids = [...input.sourceUpdateIds];
|
|
2873
|
+
const expectedOwner = owner, binding = deps.getJournalBindingKey?.(), onTransferred = input.onTransferred, ids = [...input.sourceUpdateIds];
|
|
2860
2874
|
const sources = ids.map((id) => deferredSources.get(id));
|
|
2861
2875
|
const ownerCurrent = () => !!expectedOwner &&
|
|
2862
2876
|
owner === expectedOwner &&
|
|
@@ -2867,7 +2881,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2867
2881
|
deps.hasAuthority(expectedOwner.ctx) &&
|
|
2868
2882
|
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2869
2883
|
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2870
|
-
input.isCurrent();
|
|
2884
|
+
input.onTransferred === onTransferred && input.isCurrent();
|
|
2871
2885
|
const sourcesCurrent = () => ownerCurrent() &&
|
|
2872
2886
|
sources.every((source, index) => !!source &&
|
|
2873
2887
|
deferredSources.get(ids[index]) === source &&
|
|
@@ -2946,10 +2960,11 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2946
2960
|
held.discardIssued = true;
|
|
2947
2961
|
try {
|
|
2948
2962
|
const result = completeExact(ids, expected, undefined, () => current() && canSettle());
|
|
2949
|
-
if (!
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2963
|
+
if (result.removedUpdateIds.length !== ids.length || !ids.every(id => result.removedUpdateIds.includes(id)))
|
|
2964
|
+
return "unknown";
|
|
2965
|
+
// A consumed routing clock is expected only after this exact ACK, never after uncertain removal.
|
|
2966
|
+
onTransferred?.call(input);
|
|
2967
|
+
if (!current() || !canSettle())
|
|
2953
2968
|
return "unknown";
|
|
2954
2969
|
}
|
|
2955
2970
|
catch (error) {
|
|
@@ -3063,7 +3078,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
3063
3078
|
}
|
|
3064
3079
|
};
|
|
3065
3080
|
const finish = () => {
|
|
3066
|
-
if (!held.released || !held.status)
|
|
3081
|
+
if (!held.released || !held.status && !held.target)
|
|
3067
3082
|
livePreparation = undefined;
|
|
3068
3083
|
updateClaimCounts();
|
|
3069
3084
|
notifyStateChange();
|
|
@@ -3180,7 +3195,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
3180
3195
|
};
|
|
3181
3196
|
return held.status.current();
|
|
3182
3197
|
},
|
|
3183
|
-
release(canRelease) {
|
|
3198
|
+
release(canRelease, target) {
|
|
3184
3199
|
if (!current() ||
|
|
3185
3200
|
held.discardIssued ||
|
|
3186
3201
|
!held.saved ||
|
|
@@ -3193,6 +3208,16 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
3193
3208
|
!canRelease() ||
|
|
3194
3209
|
!current())
|
|
3195
3210
|
return false;
|
|
3211
|
+
if (target) {
|
|
3212
|
+
if (!Number.isSafeInteger(target.chatId) || target.chatId === 0 || !Number.isSafeInteger(target.threadId) || target.threadId <= 0 ||
|
|
3213
|
+
entries.some(entry => {
|
|
3214
|
+
const message = (entry.update.message ?? entry.update.edited_message);
|
|
3215
|
+
return message?.chat?.id !== target.chatId;
|
|
3216
|
+
}))
|
|
3217
|
+
return false;
|
|
3218
|
+
if (!held.status)
|
|
3219
|
+
held.target = { ...target };
|
|
3220
|
+
}
|
|
3196
3221
|
released = true;
|
|
3197
3222
|
held.released = true;
|
|
3198
3223
|
finish();
|
|
@@ -5257,7 +5282,8 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
|
|
|
5257
5282
|
signal,
|
|
5258
5283
|
})
|
|
5259
5284
|
: undefined,
|
|
5260
|
-
|
|
5285
|
+
acknowledgeTransferred: () => { settlementReported = true; },
|
|
5286
|
+
prepareLiveInput: (sourceUpdateIds, isCurrent, onTransferred) => execution.isCurrent() &&
|
|
5261
5287
|
!immediate &&
|
|
5262
5288
|
!settlementReported &&
|
|
5263
5289
|
!suspended
|
|
@@ -5266,6 +5292,7 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
|
|
|
5266
5292
|
sourceUpdateIds,
|
|
5267
5293
|
signal,
|
|
5268
5294
|
isCurrent: () => execution.isCurrent() && isCurrent(),
|
|
5295
|
+
onTransferred,
|
|
5269
5296
|
})
|
|
5270
5297
|
: undefined,
|
|
5271
5298
|
isHistorical: (matchesOriginal) => !signal.aborted &&
|
|
@@ -6299,10 +6326,18 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
6299
6326
|
update, ctx, signal),
|
|
6300
6327
|
}
|
|
6301
6328
|
: {}),
|
|
6302
|
-
async executeUpdate(update, ctx, signal) {
|
|
6329
|
+
async executeUpdate(update, ctx, signal, target) {
|
|
6303
6330
|
const typedUpdate = update;
|
|
6331
|
+
const prepared = deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate;
|
|
6332
|
+
// Journal/source evidence remains the original; only the warm released handler projection changes destination.
|
|
6333
|
+
const routed = target ? { ...prepared,
|
|
6334
|
+
...(prepared.message ? { message: prepared.message.message_thread_id === target.threadId ? { ...prepared.message }
|
|
6335
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.message, message_thread_id: target.threadId, reply_to_message: undefined }) } : {}),
|
|
6336
|
+
...(prepared.edited_message ? { edited_message: prepared.edited_message.message_thread_id === target.threadId ? { ...prepared.edited_message }
|
|
6337
|
+
: withoutTelegramTurnReplyAnchor({ ...prepared.edited_message, message_thread_id: target.threadId, reply_to_message: undefined }) } : {}),
|
|
6338
|
+
} : prepared;
|
|
6304
6339
|
try {
|
|
6305
|
-
return await executeUpdate(
|
|
6340
|
+
return await executeUpdate(routed, ctx, signal);
|
|
6306
6341
|
}
|
|
6307
6342
|
catch (error) {
|
|
6308
6343
|
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
package/dist/package.json
CHANGED
package/docs/activity.md
CHANGED
|
@@ -222,7 +222,7 @@ Default scope:
|
|
|
222
222
|
- `telegram` uses `{ kind: "target", target: event.target }`, binding delayed handlers to the immutable originating thread rather than whichever turn happens to be active later.
|
|
223
223
|
- `local`, `autonomous`, and `unknown` use `{ kind: "instance" }`.
|
|
224
224
|
|
|
225
|
-
Handlers may explicitly choose aggregate
|
|
225
|
+
Handlers may explicitly choose an authorized scope; aggregate output is available only in Classic Mode, never Threaded Mode's input-only All. Context methods delegate to the public delivery runtime on every call; they do not retain Telegram clients, Pi contexts, or transport ownership objects. The Delivery API rechecks authorization when an operation runs, so a captured target that is no longer owned fails closed instead of rerouting.
|
|
226
226
|
|
|
227
227
|
A handler may still receive events while no target is currently deliverable. Delivery then returns the normal structured `target-unavailable`, `target-unauthorized`, or `runtime-unavailable` result.
|
|
228
228
|
|