@llblab/pi-kit 0.35.0 → 0.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKLOG.md +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -6
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +22 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +75 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +11 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +30 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +61 -46
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +36 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +80 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +18 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +33 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +31 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +12 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +21 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +26 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +57 -12
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +6 -2
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +13 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +111 -15
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +30 -21
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +64 -56
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +122 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +22 -3
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +44 -10
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +40 -7
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -10
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +37 -20
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +69 -17
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -1307,8 +1307,14 @@ function liveRebindOldTargetUnknown(value) {
|
|
|
1307
1307
|
function retainLiveRebind(value) {
|
|
1308
1308
|
return liveRebindActive(value) || liveRebindOldTargetUnknown(value) || value.oldTargetProtected === true;
|
|
1309
1309
|
}
|
|
1310
|
+
function provisioningProtectionIntents(snapshot) {
|
|
1311
|
+
return [...snapshot.operations, ...(snapshot.liveRebindings ?? []).filter(retainLiveRebind)];
|
|
1312
|
+
}
|
|
1313
|
+
function protectedLiveRebindOldTargetIntents(snapshot) {
|
|
1314
|
+
return (snapshot?.liveRebindings ?? []).filter(value => value.oldTargetProtected === true || liveRebindOldTargetUnknown(value) || value.inputRelease !== undefined && value.cleanup === "issued");
|
|
1315
|
+
}
|
|
1310
1316
|
function protectedLiveRebindOldTargets(snapshot) {
|
|
1311
|
-
return (snapshot
|
|
1317
|
+
return protectedLiveRebindOldTargetIntents(snapshot).map(value => ({ ...value.request.binding.target }));
|
|
1312
1318
|
}
|
|
1313
1319
|
function pendingWorkspaceRelocationRequests(snapshot) {
|
|
1314
1320
|
return [
|
|
@@ -1362,10 +1368,11 @@ function assertWorkspaceRestoreBindingProtection(file) {
|
|
|
1362
1368
|
throw new Error("Protected Workspace Restore owner target changed.");
|
|
1363
1369
|
}
|
|
1364
1370
|
}
|
|
1365
|
-
for (const
|
|
1371
|
+
for (const { request } of protectedLiveRebindOldTargetIntents(file.workspaceRestore)) {
|
|
1372
|
+
const old = request.binding.target;
|
|
1366
1373
|
if (file.workspaceBindings?.some(value => targetMatches(value.target, old)) ||
|
|
1367
1374
|
file.threads?.some(value => targetMatches(value.target, old) && ["active", "starting", "pending", "probe-required"].includes(value.status)) ||
|
|
1368
|
-
file.pendingProvisions?.some(value =>
|
|
1375
|
+
file.pendingProvisions?.some(value => conflictsWithWorkspaceRestoreProvision(request, value)) ||
|
|
1369
1376
|
file.reservations?.some(value => targetMatches(value.target, old)))
|
|
1370
1377
|
throw new Error("Unknown live cleanup target remains protected.");
|
|
1371
1378
|
}
|
|
@@ -2942,7 +2949,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2942
2949
|
!isDeepStrictEqual(file.workspaceRestore?.liveRebindings?.find((intent) => intent.request.operationId === expected.request.operationId), expected))
|
|
2943
2950
|
throw new Error("Workspace live-rebind observation changed.");
|
|
2944
2951
|
assertWorkspaceRestoreBindingProtection(file);
|
|
2945
|
-
assertRestoreRecoveryProtection(path, file.workspaceRestore
|
|
2952
|
+
assertRestoreRecoveryProtection(path, provisioningProtectionIntents(file.workspaceRestore), file.pendingProvisions ?? []);
|
|
2946
2953
|
observe({
|
|
2947
2954
|
threads: file.threads,
|
|
2948
2955
|
workspaceBindings: file.workspaceBindings,
|
|
@@ -2976,7 +2983,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2976
2983
|
!isDeepStrictEqual(file.workspaceRestore?.liveRebindings?.find((intent) => intent.request.operationId === request.operationId), expected))
|
|
2977
2984
|
return true;
|
|
2978
2985
|
assertWorkspaceRestoreBindingProtection(file);
|
|
2979
|
-
assertRestoreRecoveryProtection(path, file.workspaceRestore
|
|
2986
|
+
assertRestoreRecoveryProtection(path, provisioningProtectionIntents(file.workspaceRestore), file.pendingProvisions ?? []);
|
|
2980
2987
|
const canonical = file.workspaceBindings ?? [], warm = Array.from(workspaceBindings.values());
|
|
2981
2988
|
const matchesRecipient = (bindings, rows) => {
|
|
2982
2989
|
const bindingsForKey = bindings.filter((binding) => binding.bindingKey === request.binding.bindingKey);
|
|
@@ -3008,7 +3015,9 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3008
3015
|
const protectedTarget = (bindings, rows, reserved, provisions, cleanups) => bindings.some((binding) => targetMatches(binding.target, old)) ||
|
|
3009
3016
|
rows.some((row) => currentRow(row) && targetMatches(row.target, old)) ||
|
|
3010
3017
|
reserved.some((value) => targetMatches(value.target, old)) ||
|
|
3011
|
-
|
|
3018
|
+
// Use the same binding/slot/owner/target conflicts as rebind admission. An unrelated unlocated
|
|
3019
|
+
// create must not globally veto cleanup of this exact former binding (nor its issued marker).
|
|
3020
|
+
provisions.some((value) => conflictsWithWorkspaceRestoreProvision(request, value)) ||
|
|
3012
3021
|
cleanups.some((value) => targetMatches(value.target, old));
|
|
3013
3022
|
if (protectedTarget(canonical, file.threads, file.reservations ?? [], file.pendingProvisions ?? [], file.pendingCleanups ?? []) ||
|
|
3014
3023
|
protectedTarget(warm, Array.from(records.values()), reservations, pendingProvisions, pendingCleanups) ||
|
|
@@ -3153,7 +3162,10 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3153
3162
|
if (bindings.length > 1 ||
|
|
3154
3163
|
file.threads.some((value) => targetMatches(value.target, target)) ||
|
|
3155
3164
|
file.reservations?.some((value) => targetMatches(value.target, target)) ||
|
|
3156
|
-
|
|
3165
|
+
// This exact created frame already proves a separately created routing tab (API ACK or authenticated
|
|
3166
|
+
// implicit creation). An unlocated Workspace create is not its owner; keep that intent unresolved,
|
|
3167
|
+
// but veto a concrete target collision. Generic unknown-target/Restore cleanup keeps its own guards.
|
|
3168
|
+
file.pendingProvisions?.some((value) => !!value.target && targetMatches(value.target, target)) ||
|
|
3157
3169
|
file.pendingCleanups?.some((value) => targetMatches(value.target, target)) ||
|
|
3158
3170
|
file.workspaceRetirements?.some((value) => targetMatches(value.binding.target, target)) ||
|
|
3159
3171
|
before.state.operations.some((value) => targetMatches(value.request.target, target)))
|
|
@@ -3274,7 +3286,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3274
3286
|
}
|
|
3275
3287
|
assertWorkspaceRestoreBindingProtection(before.file);
|
|
3276
3288
|
const provisions = parseTopicTargetFile(before.file).pendingProvisions ?? [];
|
|
3277
|
-
assertRestoreRecoveryProtection(path, before.state
|
|
3289
|
+
assertRestoreRecoveryProtection(path, provisioningProtectionIntents(before.state), provisions);
|
|
3278
3290
|
const after = { ...before.file, workspaceRestore: next };
|
|
3279
3291
|
// New temporary targets must be checked against the file they will be published into.
|
|
3280
3292
|
assertWorkspaceRestoreBindingProtection(parseTopicTargetFile(after));
|
|
@@ -3284,7 +3296,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3284
3296
|
if (workspaceIO) {
|
|
3285
3297
|
if (!workspaceFrame || !currentScope() || !isCurrent())
|
|
3286
3298
|
return false;
|
|
3287
|
-
assertRestoreRecoveryProtection(path, [...before.state
|
|
3299
|
+
assertRestoreRecoveryProtection(path, [...provisioningProtectionIntents(before.state), ...provisioningProtectionIntents(next)], provisions);
|
|
3288
3300
|
committedBefore = before;
|
|
3289
3301
|
committedAfter = JSON.parse(content);
|
|
3290
3302
|
workspaceFrame.value = committedAfter;
|
|
@@ -3304,7 +3316,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
3304
3316
|
return;
|
|
3305
3317
|
if (!isDeepStrictEqual(read().file, before.file))
|
|
3306
3318
|
throw new Error("Workspace Restore evidence changed before publication.");
|
|
3307
|
-
assertRestoreRecoveryProtection(path, [...before.state
|
|
3319
|
+
assertRestoreRecoveryProtection(path, [...provisioningProtectionIntents(before.state), ...provisioningProtectionIntents(next)], provisions);
|
|
3308
3320
|
if (!currentScope() || !isCurrent())
|
|
3309
3321
|
return;
|
|
3310
3322
|
replaceTelegramWorkspaceFile(temporary, path);
|
|
@@ -4175,7 +4187,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4175
4187
|
upsertPendingProvision(provision) {
|
|
4176
4188
|
if (pendingWorkspaceRelocationRequests(workspaceRestore).some((request) => conflictsWithWorkspaceRestoreProvision(request, provision)) ||
|
|
4177
4189
|
conflictsWithTemporaryThread(workspaceRestore, provision.target) ||
|
|
4178
|
-
|
|
4190
|
+
protectedLiveRebindOldTargetIntents(workspaceRestore).some(({ request }) => conflictsWithWorkspaceRestoreProvision(request, provision))) {
|
|
4179
4191
|
throw new Error("Protected Workspace Restore provisioning conflict.");
|
|
4180
4192
|
}
|
|
4181
4193
|
const next = {
|
|
@@ -4669,7 +4681,8 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4669
4681
|
},
|
|
4670
4682
|
captureWorkspaceSlotOccupancy(getExternalProtection, options) {
|
|
4671
4683
|
const nowMs = getNowMs();
|
|
4672
|
-
const
|
|
4684
|
+
const inSnapshot = getExternalProtection.withSnapshot ?? ((capture) => capture());
|
|
4685
|
+
const bindings = inSnapshot(() => Array.from(workspaceBindings.values()).map((binding) => {
|
|
4673
4686
|
const slot = binding.slot?.toLowerCase() ?? "";
|
|
4674
4687
|
const locallyProtected = isWorkspaceBindingLocallyProtected(binding, nowMs, {
|
|
4675
4688
|
requireBindingSlot: true,
|
|
@@ -4699,7 +4712,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4699
4712
|
: {}),
|
|
4700
4713
|
protection,
|
|
4701
4714
|
};
|
|
4702
|
-
});
|
|
4715
|
+
}));
|
|
4703
4716
|
const localReservedSlots = [
|
|
4704
4717
|
...Array.from(workspaceClaims.values()).map((claim) => claim.identity.slot),
|
|
4705
4718
|
...Array.from(records.values())
|
|
@@ -57,6 +57,8 @@ export declare function captureTelegramWorkspaceJournalProtectionSources(input:
|
|
|
57
57
|
complete: boolean;
|
|
58
58
|
createResolver: (path: string) => () => TelegramWorkspaceJournalReader | undefined;
|
|
59
59
|
};
|
|
60
|
+
/** Snapshot-scoped reuse of binding-independent (shared/discovered) source reads. */
|
|
61
|
+
sharedSourceReads?: Map<string, TelegramWorkspaceJournalProtectionSource>;
|
|
60
62
|
}): TelegramWorkspaceJournalProtectionCapture;
|
|
61
63
|
export type TelegramWorkspaceJournalProtectionSource = {
|
|
62
64
|
kind: "available";
|
|
@@ -158,9 +160,11 @@ interface TelegramWorkspaceProtectionObserverDeps {
|
|
|
158
160
|
getDeliveryAuthorityProtection?: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceProtectionState;
|
|
159
161
|
}
|
|
160
162
|
export interface TelegramWorkspaceProtectionObserver {
|
|
161
|
-
capture: (binding: TelegramWorkspaceThreadBinding, options?: {
|
|
163
|
+
capture: ((binding: TelegramWorkspaceThreadBinding, options?: {
|
|
162
164
|
requireBindingProvenance?: boolean;
|
|
163
|
-
}) => TelegramWorkspaceExternalProtectionEvidence
|
|
165
|
+
}) => TelegramWorkspaceExternalProtectionEvidence) & {
|
|
166
|
+
withSnapshot?: <T>(capture: () => T) => T;
|
|
167
|
+
};
|
|
164
168
|
captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
|
|
165
169
|
getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
|
|
166
170
|
}
|
|
@@ -46,27 +46,41 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
|
|
|
46
46
|
let complete = input.binding.journalBindingsComplete === true ||
|
|
47
47
|
input.discovery?.complete === true;
|
|
48
48
|
const capture = (scope, resolve) => {
|
|
49
|
+
const sharedKey = scope.kind === "shared"
|
|
50
|
+
? "shared"
|
|
51
|
+
: scope.kind === "discovered"
|
|
52
|
+
? `discovered:${scope.path}`
|
|
53
|
+
: undefined;
|
|
54
|
+
let source = sharedKey === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: input.sharedSourceReads?.get(sharedKey);
|
|
57
|
+
if (!source) {
|
|
58
|
+
source = readSource(scope, resolve);
|
|
59
|
+
if (sharedKey !== undefined)
|
|
60
|
+
input.sharedSourceReads?.set(sharedKey, source);
|
|
61
|
+
}
|
|
62
|
+
if (source.kind === "unknown")
|
|
63
|
+
complete = false;
|
|
64
|
+
sources.push(source);
|
|
65
|
+
};
|
|
66
|
+
const readSource = (scope, resolve) => {
|
|
49
67
|
try {
|
|
50
68
|
const binding = resolve();
|
|
51
|
-
if (!binding)
|
|
52
|
-
|
|
53
|
-
sources.push({ kind: "unknown", scope });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
69
|
+
if (!binding)
|
|
70
|
+
return { kind: "unknown", scope };
|
|
56
71
|
const read = binding.readForProtection ?? binding.journal.read;
|
|
57
72
|
const snapshot = input.withJournalReference
|
|
58
73
|
? input.withJournalReference(binding, read)
|
|
59
74
|
: read();
|
|
60
|
-
|
|
75
|
+
return {
|
|
61
76
|
kind: "available",
|
|
62
77
|
scope,
|
|
63
78
|
entries: snapshot.entries,
|
|
64
79
|
...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }),
|
|
65
|
-
}
|
|
80
|
+
};
|
|
66
81
|
}
|
|
67
82
|
catch {
|
|
68
|
-
|
|
69
|
-
sources.push({ kind: "unknown", scope });
|
|
83
|
+
return { kind: "unknown", scope };
|
|
70
84
|
}
|
|
71
85
|
};
|
|
72
86
|
capture({ kind: "shared" }, input.resolveLeader);
|
|
@@ -292,8 +306,38 @@ export function captureTelegramWorkspaceExternalProtection(input) {
|
|
|
292
306
|
}
|
|
293
307
|
/** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
|
|
294
308
|
export function createTelegramWorkspaceProtectionObserver(deps) {
|
|
309
|
+
// One occupancy snapshot probes every binding synchronously; namespace and shared/discovered
|
|
310
|
+
// journal reads are binding-independent, so read them once per snapshot instead of per binding.
|
|
311
|
+
// Never widen this beyond one synchronous capture: post-fence rechecks need fresh evidence.
|
|
312
|
+
let snapshot;
|
|
313
|
+
const inspectNamespace = () => {
|
|
314
|
+
if (!snapshot)
|
|
315
|
+
return deps.inspectJournalNamespace?.();
|
|
316
|
+
if (!snapshot.namespace) {
|
|
317
|
+
try {
|
|
318
|
+
snapshot.namespace = { value: deps.inspectJournalNamespace?.() };
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
snapshot.namespace = { error };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if ("error" in snapshot.namespace)
|
|
325
|
+
throw snapshot.namespace.error;
|
|
326
|
+
return snapshot.namespace.value;
|
|
327
|
+
};
|
|
328
|
+
const withSnapshot = (run) => {
|
|
329
|
+
if (snapshot)
|
|
330
|
+
return run();
|
|
331
|
+
snapshot = { reads: new Map() };
|
|
332
|
+
try {
|
|
333
|
+
return run();
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
snapshot = undefined;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
295
339
|
const captureJournalSources = (candidate) => {
|
|
296
|
-
const namespace =
|
|
340
|
+
const namespace = inspectNamespace();
|
|
297
341
|
if (deps.inspectJournalNamespace &&
|
|
298
342
|
(!namespace ||
|
|
299
343
|
namespace.sources.filter((source) => source.role === "polling")
|
|
@@ -332,9 +376,10 @@ export function createTelegramWorkspaceProtectionObserver(deps) {
|
|
|
332
376
|
},
|
|
333
377
|
}
|
|
334
378
|
: {}),
|
|
379
|
+
...(snapshot ? { sharedSourceReads: snapshot.reads } : {}),
|
|
335
380
|
});
|
|
336
381
|
};
|
|
337
|
-
const
|
|
382
|
+
const captureOne = function (binding, options) {
|
|
338
383
|
return captureTelegramWorkspaceExternalProtection({
|
|
339
384
|
binding,
|
|
340
385
|
requireBindingProvenance: options?.requireBindingProvenance,
|
|
@@ -384,7 +429,7 @@ export function createTelegramWorkspaceProtectionObserver(deps) {
|
|
|
384
429
|
});
|
|
385
430
|
};
|
|
386
431
|
return {
|
|
387
|
-
capture,
|
|
432
|
+
capture: Object.assign(captureOne, { withSnapshot }),
|
|
388
433
|
captureJournalSources,
|
|
389
434
|
getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown"),
|
|
390
435
|
};
|
|
@@ -485,6 +485,10 @@ This multi-input lifecycle is implemented in the local candidate, not authority
|
|
|
485
485
|
|
|
486
486
|
Temporary Reroute publishes the prompt in its selected destination before local queue/command admission or follower IPC. The existing exact whole-group `forwardedInputs` grant precedes this one unretried Bot API request as well as native dispatch; complete distinct positive returned IDs provide display/reply anchors, never source acceptance or settlement. `forwardMessages` preserves albums and requires ascending source IDs, so the warm map correlates each destination ID with its immutable original source update ID rather than a routed zero ID or array position. Acknowledged routing copies are preferred; a proven still-retained All original or real native-Thread original may supply the source instead. Before issuance and after awaits, guards recheck source/executor/operator/context/generation/profile, exact issued group, live recipient record and Workspace session binding; followers also retain their exact registration/protocol ownership. API and IPC callbacks stay local. Reroute passes recipient/session authority plus each original execution fence to the existing socket connect/write/ACK checks, with one guarded transport attempt; a stale post-write ACK cannot settle donor custody or undo accepted recipient work. The input's warm projection uses the confirmed copy ID and clears former-tab reply suppression before carrying the original execution fence; donor journal originals remain untouched. A partial/malformed/lost API reply or revoked authority keeps the issued group nonterminal with no output or native retry, even after restart. A lost native IPC ACK similarly retains donor authority and cannot replay an already accepted recipient input or its display copies. Local receipt disposal and exact whole-group foreign source-removal ACKs still own temporary cleanup; neither a visible copy nor model completion grants deletion. A follower donor ACK may precede recipient receipt disposal, so a fully resolved but protected, unissued tab retains one metadata-only one-second recheck and accepts existing session-delivery drain wakes. Every attempt reacquires profile admission and fresh namespace/binding/source/authority proof; time is not a deletion grant. A fresh runtime drain can reobserve retained completed membership without replaying its input. Issued or unknown deletion never repeats. Restore's held-input sequencing and cleanup schedules are unchanged.
|
|
487
487
|
|
|
488
|
+
A canonical temporary frame in phase `created` identifies its own independently created tab: either the exact one-shot `createForumTopic` response was acknowledged, or an owner-authenticated fresh implicit creation was admitted. An unrelated Workspace creation intent without a target therefore does not turn this positively identified tab into an unknown target. That uncertain intent is retained unchanged; this is neither age-based expiry nor permission to discard its recovery evidence. Any concrete provision targeting this tab, binding, owner, reservation, Restore operation, unresolved source or uncertain issued deletion retains its existing veto. `creating` frames never acquire this exemption, and generic unknown-target cleanup keeps its separate conservative rules. Restore old-target protection uses its exact binding/slot/owner/target conflict contract, including late recovered target evidence, rather than treating every unrelated targetless creation as a global veto; see the [Live-Rebind Channel](./multi-instance-bus.md#live-rebind-channel).
|
|
489
|
+
|
|
490
|
+
Fresh local menu Reroute (`/start`, `/help`, `/status`, `/model`, `/thinking`, `/queue`, `/settings`) uses the existing selected-command owner with a release barrier captured during preparation. After acknowledged destination publication, exact worker completion readiness and paired-recipient validation permit one acceptance report, not immediate menu effects. The routing operation finalizes its chooser and releases profile admission while retaining its exact source references. A bounded subscription then requires the worker's exact removal ACK; absence, a thrown removal after commit, timeout or ended source authority is not completion. Cleanup acquires a new profile admission, behind source-completion bookkeeping, and rechecks every ordinary temporary-tab protection. Only then are the accepted menu effects released under the captured recipient/session/target guard. Siblings, live queue custody, unconfirmed deletion and other protection may retain the tab without cancelling the independently accepted menu. An issued deletion never repeats. Lost recipient authority or an unconfirmed source ACK suppresses effects without replay. Sources lacking fresh exact completion readiness, other command kinds, follower delivery and Restore retain their existing paths; neither queue-producing commands nor cold inputs are pre-completed to manufacture cleanup authority. Ordinary follower ACK proves durable custody but does not hold the recipient worker before execution. Restore's held-command RPCs additionally bind relocation, saved input and canonical release authority; they are not a negotiated Reroute release protocol. Mutating local commands complete after their semantic action, and queue-producing commands retain their separately acknowledged receipts. Startup/historical inputs are excluded by the worker's fresh-source readiness predicate. Those contracts cannot borrow the menu-only barrier by changing command classification or weakening source protection.
|
|
491
|
+
|
|
488
492
|
- `Membership`: A positively source-linked disposable temporary Thread owns no Pi binding or A–Z slot and may contain multiple independent unassigned inputs, each with its own routing controls. Track exact journal source/group membership, including additional inputs, rather than treating the first chooser or a process-local count as the whole Thread. Serialize admission, selection, cancellation, Restore and cleanup through the existing Workspace admission and mutation owners; unreadable, incomplete, selected, accepted, running or unknown-issued evidence blocks deletion.
|
|
489
493
|
- `Per-input Cancel`: Privately retain the exact original and positively commit source-only cancellation before acknowledging `Routing cancelled` and deleting its one-shot chooser message. Other unassigned inputs retain their choosers and custody; cancelling one input neither delivers nor cancels siblings. No separate Cancel-and-delete action or extra confirmation dialog is required, but the chooser must disclose the conditional whole-tab deletion consequence.
|
|
490
494
|
- `Last-resolution cleanup`: Only an explicit Cancel or positive completion of an explicitly issued Forward that resolves the final known group may schedule deletion of the entire still-temporary Telegram tab, including all its messages. The operator-approved grace is now zero, superseding the earlier 1-second and 2–3-second designs: deletion is attempted right after the chooser is deleted. For Last Cancel, scheduling follows positively recorded whole-group cancellation and successful retirement of the final chooser, not the initial button tap. The timer establishes earliest eligibility for one proof-gated cleanup attempt, not an exact deletion time: Workspace admission, fresh protection checks and Bot API latency may delay or refuse it. Actual client timing was accepted in the operator's 0.52.0 live smoke. Mirrored native witnesses use shortened injected delays, not a production-default or Telegram-client timing measurement. This grace is presentation timing, not proof of emptiness or non-delivery. New input or a changed binding/authority cancels the scheduled cleanup. Immediately before issuance, reacquire current Workspace admission, recheck exact operator/profile/session/leader/source/target authority, positive disposable-tab provenance, complete source dispositions and all Thread protection, then publish the canonical one-shot temporary cleanup grant. A restored/bound Thread or any unresolved/unverifiable work remains protected. TTL, startup, silence and a zero chooser count do not manufacture this last-Cancel grant. Failed/unknown deletion cannot undo cancellation or cause automatic repeat issuance.
|
|
@@ -694,7 +698,7 @@ Assistant delivery guarantees:
|
|
|
694
698
|
- Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest: the remote private-chat identity wins, then non-owner caller metadata, with a non-bot replied peer available only as a final identity fallback when stronger conversation evidence is absent; username falls back to the remote display name and numeric id. Reply attribution still belongs independently in `[reply|from:...]`, and a replied bot can never define or replace the current `[telegram|guest:...]` location identity. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context. Media embedded in inbound Telegram `rich_message` blocks is downloaded like ordinary message media and stays attached to its forward-source block instead of being mislabeled as current-user material. Guest-mode turns append a `[guest]` block to their turn text stating the one-reply/limited-window constraint and instructing a fast, concise, self-contained answer; the note travels with the turn text rather than the system prompt.
|
|
695
699
|
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text. Replied media runs through the same inbound handlers and voice transcription providers as current-message media, with provenance-scoped `[outputs|from:…]` appended inside the reply block.
|
|
696
700
|
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
|
|
697
|
-
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice and guest-turn suppression, and serialized flushes. Telegram Guest Mode
|
|
701
|
+
- When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice and guest-turn suppression, and serialized flushes. Telegram Guest Mode uses one guest answer, normally an early inline placeholder edited at completion, so guest turns never start ordinary preview state even while `assistant.draftPreviews` is enabled. Queued attachments replace the acknowledged placeholder through cached-media edits rather than a second guest answer; supported types, error handling and staging authority follow the [Guest Mode media boundary](./outbound.md#guest-mode-media-boundary). Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
|
|
698
702
|
- Preview flushes are single-flight within their generation. Publication and final delivery capture only preceding draft/publication dependencies, never later drafts blocked by that publication, and final delivery performs no post-final draft-clear call. Reset/invalidation detaches old-generation dependencies; previously issued transport requests may still settle, but stale draft, timer and publication results cannot clear or mutate replacement work. Successful final text delivery clears local preview state only while its captured segment, session and transport remain active.
|
|
699
703
|
|
|
700
704
|
UI/compat rendering guarantees:
|
|
@@ -128,7 +128,7 @@ This operator-approved disposable-session policy is not Thread deletion, receipt
|
|
|
128
128
|
|
|
129
129
|
### Follower Registration And Session Readiness
|
|
130
130
|
|
|
131
|
-
Manual Connect may carry a Pi command-context object distinct from the receiver's canonical session context. Binding preparation uses that canonical object only after both contexts prove the same current, leader-acknowledged session ID and session generation; a distinct object requires explicit active-context and generation ports. Readiness captures the canonical object and rechecks its identity after preparation and on every read. Missing, inactive, foreign or replaced canonical contexts grant no inbound authority. Same-session command-context refresh preserves the prepared worker and held input; generation, session or registration replacement still invalidates readiness. Worker context equality and exact journal/custody checks remain unchanged.
|
|
131
|
+
Manual Connect may carry a Pi command-context object distinct from the receiver's canonical session context. Binding preparation uses that canonical object only after both contexts prove the same current, leader-acknowledged session ID and session generation; a distinct object requires explicit active-context and generation ports. Readiness captures the canonical object and rechecks its identity after preparation and on every read. Missing, inactive, foreign or replaced canonical contexts grant no inbound authority. Same-session command-context refresh preserves the prepared worker and held input; generation, session or registration replacement still invalidates readiness. Worker context equality and exact journal/custody checks remain unchanged. That distinction is a readiness guard only: promotion and every other post-online behavior use the canonical live session context, so a fresh Pi context object for the same session never blocks or changes them, and a replaced session generation or ID revokes them.
|
|
132
132
|
|
|
133
133
|
Same-target registration distinguishes the provisional Workspace claim from the binding actually returned by commit. A reused target may retain its existing slot while changing binding key; before persistence and registration publication, the leader aligns that target's owner record and registration result with the committed slot. The final exact binding/slot/target check and leader epoch/profile/operator fences remain mandatory; drift refuses publication rather than weakening the guard. Re-keying preserves retained historical journal keys and exact session/recipient source tuples, and cannot turn unknown retained coverage into a completeness proof. Independent bindings and their slots/history remain untouched.
|
|
134
134
|
|
|
@@ -271,6 +271,8 @@ Leader election is liveness-proof-gated and lock-backed. Since 0.54 the leader w
|
|
|
271
271
|
|
|
272
272
|
Followers first try to re-register after leader reload or unknown-heartbeat responses, carrying their last known target, slot, and thread name so the new leader can reuse the same binding. Follower Bot API calls already admitted by the active Pi turn wait for that bounded re-registration and capture its new exact generation before entering transport; they do not fail merely because recovery temporarily cleared local registration, and they never replay after an ambiguous transport commit. After the grace window followers promote only when the exact observed leader lease has become stale or inactive; an unavailable IPC endpoint never authorizes replacing a still-live owner. If the exact carried target is absent from persisted bindings, the leader first runs the same synchronous visibility probe: success recovers it instead of creating another Telegram thread, explicit stale evidence provisions a replacement, and ambiguous failure rejects registration. An ambiguous absent-target probe persists only non-routable `probe-required` restoration evidence, so targetless retries, successor follower processes with the same exact Workspace claim, and leader reloads probe that exact target again instead of activating it or provisioning a speculative replacement. A carried slot survives only when that slot remains free. Every successful reuse refreshes the binding timestamp. The leader never restores persisted followers into the live registry speculatively. Absent follower records remain durable restart hints until explicit stale, deleted, offline, or reconciliation evidence invalidates them; only fresh authenticated registration creates live routing authority. This preserves real thread bindings through reload and process-absence gaps without allowing historical records or competing pollers to masquerade as live state.
|
|
273
273
|
|
|
274
|
+
Participation is a property of live registered state alone: eligibility, deferral, and promotion depend on the acknowledged slot, the authenticated registration generation, and the current session, never on how that instance came online. A fresh Pi context object for the same session carries the same authority, and only a replaced session generation or ID revokes it.
|
|
275
|
+
|
|
274
276
|
## Leader/Follower Communication
|
|
275
277
|
|
|
276
278
|
### Protocol identity and compatibility
|
|
@@ -311,6 +313,8 @@ Every participating peer must negotiate the required capabilities and retain cur
|
|
|
311
313
|
|
|
312
314
|
Semantic reports, queued receipts, completion observation and delivery remain separate. Native exact ACK and donor CAS still own source disposition; lost or mismatched evidence retains uncertainty without handler replay, another queue or fallback admission. This section owns the unified command-channel policy; [architecture](./architecture.md#live-rebind-module-map) owns the module map. Existing Workspace Restore receivers/readers and queue-handoff retain their custody contracts; the new loaded root prefers live rebinding rather than using legacy Restore as an uncertain-input fallback. Source-enabled bounded cleanup is not proof of an actual deletion or client acceptance. Supported leader/follower prompts, commands and templates use `saved → binding applied → cleanup decision → input issuance → native admission/execution`. The chooser holds its existing profile admission through the unchanged whole first delay and one fresh cleanup attempt, without nested leader admission or another queue. A negotiated follower reports work through `observe-held` before release; binding readiness is not execution permission. `inputRelease: held/issued/accepted` is independent of cleanup: an issued input is never replayed after a lost admission reply, and a finished cleanup with held input remains active binding/source protection. Confirmed, failed or unknown terminal deletion permits the one native input attempt only on the confirmed binding. Issued and unknown old-target evidence remains canonically protected against reuse and generic cleanup, including response/publication interruption; unknown evidence survives input acceptance and is never capacity-evicted or reissued. If earlier work prevents issuance, input proceeds on the confirmed new target and the existing paced scheduler owns deferred cleanup; no old-target clearance is invented. Detached command producers accept their one input grant independently from exact source ACKs and reply publication; menu/reply authority tolerates only that issued-to-accepted progress, never changed binding, cleanup, recipient or generation. Expanded templates retain their captured content/default lane and Continue its control lane. A session-replacing `/new` producer with earlier protected work records positively unissued cleanup plus durable `oldTargetProtected`; it cannot erase that old-target protection or transfer a delete grant into the successor. Actual replacement still uses the existing post-removal Pi gateway and confirmation, not Restore readiness. Follower command/control completion retains the exact prepared-source/native ACK before donor CAS; queued admission is not completion and cannot dispose the donor. Unsupported producers do not gain arbitrary follower execution. Older peers keep the preceding supported channel. In deferred cleanup, old-tab cleanup waits for the recipient session to be idle, which the moved input itself may keep busy; its paced retries keep a whole first delay so a detached command reply still sees the released, cleanup-free row, but after that a leader's `agent_settled` or drained output barrier wakes a waiting retry at once. Publication/command-work, Activity handlers, per-target companion queues and target/chat API counters capture lifetime-fenced notifications when work starts and emit only after their pending observation clears. The session adapter defers the hint without awaiting cleanup and rechecks its captured context generation, transport/profile and leader epoch/operator authority. Streaming technical events can drain the publication queue without ending the agent run. A delivery hint while SDK idle is false, unavailable or throwing is retained in the existing chain but does not clear its timer or acquire Workspace admission. `agent_settled` remains an unconditional recheck hint; after settlement, actual remaining delivery drains can wake the retry. A running attempt retains one coalesced hint without a concurrent check, then re-enters the same idle filter after releasing its admission. A fresh timer/settlement attempt consumes the previously deferred hint; signals arriving during that await are not lost. Timer retries, including busy-session samples, are unchanged. Terminal outcomes discard hints, including unknown issued deletion. The initial one-second delay, retry/window fallback, follower pacing and negotiated IPC contracts remain unchanged. Each chain also retains its original generation/profile-store/epoch/operator lifetime rather than rebasing to a reused context. The wake only re-samples every gate through fresh admission; it never grants cleanup, clears unconfirmed work, occupies the publication tail or holds Pi settlement for deletion. Windows runs the same strict paths, exercised by the CI matrix; actual Pi/client acceptance and release are not inferred from supplied IPC fixtures.
|
|
313
315
|
|
|
316
|
+
Live-rebind old-target cleanup and retained issued/unknown deletion markers use the same provisioning-conflict identity as rebind admission: binding key, slot, legacy owner identity and concrete old/new target. An unrelated intent without a target is retained, but is not a global veto on this exact former binding. The inverse provisioning write checks use the same rule. Fresh provisioning-recovery evidence participates in cleanup observation, publication and captured-recipient API-boundary checks, including after issuance; an exact late target collision or malformed matching recovery still refuses without erasing the intent or retrying a deletion. The ordering remains held input → confirmed new binding → old-tab cleanup decision → input release, not destructive deletion before a replacement binding is established. Native All `/start` regressions cover the successful old-tab deletion/new-tab preservation sequence, an unrelated ambiguous intent, protected busy work and a lost deletion reply. The paired Reroute regression deletes only the exhausted temporary tab and preserves the selected instance's binding. Post-reload operator checks confirmed both idle leader flows: Reroute removes its exhausted temporary tab, while Restore preserves the new tab and removes the old one. This acceptance does not imply every busy-session or follower scenario was exercised.
|
|
317
|
+
|
|
314
318
|
### Local IPC endpoint with bounded native paths
|
|
315
319
|
|
|
316
320
|
Leader opens a local Node `net` endpoint only for the start that still owns its lifecycle: a stop that lands while startup is still replaying cleanup or binding the endpoint makes that start release whatever it brought up and never begin polling, and the startup display-stabilization timer arms only for the start that began it. The endpoint is a Unix-domain socket under the agent temp directory on Unix-like platforms, or a deterministic Windows named pipe (`\\.\pipe\pi-telegram-...`) on native Windows. A filesystem-style endpoint supplied by legacy state or a transport harness normalizes deterministically to the same Windows pipe boundary before listen/connect. On Unix, an endpoint that would exceed conservative domain-socket pathname limits maps to a private user-scoped, hash-derived path under the OS temp directory; ordinary agent paths remain under `tmp/pi-telegram`. On Unix, each server listens on a private generation socket and atomically publishes the stable profile path as a relative symlink; delayed shutdown closes only its private path and cannot remove a replacement generation's link. Followers register, heartbeat, and exchange routed events. The transport boundary owns endpoint derivation, socket-vs-pipe detection, bounded operation-aware retry policy, timeout/transient IPC error classification, endpoint reachability probes, and request-scoped transport events. Follower registration uses a longer registration-specific response timeout than ordinary heartbeat/forwarding calls because the leader may need to provision a Telegram thread before it can return the assigned target; timing out that handshake leaves a visible tab with no follower heartbeat. Keep this handshake to the true critical path: create/reuse the target, persist the live binding, and return it. Connected notices and replaced-thread reconciliation cleanup are non-critical and should run after registration so a follower becomes routable before Telegram client/server UI convergence work finishes.
|
|
@@ -587,7 +591,7 @@ For repeated unselected All-tab `/start`, a newer durable source supersedes olde
|
|
|
587
591
|
|
|
588
592
|
The bounded pending reroute owns its original source target and the returned chooser message ID independently from remaining messages. Authenticated callbacks must match the stored chat/chooser and any supplied thread field. `InaccessibleMessage` may omit the thread field; a missing callback message, unknown chooser, conflicting identity, threadless source, or already-owned Restore source fails closed rather than forwarding to the old target. Restart or expiry without that pending identity does not authorize reconstruction from callback data alone. When the original input itself has no thread ID (including an All-tab `/start`), the chooser offers routing only, not Restore; old Restore callbacks explain that a plain message must first be sent in the destination thread. Client tab selection is not inferred from recent topic creation or from the chosen Pi instance.
|
|
589
593
|
|
|
590
|
-
After dispatch, cleanup retries retain source identity but never redispatch accepted messages. Confirmed typed HTTP 400 `message to delete not found` completes message deletion idempotently; permission and transient failures remain errors. Reroute cleanup rechecks current bindings, live targets, reservations, and pending provisions before close, before delete, and on retry. Other destructive cleanup origins use the shared synchronous target-protection policy: explicit retirement permits only its unchanged departing binding, persisted shutdown intent permits only its original pre-intent binding, and reservation/provision cleanup permits only the corresponding unchanged claim. Protection checks also guard post-API local invalidation, reservation, and disconnect completion. A newly protected target cancels remaining cleanup. Already-issued remote operations cannot be undone by a later local ownership change; checks prevent subsequent effects, not retroactive cancellation.
|
|
594
|
+
After dispatch, cleanup retries retain source identity but never redispatch accepted messages. For a fully resolved, positively unbound temporary tab, explicit cleanup may recover historical v1 queued receipts owned by confirmed-dead processes under profile admission and cross-family journal serialization. Strict complete namespace and exact-reference inspection precede whole unoffered receipt CAS recovery; alive/unverifiable owners, offered receipts, malformed sources and unsupported custody remain protective. Protection reads themselves never recover or repair files, and successful recovery is followed by a fresh ordinary protection check before one deletion grant. Confirmed typed HTTP 400 `message to delete not found` completes message deletion idempotently; permission and transient failures remain errors. Reroute cleanup rechecks current bindings, live targets, reservations, and pending provisions before close, before delete, and on retry. Other destructive cleanup origins use the shared synchronous target-protection policy: explicit retirement permits only its unchanged departing binding, persisted shutdown intent permits only its original pre-intent binding, and reservation/provision cleanup permits only the corresponding unchanged claim. Protection checks also guard post-API local invalidation, reservation, and disconnect completion. A newly protected target cancels remaining cleanup. Already-issued remote operations cannot be undone by a later local ownership change; checks prevent subsequent effects, not retroactive cancellation.
|
|
591
595
|
|
|
592
596
|
Follower Restore uses only the source-bound `leader.workspaceRestore` protocol described under [protocol identity and compatibility](#protocol-identity-and-compatibility). The follower validates the retained operation and exact registration/session authority under read-only canonical snapshot protection; it never persists leader-owned state. Lost apply replies require inspection, not target replacement or another apply grant. Legacy target-replacement requests are rejected before recipient effects.
|
|
593
597
|
|
|
@@ -80,9 +80,11 @@ Core assistant output accepts only the Markdown or HTML `InputRichMessage` forms
|
|
|
80
80
|
|
|
81
81
|
### Guest Mode media boundary
|
|
82
82
|
|
|
83
|
-
A Guest Mode reply
|
|
83
|
+
A Guest Mode reply starts with one `answerGuestQuery` call carrying exactly one `InlineQueryResult`; it is not a normal chat target and cannot receive `sendDocument`/`sendVoice` multipart uploads through sentinel `chatId: 0`. The direct owner normally answers immediately with a placeholder and retains its `inline_message_id` for the final edit. `telegram_attach` admits at most one file during a guest turn and rejects additional files before queue mutation.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Local guest documents, PNG/JPEG photos and MP3 audio use a temporary upload to the paired owner's bot chat, extraction of the returned `file_id`, and best-effort deletion of the staging message. With an acknowledged placeholder, the final result replaces that exact inline message through `editMessageMedia`; it never calls `answerGuestQuery` again. Without a placeholder, delivery retains the single cached-media guest-answer path, including OGG/OPUS voice support. The staging message can briefly appear or notify the owner. One reply carries one media item, with final text truncated to the 1024-character caption limit. No external file hosting or upload to `chatId: 0` is used.
|
|
86
|
+
|
|
87
|
+
OGG/OPUS voice attachments cannot replace the acknowledged inline placeholder and are rejected before queue mutation; voice markup produces an explicit explanation instead of silently disappearing. Use MP3 audio or a normal bot chat. Upload failures and confirmed media-edit rejections replace the placeholder with a delivery-error notice where possible. An uncertain media-edit outcome is recorded without retrying or overwriting the possibly delivered media. Session/transport authority is checked after placeholder stop and upload, and at final edit issuance; retired work cannot edit, send fallback text or delete staging messages under successor authority. Actual guest-media rendering remains a live-client acceptance check.
|
|
86
88
|
|
|
87
89
|
Configured text handlers provide `template`. A string is one command; an array is ordered composition. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values. The command-template default timeout applies automatically. Use `template: [...]` for composition; the old local `pipe` alias is removed in 0.13.0. A falsy root `when` skips that handler: inbound and voice selection continue with the next matching handler, while a text transform passes its input through unchanged. A falsy `when` on a composed step, or on any enclosing composition node, skips that step and passes the previous output on. `recover` runs only between failed `retry` attempts of the node that declares it; its output is ignored and a recovery failure stops further retries. `parallel` and `delay` are not executed by these handlers.
|
|
88
90
|
|
|
@@ -1171,7 +1171,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
1171
1171
|
shutdownGenerativeAppLiveSurfaces?: () => void;
|
|
1172
1172
|
resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
|
|
1173
1173
|
buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
|
|
1174
|
-
callMultipart:
|
|
1174
|
+
callMultipart: (...args: Parameters<TelegramApi.TelegramBridgeApiRuntime["callMultipart"]>) => Promise<unknown>;
|
|
1175
1175
|
sendChatAction: NonNullable<
|
|
1176
1176
|
OutboundHandlers.TelegramVoiceReplySenderDeps["sendChatAction"]
|
|
1177
1177
|
>;
|
|
@@ -1198,6 +1198,7 @@ interface TelegramLifecycleBindingDeps {
|
|
|
1198
1198
|
ctx: Pi.ExtensionContext,
|
|
1199
1199
|
) => void;
|
|
1200
1200
|
answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
|
|
1201
|
+
editGuestInlineMessage?: TelegramApi.TelegramBridgeApiRuntime["editGuestInlineMessage"];
|
|
1201
1202
|
deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
|
|
1202
1203
|
sendGuestReply: NonNullable<
|
|
1203
1204
|
Queue.TelegramAgentEndHookRuntimeDeps<
|
|
@@ -1276,6 +1277,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1276
1277
|
onSteerTurnEnd,
|
|
1277
1278
|
onPromptHandedOff,
|
|
1278
1279
|
answerGuestQuery,
|
|
1280
|
+
editGuestInlineMessage,
|
|
1279
1281
|
deleteMessage,
|
|
1280
1282
|
sendGuestReply,
|
|
1281
1283
|
editGuestReply,
|
|
@@ -1318,6 +1320,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1318
1320
|
turn: Queue.PendingTelegramTurn,
|
|
1319
1321
|
attachment: Queue.QueuedAttachment,
|
|
1320
1322
|
caption?: string,
|
|
1323
|
+
options?: { isDeliveryActive?: () => boolean },
|
|
1321
1324
|
): Promise<void> => {
|
|
1322
1325
|
const stagingTarget = proactivePushTargetGetter();
|
|
1323
1326
|
const stagingChatId = stagingTarget?.chatId;
|
|
@@ -1328,11 +1331,19 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
1328
1331
|
}
|
|
1329
1332
|
await OutboundAttachments.deliverTelegramGuestCachedAttachment({
|
|
1330
1333
|
guestQueryId: turn.guestQueryId!,
|
|
1334
|
+
guestInlineMessageId: turn.guestInlineMessageId,
|
|
1335
|
+
editGuestInlineMessage,
|
|
1336
|
+
isDeliveryActive: options?.isDeliveryActive,
|
|
1331
1337
|
stagingChatId,
|
|
1332
1338
|
stagingTarget,
|
|
1333
1339
|
attachment,
|
|
1334
1340
|
caption,
|
|
1335
|
-
sendMultipart:
|
|
1341
|
+
sendMultipart: (method, fields, fileField, path, name) =>
|
|
1342
|
+
callMultipart(method, fields, fileField, path, name, {
|
|
1343
|
+
assertAuthority() {
|
|
1344
|
+
if (options?.isDeliveryActive?.() === false) throw new Error("Guest attachment upload lost authority.");
|
|
1345
|
+
},
|
|
1346
|
+
}),
|
|
1336
1347
|
answerGuestQuery: (guestQueryId, result) =>
|
|
1337
1348
|
answerGuestQuery(guestQueryId, undefined, { result }),
|
|
1338
1349
|
answerGuestText: (guestQueryId, text) =>
|
|
@@ -536,9 +536,10 @@ export function createTelegramBusAwareApiRuntime(
|
|
|
536
536
|
editGuestInlineMessage(
|
|
537
537
|
inlineMessageId: string,
|
|
538
538
|
content: TelegramEditGuestInlineMessageContent,
|
|
539
|
+
options?: Pick<TelegramApiCallOptions, "assertAuthority">,
|
|
539
540
|
): Promise<void> {
|
|
540
541
|
return deps.ownsDirect()
|
|
541
|
-
? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content)
|
|
542
|
+
? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content, options)
|
|
542
543
|
: rejectTelegramDirectOwnership("editGuestInlineMessage");
|
|
543
544
|
},
|
|
544
545
|
async deleteMessage(chatId: number, messageId: number): Promise<void> {
|