@llblab/pi-kit 0.5.2 → 0.7.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 (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -49,6 +49,7 @@ import {
49
49
  type TelegramQueueHandoffPayload,
50
50
  } from "./queue.ts";
51
51
  import type { TelegramTarget } from "./target.ts";
52
+ import type { TelegramThreadDisplayMode } from "./config.ts";
52
53
  import { isProcessAlive } from "./locks.ts";
53
54
  import { resolveAgentDir } from "./paths.ts";
54
55
 
@@ -152,6 +153,22 @@ export function getTelegramProcessLiveness(
152
153
  return proof.identity === owner.processBirthId ? "alive" : "dead";
153
154
  }
154
155
 
156
+ export function getTelegramProcessBirthIdentityLiveness(
157
+ processBirthId: string,
158
+ options: TelegramProcessLivenessOptions = {},
159
+ ): TelegramProcessLiveness {
160
+ const match = /^(\d+):(start|generation):(.+)$/u.exec(processBirthId);
161
+ if (!match) return "unverifiable";
162
+ const processId = Number(match[1]);
163
+ if (!Number.isSafeInteger(processId) || processId <= 0) return "unverifiable";
164
+ const processAlive = options.isProcessAlive ?? isProcessAlive;
165
+ if (!processAlive(processId)) return "dead";
166
+ if (match[2] === "generation") return "unverifiable";
167
+ const proof = getTelegramProcessBirthProof(processId, options);
168
+ if (proof.status === "unverifiable") return "unverifiable";
169
+ return proof.identity === processBirthId ? "alive" : "dead";
170
+ }
171
+
155
172
  export function createCurrentTelegramBusProcessRuntime(input: {
156
173
  getActiveProfileName: () => string | undefined;
157
174
  pid?: number;
@@ -208,6 +225,13 @@ export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION =
208
225
  "durable-follower-admission-v1" as const;
209
226
  export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
210
227
  "queue-handoff-v1" as const;
228
+ export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
229
+ "input-custody-reference-v1" as const;
230
+ export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
231
+ "workspace-thread-rename-v1" as const;
232
+ export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE = "thread-display-mode-v1" as const;
233
+ export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT =
234
+ "workspace-follower-auto-connect-v1" as const;
211
235
 
212
236
  export interface TelegramBusProtocolIdentity {
213
237
  protocolVersion: number;
@@ -269,6 +293,19 @@ export function hasTelegramBusCapability(
269
293
  return identity?.capabilities.includes(capability) ?? false;
270
294
  }
271
295
 
296
+ export function getTelegramInputCustodyPeerReadiness(
297
+ followers: readonly Pick<TelegramBusFollowerView,
298
+ "registrationGeneration" | "protocol">[],
299
+ ): ("ready" | "legacy" | "unknown")[] {
300
+ return followers.map(follower => {
301
+ if (!follower.registrationGeneration || !follower.protocol) return "unknown";
302
+ return hasTelegramBusCapability(follower.protocol,
303
+ TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) &&
304
+ hasTelegramBusCapability(follower.protocol,
305
+ TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) ? "ready" : "legacy";
306
+ });
307
+ }
308
+
272
309
  export function getTelegramBusProtocolCompatibility(input: {
273
310
  local: TelegramBusProtocolIdentity;
274
311
  remote?: TelegramBusProtocolIdentity;
@@ -361,6 +398,7 @@ export function getTelegramFollowerTargetOwnership(input: {
361
398
  instanceId: string;
362
399
  ownerGeneration: string;
363
400
  recipientBindingKey: string;
401
+ protocolIdentity: TelegramBusProtocolIdentity;
364
402
  } | undefined {
365
403
  const liveFollower = input.followers.find((follower) => {
366
404
  return (
@@ -379,6 +417,7 @@ export function getTelegramFollowerTargetOwnership(input: {
379
417
  instanceId: liveFollower.instanceId,
380
418
  ownerGeneration: liveFollower.registrationGeneration,
381
419
  recipientBindingKey: liveFollower.profileKey,
420
+ protocolIdentity: liveFollower.protocol,
382
421
  };
383
422
  }
384
423
  // Persisted records are restart hints, not live routing authority. Only an
@@ -622,12 +661,19 @@ export interface TelegramBusFollowerDeliveryIdentity {
622
661
  deliveryId: string;
623
662
  sourceUpdateId: number;
624
663
  recipientBindingKey: string;
664
+ sourceRecoveryKey?: string;
665
+ sourceClaim?: {
666
+ acquisitionId: string;
667
+ handoffId: string;
668
+ };
625
669
  }
626
670
 
627
671
  export type TelegramBusForeignUpdateFailureClass =
628
672
  | "source-update-identity-missing"
629
673
  | "recipient-binding-missing"
630
674
  | "recipient-generation-missing"
675
+ | "source-reference-missing"
676
+ | "recipient-ownership-stale"
631
677
  | "transport-failed"
632
678
  | "acknowledgement-missing"
633
679
  | "acknowledgement-rejected"
@@ -653,14 +699,23 @@ export function createTelegramBusFollowerDeliveryIdentity(input: {
653
699
  | "leader.forwardCallback"
654
700
  | "leader.forwardReaction"
655
701
  | "leader.forwardMessage"
656
- | "leader.forwardEditedMessage";
702
+ | "leader.forwardEditedMessage"
703
+ | "leader.wakeInputCustody";
657
704
  recipientBindingKey: string;
658
705
  sourceUpdateId: number;
706
+ sourceRecoveryKey?: string;
707
+ sourceClaim?: {
708
+ acquisitionId: string;
709
+ handoffId: string;
710
+ };
659
711
  }): TelegramBusFollowerDeliveryIdentity {
660
712
  if (
661
713
  !input.recipientBindingKey ||
662
714
  !Number.isSafeInteger(input.sourceUpdateId) ||
663
- input.sourceUpdateId < 0
715
+ input.sourceUpdateId < 0 ||
716
+ (input.sourceClaim !== undefined &&
717
+ (!input.sourceClaim.acquisitionId || input.sourceClaim.acquisitionId.length > 256 ||
718
+ !input.sourceClaim.handoffId || input.sourceClaim.handoffId.length > 256))
664
719
  ) {
665
720
  throw new Error("Telegram follower delivery identity is incomplete.");
666
721
  }
@@ -678,15 +733,48 @@ export function createTelegramBusFollowerDeliveryIdentity(input: {
678
733
  deliveryId: `telegram-follower-v1-${deliveryId}`,
679
734
  sourceUpdateId: input.sourceUpdateId,
680
735
  recipientBindingKey: input.recipientBindingKey,
736
+ ...(input.sourceRecoveryKey ? { sourceRecoveryKey: input.sourceRecoveryKey } : {}),
737
+ ...(input.sourceClaim ? { sourceClaim: { ...input.sourceClaim } } : {}),
681
738
  };
682
739
  }
683
740
 
741
+ export function canUseTelegramBusInputCustodyReference(input: {
742
+ local?: TelegramBusProtocolIdentity;
743
+ remote?: TelegramBusProtocolIdentity;
744
+ }): boolean {
745
+ return hasTelegramBusCapability(input.local, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) &&
746
+ hasTelegramBusCapability(input.remote, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE);
747
+ }
748
+
749
+ export function createTelegramBusFollowerSourceReferenceDeliveryIdentity(input: {
750
+ kind: "leader.forwardCallback" | "leader.forwardReaction" |
751
+ "leader.forwardMessage" | "leader.forwardEditedMessage" | "leader.wakeInputCustody";
752
+ recipientBindingKey: string;
753
+ sourceRecoveryKey: string;
754
+ source: { updateId: number; owner: { acquisitionId: string; handoffId?: string } };
755
+ }): TelegramBusFollowerDeliveryIdentity {
756
+ if (!input.sourceRecoveryKey || !input.source.owner.handoffId) throw new Error(
757
+ "Telegram follower source-reference delivery requires an accepted handoff.",
758
+ );
759
+ return createTelegramBusFollowerDeliveryIdentity({ kind: input.kind,
760
+ recipientBindingKey: input.recipientBindingKey,
761
+ sourceUpdateId: input.source.updateId,
762
+ sourceRecoveryKey: input.sourceRecoveryKey,
763
+ sourceClaim: { acquisitionId: input.source.owner.acquisitionId,
764
+ handoffId: input.source.owner.handoffId } });
765
+ }
766
+
684
767
  export type TelegramBusEnvelope = (
685
768
  | {
686
769
  kind: "follower.register";
687
770
  requestId: string;
688
771
  registration: TelegramBusInstanceRegistration;
689
772
  }
773
+ | {
774
+ kind: "follower.restoreWorkspace";
775
+ requestId: string;
776
+ registration: TelegramBusInstanceRegistration;
777
+ }
690
778
  | {
691
779
  kind: "follower.heartbeat";
692
780
  requestId: string;
@@ -701,6 +789,30 @@ export type TelegramBusEnvelope = (
701
789
  registrationGeneration?: string;
702
790
  sentAtMs: number;
703
791
  }
792
+ | {
793
+ kind: "follower.setThreadDisplayMode";
794
+ requestId: string;
795
+ instanceId: string;
796
+ registrationGeneration: string;
797
+ mode: TelegramThreadDisplayMode;
798
+ }
799
+ | {
800
+ kind: "follower.renameThread";
801
+ requestId: string;
802
+ instanceId: string;
803
+ registrationGeneration: string;
804
+ target: TelegramTarget & { threadId: number };
805
+ threadName: string;
806
+ sentAtMs: number;
807
+ }
808
+ | {
809
+ kind: "follower.resetThreadName";
810
+ requestId: string;
811
+ instanceId: string;
812
+ registrationGeneration: string;
813
+ target: TelegramTarget & { threadId: number };
814
+ sentAtMs: number;
815
+ }
704
816
  | {
705
817
  kind: "leader.forwardCallback";
706
818
  requestId: string;
@@ -738,6 +850,25 @@ export type TelegramBusEnvelope = (
738
850
  message: unknown;
739
851
  sentAtMs: number;
740
852
  }
853
+ | {
854
+ kind: "leader.offerInputCustodyHandoff";
855
+ requestId: string;
856
+ recipientInstanceId: string;
857
+ recipientRegistrationGeneration: string;
858
+ recipientBindingKey: string;
859
+ sourceRecoveryKey: string;
860
+ source: { journalBindingKey: string; tokenSha256: string; updateId: number };
861
+ handoffId: string;
862
+ sentAtMs: number;
863
+ }
864
+ | {
865
+ kind: "leader.wakeInputCustody";
866
+ requestId: string;
867
+ recipientInstanceId: string;
868
+ recipientRegistrationGeneration: string;
869
+ delivery: TelegramBusFollowerDeliveryIdentity;
870
+ sentAtMs: number;
871
+ }
741
872
  | {
742
873
  kind: "leader.replaceFollowerTarget";
743
874
  requestId: string;
@@ -817,7 +948,8 @@ export type TelegramBusEnvelope = (
817
948
  | "request-id-collision"
818
949
  | "ledger-overloaded"
819
950
  | "incompatible-protocol"
820
- | "stale-target";
951
+ | "stale-target"
952
+ | "workspace-binding-unavailable";
821
953
  method?: string;
822
954
  chatId?: number;
823
955
  threadId?: number;
@@ -833,7 +965,10 @@ export type TelegramBusEnvelopeTrafficClass =
833
965
  export function getTelegramBusEnvelopeTrafficClass(
834
966
  envelope: TelegramBusEnvelope,
835
967
  ): TelegramBusEnvelopeTrafficClass {
836
- if (envelope.kind === "follower.register") return "bootstrap";
968
+ if (
969
+ envelope.kind === "follower.register" ||
970
+ envelope.kind === "follower.restoreWorkspace"
971
+ ) return "bootstrap";
837
972
  if (envelope.kind === "bus.ack") return "response";
838
973
  return "generation-fenced";
839
974
  }
@@ -879,7 +1014,8 @@ export function parseTelegramBusEnvelope(
879
1014
  let envelope: TelegramBusEnvelope | undefined;
880
1015
  switch (kind) {
881
1016
  case "follower.register":
882
- envelope = parseRegisterEnvelope(value, requestId);
1017
+ case "follower.restoreWorkspace":
1018
+ envelope = parseRegisterEnvelope(value, requestId, kind);
883
1019
  break;
884
1020
  case "follower.heartbeat":
885
1021
  envelope = parseHeartbeatEnvelope(value, requestId);
@@ -887,12 +1023,46 @@ export function parseTelegramBusEnvelope(
887
1023
  case "follower.disconnect":
888
1024
  envelope = parseDisconnectEnvelope(value, requestId);
889
1025
  break;
1026
+ case "follower.setThreadDisplayMode":
1027
+ if (typeof value.instanceId === "string" &&
1028
+ typeof value.registrationGeneration === "string" &&
1029
+ (value.mode === "letters" || value.mode === "names" || value.mode === "directories")) {
1030
+ envelope = { kind, requestId, instanceId: value.instanceId,
1031
+ registrationGeneration: value.registrationGeneration, mode: value.mode };
1032
+ }
1033
+ break;
1034
+ case "follower.renameThread":
1035
+ envelope = parseRenameThreadEnvelope(value, requestId);
1036
+ break;
1037
+ case "follower.resetThreadName": {
1038
+ const target = parseTarget(value.target);
1039
+ if (typeof value.instanceId === "string" &&
1040
+ typeof value.registrationGeneration === "string" &&
1041
+ typeof value.sentAtMs === "number" &&
1042
+ target?.threadId !== undefined) {
1043
+ envelope = {
1044
+ kind,
1045
+ requestId,
1046
+ instanceId: value.instanceId,
1047
+ registrationGeneration: value.registrationGeneration,
1048
+ target: { chatId: target.chatId, threadId: target.threadId },
1049
+ sentAtMs: value.sentAtMs,
1050
+ };
1051
+ }
1052
+ break;
1053
+ }
890
1054
  case "leader.forwardCallback":
891
1055
  envelope = parseForwardCallbackEnvelope(value, requestId);
892
1056
  break;
893
1057
  case "leader.forwardReaction":
894
1058
  envelope = parseForwardReactionEnvelope(value, requestId);
895
1059
  break;
1060
+ case "leader.offerInputCustodyHandoff":
1061
+ envelope = parseOfferInputCustodyHandoffEnvelope(value, requestId);
1062
+ break;
1063
+ case "leader.wakeInputCustody":
1064
+ envelope = parseWakeInputCustodyEnvelope(value, requestId);
1065
+ break;
896
1066
  case "leader.forwardMessage":
897
1067
  envelope = parseForwardMessageEnvelope(
898
1068
  value,
@@ -1031,6 +1201,14 @@ export interface TelegramBusForeignOwnedForwarderDeps<TMessage = unknown> {
1031
1201
  getForwardCommentBatchPosition?: (
1032
1202
  message: TMessage,
1033
1203
  ) => "comment" | "forward" | undefined;
1204
+ localProtocolIdentity?: TelegramBusProtocolIdentity;
1205
+ validateForwardOwnership?: (ownership: TelegramBusForwardOwnership) => boolean;
1206
+ resolveInputCustodyReference?: (input: {
1207
+ sourceUpdateId: number;
1208
+ recipientBindingKey: string;
1209
+ }) => { sourceRecoveryKey: string; source: {
1210
+ updateId: number; owner: { acquisitionId: string; handoffId?: string };
1211
+ } } | undefined;
1034
1212
  recordRuntimeEvent?: (
1035
1213
  category: string,
1036
1214
  error: unknown,
@@ -1038,10 +1216,21 @@ export interface TelegramBusForeignOwnedForwarderDeps<TMessage = unknown> {
1038
1216
  ) => void;
1039
1217
  }
1040
1218
 
1041
- interface TelegramBusForwardOwnership {
1219
+ export interface TelegramBusForwardOwnership {
1042
1220
  instanceId: string;
1043
1221
  ownerGeneration?: string;
1044
1222
  recipientBindingKey?: string;
1223
+ protocolIdentity?: TelegramBusProtocolIdentity;
1224
+ }
1225
+
1226
+ export function isTelegramBusForwardOwnershipCurrent(
1227
+ expected: TelegramBusForwardOwnership,
1228
+ current: TelegramBusForwardOwnership | undefined,
1229
+ ): boolean {
1230
+ return Boolean(current && current.instanceId === expected.instanceId &&
1231
+ current.ownerGeneration === expected.ownerGeneration &&
1232
+ current.recipientBindingKey === expected.recipientBindingKey &&
1233
+ JSON.stringify(current.protocolIdentity) === JSON.stringify(expected.protocolIdentity));
1045
1234
  }
1046
1235
 
1047
1236
  type TelegramBusDurableForwardEnvelope = Extract<
@@ -1051,7 +1240,8 @@ type TelegramBusDurableForwardEnvelope = Extract<
1051
1240
  | "leader.forwardCallback"
1052
1241
  | "leader.forwardReaction"
1053
1242
  | "leader.forwardMessage"
1054
- | "leader.forwardEditedMessage";
1243
+ | "leader.forwardEditedMessage"
1244
+ | "leader.wakeInputCustody";
1055
1245
  }
1056
1246
  > & {
1057
1247
  recipientRegistrationGeneration: string;
@@ -1145,13 +1335,15 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1145
1335
  return settlement;
1146
1336
  };
1147
1337
  const prepare = (
1148
- kind: TelegramBusDurableForwardEnvelope["kind"],
1338
+ kind: "leader.forwardCallback" | "leader.forwardReaction" |
1339
+ "leader.forwardMessage" | "leader.forwardEditedMessage",
1149
1340
  value: unknown,
1150
1341
  ownership: TelegramBusForwardOwnership,
1151
1342
  ):
1152
1343
  | {
1153
1344
  delivery: TelegramBusFollowerDeliveryIdentity;
1154
1345
  recipientRegistrationGeneration: string;
1346
+ sourceReference: boolean;
1155
1347
  }
1156
1348
  | { settlement: TelegramBusForeignUpdateSettlement } => {
1157
1349
  const sourceUpdateId = getTelegramBusForwardSourceUpdateId(value);
@@ -1178,11 +1370,19 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1178
1370
  }),
1179
1371
  };
1180
1372
  }
1181
- const delivery = createTelegramBusForwardDelivery(
1182
- kind,
1183
- sourceUpdateId,
1184
- ownership.recipientBindingKey,
1185
- );
1373
+ const sourceReference = canUseTelegramBusInputCustodyReference({
1374
+ local: deps.localProtocolIdentity, remote: ownership.protocolIdentity });
1375
+ const reference = sourceReference ? deps.resolveInputCustodyReference?.({
1376
+ sourceUpdateId, recipientBindingKey: ownership.recipientBindingKey }) : undefined;
1377
+ if (sourceReference && !reference) return { settlement: reject({
1378
+ status: "retryable", failureClass: "source-reference-missing",
1379
+ message: "Forwarded Telegram update has no exact custody reference.",
1380
+ envelopeKind: kind, ownership, sourceUpdateId }) };
1381
+ const delivery = reference
1382
+ ? createTelegramBusFollowerSourceReferenceDeliveryIdentity({
1383
+ kind: "leader.wakeInputCustody", recipientBindingKey: ownership.recipientBindingKey,
1384
+ sourceRecoveryKey: reference.sourceRecoveryKey, source: reference.source })
1385
+ : createTelegramBusForwardDelivery(kind, sourceUpdateId, ownership.recipientBindingKey);
1186
1386
  if (!ownership.ownerGeneration) {
1187
1387
  return {
1188
1388
  settlement: reject({
@@ -1198,12 +1398,17 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1198
1398
  return {
1199
1399
  delivery,
1200
1400
  recipientRegistrationGeneration: ownership.ownerGeneration,
1401
+ sourceReference,
1201
1402
  };
1202
1403
  };
1203
1404
  const send = async (
1204
1405
  envelope: TelegramBusDurableForwardEnvelope,
1205
1406
  ownership: TelegramBusForwardOwnership,
1206
1407
  ): Promise<TelegramBusForeignUpdateSettlement> => {
1408
+ if (deps.validateForwardOwnership && !deps.validateForwardOwnership(ownership)) return reject({
1409
+ status: "retryable", failureClass: "recipient-ownership-stale",
1410
+ message: "Telegram follower ownership changed before forwarding.",
1411
+ envelopeKind: envelope.kind, ownership, delivery: envelope.delivery });
1207
1412
  if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
1208
1413
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
1209
1414
  let response: TelegramBusEnvelope | undefined;
@@ -1295,6 +1500,10 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1295
1500
  forwardCallback: ({ query, ownership }) => {
1296
1501
  const prepared = prepare("leader.forwardCallback", query, ownership);
1297
1502
  if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1503
+ if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1504
+ requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1505
+ recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1506
+ delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1298
1507
  return send(
1299
1508
  {
1300
1509
  kind: "leader.forwardCallback",
@@ -1316,6 +1525,10 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1316
1525
  ownership,
1317
1526
  );
1318
1527
  if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1528
+ if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1529
+ requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1530
+ recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1531
+ delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1319
1532
  return send(
1320
1533
  {
1321
1534
  kind: "leader.forwardReaction",
@@ -1333,6 +1546,10 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1333
1546
  forwardMessage: ({ message, ownership }) => {
1334
1547
  const prepared = prepare("leader.forwardMessage", message, ownership);
1335
1548
  if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1549
+ if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1550
+ requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1551
+ recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1552
+ delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1336
1553
  return send(
1337
1554
  {
1338
1555
  kind: "leader.forwardMessage",
@@ -1360,6 +1577,10 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
1360
1577
  ownership,
1361
1578
  );
1362
1579
  if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1580
+ if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
1581
+ requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
1582
+ recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
1583
+ delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
1363
1584
  return send(
1364
1585
  {
1365
1586
  kind: "leader.forwardEditedMessage",
@@ -1512,7 +1733,8 @@ export function createTelegramBusLocalServer(
1512
1733
  );
1513
1734
  const getRequestLedgerKey = (envelope: TelegramBusEnvelope): string => {
1514
1735
  const identity =
1515
- envelope.kind === "follower.register"
1736
+ envelope.kind === "follower.register" ||
1737
+ envelope.kind === "follower.restoreWorkspace"
1516
1738
  ? envelope.registration.instanceId
1517
1739
  : "instanceId" in envelope
1518
1740
  ? envelope.instanceId
@@ -1823,21 +2045,32 @@ function sendTelegramBusLocalEnvelopeOnce(
1823
2045
  const socket = createConnection(options.socketPath);
1824
2046
  let settled = false;
1825
2047
  let buffer = "";
2048
+ let timeoutFinalizer: ReturnType<typeof setImmediate> | undefined;
1826
2049
  const settle = (callback: () => void) => {
1827
2050
  if (settled) return;
1828
2051
  settled = true;
1829
2052
  clearTimeout(timeout);
2053
+ if (timeoutFinalizer) clearImmediate(timeoutFinalizer);
1830
2054
  socket.destroy();
1831
2055
  callback();
1832
2056
  };
1833
2057
  const timeout = setTimeout(() => {
1834
- settle(() =>
1835
- reject(
1836
- createTelegramBusTransportTimeoutError(
1837
- "Timed out waiting for Telegram bus response",
1838
- ),
1839
- ),
1840
- );
2058
+ // A long synchronous Pi/TUI turn can resume in the timers phase after
2059
+ // the peer acknowledgement is already buffered. Give pending socket I/O
2060
+ // two poll phases before converting elapsed wall time into a transport
2061
+ // failure; Darwin can surface buffered Unix-socket input only on the
2062
+ // second cycle after a long stall, while a silent peer stays bounded.
2063
+ timeoutFinalizer = setImmediate(() => {
2064
+ timeoutFinalizer = setImmediate(() => {
2065
+ settle(() =>
2066
+ reject(
2067
+ createTelegramBusTransportTimeoutError(
2068
+ "Timed out waiting for Telegram bus response",
2069
+ ),
2070
+ ),
2071
+ );
2072
+ });
2073
+ });
1841
2074
  }, timeoutMs);
1842
2075
  socket.setEncoding("utf8");
1843
2076
  socket.once("connect", () => {
@@ -1911,6 +2144,21 @@ export interface TelegramBusFollowerRegistry {
1911
2144
  ) => TelegramBusFollowerView[];
1912
2145
  }
1913
2146
 
2147
+ export function createTelegramBusForwardOwnershipValidator(
2148
+ registry: Pick<TelegramBusFollowerRegistry, "get">,
2149
+ ): (ownership: TelegramBusForwardOwnership) => boolean {
2150
+ return ownership => {
2151
+ const follower = registry.get(ownership.instanceId);
2152
+ return isTelegramBusForwardOwnershipCurrent(ownership,
2153
+ follower?.registrationGeneration && follower.profileKey && follower.protocol ? {
2154
+ instanceId: follower.instanceId,
2155
+ ownerGeneration: follower.registrationGeneration,
2156
+ recipientBindingKey: follower.profileKey,
2157
+ protocolIdentity: follower.protocol,
2158
+ } : undefined);
2159
+ };
2160
+ }
2161
+
1914
2162
  export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry {
1915
2163
  const followers = new Map<string, TelegramBusFollowerView>();
1916
2164
  const clone = (
@@ -2047,11 +2295,10 @@ async function handleTelegramBusSocketLine(
2047
2295
  function parseRegisterEnvelope(
2048
2296
  value: Record<string, unknown>,
2049
2297
  requestId: string,
2298
+ kind: "follower.register" | "follower.restoreWorkspace",
2050
2299
  ): TelegramBusEnvelope | undefined {
2051
2300
  const registration = parseRegistration(value.registration);
2052
- return registration
2053
- ? { kind: "follower.register", requestId, registration }
2054
- : undefined;
2301
+ return registration ? { kind, requestId, registration } : undefined;
2055
2302
  }
2056
2303
 
2057
2304
  function parseHeartbeatEnvelope(
@@ -2090,6 +2337,28 @@ function parseDisconnectEnvelope(
2090
2337
  : undefined;
2091
2338
  }
2092
2339
 
2340
+ function parseRenameThreadEnvelope(
2341
+ value: Record<string, unknown>,
2342
+ requestId: string,
2343
+ ): TelegramBusEnvelope | undefined {
2344
+ const target = parseTarget(value.target);
2345
+ return typeof value.instanceId === "string" &&
2346
+ typeof value.registrationGeneration === "string" &&
2347
+ target?.threadId !== undefined &&
2348
+ typeof value.threadName === "string" &&
2349
+ typeof value.sentAtMs === "number"
2350
+ ? {
2351
+ kind: "follower.renameThread",
2352
+ requestId,
2353
+ instanceId: value.instanceId,
2354
+ registrationGeneration: value.registrationGeneration,
2355
+ target: { chatId: target.chatId, threadId: target.threadId },
2356
+ threadName: value.threadName,
2357
+ sentAtMs: value.sentAtMs,
2358
+ }
2359
+ : undefined;
2360
+ }
2361
+
2093
2362
  function parseTelegramBusFollowerDeliveryIdentity(
2094
2363
  value: unknown,
2095
2364
  ): TelegramBusFollowerDeliveryIdentity | undefined {
@@ -2100,7 +2369,16 @@ function parseTelegramBusFollowerDeliveryIdentity(
2100
2369
  !Number.isSafeInteger(value.sourceUpdateId) ||
2101
2370
  (value.sourceUpdateId as number) < 0 ||
2102
2371
  typeof value.recipientBindingKey !== "string" ||
2103
- !value.recipientBindingKey
2372
+ !value.recipientBindingKey ||
2373
+ (value.sourceRecoveryKey !== undefined &&
2374
+ (typeof value.sourceRecoveryKey !== "string" || !value.sourceRecoveryKey ||
2375
+ value.sourceRecoveryKey.length > 1_024)) ||
2376
+ (value.sourceClaim !== undefined &&
2377
+ (!isRecord(value.sourceClaim) ||
2378
+ typeof value.sourceClaim.acquisitionId !== "string" ||
2379
+ !value.sourceClaim.acquisitionId || value.sourceClaim.acquisitionId.length > 256 ||
2380
+ typeof value.sourceClaim.handoffId !== "string" ||
2381
+ !value.sourceClaim.handoffId || value.sourceClaim.handoffId.length > 256))
2104
2382
  ) {
2105
2383
  return undefined;
2106
2384
  }
@@ -2108,6 +2386,11 @@ function parseTelegramBusFollowerDeliveryIdentity(
2108
2386
  deliveryId: value.deliveryId,
2109
2387
  sourceUpdateId: value.sourceUpdateId as number,
2110
2388
  recipientBindingKey: value.recipientBindingKey,
2389
+ ...(typeof value.sourceRecoveryKey === "string"
2390
+ ? { sourceRecoveryKey: value.sourceRecoveryKey } : {}),
2391
+ ...(isRecord(value.sourceClaim)
2392
+ ? { sourceClaim: { acquisitionId: value.sourceClaim.acquisitionId as string,
2393
+ handoffId: value.sourceClaim.handoffId as string } } : {}),
2111
2394
  };
2112
2395
  }
2113
2396
 
@@ -2182,6 +2465,46 @@ function parseForwardMessageEnvelope(
2182
2465
  : undefined;
2183
2466
  }
2184
2467
 
2468
+ function parseOfferInputCustodyHandoffEnvelope(
2469
+ value: Record<string, unknown>, requestId: string,
2470
+ ): TelegramBusEnvelope | undefined {
2471
+ const source = value.source;
2472
+ if (!isRecord(source) || typeof value.recipientInstanceId !== "string" ||
2473
+ typeof value.recipientRegistrationGeneration !== "string" ||
2474
+ typeof value.recipientBindingKey !== "string" || !value.recipientBindingKey ||
2475
+ typeof value.sourceRecoveryKey !== "string" || !value.sourceRecoveryKey ||
2476
+ value.sourceRecoveryKey.length > 1_024 ||
2477
+ typeof source.journalBindingKey !== "string" || !source.journalBindingKey ||
2478
+ source.journalBindingKey !== value.sourceRecoveryKey ||
2479
+ typeof source.tokenSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
2480
+ !Number.isSafeInteger(source.updateId) || (source.updateId as number) < 0 ||
2481
+ typeof value.handoffId !== "string" || !value.handoffId || value.handoffId.length > 256 ||
2482
+ typeof value.sentAtMs !== "number") return undefined;
2483
+ return { kind: "leader.offerInputCustodyHandoff", requestId,
2484
+ recipientInstanceId: value.recipientInstanceId,
2485
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2486
+ recipientBindingKey: value.recipientBindingKey,
2487
+ sourceRecoveryKey: value.sourceRecoveryKey,
2488
+ source: { journalBindingKey: source.journalBindingKey,
2489
+ tokenSha256: source.tokenSha256, updateId: source.updateId as number },
2490
+ handoffId: value.handoffId, sentAtMs: value.sentAtMs };
2491
+ }
2492
+
2493
+ function parseWakeInputCustodyEnvelope(
2494
+ value: Record<string, unknown>, requestId: string,
2495
+ ): TelegramBusEnvelope | undefined {
2496
+ const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
2497
+ return delivery && delivery.sourceRecoveryKey && delivery.sourceClaim &&
2498
+ typeof value.recipientInstanceId === "string" &&
2499
+ typeof value.recipientRegistrationGeneration === "string" &&
2500
+ typeof value.sentAtMs === "number"
2501
+ ? { kind: "leader.wakeInputCustody", requestId,
2502
+ recipientInstanceId: value.recipientInstanceId,
2503
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2504
+ delivery, sentAtMs: value.sentAtMs }
2505
+ : undefined;
2506
+ }
2507
+
2185
2508
  function parseReplaceFollowerTargetEnvelope(
2186
2509
  value: Record<string, unknown>,
2187
2510
  requestId: string,
@@ -2578,7 +2901,8 @@ function parseAckEnvelope(
2578
2901
  code === "request-id-collision" ||
2579
2902
  code === "ledger-overloaded" ||
2580
2903
  code === "incompatible-protocol" ||
2581
- code === "stale-target"
2904
+ code === "stale-target" ||
2905
+ code === "workspace-binding-unavailable"
2582
2906
  ) {
2583
2907
  const chatId = value.error.chatId;
2584
2908
  const threadId = value.error.threadId;