@llblab/pi-kit 0.33.0 → 0.34.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/BACKLOG.md +2 -1
- package/CHANGELOG.md +9 -0
- package/README.md +3 -3
- package/node_modules/@llblab/pi-telegram/AGENTS.md +5 -4
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
- package/node_modules/@llblab/pi-telegram/README.md +29 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +8 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +41 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +10 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +34 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +10 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +32 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +33 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +18 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +28 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +102 -99
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +3 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +3 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +57 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +313 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +323 -30
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +6 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +11 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +11 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +57 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +6 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +14 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -9
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +27 -14
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -4
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +14 -10
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +36 -10
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +116 -162
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +350 -30
- package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +46 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -2304,7 +2304,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2304
2304
|
}
|
|
2305
2305
|
return false;
|
|
2306
2306
|
};
|
|
2307
|
-
const persistSnapshot = (transition, isCurrentPublication) => {
|
|
2307
|
+
const persistSnapshot = (transition, isCurrentPublication, resetProvisions) => {
|
|
2308
2308
|
const submittedPath = getPath(), submittedProfile = activeProfile();
|
|
2309
2309
|
const publishWorkspace = workspaceIO?.capturePublication();
|
|
2310
2310
|
const persist = persistQueue.then(async () => {
|
|
@@ -2437,6 +2437,12 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2437
2437
|
: readStoreSnapshot(path, transition?.kind === "relocate" ||
|
|
2438
2438
|
transition?.kind === "live-rebind" ||
|
|
2439
2439
|
!!expectedRestore));
|
|
2440
|
+
if (resetProvisions) {
|
|
2441
|
+
const recoveries = readProvisionRecoveries(path, true);
|
|
2442
|
+
if (mutationRevision !== persistedRevision || resetProvisions.some(expected => expected.target || Object.hasOwn(recoveries, expected.id) ||
|
|
2443
|
+
!isDeepStrictEqual(disk.pendingProvisions?.find(value => value.id === expected.id), expected)))
|
|
2444
|
+
throw new Error("Telegram connection recovery provisioning evidence changed.");
|
|
2445
|
+
}
|
|
2440
2446
|
if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
|
|
2441
2447
|
throw new Error("Workspace Restore evidence changed before publication.");
|
|
2442
2448
|
if (!manualNameMatches(disk) ||
|
|
@@ -2471,7 +2477,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2471
2477
|
}
|
|
2472
2478
|
: {}),
|
|
2473
2479
|
reservations: reservations.map((reservation) => ({ ...reservation })),
|
|
2474
|
-
pendingProvisions: pendingProvisions.map((provision) => ({
|
|
2480
|
+
pendingProvisions: pendingProvisions.filter(provision => !resetProvisions?.some(expected => expected.id === provision.id)).map((provision) => ({
|
|
2475
2481
|
...provision,
|
|
2476
2482
|
...(provision.target ? { target: { ...provision.target } } : {}),
|
|
2477
2483
|
})),
|
|
@@ -2626,6 +2632,10 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2626
2632
|
loadedProfile = submittedProfile;
|
|
2627
2633
|
loaded = true;
|
|
2628
2634
|
observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
|
|
2635
|
+
if (resetProvisions) {
|
|
2636
|
+
pendingProvisions = file.pendingProvisions;
|
|
2637
|
+
mutationRevision += 1;
|
|
2638
|
+
}
|
|
2629
2639
|
if (transition) {
|
|
2630
2640
|
records = new Map(file.threads.map((record) => {
|
|
2631
2641
|
const normalized = normalizeRecord(record);
|
|
@@ -2765,6 +2775,11 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2765
2775
|
return false;
|
|
2766
2776
|
loadedPath = path;
|
|
2767
2777
|
loaded = true;
|
|
2778
|
+
if (resetProvisions) {
|
|
2779
|
+
if (mutationRevision !== persistedRevision)
|
|
2780
|
+
return false;
|
|
2781
|
+
pendingProvisions = file.pendingProvisions;
|
|
2782
|
+
}
|
|
2768
2783
|
if (mutationRevision === persistedRevision)
|
|
2769
2784
|
dirty = false;
|
|
2770
2785
|
return true;
|
|
@@ -4209,6 +4224,12 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4209
4224
|
current.status = "ambiguous";
|
|
4210
4225
|
return true;
|
|
4211
4226
|
},
|
|
4227
|
+
resetPendingProvisionForConnect(expected, isCurrent) {
|
|
4228
|
+
// Stage no in-memory deletion: refusal or a stale publication must keep its protective hold.
|
|
4229
|
+
if (expected.target || !isCurrent())
|
|
4230
|
+
return Promise.resolve(false);
|
|
4231
|
+
return persistSnapshot(undefined, isCurrent, [structuredClone(expected)]);
|
|
4232
|
+
},
|
|
4212
4233
|
removePendingProvision(id) {
|
|
4213
4234
|
const before = pendingProvisions.length;
|
|
4214
4235
|
pendingProvisions = pendingProvisions.filter((provision) => provision.id !== id);
|
|
@@ -4865,8 +4886,14 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4865
4886
|
liveRecord.instanceId !== effectivePreviousInstanceId) {
|
|
4866
4887
|
return undefined;
|
|
4867
4888
|
}
|
|
4868
|
-
const
|
|
4869
|
-
|
|
4889
|
+
const ownedProvisions = pendingProvisions.filter(value => value.workspaceBindingKey === identity.bindingKey &&
|
|
4890
|
+
(value.instanceId === instanceId || value.instanceId === effectivePreviousInstanceId ||
|
|
4891
|
+
(options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
|
|
4892
|
+
if (ownedProvisions.length > 1)
|
|
4893
|
+
return undefined;
|
|
4894
|
+
const ownProvision = ownedProvisions[0];
|
|
4895
|
+
const retainedTarget = ownProvision?.target ?? binding?.target ?? legacyRecord?.target;
|
|
4896
|
+
const retainedSlot = ownProvision?.slot ?? binding?.slot ?? legacyRecord?.slot;
|
|
4870
4897
|
const otherBindings = Array.from(workspaceBindings.values())
|
|
4871
4898
|
.filter((other) => other.bindingKey !== identity.bindingKey && other.slot)
|
|
4872
4899
|
.map((other) => ({
|
|
@@ -4886,7 +4913,8 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
4886
4913
|
reservation.expiresAtMs > nowMs)
|
|
4887
4914
|
.map((reservation) => reservation.slot),
|
|
4888
4915
|
...pendingProvisions
|
|
4889
|
-
.filter((provision) =>
|
|
4916
|
+
.filter((provision) => provision !== ownProvision &&
|
|
4917
|
+
isPendingProvisionLiveOrTargeted(provision, nowMs))
|
|
4890
4918
|
.map((provision) => provision.slot),
|
|
4891
4919
|
...externalReservedSlots,
|
|
4892
4920
|
]
|
|
@@ -6349,6 +6377,29 @@ export function createTelegramTopicTargetRenamer(deps) {
|
|
|
6349
6377
|
return renamed;
|
|
6350
6378
|
};
|
|
6351
6379
|
}
|
|
6380
|
+
/** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
|
|
6381
|
+
export async function resetTelegramConnectionProvisioning(input) {
|
|
6382
|
+
if (!input.isCurrent())
|
|
6383
|
+
throw new Error("Telegram connection recovery authority changed.");
|
|
6384
|
+
const removed = [];
|
|
6385
|
+
for (const pending of input.store.listPendingProvisions()) {
|
|
6386
|
+
if (pending.owner !== input.owner || pending.target ||
|
|
6387
|
+
(pending.profileKey !== input.profileKey && pending.instanceId !== input.instanceId))
|
|
6388
|
+
continue;
|
|
6389
|
+
if (!input.isCurrent())
|
|
6390
|
+
throw new Error("Telegram connection recovery authority changed.");
|
|
6391
|
+
if (!await input.store.resetPendingProvisionForConnect(pending, input.isCurrent))
|
|
6392
|
+
throw new Error("Telegram connection recovery lost publication authority.");
|
|
6393
|
+
if (!input.isCurrent())
|
|
6394
|
+
throw new Error("Telegram connection recovery authority changed.");
|
|
6395
|
+
removed.push(pending);
|
|
6396
|
+
}
|
|
6397
|
+
if (removed.length === 0)
|
|
6398
|
+
return;
|
|
6399
|
+
input.recordEvent("connection", "Explicit connect reset unfinished Thread creation; an unconfirmed Telegram Thread may remain.", {
|
|
6400
|
+
phase: "connect-provision-reset", count: removed.length, owner: input.owner,
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6352
6403
|
export function createTelegramTopicTargetProvisioner(deps) {
|
|
6353
6404
|
const getNowMs = deps.getNowMs ?? (() => 0);
|
|
6354
6405
|
const getRandom = deps.getRandom;
|
|
@@ -6554,7 +6605,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
6554
6605
|
}
|
|
6555
6606
|
displayTitle = buildTelegramTopicName({ ...request, threadName: projectedTitle }, "{threadName}", slot);
|
|
6556
6607
|
}
|
|
6557
|
-
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
|
|
6608
|
+
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}:${randomUUID()}`;
|
|
6558
6609
|
const pendingOwner = request.owner?.kind === "leader" ? "leader" : "manual-follower";
|
|
6559
6610
|
assertLeaderEpoch("before-pending-intent");
|
|
6560
6611
|
const pendingBase = {
|
|
@@ -9,6 +9,10 @@ import { type TelegramVoiceReplyMode } from "./voice.ts";
|
|
|
9
9
|
export declare const TELEGRAM_PREFIX = "[telegram]";
|
|
10
10
|
/** A routed source keeps its unique message/file/group identity, but cannot quote an anchor in its former tab. */
|
|
11
11
|
export declare function withoutTelegramTurnReplyAnchor<TMessage extends object>(message: TMessage): TMessage;
|
|
12
|
+
/** A confirmed same-target copy may replace the former-tab suppression on a warm input projection. */
|
|
13
|
+
export declare function withTelegramTurnReplyAnchor<TMessage extends {
|
|
14
|
+
message_id: number;
|
|
15
|
+
}>(message: TMessage): TMessage;
|
|
12
16
|
export declare const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
|
|
13
17
|
export interface TelegramTurnTarget {
|
|
14
18
|
chatId: number;
|
|
@@ -14,6 +14,12 @@ const REPLY_ANCHOR_SUPPRESSED = Symbol.for("@llblab/pi-telegram:reply-anchor-sup
|
|
|
14
14
|
export function withoutTelegramTurnReplyAnchor(message) {
|
|
15
15
|
return { ...message, [REPLY_ANCHOR_SUPPRESSED]: true };
|
|
16
16
|
}
|
|
17
|
+
/** A confirmed same-target copy may replace the former-tab suppression on a warm input projection. */
|
|
18
|
+
export function withTelegramTurnReplyAnchor(message) {
|
|
19
|
+
if (!Number.isSafeInteger(message.message_id) || message.message_id <= 0)
|
|
20
|
+
throw new Error("Telegram reply anchor requires a positive message ID.");
|
|
21
|
+
return { ...message, [REPLY_ANCHOR_SUPPRESSED]: false };
|
|
22
|
+
}
|
|
17
23
|
export const TELEGRAM_GUEST_TURN_NOTE = "[guest] delivery: answer quickly with one concise, self-contained reply";
|
|
18
24
|
function getTelegramTurnTarget(message) {
|
|
19
25
|
return Number.isInteger(message.message_thread_id)
|
|
@@ -22,10 +22,6 @@ export declare const TELEGRAM_PRIORITY_REACTIONS: readonly [{
|
|
|
22
22
|
readonly id: 10;
|
|
23
23
|
readonly name: "like";
|
|
24
24
|
readonly emoji: "👍";
|
|
25
|
-
}, {
|
|
26
|
-
readonly id: 11;
|
|
27
|
-
readonly name: "lightning";
|
|
28
|
-
readonly emoji: "⚡";
|
|
29
25
|
}, {
|
|
30
26
|
readonly id: 12;
|
|
31
27
|
readonly name: "heart";
|
|
@@ -34,7 +30,8 @@ export declare const TELEGRAM_PRIORITY_REACTIONS: readonly [{
|
|
|
34
30
|
readonly id: 13;
|
|
35
31
|
readonly name: "dove";
|
|
36
32
|
readonly emoji: "🕊";
|
|
37
|
-
}
|
|
33
|
+
}];
|
|
34
|
+
export declare const TELEGRAM_STEER_REACTIONS: readonly [{
|
|
38
35
|
readonly id: 14;
|
|
39
36
|
readonly name: "fire";
|
|
40
37
|
readonly emoji: "🔥";
|
|
@@ -60,7 +57,7 @@ export declare const TELEGRAM_REMOVAL_REACTIONS: readonly [{
|
|
|
60
57
|
readonly name: "wastebasket";
|
|
61
58
|
readonly emoji: "🗑";
|
|
62
59
|
}];
|
|
63
|
-
export declare const TELEGRAM_PRIORITY_REACTION_EMOJIS: ("👍" | "
|
|
60
|
+
export declare const TELEGRAM_PRIORITY_REACTION_EMOJIS: ("👍" | "❤" | "🕊")[];
|
|
64
61
|
export declare const TELEGRAM_REMOVAL_REACTION_EMOJIS: ("👎" | "👻" | "💔" | "💩" | "🗑")[];
|
|
65
62
|
export interface TelegramUpdateDeletion {
|
|
66
63
|
deleted_business_messages?: {
|
|
@@ -141,6 +138,8 @@ export interface TelegramForeignOwnedUpdateForwarder<TContext, TReactionUpdate e
|
|
|
141
138
|
message: TMessage;
|
|
142
139
|
ownership: TelegramTargetOwnershipView;
|
|
143
140
|
ctx: TContext;
|
|
141
|
+
/** Local issuance/ACK fence; implementations must not serialize it into payloads. */
|
|
142
|
+
assertAuthority?: () => void;
|
|
144
143
|
}) => Promise<TelegramBusForeignUpdateSettlement> | TelegramBusForeignUpdateSettlement;
|
|
145
144
|
forwardEditedMessage?: (input: {
|
|
146
145
|
message: TMessage;
|
|
@@ -228,6 +227,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
228
227
|
inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
|
|
229
228
|
inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
|
|
230
229
|
inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
|
|
230
|
+
subscribeCompletion?: (notify: () => void) => () => void;
|
|
231
231
|
isSourceUnsettled?: () => boolean;
|
|
232
232
|
prepareLiveInput?: (sourceUpdateIds: readonly number[], isCurrent: () => boolean, onTransferred?: () => void) => TelegramLiveDeferredInputPreparation | undefined;
|
|
233
233
|
/** Internal notification after the worker's exact donor-removal ACK; never issues completion itself. */
|
|
@@ -243,7 +243,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
243
243
|
export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
|
|
244
244
|
export declare function bindTelegramUpdateAdmissionSource<TUpdate extends TelegramUpdateFlow & {
|
|
245
245
|
update_id: number;
|
|
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;
|
|
246
|
+
}>(update: TUpdate, report: TelegramUpdateAdmissionBinding["report"], controls?: Pick<TelegramUpdateAdmissionBinding, "abandon" | "armRoutingInput" | "getRoutingInput" | "acquireRouting" | "inspectSource" | "inspectSourceSnapshot" | "inspectCompletion" | "subscribeCompletion" | "isSourceUnsettled" | "acknowledgeTransferred" | "prepareLiveInput" | "prepareQueueAdmission" | "prepareSourceCompletion" | "supportsAbandonment" | "inspectAbandoning" | "inspectHistorical" | "isHistorical" | "isHistoricalReviewHeld">): TUpdate;
|
|
247
247
|
/**
|
|
248
248
|
* Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
|
|
249
249
|
* is unresolved custody, except a lone button tap, which carries no input (the in-flight Cancel itself).
|
|
@@ -263,6 +263,8 @@ export declare function inspectTelegramDeferredSource(value: unknown): TelegramD
|
|
|
263
263
|
export declare function inspectTelegramDeferredSourceSnapshot(value: unknown): TelegramDeferredSourceSnapshot | undefined;
|
|
264
264
|
/** Exact warm removal ACK only; reporting, source absence and a replaced worker cannot supply it. */
|
|
265
265
|
export declare function inspectTelegramDeferredSourceCompletion(value: unknown): TelegramDeferredSourceEvidence | undefined;
|
|
266
|
+
/** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
|
|
267
|
+
export declare function subscribeTelegramDeferredSourceCompletion(value: unknown, notify: () => void): (() => void) | undefined;
|
|
266
268
|
/** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
|
|
267
269
|
export declare function prepareTelegramLiveDeferredInput(values: readonly unknown[], isCurrent: () => boolean): TelegramLiveDeferredInputPreparation | undefined;
|
|
268
270
|
/** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
|
|
@@ -1317,6 +1319,7 @@ export interface TelegramQueueHandoffControlExecutionDeps<TContext> {
|
|
|
1317
1319
|
export declare function createTelegramQueueHandoffControlExecutionFactory<TContext>(deps: TelegramQueueHandoffControlExecutionDeps<TContext>): (payload: TelegramControlQueueHandoffPayload) => PendingTelegramControlItem<TContext>["execute"];
|
|
1318
1320
|
export interface TelegramQueueHandoffCoordinatorInput<TContext> {
|
|
1319
1321
|
item: TelegramQueueItem<TContext>;
|
|
1322
|
+
steerSupported?: boolean;
|
|
1320
1323
|
expectedOwner: TelegramUpdateJournalQueueOwner;
|
|
1321
1324
|
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
1322
1325
|
handoffToken: string;
|
|
@@ -1356,6 +1359,8 @@ export declare function createTelegramQueueHandoffRecipientRuntime<TContext>(dep
|
|
|
1356
1359
|
kind: "leader.offerQueueHandoff";
|
|
1357
1360
|
}>, ctx: TContext) => Promise<TelegramQueueHandoffStageResult>;
|
|
1358
1361
|
export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
1362
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
1363
|
+
hasPendingDispatch?: () => boolean;
|
|
1359
1364
|
ownsDirect: () => boolean;
|
|
1360
1365
|
isFollowerRegistered: () => boolean;
|
|
1361
1366
|
isBusEnabled: () => boolean;
|
|
@@ -1380,6 +1385,8 @@ export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
|
1380
1385
|
recordFailure?: (error: unknown, details: Record<string, unknown>) => void;
|
|
1381
1386
|
}
|
|
1382
1387
|
export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<TContext> {
|
|
1388
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
1389
|
+
hasPendingDispatch?: () => boolean;
|
|
1383
1390
|
ownsDirect: () => boolean;
|
|
1384
1391
|
isFollowerRegistered: () => boolean;
|
|
1385
1392
|
isBusEnabled: () => boolean;
|
|
@@ -14,11 +14,10 @@ import { createTelegramPrivateTarget, createTelegramThreadTarget, } from "./targ
|
|
|
14
14
|
import { isWireRecord as isTelegramUpdateAdmissionRecord, isNonEmptyWireString as isTelegramUpdateAdmissionString, } from "./wire.js";
|
|
15
15
|
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
16
16
|
{ id: 10, name: "like", emoji: "👍" },
|
|
17
|
-
{ id: 11, name: "lightning", emoji: "⚡" },
|
|
18
17
|
{ id: 12, name: "heart", emoji: "❤" },
|
|
19
18
|
{ id: 13, name: "dove", emoji: "🕊" },
|
|
20
|
-
{ id: 14, name: "fire", emoji: "🔥" },
|
|
21
19
|
];
|
|
20
|
+
export const TELEGRAM_STEER_REACTIONS = [{ id: 14, name: "fire", emoji: "🔥" }];
|
|
22
21
|
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
23
22
|
{ id: 20, name: "dislike", emoji: "👎" },
|
|
24
23
|
{ id: 21, name: "ghost", emoji: "👻" },
|
|
@@ -47,8 +46,10 @@ function getTelegramReactionEmoji(emojis, candidates) {
|
|
|
47
46
|
function getTelegramQueueReactionTransition(oldReactions, newReactions) {
|
|
48
47
|
const oldEmojis = collectTelegramReactionEmojis(oldReactions);
|
|
49
48
|
const newEmojis = collectTelegramReactionEmojis(newReactions);
|
|
50
|
-
|
|
51
|
-
const
|
|
49
|
+
// Fire is effective Steer intent; weaker positive aliases under it do not reorder the lane.
|
|
50
|
+
const steerEmoji = TELEGRAM_STEER_REACTIONS[0].emoji;
|
|
51
|
+
const oldPriorityEmoji = oldEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
52
|
+
const newPriorityEmoji = newEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
52
53
|
const oldSuppressionEmoji = getTelegramReactionEmoji(oldEmojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
|
|
53
54
|
const newSuppressionEmoji = getTelegramReactionEmoji(newEmojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
|
|
54
55
|
if (oldPriorityEmoji === newPriorityEmoji &&
|
|
@@ -58,6 +59,8 @@ function getTelegramQueueReactionTransition(oldReactions, newReactions) {
|
|
|
58
59
|
const transition = { kind: "reaction-transition" };
|
|
59
60
|
if (oldPriorityEmoji !== newPriorityEmoji) {
|
|
60
61
|
transition.priorityEmoji = newPriorityEmoji ?? null;
|
|
62
|
+
if (newPriorityEmoji === steerEmoji)
|
|
63
|
+
transition.promptLane = "steer";
|
|
61
64
|
}
|
|
62
65
|
if (oldSuppressionEmoji !== newSuppressionEmoji) {
|
|
63
66
|
transition.suppressionEmoji = newSuppressionEmoji ?? null;
|
|
@@ -252,6 +255,16 @@ export function inspectTelegramDeferredSourceCompletion(value) {
|
|
|
252
255
|
const source = getTelegramUpdateAdmissionBinding(value)?.inspectCompletion?.();
|
|
253
256
|
return execution.isCurrent() && source ? { ...source } : undefined;
|
|
254
257
|
}
|
|
258
|
+
/** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
|
|
259
|
+
export function subscribeTelegramDeferredSourceCompletion(value, notify) {
|
|
260
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
261
|
+
if (execution?.isCurrent() !== true)
|
|
262
|
+
return undefined;
|
|
263
|
+
return getTelegramUpdateAdmissionBinding(value)?.subscribeCompletion?.(() => {
|
|
264
|
+
if (execution.isCurrent())
|
|
265
|
+
notify();
|
|
266
|
+
});
|
|
267
|
+
}
|
|
255
268
|
/** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
|
|
256
269
|
export function prepareTelegramLiveDeferredInput(values, isCurrent) {
|
|
257
270
|
const carriers = [...values], first = carriers[0], execution = getTelegramUpdateExecutionFence(first);
|
|
@@ -5152,6 +5165,7 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
|
|
|
5152
5165
|
let outcome;
|
|
5153
5166
|
let completionAttempted = false;
|
|
5154
5167
|
let completedSource;
|
|
5168
|
+
const completionObservers = new Set();
|
|
5155
5169
|
const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
5156
5170
|
if (!execution.isCurrent())
|
|
5157
5171
|
return;
|
|
@@ -5196,11 +5210,15 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
|
|
|
5196
5210
|
completion &&
|
|
5197
5211
|
execution.isCurrent() &&
|
|
5198
5212
|
completion.isCurrent() &&
|
|
5199
|
-
isDeepStrictEqual(completion.source, expectedSource))
|
|
5200
|
-
completedSource = {
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5213
|
+
isDeepStrictEqual(completion.source, expectedSource)) {
|
|
5214
|
+
completedSource = { ...completion, source: { ...expectedSource } };
|
|
5215
|
+
for (const notify of completionObservers) {
|
|
5216
|
+
try {
|
|
5217
|
+
notify();
|
|
5218
|
+
}
|
|
5219
|
+
catch { /* Observation cannot invalidate an exact removal ACK. */ }
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5204
5222
|
})
|
|
5205
5223
|
.catch((error) => {
|
|
5206
5224
|
try {
|
|
@@ -5257,6 +5275,10 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
|
|
|
5257
5275
|
signal,
|
|
5258
5276
|
})
|
|
5259
5277
|
: undefined,
|
|
5278
|
+
subscribeCompletion: (notify) => {
|
|
5279
|
+
completionObservers.add(notify);
|
|
5280
|
+
return () => { completionObservers.delete(notify); };
|
|
5281
|
+
},
|
|
5260
5282
|
inspectCompletion: () => execution.isCurrent() && completedSource?.isCurrent()
|
|
5261
5283
|
? { ...completedSource.source }
|
|
5262
5284
|
: undefined,
|
|
@@ -5533,6 +5555,7 @@ export async function coordinateTelegramQueueHandoff(input) {
|
|
|
5533
5555
|
const handoff = createTelegramQueueHandoff({
|
|
5534
5556
|
handoffToken: input.handoffToken,
|
|
5535
5557
|
item: input.item,
|
|
5558
|
+
steerSupported: input.steerSupported,
|
|
5536
5559
|
});
|
|
5537
5560
|
const receipt = handoff.payload.admissionReceipts[0];
|
|
5538
5561
|
if (!receipt || handoff.payload.admissionReceipts.length !== 1) {
|
|
@@ -5670,6 +5693,8 @@ export function createTelegramQueueHandoffRecipientRuntime(deps) {
|
|
|
5670
5693
|
/** Own queue-handoff projections over journals, admission, IPC, and live queue state. */
|
|
5671
5694
|
export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
|
|
5672
5695
|
return createTelegramQueueHandoffReconciler({
|
|
5696
|
+
canHandoffSteerWithRecipient: deps.canHandoffSteerWithRecipient,
|
|
5697
|
+
hasPendingDispatch: deps.hasPendingDispatch,
|
|
5673
5698
|
ownsDirect: deps.ownsDirect,
|
|
5674
5699
|
isFollowerRegistered: deps.isFollowerRegistered,
|
|
5675
5700
|
isBusEnabled: deps.isBusEnabled,
|
|
@@ -5738,6 +5763,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
5738
5763
|
if (followers.length === 0)
|
|
5739
5764
|
return;
|
|
5740
5765
|
for (const item of [...deps.getQueuedItems()]) {
|
|
5766
|
+
if ((deps.hasPendingDispatch?.() && deps.getQueuedItems()[0] === item) ||
|
|
5767
|
+
(item.kind === "prompt" && (item.steerAttempted || item.steerPacketTail !== undefined || item.steerPacketSourceOrders !== undefined)))
|
|
5768
|
+
continue;
|
|
5741
5769
|
const target = item.target;
|
|
5742
5770
|
const recipient = target
|
|
5743
5771
|
? followers.find((candidate) => candidate.target?.chatId === target.chatId &&
|
|
@@ -5745,6 +5773,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
5745
5773
|
: undefined;
|
|
5746
5774
|
if (!recipient)
|
|
5747
5775
|
continue;
|
|
5776
|
+
const steerSupported = item.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) === true;
|
|
5777
|
+
if (item.queueLane === "steer" && !steerSupported)
|
|
5778
|
+
continue;
|
|
5748
5779
|
const receipt = item.admissionReceipts?.[0];
|
|
5749
5780
|
if (!receipt || item.admissionReceipts?.length !== 1)
|
|
5750
5781
|
continue;
|
|
@@ -5768,6 +5799,7 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
5768
5799
|
const handoffToken = deps.createHandoffToken();
|
|
5769
5800
|
const result = await coordinateTelegramQueueHandoff({
|
|
5770
5801
|
item,
|
|
5802
|
+
steerSupported,
|
|
5771
5803
|
expectedOwner,
|
|
5772
5804
|
recipientOwner: {
|
|
5773
5805
|
instanceId: recipient.instanceId,
|
|
@@ -5778,6 +5810,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
5778
5810
|
handoffToken,
|
|
5779
5811
|
lifecycle,
|
|
5780
5812
|
stageRemote: async ({ payload: sourcePayload }) => {
|
|
5813
|
+
if (sourcePayload.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) !== true) {
|
|
5814
|
+
throw new Error("Steer queue handoff capability changed before staging.");
|
|
5815
|
+
}
|
|
5781
5816
|
const payload = {
|
|
5782
5817
|
...sourcePayload,
|
|
5783
5818
|
admissionReceipts: [
|
|
@@ -8,7 +8,7 @@ Living index of project documentation in `/docs`.
|
|
|
8
8
|
|
|
9
9
|
## Documents
|
|
10
10
|
|
|
11
|
-
- [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, optional profile isolation, queueing model and Priority/
|
|
11
|
+
- [architecture.md](./architecture.md) — Overview of the Telegram bridge runtime, optional profile isolation, queueing model and Normal/Priority/Steer plus Keep/Skip reaction policy, native Rich Markdown delivery, UI/compat rendering, and interactive controls
|
|
12
12
|
- [public-api.md](./public-api.md) — Stable public API map: package entrypoints, commands, config, assistant markup, extension APIs, smoke examples, and compatibility boundaries
|
|
13
13
|
- [delivery.md](./delivery.md) — Target-aware companion delivery contract for operational views, logical message handles, target scopes, lifecycle fencing, and leader/follower transport
|
|
14
14
|
- [activity.md](./activity.md) — Normalized Pi lifecycle API for extension-owned reasoning, intermediate prose, tool activity, source identity, delivery contexts, and consumer policy examples
|