@llblab/pi-kit 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) 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 +5 -4
  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 +29 -18
  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 +2 -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 +10 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +32 -13
  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 +102 -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 +5 -1
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +323 -30
  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/turns.d.ts +4 -0
  39. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +6 -0
  40. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +14 -7
  41. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +44 -9
  42. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  43. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  44. package/node_modules/@llblab/pi-telegram/docs/architecture.md +27 -14
  45. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +4 -0
  46. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +8 -4
  47. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +14 -10
  48. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +52 -3
  49. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +13 -5
  50. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +37 -23
  51. package/node_modules/@llblab/pi-telegram/lib/bus.ts +36 -10
  52. package/node_modules/@llblab/pi-telegram/lib/commands.ts +52 -47
  53. package/node_modules/@llblab/pi-telegram/lib/extension.ts +18 -2
  54. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +34 -28
  55. package/node_modules/@llblab/pi-telegram/lib/locks.ts +5 -2
  56. package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +116 -162
  57. package/node_modules/@llblab/pi-telegram/lib/pi.ts +8 -0
  58. package/node_modules/@llblab/pi-telegram/lib/polling.ts +7 -2
  59. package/node_modules/@llblab/pi-telegram/lib/queue.ts +328 -33
  60. package/node_modules/@llblab/pi-telegram/lib/routing.ts +350 -30
  61. package/node_modules/@llblab/pi-telegram/lib/status.ts +7 -7
  62. package/node_modules/@llblab/pi-telegram/lib/sync.ts +11 -0
  63. package/node_modules/@llblab/pi-telegram/lib/threads.ts +57 -5
  64. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  65. package/node_modules/@llblab/pi-telegram/lib/updates.ts +46 -15
  66. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  67. package/package.json +2 -2
@@ -18,6 +18,7 @@ import {
18
18
  isSameTelegramProcessInstance,
19
19
  isTelegramTopicTargetStaleError,
20
20
  provisionOwnBusTopic,
21
+ resetTelegramConnectionProvisioning,
21
22
  type TelegramOwnTopicProvisionResult,
22
23
  type TelegramTopicTargetStore,
23
24
  type TelegramWorkspaceDisplayBinding,
@@ -826,6 +827,15 @@ export async function ensureTelegramLeaderThreadBinding(
826
827
  ...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
827
828
  };
828
829
  const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
830
+ if (deps.forceFreshUnnamed) {
831
+ await resetTelegramConnectionProvisioning({
832
+ store: deps.topicTargetStore, owner: "leader", instanceId: deps.instanceId,
833
+ profileKey: leaderProfileKey, isCurrent: () => {
834
+ assertLeaderEpoch("connect-recovery");
835
+ return true;
836
+ }, recordEvent: deps.recordEvent,
837
+ });
838
+ }
829
839
  const legacyLeaderRecord =
830
840
  deps.topicTargetStore.getByProfileKey(leaderProfileKey);
831
841
  let capacityUnavailable = false;
@@ -836,6 +846,7 @@ export async function ensureTelegramLeaderThreadBinding(
836
846
  legacyLeaderRecord?.instanceId,
837
847
  {
838
848
  sessionId: deps.sessionId,
849
+ provisionProfileKey: leaderProfileKey,
839
850
  onCapacityUnavailable() {
840
851
  capacityUnavailable = true;
841
852
  },
@@ -783,6 +783,7 @@ export interface TelegramTopicTargetStore {
783
783
  ) => { slot?: string; threadName?: string } | undefined;
784
784
  listReservations: () => TelegramThreadReservation[];
785
785
  listPendingProvisions: () => TelegramThreadPendingProvision[];
786
+ resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
786
787
  listPendingCleanups: () => TelegramThreadCleanupIntent[];
787
788
  listSyncObservations: () => TelegramTopicSyncObservation[];
788
789
  reserveThread: (reservation: TelegramThreadReservation) => void;
@@ -902,6 +903,7 @@ export interface TelegramTopicTargetStore {
902
903
  options?: {
903
904
  existingBindingOnly?: boolean;
904
905
  sessionId?: string;
906
+ provisionProfileKey?: string;
905
907
  onCapacityUnavailable?: () => void;
906
908
  },
907
909
  ) => TelegramWorkspaceBindingIdentity | undefined;
@@ -4168,6 +4170,7 @@ export function createTelegramTopicTargetStore(
4168
4170
  }
4169
4171
  ),
4170
4172
  isCurrentPublication?: () => boolean,
4173
+ resetProvisions?: readonly TelegramThreadPendingProvision[],
4171
4174
  ): Promise<boolean> => {
4172
4175
  const submittedPath = getPath(),
4173
4176
  submittedProfile = activeProfile();
@@ -4367,6 +4370,13 @@ export function createTelegramTopicTargetStore(
4367
4370
  !!expectedRestore,
4368
4371
  ),
4369
4372
  );
4373
+ if (resetProvisions) {
4374
+ const recoveries = readProvisionRecoveries(path, true);
4375
+ if (mutationRevision !== persistedRevision || resetProvisions.some(expected =>
4376
+ expected.target || Object.hasOwn(recoveries, expected.id) ||
4377
+ !isDeepStrictEqual(disk.pendingProvisions?.find(value => value.id === expected.id), expected)))
4378
+ throw new Error("Telegram connection recovery provisioning evidence changed.");
4379
+ }
4370
4380
  if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
4371
4381
  throw new Error(
4372
4382
  "Workspace Restore evidence changed before publication.",
@@ -4429,7 +4439,7 @@ export function createTelegramTopicTargetStore(
4429
4439
  }
4430
4440
  : {}),
4431
4441
  reservations: reservations.map((reservation) => ({ ...reservation })),
4432
- pendingProvisions: pendingProvisions.map((provision) => ({
4442
+ pendingProvisions: pendingProvisions.filter(provision => !resetProvisions?.some(expected => expected.id === provision.id)).map((provision) => ({
4433
4443
  ...provision,
4434
4444
  ...(provision.target ? { target: { ...provision.target } } : {}),
4435
4445
  })),
@@ -4636,6 +4646,7 @@ export function createTelegramTopicTargetStore(
4636
4646
  loadedProfile = submittedProfile;
4637
4647
  loaded = true;
4638
4648
  observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
4649
+ if (resetProvisions) { pendingProvisions = file.pendingProvisions; mutationRevision += 1; }
4639
4650
  if (transition) {
4640
4651
  records = new Map(
4641
4652
  file.threads.map((record) => {
@@ -4808,6 +4819,10 @@ export function createTelegramTopicTargetStore(
4808
4819
  if (isCurrentPublication?.() === false) return false;
4809
4820
  loadedPath = path;
4810
4821
  loaded = true;
4822
+ if (resetProvisions) {
4823
+ if (mutationRevision !== persistedRevision) return false;
4824
+ pendingProvisions = file.pendingProvisions;
4825
+ }
4811
4826
  if (mutationRevision === persistedRevision) dirty = false;
4812
4827
  return true;
4813
4828
  });
@@ -6814,6 +6829,11 @@ export function createTelegramTopicTargetStore(
6814
6829
  current.status = "ambiguous";
6815
6830
  return true;
6816
6831
  },
6832
+ resetPendingProvisionForConnect(expected, isCurrent) {
6833
+ // Stage no in-memory deletion: refusal or a stale publication must keep its protective hold.
6834
+ if (expected.target || !isCurrent()) return Promise.resolve(false);
6835
+ return persistSnapshot(undefined, isCurrent, [structuredClone(expected)]);
6836
+ },
6817
6837
  removePendingProvision(id) {
6818
6838
  const before = pendingProvisions.length;
6819
6839
  pendingProvisions = pendingProvisions.filter(
@@ -7608,8 +7628,14 @@ export function createTelegramTopicTargetStore(
7608
7628
  ) {
7609
7629
  return undefined;
7610
7630
  }
7611
- const retainedTarget = binding?.target ?? legacyRecord?.target;
7612
- const retainedSlot = binding?.slot ?? legacyRecord?.slot;
7631
+ const ownedProvisions = pendingProvisions.filter(value =>
7632
+ value.workspaceBindingKey === identity.bindingKey &&
7633
+ (value.instanceId === instanceId || value.instanceId === effectivePreviousInstanceId ||
7634
+ (options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
7635
+ if (ownedProvisions.length > 1) return undefined;
7636
+ const ownProvision = ownedProvisions[0];
7637
+ const retainedTarget = ownProvision?.target ?? binding?.target ?? legacyRecord?.target;
7638
+ const retainedSlot = ownProvision?.slot ?? binding?.slot ?? legacyRecord?.slot;
7613
7639
  const otherBindings = Array.from(workspaceBindings.values())
7614
7640
  .filter(
7615
7641
  (other) => other.bindingKey !== identity.bindingKey && other.slot,
@@ -7641,7 +7667,7 @@ export function createTelegramTopicTargetStore(
7641
7667
  )
7642
7668
  .map((reservation) => reservation.slot),
7643
7669
  ...pendingProvisions
7644
- .filter((provision) =>
7670
+ .filter((provision) => provision !== ownProvision &&
7645
7671
  isPendingProvisionLiveOrTargeted(provision, nowMs),
7646
7672
  )
7647
7673
  .map((provision) => provision.slot),
@@ -9661,6 +9687,32 @@ export function createTelegramTopicTargetRenamer(
9661
9687
  };
9662
9688
  }
9663
9689
 
9690
+ /** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
9691
+ export async function resetTelegramConnectionProvisioning(input: {
9692
+ store: TelegramTopicTargetStore;
9693
+ owner: TelegramThreadPendingProvision["owner"];
9694
+ instanceId: string;
9695
+ profileKey: string;
9696
+ isCurrent: () => boolean;
9697
+ recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
9698
+ }): Promise<void> {
9699
+ if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
9700
+ const removed: TelegramThreadPendingProvision[] = [];
9701
+ for (const pending of input.store.listPendingProvisions()) {
9702
+ if (pending.owner !== input.owner || pending.target ||
9703
+ (pending.profileKey !== input.profileKey && pending.instanceId !== input.instanceId)) continue;
9704
+ if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
9705
+ if (!await input.store.resetPendingProvisionForConnect(pending, input.isCurrent))
9706
+ throw new Error("Telegram connection recovery lost publication authority.");
9707
+ if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
9708
+ removed.push(pending);
9709
+ }
9710
+ if (removed.length === 0) return;
9711
+ input.recordEvent("connection", "Explicit connect reset unfinished Thread creation; an unconfirmed Telegram Thread may remain.", {
9712
+ phase: "connect-provision-reset", count: removed.length, owner: input.owner,
9713
+ });
9714
+ }
9715
+
9664
9716
  export function createTelegramTopicTargetProvisioner(
9665
9717
  deps: TelegramTopicTargetProvisionerDeps,
9666
9718
  ): (
@@ -9923,7 +9975,7 @@ export function createTelegramTopicTargetProvisioner(
9923
9975
  slot,
9924
9976
  );
9925
9977
  }
9926
- const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
9978
+ const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}:${randomUUID()}`;
9927
9979
  const pendingOwner =
9928
9980
  request.owner?.kind === "leader" ? "leader" : "manual-follower";
9929
9981
  assertLeaderEpoch("before-pending-intent");
@@ -51,6 +51,13 @@ export function withoutTelegramTurnReplyAnchor<TMessage extends object>(message:
51
51
  return { ...message, [REPLY_ANCHOR_SUPPRESSED]: true };
52
52
  }
53
53
 
54
+ /** A confirmed same-target copy may replace the former-tab suppression on a warm input projection. */
55
+ export function withTelegramTurnReplyAnchor<TMessage extends { message_id: number }>(message: TMessage): TMessage {
56
+ if (!Number.isSafeInteger(message.message_id) || message.message_id <= 0)
57
+ throw new Error("Telegram reply anchor requires a positive message ID.");
58
+ return { ...message, [REPLY_ANCHOR_SUPPRESSED]: false };
59
+ }
60
+
54
61
  export const TELEGRAM_GUEST_TURN_NOTE =
55
62
  "[guest] delivery: answer quickly with one concise, self-contained reply";
56
63
 
@@ -108,11 +108,10 @@ export type TelegramReactionType =
108
108
 
109
109
  export const TELEGRAM_PRIORITY_REACTIONS = [
110
110
  { id: 10, name: "like", emoji: "👍" },
111
- { id: 11, name: "lightning", emoji: "⚡" },
112
111
  { id: 12, name: "heart", emoji: "❤" },
113
112
  { id: 13, name: "dove", emoji: "🕊" },
114
- { id: 14, name: "fire", emoji: "🔥" },
115
113
  ] as const;
114
+ export const TELEGRAM_STEER_REACTIONS = [{ id: 14, name: "fire", emoji: "🔥" }] as const;
116
115
  export const TELEGRAM_REMOVAL_REACTIONS = [
117
116
  { id: 20, name: "dislike", emoji: "👎" },
118
117
  { id: 21, name: "ghost", emoji: "👻" },
@@ -160,14 +159,10 @@ function getTelegramQueueReactionTransition(
160
159
  ): TelegramQueueReactionDisposition | undefined {
161
160
  const oldEmojis = collectTelegramReactionEmojis(oldReactions);
162
161
  const newEmojis = collectTelegramReactionEmojis(newReactions);
163
- const oldPriorityEmoji = getTelegramReactionEmoji(
164
- oldEmojis,
165
- TELEGRAM_PRIORITY_REACTION_EMOJIS,
166
- );
167
- const newPriorityEmoji = getTelegramReactionEmoji(
168
- newEmojis,
169
- TELEGRAM_PRIORITY_REACTION_EMOJIS,
170
- );
162
+ // Fire is effective Steer intent; weaker positive aliases under it do not reorder the lane.
163
+ const steerEmoji = TELEGRAM_STEER_REACTIONS[0].emoji;
164
+ const oldPriorityEmoji = oldEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
165
+ const newPriorityEmoji = newEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
171
166
  const oldSuppressionEmoji = getTelegramReactionEmoji(
172
167
  oldEmojis,
173
168
  TELEGRAM_REMOVAL_REACTION_EMOJIS,
@@ -188,6 +183,7 @@ function getTelegramQueueReactionTransition(
188
183
  > = { kind: "reaction-transition" };
189
184
  if (oldPriorityEmoji !== newPriorityEmoji) {
190
185
  transition.priorityEmoji = newPriorityEmoji ?? null;
186
+ if (newPriorityEmoji === steerEmoji) transition.promptLane = "steer";
191
187
  }
192
188
  if (oldSuppressionEmoji !== newSuppressionEmoji) {
193
189
  transition.suppressionEmoji = newSuppressionEmoji ?? null;
@@ -374,6 +370,8 @@ export interface TelegramForeignOwnedUpdateForwarder<
374
370
  message: TMessage;
375
371
  ownership: TelegramTargetOwnershipView;
376
372
  ctx: TContext;
373
+ /** Local issuance/ACK fence; implementations must not serialize it into payloads. */
374
+ assertAuthority?: () => void;
377
375
  }) =>
378
376
  | Promise<TelegramBusForeignUpdateSettlement>
379
377
  | TelegramBusForeignUpdateSettlement;
@@ -549,6 +547,7 @@ interface TelegramUpdateAdmissionBinding {
549
547
  inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
550
548
  inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
551
549
  inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
550
+ subscribeCompletion?: (notify: () => void) => () => void;
552
551
  isSourceUnsettled?: () => boolean;
553
552
  prepareLiveInput?: (
554
553
  sourceUpdateIds: readonly number[],
@@ -617,6 +616,7 @@ export function bindTelegramUpdateAdmissionSource<
617
616
  | "inspectSource"
618
617
  | "inspectSourceSnapshot"
619
618
  | "inspectCompletion"
619
+ | "subscribeCompletion"
620
620
  | "isSourceUnsettled"
621
621
  | "acknowledgeTransferred"
622
622
  | "prepareLiveInput"
@@ -745,6 +745,15 @@ export function inspectTelegramDeferredSourceCompletion(
745
745
  return execution.isCurrent() && source ? { ...source } : undefined;
746
746
  }
747
747
 
748
+ /** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
749
+ export function subscribeTelegramDeferredSourceCompletion(value: unknown, notify: () => void): (() => void) | undefined {
750
+ const execution = getTelegramUpdateExecutionFence(value);
751
+ if (execution?.isCurrent() !== true) return undefined;
752
+ return getTelegramUpdateAdmissionBinding(value)?.subscribeCompletion?.(() => {
753
+ if (execution.isCurrent()) notify();
754
+ });
755
+ }
756
+
748
757
  /** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
749
758
  export function prepareTelegramLiveDeferredInput(
750
759
  values: readonly unknown[],
@@ -8723,6 +8732,7 @@ function createTelegramUpdateSourceAdmissionHandle<
8723
8732
  let outcome: TelegramUpdateAdmissionOutcome | undefined;
8724
8733
  let completionAttempted = false;
8725
8734
  let completedSource: TelegramDeferredSourceCompletion | undefined;
8735
+ const completionObservers = new Set<() => void>();
8726
8736
  const boundUpdate = bindTelegramUpdateExecutionFence(
8727
8737
  bindTelegramUpdateAdmissionSource(
8728
8738
  update,
@@ -8779,11 +8789,12 @@ function createTelegramUpdateSourceAdmissionHandle<
8779
8789
  execution.isCurrent() &&
8780
8790
  completion.isCurrent() &&
8781
8791
  isDeepStrictEqual(completion.source, expectedSource)
8782
- )
8783
- completedSource = {
8784
- ...completion,
8785
- source: { ...expectedSource },
8786
- };
8792
+ ) {
8793
+ completedSource = { ...completion, source: { ...expectedSource } };
8794
+ for (const notify of completionObservers) {
8795
+ try { notify(); } catch { /* Observation cannot invalidate an exact removal ACK. */ }
8796
+ }
8797
+ }
8787
8798
  })
8788
8799
  .catch((error) => {
8789
8800
  try {
@@ -8844,6 +8855,10 @@ function createTelegramUpdateSourceAdmissionHandle<
8844
8855
  signal,
8845
8856
  })
8846
8857
  : undefined,
8858
+ subscribeCompletion: (notify) => {
8859
+ completionObservers.add(notify);
8860
+ return () => { completionObservers.delete(notify); };
8861
+ },
8847
8862
  inspectCompletion: () =>
8848
8863
  execution.isCurrent() && completedSource?.isCurrent()
8849
8864
  ? { ...completedSource.source }
@@ -9320,6 +9335,7 @@ export function createTelegramQueueHandoffControlExecutionFactory<TContext>(
9320
9335
 
9321
9336
  export interface TelegramQueueHandoffCoordinatorInput<TContext> {
9322
9337
  item: TelegramQueueItem<TContext>;
9338
+ steerSupported?: boolean;
9323
9339
  expectedOwner: TelegramUpdateJournalQueueOwner;
9324
9340
  recipientOwner: TelegramUpdateJournalQueueOwnerIdentity;
9325
9341
  handoffToken: string;
@@ -9375,6 +9391,7 @@ export async function coordinateTelegramQueueHandoff<TContext>(
9375
9391
  const handoff = createTelegramQueueHandoff({
9376
9392
  handoffToken: input.handoffToken,
9377
9393
  item: input.item,
9394
+ steerSupported: input.steerSupported,
9378
9395
  });
9379
9396
  const receipt = handoff.payload.admissionReceipts[0];
9380
9397
  if (!receipt || handoff.payload.admissionReceipts.length !== 1) {
@@ -9551,6 +9568,8 @@ export function createTelegramQueueHandoffRecipientRuntime<TContext>(
9551
9568
  }
9552
9569
 
9553
9570
  export interface TelegramQueueHandoffReconcilerDeps<TContext> {
9571
+ canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
9572
+ hasPendingDispatch?: () => boolean;
9554
9573
  ownsDirect: () => boolean;
9555
9574
  isFollowerRegistered: () => boolean;
9556
9575
  isBusEnabled: () => boolean;
@@ -9589,6 +9608,8 @@ export interface TelegramQueueHandoffReconcilerDeps<TContext> {
9589
9608
  export interface TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<
9590
9609
  TContext,
9591
9610
  > {
9611
+ canHandoffSteerWithRecipient?: (recipient: TelegramBusFollowerView) => boolean;
9612
+ hasPendingDispatch?: () => boolean;
9592
9613
  ownsDirect: () => boolean;
9593
9614
  isFollowerRegistered: () => boolean;
9594
9615
  isBusEnabled: () => boolean;
@@ -9628,6 +9649,8 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly<
9628
9649
  deps: TelegramQueueHandoffReconciliationRuntimeAssemblyDeps<TContext>,
9629
9650
  ): (ctx: TContext) => Promise<void> {
9630
9651
  return createTelegramQueueHandoffReconciler({
9652
+ canHandoffSteerWithRecipient: deps.canHandoffSteerWithRecipient,
9653
+ hasPendingDispatch: deps.hasPendingDispatch,
9631
9654
  ownsDirect: deps.ownsDirect,
9632
9655
  isFollowerRegistered: deps.isFollowerRegistered,
9633
9656
  isBusEnabled: deps.isBusEnabled,
@@ -9704,6 +9727,8 @@ export function createTelegramQueueHandoffReconciler<TContext>(
9704
9727
  .filter((follower) => follower.instanceId !== deps.donorInstanceId);
9705
9728
  if (followers.length === 0) return;
9706
9729
  for (const item of [...deps.getQueuedItems()]) {
9730
+ if ((deps.hasPendingDispatch?.() && deps.getQueuedItems()[0] === item) ||
9731
+ (item.kind === "prompt" && (item.steerAttempted || item.steerPacketTail !== undefined || item.steerPacketSourceOrders !== undefined))) continue;
9707
9732
  const target = item.target;
9708
9733
  const recipient = target
9709
9734
  ? followers.find(
@@ -9713,6 +9738,8 @@ export function createTelegramQueueHandoffReconciler<TContext>(
9713
9738
  )
9714
9739
  : undefined;
9715
9740
  if (!recipient) continue;
9741
+ const steerSupported = item.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) === true;
9742
+ if (item.queueLane === "steer" && !steerSupported) continue;
9716
9743
  const receipt = item.admissionReceipts?.[0];
9717
9744
  if (!receipt || item.admissionReceipts?.length !== 1) continue;
9718
9745
  if (
@@ -9739,6 +9766,7 @@ export function createTelegramQueueHandoffReconciler<TContext>(
9739
9766
  const handoffToken = deps.createHandoffToken();
9740
9767
  const result = await coordinateTelegramQueueHandoff({
9741
9768
  item,
9769
+ steerSupported,
9742
9770
  expectedOwner,
9743
9771
  recipientOwner: {
9744
9772
  instanceId: recipient.instanceId,
@@ -9749,6 +9777,9 @@ export function createTelegramQueueHandoffReconciler<TContext>(
9749
9777
  handoffToken,
9750
9778
  lifecycle,
9751
9779
  stageRemote: async ({ payload: sourcePayload }) => {
9780
+ if (sourcePayload.queueLane === "steer" && deps.canHandoffSteerWithRecipient?.(recipient) !== true) {
9781
+ throw new Error("Steer queue handoff capability changed before staging.");
9782
+ }
9752
9783
  const payload = {
9753
9784
  ...sourcePayload,
9754
9785
  admissionReceipts: [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.55.0",
3
+ "version": "0.56.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -47,7 +47,7 @@
47
47
  "@llblab/pi-codex-usage": "0.13.0",
48
48
  "@llblab/pi-grow-loop": "0.9.0",
49
49
  "@llblab/pi-state-flow": "0.26.4",
50
- "@llblab/pi-telegram": "0.55.0",
50
+ "@llblab/pi-telegram": "0.56.1",
51
51
  "@llblab/skills": "1.15.0"
52
52
  },
53
53
  "bundledDependencies": [