@llblab/pi-kit 0.33.1 → 0.34.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.
Files changed (64) hide show
  1. package/BACKLOG.md +2 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +3 -3
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +6 -5
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +20 -0
  7. package/node_modules/@llblab/pi-telegram/README.md +28 -17
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +8 -4
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +41 -1
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -0
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +10 -5
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +2 -0
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +34 -20
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +8 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +21 -7
  16. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +1 -1
  17. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +33 -28
  18. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +18 -3
  19. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
  20. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +28 -20
  21. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +6 -2
  22. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +3 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +1 -0
  24. package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +100 -99
  25. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +3 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +3 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +6 -2
  28. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -3
  29. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +57 -5
  30. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +313 -25
  31. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +4 -0
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +62 -1
  33. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +1 -2
  34. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +6 -7
  35. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +11 -1
  36. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +11 -0
  37. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +57 -6
  38. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +13 -32
  39. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -15
  40. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  41. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  42. package/node_modules/@llblab/pi-telegram/docs/architecture.md +25 -14
  43. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
  44. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -6
  45. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +17 -17
  46. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
  47. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
  48. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
  49. package/node_modules/@llblab/pi-telegram/lib/bus.ts +23 -7
  50. package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
  51. package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
  52. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
  53. package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
  54. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +114 -162
  55. package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
  56. package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
  57. package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
  58. package/node_modules/@llblab/pi-telegram/lib/routing.ts +51 -1
  59. package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
  60. package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
  61. package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
  62. package/node_modules/@llblab/pi-telegram/lib/updates.ts +44 -21
  63. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  64. package/package.json +2 -2
@@ -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),
@@ -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
  },
@@ -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>;
@@ -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 retainedTarget = binding?.target ?? legacyRecord?.target;
4869
- const retainedSlot = binding?.slot ?? legacyRecord?.slot;
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) => isPendingProvisionLiveOrTargeted(provision, nowMs))
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 = {
@@ -22,19 +22,8 @@ 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
- }, {
30
- readonly id: 12;
31
- readonly name: "heart";
32
- readonly emoji: "❤";
33
- }, {
34
- readonly id: 13;
35
- readonly name: "dove";
36
- readonly emoji: "🕊";
37
- }, {
25
+ }];
26
+ export declare const TELEGRAM_STEER_REACTIONS: readonly [{
38
27
  readonly id: 14;
39
28
  readonly name: "fire";
40
29
  readonly emoji: "🔥";
@@ -43,25 +32,9 @@ export declare const TELEGRAM_REMOVAL_REACTIONS: readonly [{
43
32
  readonly id: 20;
44
33
  readonly name: "dislike";
45
34
  readonly emoji: "👎";
46
- }, {
47
- readonly id: 21;
48
- readonly name: "ghost";
49
- readonly emoji: "👻";
50
- }, {
51
- readonly id: 22;
52
- readonly name: "broken-heart";
53
- readonly emoji: "💔";
54
- }, {
55
- readonly id: 23;
56
- readonly name: "poop";
57
- readonly emoji: "💩";
58
- }, {
59
- readonly id: 24;
60
- readonly name: "wastebasket";
61
- readonly emoji: "🗑";
62
35
  }];
63
- export declare const TELEGRAM_PRIORITY_REACTION_EMOJIS: ("👍" | "⚡" | "❤" | "🕊" | "🔥")[];
64
- export declare const TELEGRAM_REMOVAL_REACTION_EMOJIS: ("👎" | "👻" | "💔" | "💩" | "🗑")[];
36
+ export declare const TELEGRAM_PRIORITY_REACTION_EMOJIS: "👍"[];
37
+ export declare const TELEGRAM_REMOVAL_REACTION_EMOJIS: "👎"[];
65
38
  export interface TelegramUpdateDeletion {
66
39
  deleted_business_messages?: {
67
40
  message_ids?: unknown;
@@ -230,6 +203,7 @@ interface TelegramUpdateAdmissionBinding {
230
203
  inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
231
204
  inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
232
205
  inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
206
+ subscribeCompletion?: (notify: () => void) => () => void;
233
207
  isSourceUnsettled?: () => boolean;
234
208
  prepareLiveInput?: (sourceUpdateIds: readonly number[], isCurrent: () => boolean, onTransferred?: () => void) => TelegramLiveDeferredInputPreparation | undefined;
235
209
  /** Internal notification after the worker's exact donor-removal ACK; never issues completion itself. */
@@ -245,7 +219,7 @@ interface TelegramUpdateAdmissionBinding {
245
219
  export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
246
220
  export declare function bindTelegramUpdateAdmissionSource<TUpdate extends TelegramUpdateFlow & {
247
221
  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;
222
+ }>(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
223
  /**
250
224
  * Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
251
225
  * is unresolved custody, except a lone button tap, which carries no input (the in-flight Cancel itself).
@@ -265,6 +239,8 @@ export declare function inspectTelegramDeferredSource(value: unknown): TelegramD
265
239
  export declare function inspectTelegramDeferredSourceSnapshot(value: unknown): TelegramDeferredSourceSnapshot | undefined;
266
240
  /** Exact warm removal ACK only; reporting, source absence and a replaced worker cannot supply it. */
267
241
  export declare function inspectTelegramDeferredSourceCompletion(value: unknown): TelegramDeferredSourceEvidence | undefined;
242
+ /** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
243
+ export declare function subscribeTelegramDeferredSourceCompletion(value: unknown, notify: () => void): (() => void) | undefined;
268
244
  /** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
269
245
  export declare function prepareTelegramLiveDeferredInput(values: readonly unknown[], isCurrent: () => boolean): TelegramLiveDeferredInputPreparation | undefined;
270
246
  /** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
@@ -1319,6 +1295,7 @@ export interface TelegramQueueHandoffControlExecutionDeps<TContext> {
1319
1295
  export declare function createTelegramQueueHandoffControlExecutionFactory<TContext>(deps: TelegramQueueHandoffControlExecutionDeps<TContext>): (payload: TelegramControlQueueHandoffPayload) => PendingTelegramControlItem<TContext>["execute"];
1320
1296
  export interface TelegramQueueHandoffCoordinatorInput<TContext> {
1321
1297
  item: TelegramQueueItem<TContext>;
1298
+ steerSupported?: boolean;
1322
1299
  expectedOwner: TelegramUpdateJournalQueueOwner;
1323
1300
  recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
1324
1301
  handoffToken: string;
@@ -1358,6 +1335,8 @@ export declare function createTelegramQueueHandoffRecipientRuntime<TContext>(dep
1358
1335
  kind: "leader.offerQueueHandoff";
1359
1336
  }>, ctx: TContext) => Promise<TelegramQueueHandoffStageResult>;
1360
1337
  export interface TelegramQueueHandoffReconcilerDeps<TContext> {
1338
+ canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
1339
+ hasPendingDispatch?: () => boolean;
1361
1340
  ownsDirect: () => boolean;
1362
1341
  isFollowerRegistered: () => boolean;
1363
1342
  isBusEnabled: () => boolean;
@@ -1382,6 +1361,8 @@ export interface TelegramQueueHandoffReconcilerDeps<TContext> {
1382
1361
  recordFailure?: (error: unknown, details: Record<string, unknown>) => void;
1383
1362
  }
1384
1363
  export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<TContext> {
1364
+ canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
1365
+ hasPendingDispatch?: () => boolean;
1385
1366
  ownsDirect: () => boolean;
1386
1367
  isFollowerRegistered: () => boolean;
1387
1368
  isBusEnabled: () => boolean;
@@ -14,17 +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
- { id: 12, name: "heart", emoji: "❤" },
19
- { id: 13, name: "dove", emoji: "🕊" },
20
- { id: 14, name: "fire", emoji: "🔥" },
21
17
  ];
18
+ export const TELEGRAM_STEER_REACTIONS = [{ id: 14, name: "fire", emoji: "🔥" }];
22
19
  export const TELEGRAM_REMOVAL_REACTIONS = [
23
20
  { id: 20, name: "dislike", emoji: "👎" },
24
- { id: 21, name: "ghost", emoji: "👻" },
25
- { id: 22, name: "broken-heart", emoji: "💔" },
26
- { id: 23, name: "poop", emoji: "💩" },
27
- { id: 24, name: "wastebasket", emoji: "🗑" },
28
21
  ];
29
22
  export const TELEGRAM_PRIORITY_REACTION_EMOJIS = TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
30
23
  export const TELEGRAM_REMOVAL_REACTION_EMOJIS = TELEGRAM_REMOVAL_REACTIONS.map((reaction) => reaction.emoji);
@@ -47,8 +40,10 @@ function getTelegramReactionEmoji(emojis, candidates) {
47
40
  function getTelegramQueueReactionTransition(oldReactions, newReactions) {
48
41
  const oldEmojis = collectTelegramReactionEmojis(oldReactions);
49
42
  const newEmojis = collectTelegramReactionEmojis(newReactions);
50
- const oldPriorityEmoji = getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
51
- const newPriorityEmoji = getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
43
+ // Fire selects Steer; thumbs-up beneath it does not reorder the lane. Other emoji carry no queue intent.
44
+ const steerEmoji = TELEGRAM_STEER_REACTIONS[0].emoji;
45
+ const oldPriorityEmoji = oldEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
46
+ const newPriorityEmoji = newEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
52
47
  const oldSuppressionEmoji = getTelegramReactionEmoji(oldEmojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
53
48
  const newSuppressionEmoji = getTelegramReactionEmoji(newEmojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
54
49
  if (oldPriorityEmoji === newPriorityEmoji &&
@@ -58,6 +53,8 @@ function getTelegramQueueReactionTransition(oldReactions, newReactions) {
58
53
  const transition = { kind: "reaction-transition" };
59
54
  if (oldPriorityEmoji !== newPriorityEmoji) {
60
55
  transition.priorityEmoji = newPriorityEmoji ?? null;
56
+ if (newPriorityEmoji === steerEmoji)
57
+ transition.promptLane = "steer";
61
58
  }
62
59
  if (oldSuppressionEmoji !== newSuppressionEmoji) {
63
60
  transition.suppressionEmoji = newSuppressionEmoji ?? null;
@@ -252,6 +249,16 @@ export function inspectTelegramDeferredSourceCompletion(value) {
252
249
  const source = getTelegramUpdateAdmissionBinding(value)?.inspectCompletion?.();
253
250
  return execution.isCurrent() && source ? { ...source } : undefined;
254
251
  }
252
+ /** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
253
+ export function subscribeTelegramDeferredSourceCompletion(value, notify) {
254
+ const execution = getTelegramUpdateExecutionFence(value);
255
+ if (execution?.isCurrent() !== true)
256
+ return undefined;
257
+ return getTelegramUpdateAdmissionBinding(value)?.subscribeCompletion?.(() => {
258
+ if (execution.isCurrent())
259
+ notify();
260
+ });
261
+ }
255
262
  /** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
256
263
  export function prepareTelegramLiveDeferredInput(values, isCurrent) {
257
264
  const carriers = [...values], first = carriers[0], execution = getTelegramUpdateExecutionFence(first);
@@ -5152,6 +5159,7 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
5152
5159
  let outcome;
5153
5160
  let completionAttempted = false;
5154
5161
  let completedSource;
5162
+ const completionObservers = new Set();
5155
5163
  const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
5156
5164
  if (!execution.isCurrent())
5157
5165
  return;
@@ -5196,11 +5204,15 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
5196
5204
  completion &&
5197
5205
  execution.isCurrent() &&
5198
5206
  completion.isCurrent() &&
5199
- isDeepStrictEqual(completion.source, expectedSource))
5200
- completedSource = {
5201
- ...completion,
5202
- source: { ...expectedSource },
5203
- };
5207
+ isDeepStrictEqual(completion.source, expectedSource)) {
5208
+ completedSource = { ...completion, source: { ...expectedSource } };
5209
+ for (const notify of completionObservers) {
5210
+ try {
5211
+ notify();
5212
+ }
5213
+ catch { /* Observation cannot invalidate an exact removal ACK. */ }
5214
+ }
5215
+ }
5204
5216
  })
5205
5217
  .catch((error) => {
5206
5218
  try {
@@ -5257,6 +5269,10 @@ function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
5257
5269
  signal,
5258
5270
  })
5259
5271
  : undefined,
5272
+ subscribeCompletion: (notify) => {
5273
+ completionObservers.add(notify);
5274
+ return () => { completionObservers.delete(notify); };
5275
+ },
5260
5276
  inspectCompletion: () => execution.isCurrent() && completedSource?.isCurrent()
5261
5277
  ? { ...completedSource.source }
5262
5278
  : undefined,
@@ -5533,6 +5549,7 @@ export async function coordinateTelegramQueueHandoff(input) {
5533
5549
  const handoff = createTelegramQueueHandoff({
5534
5550
  handoffToken: input.handoffToken,
5535
5551
  item: input.item,
5552
+ steerSupported: input.steerSupported,
5536
5553
  });
5537
5554
  const receipt = handoff.payload.admissionReceipts[0];
5538
5555
  if (!receipt || handoff.payload.admissionReceipts.length !== 1) {
@@ -5670,6 +5687,8 @@ export function createTelegramQueueHandoffRecipientRuntime(deps) {
5670
5687
  /** Own queue-handoff projections over journals, admission, IPC, and live queue state. */
5671
5688
  export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
5672
5689
  return createTelegramQueueHandoffReconciler({
5690
+ canHandoffSteerWithRecipient: deps.canHandoffSteerWithRecipient,
5691
+ hasPendingDispatch: deps.hasPendingDispatch,
5673
5692
  ownsDirect: deps.ownsDirect,
5674
5693
  isFollowerRegistered: deps.isFollowerRegistered,
5675
5694
  isBusEnabled: deps.isBusEnabled,
@@ -5738,6 +5757,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
5738
5757
  if (followers.length === 0)
5739
5758
  return;
5740
5759
  for (const item of [...deps.getQueuedItems()]) {
5760
+ if ((deps.hasPendingDispatch?.() && deps.getQueuedItems()[0] === item) ||
5761
+ (item.kind === "prompt" && (item.steerAttempted || item.steerPacketTail !== undefined || item.steerPacketSourceOrders !== undefined)))
5762
+ continue;
5741
5763
  const target = item.target;
5742
5764
  const recipient = target
5743
5765
  ? followers.find((candidate) => candidate.target?.chatId === target.chatId &&
@@ -5745,6 +5767,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
5745
5767
  : undefined;
5746
5768
  if (!recipient)
5747
5769
  continue;
5770
+ const steerSupported = item.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) === true;
5771
+ if (item.queueLane === "steer" && !steerSupported)
5772
+ continue;
5748
5773
  const receipt = item.admissionReceipts?.[0];
5749
5774
  if (!receipt || item.admissionReceipts?.length !== 1)
5750
5775
  continue;
@@ -5768,6 +5793,7 @@ export function createTelegramQueueHandoffReconciler(deps) {
5768
5793
  const handoffToken = deps.createHandoffToken();
5769
5794
  const result = await coordinateTelegramQueueHandoff({
5770
5795
  item,
5796
+ steerSupported,
5771
5797
  expectedOwner,
5772
5798
  recipientOwner: {
5773
5799
  instanceId: recipient.instanceId,
@@ -5778,6 +5804,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
5778
5804
  handoffToken,
5779
5805
  lifecycle,
5780
5806
  stageRemote: async ({ payload: sourcePayload }) => {
5807
+ if (sourcePayload.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) !== true) {
5808
+ throw new Error("Steer queue handoff capability changed before staging.");
5809
+ }
5781
5810
  const payload = {
5782
5811
  ...sourcePayload,
5783
5812
  admissionReceipts: [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.55.1",
3
+ "version": "0.56.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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/Normal plus Keep/Skip reaction policy, native Rich Markdown delivery, UI/compat rendering, and interactive controls
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