@llblab/pi-telegram 0.55.1 → 0.56.1
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 +5 -4
- package/BACKLOG.md +6 -1
- package/CHANGELOG.md +15 -0
- package/README.md +28 -17
- package/dist/lib/bindings.d.ts +8 -4
- package/dist/lib/bindings.js +41 -1
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +10 -5
- package/dist/lib/bus-leader.d.ts +2 -0
- package/dist/lib/bus-leader.js +34 -20
- package/dist/lib/bus.d.ts +8 -0
- package/dist/lib/bus.js +21 -7
- package/dist/lib/commands.d.ts +1 -1
- package/dist/lib/commands.js +33 -28
- package/dist/lib/extension.js +18 -3
- package/dist/lib/lifecycle.d.ts +2 -1
- package/dist/lib/lifecycle.js +28 -20
- package/dist/lib/locks.d.ts +6 -2
- package/dist/lib/locks.js +3 -3
- package/dist/lib/menu-queue.d.ts +1 -0
- package/dist/lib/menu-queue.js +102 -99
- package/dist/lib/pi.d.ts +3 -2
- package/dist/lib/pi.js +3 -0
- package/dist/lib/polling.d.ts +6 -2
- package/dist/lib/polling.js +5 -3
- package/dist/lib/queue.d.ts +57 -5
- package/dist/lib/queue.js +313 -25
- package/dist/lib/routing.d.ts +4 -0
- package/dist/lib/routing.js +62 -1
- package/dist/lib/status.d.ts +1 -2
- package/dist/lib/status.js +6 -7
- package/dist/lib/sync.js +11 -1
- package/dist/lib/threads.d.ts +11 -0
- package/dist/lib/threads.js +57 -6
- package/dist/lib/updates.d.ts +12 -7
- package/dist/lib/updates.js +44 -9
- package/dist/package.json +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +24 -13
- package/docs/callback-namespaces.md +4 -0
- package/docs/multi-instance-bus.md +6 -4
- package/docs/ui-style.md +14 -10
- package/lib/bindings.ts +52 -3
- package/lib/bus-follower.ts +13 -5
- package/lib/bus-leader.ts +37 -23
- package/lib/bus.ts +23 -7
- package/lib/commands.ts +52 -47
- package/lib/extension.ts +18 -2
- package/lib/lifecycle.ts +34 -28
- package/lib/locks.ts +5 -2
- package/lib/menu-queue.ts +116 -162
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +7 -2
- package/lib/queue.ts +328 -33
- package/lib/routing.ts +51 -1
- package/lib/status.ts +7 -7
- package/lib/sync.ts +11 -0
- package/lib/threads.ts +57 -5
- package/lib/updates.ts +44 -15
- package/package.json +1 -1
package/dist/lib/status.js
CHANGED
|
@@ -605,9 +605,7 @@ function getTelegramStatusBarLabel(state) {
|
|
|
605
605
|
}
|
|
606
606
|
export function buildTelegramStatusBarText(theme, state) {
|
|
607
607
|
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
|
|
608
|
-
const queued = state.queuedStatus
|
|
609
|
-
? theme.fg("warning", state.queuedStatus)
|
|
610
|
-
: "";
|
|
608
|
+
const queued = state.queuedStatus ? theme.fg("warning", state.queuedStatus) : "";
|
|
611
609
|
if (!state.hasBotToken)
|
|
612
610
|
return `${label} ${theme.fg("muted", "not configured")}${queued}`;
|
|
613
611
|
if (state.pollingStopReason === "persistent-conflict" &&
|
|
@@ -949,6 +947,7 @@ function buildTelegramInboundWorkerDiagnosticLines(worker) {
|
|
|
949
947
|
function countTelegramQueueLanes(items) {
|
|
950
948
|
const counts = {
|
|
951
949
|
control: 0,
|
|
950
|
+
steer: 0,
|
|
952
951
|
priority: 0,
|
|
953
952
|
default: 0,
|
|
954
953
|
};
|
|
@@ -957,9 +956,9 @@ function countTelegramQueueLanes(items) {
|
|
|
957
956
|
return counts;
|
|
958
957
|
}
|
|
959
958
|
function buildTelegramBridgeCompactStatusLines(state) {
|
|
960
|
-
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
959
|
+
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, steer: steerQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
961
960
|
const queueLine = `- queued turns: ${state.queuedItems.length}${state.queuedItems.length > 0
|
|
962
|
-
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
961
|
+
? ` (control=${controlQueueCount}${steerQueueCount > 0 ? `, steer=${steerQueueCount}` : ""}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
963
962
|
: ""}`;
|
|
964
963
|
const executionState = state.pendingDispatch
|
|
965
964
|
? "pending dispatch"
|
|
@@ -1017,7 +1016,7 @@ export function buildTelegramBridgeStatusLines(state, options = {}) {
|
|
|
1017
1016
|
return buildTelegramBridgeCompactStatusLines(state);
|
|
1018
1017
|
}
|
|
1019
1018
|
function buildTelegramBridgeDiagnosticStatusLines(state) {
|
|
1020
|
-
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
1019
|
+
const { control: controlQueueCount, priority: priorityQueueCount, default: defaultQueueCount, steer: steerQueueCount, } = countTelegramQueueLanes(state.queuedItems);
|
|
1021
1020
|
return [
|
|
1022
1021
|
"connection:",
|
|
1023
1022
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
@@ -1063,7 +1062,7 @@ function buildTelegramBridgeDiagnosticStatusLines(state) {
|
|
|
1063
1062
|
"",
|
|
1064
1063
|
"queue:",
|
|
1065
1064
|
`- queued turns: ${state.queuedItems.length}`,
|
|
1066
|
-
`- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1065
|
+
`- lanes: control=${controlQueueCount}${steerQueueCount > 0 ? `, steer=${steerQueueCount}` : ""}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
|
|
1067
1066
|
...buildTelegramBusFollowerLines(state),
|
|
1068
1067
|
...buildTelegramLocalBusLines(state, { verbose: true }),
|
|
1069
1068
|
...buildTelegramTopicTargetDiagnosticLines(state),
|
package/dist/lib/sync.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { getTelegramTargetKey } from "./target.js";
|
|
7
7
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError, } from "./telegram-api.js";
|
|
8
8
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
9
|
-
import { commitTelegramWorkspaceProvisionBinding, createTelegramCleanupTargetProtection, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, } from "./threads.js";
|
|
9
|
+
import { commitTelegramWorkspaceProvisionBinding, createTelegramCleanupTargetProtection, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, resetTelegramConnectionProvisioning, } from "./threads.js";
|
|
10
10
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
11
11
|
import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
|
|
12
12
|
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
|
|
@@ -463,11 +463,21 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
463
463
|
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
464
464
|
};
|
|
465
465
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
466
|
+
if (deps.forceFreshUnnamed) {
|
|
467
|
+
await resetTelegramConnectionProvisioning({
|
|
468
|
+
store: deps.topicTargetStore, owner: "leader", instanceId: deps.instanceId,
|
|
469
|
+
profileKey: leaderProfileKey, isCurrent: () => {
|
|
470
|
+
assertLeaderEpoch("connect-recovery");
|
|
471
|
+
return true;
|
|
472
|
+
}, recordEvent: deps.recordEvent,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
466
475
|
const legacyLeaderRecord = deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
467
476
|
let capacityUnavailable = false;
|
|
468
477
|
const workspaceIdentity = normalizedLeaderCwd
|
|
469
478
|
? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, {
|
|
470
479
|
sessionId: deps.sessionId,
|
|
480
|
+
provisionProfileKey: leaderProfileKey,
|
|
471
481
|
onCapacityUnavailable() {
|
|
472
482
|
capacityUnavailable = true;
|
|
473
483
|
},
|
package/dist/lib/threads.d.ts
CHANGED
|
@@ -495,6 +495,7 @@ export interface TelegramTopicTargetStore {
|
|
|
495
495
|
} | undefined;
|
|
496
496
|
listReservations: () => TelegramThreadReservation[];
|
|
497
497
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
498
|
+
resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
|
|
498
499
|
listPendingCleanups: () => TelegramThreadCleanupIntent[];
|
|
499
500
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
500
501
|
reserveThread: (reservation: TelegramThreadReservation) => void;
|
|
@@ -559,6 +560,7 @@ export interface TelegramTopicTargetStore {
|
|
|
559
560
|
claimWorkspaceIdentity: (cwd: string, instanceId: string, previousInstanceId?: string, options?: {
|
|
560
561
|
existingBindingOnly?: boolean;
|
|
561
562
|
sessionId?: string;
|
|
563
|
+
provisionProfileKey?: string;
|
|
562
564
|
onCapacityUnavailable?: () => void;
|
|
563
565
|
}) => TelegramWorkspaceBindingIdentity | undefined;
|
|
564
566
|
releaseWorkspaceClaim: (instanceId: string) => boolean;
|
|
@@ -957,4 +959,13 @@ export declare function getTelegramTargetFromApiBody(body: unknown): (TelegramTa
|
|
|
957
959
|
export declare function isTelegramTopicTargetStaleError(error: unknown): boolean;
|
|
958
960
|
export declare function isTelegramTopicModeUnavailableError(error: unknown): boolean;
|
|
959
961
|
export declare function createTelegramTopicTargetRenamer(deps: TelegramTopicTargetRenamerDeps): (request: TelegramTopicTargetRenameRequest) => Promise<TelegramTopicTargetRecord | undefined>;
|
|
962
|
+
/** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
|
|
963
|
+
export declare function resetTelegramConnectionProvisioning(input: {
|
|
964
|
+
store: TelegramTopicTargetStore;
|
|
965
|
+
owner: TelegramThreadPendingProvision["owner"];
|
|
966
|
+
instanceId: string;
|
|
967
|
+
profileKey: string;
|
|
968
|
+
isCurrent: () => boolean;
|
|
969
|
+
recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
970
|
+
}): Promise<void>;
|
|
960
971
|
export declare function createTelegramTopicTargetProvisioner(deps: TelegramTopicTargetProvisionerDeps): (request: TelegramTopicTargetProvisionRequest) => Promise<TelegramTopicTargetProvisionResult>;
|
package/dist/lib/threads.js
CHANGED
|
@@ -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 = {
|
package/dist/lib/updates.d.ts
CHANGED
|
@@ -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?: {
|
|
@@ -230,6 +227,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
230
227
|
inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
|
|
231
228
|
inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
|
|
232
229
|
inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
|
|
230
|
+
subscribeCompletion?: (notify: () => void) => () => void;
|
|
233
231
|
isSourceUnsettled?: () => boolean;
|
|
234
232
|
prepareLiveInput?: (sourceUpdateIds: readonly number[], isCurrent: () => boolean, onTransferred?: () => void) => TelegramLiveDeferredInputPreparation | undefined;
|
|
235
233
|
/** Internal notification after the worker's exact donor-removal ACK; never issues completion itself. */
|
|
@@ -245,7 +243,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
245
243
|
export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
|
|
246
244
|
export declare function bindTelegramUpdateAdmissionSource<TUpdate extends TelegramUpdateFlow & {
|
|
247
245
|
update_id: number;
|
|
248
|
-
}>(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;
|
|
249
247
|
/**
|
|
250
248
|
* Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
|
|
251
249
|
* is unresolved custody, except a lone button tap, which carries no input (the in-flight Cancel itself).
|
|
@@ -265,6 +263,8 @@ export declare function inspectTelegramDeferredSource(value: unknown): TelegramD
|
|
|
265
263
|
export declare function inspectTelegramDeferredSourceSnapshot(value: unknown): TelegramDeferredSourceSnapshot | undefined;
|
|
266
264
|
/** Exact warm removal ACK only; reporting, source absence and a replaced worker cannot supply it. */
|
|
267
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;
|
|
268
268
|
/** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
|
|
269
269
|
export declare function prepareTelegramLiveDeferredInput(values: readonly unknown[], isCurrent: () => boolean): TelegramLiveDeferredInputPreparation | undefined;
|
|
270
270
|
/** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
|
|
@@ -1319,6 +1319,7 @@ export interface TelegramQueueHandoffControlExecutionDeps<TContext> {
|
|
|
1319
1319
|
export declare function createTelegramQueueHandoffControlExecutionFactory<TContext>(deps: TelegramQueueHandoffControlExecutionDeps<TContext>): (payload: TelegramControlQueueHandoffPayload) => PendingTelegramControlItem<TContext>["execute"];
|
|
1320
1320
|
export interface TelegramQueueHandoffCoordinatorInput<TContext> {
|
|
1321
1321
|
item: TelegramQueueItem<TContext>;
|
|
1322
|
+
steerSupported?: boolean;
|
|
1322
1323
|
expectedOwner: TelegramUpdateJournalQueueOwner;
|
|
1323
1324
|
recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
|
|
1324
1325
|
handoffToken: string;
|
|
@@ -1358,6 +1359,8 @@ export declare function createTelegramQueueHandoffRecipientRuntime<TContext>(dep
|
|
|
1358
1359
|
kind: "leader.offerQueueHandoff";
|
|
1359
1360
|
}>, ctx: TContext) => Promise<TelegramQueueHandoffStageResult>;
|
|
1360
1361
|
export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
1362
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
1363
|
+
hasPendingDispatch?: () => boolean;
|
|
1361
1364
|
ownsDirect: () => boolean;
|
|
1362
1365
|
isFollowerRegistered: () => boolean;
|
|
1363
1366
|
isBusEnabled: () => boolean;
|
|
@@ -1382,6 +1385,8 @@ export interface TelegramQueueHandoffReconcilerDeps<TContext> {
|
|
|
1382
1385
|
recordFailure?: (error: unknown, details: Record<string, unknown>) => void;
|
|
1383
1386
|
}
|
|
1384
1387
|
export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<TContext> {
|
|
1388
|
+
canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
|
|
1389
|
+
hasPendingDispatch?: () => boolean;
|
|
1385
1390
|
ownsDirect: () => boolean;
|
|
1386
1391
|
isFollowerRegistered: () => boolean;
|
|
1387
1392
|
isBusEnabled: () => boolean;
|
package/dist/lib/updates.js
CHANGED
|
@@ -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: [
|
package/dist/package.json
CHANGED
package/docs/README.md
CHANGED
|
@@ -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
|