@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.
Files changed (60) hide show
  1. package/AGENTS.md +5 -4
  2. package/BACKLOG.md +6 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +28 -17
  5. package/dist/lib/bindings.d.ts +8 -4
  6. package/dist/lib/bindings.js +41 -1
  7. package/dist/lib/bus-follower.d.ts +1 -0
  8. package/dist/lib/bus-follower.js +10 -5
  9. package/dist/lib/bus-leader.d.ts +2 -0
  10. package/dist/lib/bus-leader.js +34 -20
  11. package/dist/lib/bus.d.ts +8 -0
  12. package/dist/lib/bus.js +21 -7
  13. package/dist/lib/commands.d.ts +1 -1
  14. package/dist/lib/commands.js +33 -28
  15. package/dist/lib/extension.js +18 -3
  16. package/dist/lib/lifecycle.d.ts +2 -1
  17. package/dist/lib/lifecycle.js +28 -20
  18. package/dist/lib/locks.d.ts +6 -2
  19. package/dist/lib/locks.js +3 -3
  20. package/dist/lib/menu-queue.d.ts +1 -0
  21. package/dist/lib/menu-queue.js +102 -99
  22. package/dist/lib/pi.d.ts +3 -2
  23. package/dist/lib/pi.js +3 -0
  24. package/dist/lib/polling.d.ts +6 -2
  25. package/dist/lib/polling.js +5 -3
  26. package/dist/lib/queue.d.ts +57 -5
  27. package/dist/lib/queue.js +313 -25
  28. package/dist/lib/routing.d.ts +4 -0
  29. package/dist/lib/routing.js +62 -1
  30. package/dist/lib/status.d.ts +1 -2
  31. package/dist/lib/status.js +6 -7
  32. package/dist/lib/sync.js +11 -1
  33. package/dist/lib/threads.d.ts +11 -0
  34. package/dist/lib/threads.js +57 -6
  35. package/dist/lib/updates.d.ts +12 -7
  36. package/dist/lib/updates.js +44 -9
  37. package/dist/package.json +1 -1
  38. package/docs/README.md +1 -1
  39. package/docs/architecture.md +24 -13
  40. package/docs/callback-namespaces.md +4 -0
  41. package/docs/multi-instance-bus.md +6 -4
  42. package/docs/ui-style.md +14 -10
  43. package/lib/bindings.ts +52 -3
  44. package/lib/bus-follower.ts +13 -5
  45. package/lib/bus-leader.ts +37 -23
  46. package/lib/bus.ts +23 -7
  47. package/lib/commands.ts +52 -47
  48. package/lib/extension.ts +18 -2
  49. package/lib/lifecycle.ts +34 -28
  50. package/lib/locks.ts +5 -2
  51. package/lib/menu-queue.ts +116 -162
  52. package/lib/pi.ts +8 -0
  53. package/lib/polling.ts +7 -2
  54. package/lib/queue.ts +328 -33
  55. package/lib/routing.ts +51 -1
  56. package/lib/status.ts +7 -7
  57. package/lib/sync.ts +11 -0
  58. package/lib/threads.ts +57 -5
  59. package/lib/updates.ts +44 -15
  60. package/package.json +1 -1
@@ -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
  },
@@ -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,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;
@@ -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
- const oldPriorityEmoji = getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
51
- const newPriorityEmoji = getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
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
- ...completion,
5202
- source: { ...expectedSource },
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.55.1",
3
+ "version": "0.56.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
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/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