@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
@@ -10,7 +10,7 @@ import { basename } from "node:path";
10
10
 
11
11
  import * as Sync from "./sync.ts";
12
12
  import * as Threads from "./threads.ts";
13
- import { parseTelegramUpdateJournalQueueOwner } from "./journal.ts";
13
+ import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
14
14
  import type { TelegramLockEntry, TelegramLockState } from "./locks.ts";
15
15
  import type {
16
16
  TelegramQueueHandoffPayload,
@@ -31,19 +31,29 @@ import {
31
31
  createUnauthorizedBusAck,
32
32
  getTelegramBusProtocolCompatibility,
33
33
  getTelegramBusSocketPath,
34
+ hasTelegramBusCapability,
34
35
  isTelegramBusEnvelopeAuthorized,
35
36
  resolveTelegramBusSocketPath,
36
37
  sendTelegramBusLocalEnvelope,
37
38
  type TelegramBusAgentMessage,
38
39
  type TelegramBusAgentTargetSelector,
39
40
  type TelegramBusEnvelope,
41
+ type TelegramBusForwardOwnership,
40
42
  type TelegramBusProtocolIdentity,
41
43
  type TelegramBusSocketPathSource,
44
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
45
+ TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
42
46
  } from "./bus.ts";
47
+ import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
43
48
  import {
44
49
  getTelegramBusTransportRetryPolicy,
45
50
  TELEGRAM_BUS_REGISTRATION_RETRY,
46
51
  } from "./bus-transport.ts";
52
+ import {
53
+ createTelegramWorkspaceAdmissionOperationId,
54
+ runWithTelegramWorkspaceAdmissionsAsync,
55
+ type TelegramWorkspaceAdmissionLedger,
56
+ } from "./workspace-admission.ts";
47
57
 
48
58
  export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
49
59
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
@@ -109,10 +119,22 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
109
119
  registerWithLeader: (
110
120
  ctx: TContext,
111
121
  leader: { busSocketPath?: string; busSecret?: string },
112
- options?: { target?: TelegramTarget; previousInstanceId?: string },
122
+ options?: {
123
+ target?: TelegramTarget;
124
+ previousInstanceId?: string;
125
+ restoreWorkspace?: boolean;
126
+ },
113
127
  ) => Promise<boolean>;
114
128
  setContext: (ctx: TContext) => void;
115
129
  disconnectFromLeader?: () => Promise<boolean>;
130
+ renameThread?: (
131
+ target: TelegramTarget & { threadId: number },
132
+ threadName: string,
133
+ ) => Promise<string>;
134
+ resetThreadName?: (
135
+ target: TelegramTarget & { threadId: number },
136
+ ) => Promise<string>;
137
+ setThreadDisplayMode?: (mode: TelegramThreadDisplayMode) => Promise<void>;
116
138
  stop: () => void;
117
139
  }
118
140
 
@@ -168,6 +190,8 @@ export interface TelegramBusFollowerRegistrationState {
168
190
  getTarget: () => TelegramTarget | undefined;
169
191
  getSlot: () => string | undefined;
170
192
  getThreadName: () => string | undefined;
193
+ getDisplayTitle: () => string | undefined;
194
+ setDisplayTitle: (title: string, generation: string) => boolean;
171
195
  getGeneration: () => string | undefined;
172
196
  beginRecovery: () => number;
173
197
  cancelRecovery: () => void;
@@ -181,6 +205,7 @@ export interface TelegramBusFollowerRegistrationState {
181
205
  metadata?: {
182
206
  slot?: string;
183
207
  threadName?: string;
208
+ displayTitle?: string;
184
209
  generation?: string;
185
210
  leaderProtocol?: TelegramBusProtocolIdentity;
186
211
  },
@@ -206,7 +231,8 @@ export interface TelegramBusFollowerDurableAdmissionPort<TContext> {
206
231
  | "leader.forwardCallback"
207
232
  | "leader.forwardReaction"
208
233
  | "leader.forwardMessage"
209
- | "leader.forwardEditedMessage";
234
+ | "leader.forwardEditedMessage"
235
+ | "leader.wakeInputCustody";
210
236
  }
211
237
  >,
212
238
  ctx: TContext,
@@ -225,6 +251,7 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
225
251
  getForwardCommentBatchPosition?: (
226
252
  message: TMessage,
227
253
  ) => "comment" | "forward" | undefined;
254
+ validateForwardOwnership?: (ownership: TelegramBusForwardOwnership) => boolean;
228
255
  recordRuntimeEvent?: (
229
256
  category: string,
230
257
  error: unknown,
@@ -279,6 +306,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
279
306
  ) => void;
280
307
  onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
281
308
  onRegistered?: (ctx: TContext) => Promise<void> | void;
309
+ onDisplayTitleChanged?: (ctx: TContext) => void;
282
310
  }
283
311
 
284
312
  export function createTelegramManualFollowerProfileKeyResolver(input: {
@@ -303,6 +331,43 @@ export type TelegramBusFollowerPromotionHandler<TContext> = (
303
331
  election: TelegramBusFollowerElection,
304
332
  ) => Promise<boolean>;
305
333
 
334
+ type TelegramBusFollowerWorkspaceAdmissionDeps = {
335
+ getWorkspaceAdmission?: () => Pick<
336
+ TelegramWorkspaceAdmissionLedger,
337
+ "acquireAdmission" | "releaseAdmission"
338
+ > | undefined;
339
+ recordRuntimeEvent?: (
340
+ category: string,
341
+ error: unknown,
342
+ details?: Record<string, unknown>,
343
+ ) => void;
344
+ };
345
+
346
+ function runTelegramBusFollowerWorkspaceMutation<T>(
347
+ deps: TelegramBusFollowerWorkspaceAdmissionDeps,
348
+ operationKind: string,
349
+ operation: () => Promise<T>,
350
+ ): Promise<T> {
351
+ if (!deps.getWorkspaceAdmission) return operation();
352
+ const admission = deps.getWorkspaceAdmission();
353
+ if (!admission) {
354
+ throw new Error("Telegram Workspace admission authority is unavailable.");
355
+ }
356
+ return runWithTelegramWorkspaceAdmissionsAsync({
357
+ ledger: admission,
358
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
359
+ operationKind,
360
+ scopes: [{ kind: "profile" }],
361
+ operation,
362
+ onReleaseError(error) {
363
+ deps.recordRuntimeEvent?.("bus", error, {
364
+ phase: "workspace-admission-release",
365
+ operationKind,
366
+ });
367
+ },
368
+ });
369
+ }
370
+
306
371
  export function createTelegramBusFollowerPromotionHandler<
307
372
  TContext extends { cwd: string },
308
373
  >(input: {
@@ -321,66 +386,82 @@ export function createTelegramBusFollowerPromotionHandler<
321
386
  ) => void;
322
387
  getNowMs?: () => number;
323
388
  getPid?: () => number;
389
+ getWorkspaceAdmission?:
390
+ TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
324
391
  }): TelegramBusFollowerPromotionHandler<TContext> {
325
- return async (ctx, binding, election) => {
326
- const promoted = await input.startLeader(ctx, election, async () => {
327
- const promotedRecord =
328
- await Threads.promoteTelegramFollowerBindingToLeader({
329
- store: input.topicTargetStore,
330
- instanceId: input.instanceId,
392
+ return (ctx, binding, election) => runTelegramBusFollowerWorkspaceMutation(
393
+ input,
394
+ "workspace.promote-follower",
395
+ async () => {
396
+ let promotedRecord: Threads.TelegramTopicTargetRecord | undefined;
397
+ const promoted = await input.startLeader(ctx, election, async () => {
398
+ promotedRecord =
399
+ await Threads.promoteTelegramFollowerBindingToLeader({
400
+ store: input.topicTargetStore,
401
+ instanceId: input.instanceId,
402
+ cwd: ctx.cwd,
403
+ telegramProfile: input.getActiveProfileName(),
404
+ target: binding.target,
405
+ slot: binding.slot,
406
+ threadName: binding.threadName,
407
+ });
408
+ if (!promotedRecord && typeof binding.target?.threadId === "number") {
409
+ throw new Error(
410
+ "Telegram follower promotion slot authority is unavailable.",
411
+ );
412
+ }
413
+ if (promotedRecord) {
414
+ input.recordRuntimeEvent(
415
+ "bus",
416
+ "Follower thread binding promoted to leader",
417
+ {
418
+ phase: "follower-promoted-binding",
419
+ chatId: promotedRecord.target.chatId,
420
+ threadId: promotedRecord.target.threadId,
421
+ slot: promotedRecord.slot,
422
+ threadName: promotedRecord.threadName,
423
+ },
424
+ );
425
+ }
426
+ });
427
+ if (
428
+ promoted &&
429
+ promotedRecord &&
430
+ typeof binding.target?.threadId === "number"
431
+ ) {
432
+ const profileKey = Threads.getTelegramThreadOwnerKey({
433
+ kind: "leader",
331
434
  cwd: ctx.cwd,
435
+ instanceId: input.instanceId,
332
436
  telegramProfile: input.getActiveProfileName(),
333
- target: binding.target,
334
- slot: binding.slot,
335
- threadName: binding.threadName,
336
437
  });
337
- if (promotedRecord) {
438
+ Threads.setTelegramLeaderSessionHandoff({
439
+ pid: input.getPid?.() ?? process.pid,
440
+ instanceId: input.instanceId,
441
+ createdAtMs: input.getNowMs?.() ?? Date.now(),
442
+ profileKey,
443
+ target: {
444
+ chatId: binding.target.chatId,
445
+ threadId: binding.target.threadId,
446
+ },
447
+ slot: promotedRecord.slot,
448
+ threadName: promotedRecord.threadName,
449
+ });
338
450
  input.recordRuntimeEvent(
339
451
  "bus",
340
- "Follower thread binding promoted to leader",
452
+ "Promoted leader binding retained for session replacement",
341
453
  {
342
- phase: "follower-promoted-binding",
343
- chatId: promotedRecord.target.chatId,
344
- threadId: promotedRecord.target.threadId,
454
+ phase: "follower-promoted-session-handoff",
455
+ chatId: binding.target.chatId,
456
+ threadId: binding.target.threadId,
345
457
  slot: promotedRecord.slot,
346
458
  threadName: promotedRecord.threadName,
347
459
  },
348
460
  );
349
461
  }
350
- });
351
- if (promoted && typeof binding.target?.threadId === "number") {
352
- const profileKey = Threads.getTelegramThreadOwnerKey({
353
- kind: "leader",
354
- cwd: ctx.cwd,
355
- instanceId: input.instanceId,
356
- telegramProfile: input.getActiveProfileName(),
357
- });
358
- Threads.setTelegramLeaderSessionHandoff({
359
- pid: input.getPid?.() ?? process.pid,
360
- instanceId: input.instanceId,
361
- createdAtMs: input.getNowMs?.() ?? Date.now(),
362
- profileKey,
363
- target: {
364
- chatId: binding.target.chatId,
365
- threadId: binding.target.threadId,
366
- },
367
- slot: binding.slot,
368
- threadName: binding.threadName,
369
- });
370
- input.recordRuntimeEvent(
371
- "bus",
372
- "Promoted leader binding retained for session replacement",
373
- {
374
- phase: "follower-promoted-session-handoff",
375
- chatId: binding.target.chatId,
376
- threadId: binding.target.threadId,
377
- slot: binding.slot,
378
- threadName: binding.threadName,
379
- },
380
- );
381
- }
382
- return promoted;
383
- };
462
+ return promoted;
463
+ },
464
+ );
384
465
  }
385
466
 
386
467
  export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
@@ -390,7 +471,7 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
390
471
  >;
391
472
  registrationState: Pick<
392
473
  TelegramBusFollowerRegistrationState,
393
- "getTarget" | "setRegistered" | "getGeneration"
474
+ "getTarget" | "getSlot" | "setRegistered" | "getGeneration"
394
475
  >;
395
476
  instanceId: string;
396
477
  getManualFollowerProfileKey: () => string;
@@ -404,6 +485,7 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
404
485
  error: unknown,
405
486
  details?: Record<string, unknown>,
406
487
  ) => void;
488
+ getWorkspaceAdmission?: TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
407
489
  }
408
490
 
409
491
  export type TelegramBusFollowerLeaderState =
@@ -457,7 +539,14 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
457
539
  getRegistrationGeneration: () => string | undefined;
458
540
  getRecipientBindingKey: () => string | undefined;
459
541
  durableAdmission: TelegramBusFollowerDurableAdmissionPort<TContext>;
542
+ sourceReferenceAdmission?: TelegramBusFollowerDurableAdmissionPort<TContext>;
543
+ isSourceReferenceAdmissionEnabled?: () => boolean;
544
+ hasAuthenticatedSourceReferenceTransport?: () => boolean;
460
545
  getContext: () => TContext | undefined;
546
+ handleInputCustodyHandoff?: (
547
+ envelope: Extract<TelegramBusEnvelope, { kind: "leader.offerInputCustodyHandoff" }>,
548
+ ctx: TContext,
549
+ ) => Promise<unknown> | unknown;
461
550
  handleQueueHandoff?: (
462
551
  envelope: Extract<
463
552
  TelegramBusEnvelope,
@@ -568,83 +657,106 @@ export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
568
657
  TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>["handleReplaceTarget"]
569
658
  > {
570
659
  const getNowMs = deps.getNowMs ?? Date.now;
571
- return async (input, ctx) => {
572
- const assertCurrent = (expectedTarget = input.oldTarget): void => {
573
- const target = deps.registrationState.getTarget();
574
- if (!input.registrationGeneration ||
575
- deps.registrationState.getGeneration() !== input.registrationGeneration ||
576
- !input.oldTarget || !expectedTarget || target?.chatId !== expectedTarget.chatId ||
577
- target.threadId !== expectedTarget.threadId ||
578
- input.target.chatId !== input.oldTarget.chatId ||
579
- input.target.threadId === input.oldTarget.threadId) {
580
- throw new Error("Stale Telegram follower target replacement authority.");
660
+ return (input, ctx) => runTelegramBusFollowerWorkspaceMutation(
661
+ deps,
662
+ "workspace.replace-follower-target",
663
+ async () => {
664
+ const assertCurrent = (expectedTarget = input.oldTarget): void => {
665
+ const target = deps.registrationState.getTarget();
666
+ if (
667
+ !input.registrationGeneration ||
668
+ deps.registrationState.getGeneration() !==
669
+ input.registrationGeneration ||
670
+ !input.oldTarget ||
671
+ !expectedTarget ||
672
+ target?.chatId !== expectedTarget.chatId ||
673
+ target.threadId !== expectedTarget.threadId ||
674
+ input.target.chatId !== input.oldTarget.chatId ||
675
+ input.target.threadId === input.oldTarget.threadId
676
+ ) {
677
+ throw new Error(
678
+ "Stale Telegram follower target replacement authority.",
679
+ );
680
+ }
681
+ };
682
+ assertCurrent();
683
+ await deps.topicTargetStore.load();
684
+ assertCurrent();
685
+ const nowMs = getNowMs();
686
+ const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
687
+ records: deps.topicTargetStore.list(),
688
+ instanceId: deps.instanceId,
689
+ preferredTarget: input.oldTarget,
690
+ });
691
+ if (
692
+ currentRecord &&
693
+ (currentRecord.target.chatId !== input.oldTarget!.chatId ||
694
+ currentRecord.target.threadId !== input.oldTarget!.threadId)
695
+ ) {
696
+ throw new Error("Stale Telegram follower binding replacement target.");
581
697
  }
582
- };
583
- assertCurrent();
584
- await deps.topicTargetStore.load();
585
- assertCurrent();
586
- const nowMs = getNowMs();
587
- const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
588
- records: deps.topicTargetStore.list(),
589
- instanceId: deps.instanceId,
590
- preferredTarget: input.oldTarget,
591
- });
592
- if (currentRecord && (currentRecord.target.chatId !== input.oldTarget!.chatId ||
593
- currentRecord.target.threadId !== input.oldTarget!.threadId)) {
594
- throw new Error("Stale Telegram follower binding replacement target.");
595
- }
596
- deps.topicTargetStore.markStaleByTarget(
597
- input.oldTarget!,
598
- "deleted",
599
- "Follower thread was replaced by thread restore.",
600
- );
601
- const profileKey =
602
- currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
603
- deps.topicTargetStore.upsert({
604
- profileKey,
605
- owner: {
606
- kind: "manual-follower",
607
- instanceId: deps.manualFollowerOwnerId,
608
- },
609
- target: input.target,
610
- status: "active",
611
- syncStatus: "open",
612
- createdAtMs: currentRecord?.createdAtMs ?? nowMs,
613
- updatedAtMs: nowMs,
614
- lastSyncObservedAtMs: nowMs,
615
- lastReconcileAction: "follower-thread-restore",
616
- instanceId: deps.instanceId,
617
- slot: currentRecord?.slot,
618
- threadName: currentRecord?.threadName,
619
- rerouteConfirmedAtMs: nowMs,
620
- });
621
- deps.registrationState.setRegistered(true, input.target, {
622
- slot: currentRecord?.slot,
623
- threadName: currentRecord?.threadName,
624
- generation: input.registrationGeneration,
625
- });
626
- await deps.topicTargetStore.persist();
627
- assertCurrent(input.target);
628
- deps.setSyncState(
629
- Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
630
- nowMs,
631
- action: "follower-thread-restore",
632
- }),
633
- );
634
- deps.updateStatus(ctx);
635
- deps.recordRuntimeEvent?.(
636
- "bus",
637
- "Telegram follower thread target replaced",
638
- {
639
- phase: "follower-thread-restore",
640
- chatId: input.target.chatId,
641
- threadId: input.target.threadId,
642
- oldThreadId:
643
- input.oldTarget?.threadId ?? currentRecord?.target.threadId,
644
- slot: currentRecord?.slot,
645
- },
646
- );
647
- };
698
+ const currentSlot =
699
+ currentRecord?.slot ?? deps.registrationState.getSlot();
700
+ if (!currentSlot || !/^[A-Z]$/u.test(currentSlot)) {
701
+ throw new Error("Telegram Thread slot authority is unavailable.");
702
+ }
703
+ deps.topicTargetStore.markStaleByTarget(
704
+ input.oldTarget!,
705
+ "deleted",
706
+ "Follower thread was replaced by thread restore.",
707
+ );
708
+ const profileKey =
709
+ currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
710
+ deps.topicTargetStore.upsert({
711
+ profileKey,
712
+ owner: {
713
+ kind: "manual-follower",
714
+ instanceId: deps.manualFollowerOwnerId,
715
+ },
716
+ target: input.target,
717
+ status: "active",
718
+ syncStatus: "open",
719
+ createdAtMs: currentRecord?.createdAtMs ?? nowMs,
720
+ updatedAtMs: nowMs,
721
+ lastSyncObservedAtMs: nowMs,
722
+ lastReconcileAction: "follower-thread-restore",
723
+ instanceId: deps.instanceId,
724
+ slot: currentSlot,
725
+ threadName: currentRecord?.threadName,
726
+ rerouteConfirmedAtMs: nowMs,
727
+ });
728
+ deps.registrationState.setRegistered(true, input.target, {
729
+ slot: currentSlot,
730
+ threadName: currentRecord?.threadName,
731
+ generation: input.registrationGeneration,
732
+ });
733
+ await deps.topicTargetStore.persist();
734
+ assertCurrent(input.target);
735
+ deps.setSyncState(
736
+ Sync.markTelegramSyncSliceFresh(
737
+ deps.getSyncState(),
738
+ "target-bindings",
739
+ {
740
+ nowMs,
741
+ action: "follower-thread-restore",
742
+ },
743
+ ),
744
+ );
745
+ deps.updateStatus(ctx);
746
+ deps.recordRuntimeEvent?.(
747
+ "bus",
748
+ "Telegram follower thread target replaced",
749
+ {
750
+ phase: "follower-thread-restore",
751
+ chatId: input.target.chatId,
752
+ threadId: input.target.threadId,
753
+ oldThreadId:
754
+ input.oldTarget?.threadId ?? currentRecord?.target.threadId,
755
+ slot: currentSlot,
756
+ },
757
+ );
758
+ },
759
+ );
648
760
  }
649
761
 
650
762
  export function createTelegramBusFollowerClientRuntime<
@@ -686,6 +798,7 @@ export function createTelegramBusFollowerClientRuntime<
686
798
  getAuthSecret: deps.getForwardingAuthSecret,
687
799
  getForwardCommentBatchPosition:
688
800
  deps.getForwardCommentBatchPosition,
801
+ validateForwardOwnership: deps.validateForwardOwnership,
689
802
  recordRuntimeEvent: deps.recordRuntimeEvent,
690
803
  }),
691
804
  queueHandoff: createTelegramBusFollowerQueueHandoffClient({
@@ -1109,6 +1222,7 @@ export function createTelegramBusFollowerRegistrationState(
1109
1222
  let target: TelegramTarget | undefined;
1110
1223
  let slot: string | undefined;
1111
1224
  let threadName: string | undefined;
1225
+ let displayTitle: string | undefined;
1112
1226
  let generation: string | undefined;
1113
1227
  let leaderProtocol: TelegramBusProtocolIdentity | undefined;
1114
1228
  let eligibleElectionSlots: string[] = [];
@@ -1131,6 +1245,13 @@ export function createTelegramBusFollowerRegistrationState(
1131
1245
  getTarget: () => (target ? { ...target } : undefined),
1132
1246
  getSlot: () => slot,
1133
1247
  getThreadName: () => threadName,
1248
+ getDisplayTitle: () => displayTitle,
1249
+ setDisplayTitle(title, expectedGeneration) {
1250
+ if (!registered || !generation || expectedGeneration !== generation ||
1251
+ !title.trim() || title.length > 128 || displayTitle === title) return false;
1252
+ displayTitle = title;
1253
+ return true;
1254
+ },
1134
1255
  getGeneration: () => generation,
1135
1256
  beginRecovery: () => {
1136
1257
  if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
@@ -1169,11 +1290,18 @@ export function createTelegramBusFollowerRegistrationState(
1169
1290
  ).sort();
1170
1291
  },
1171
1292
  setRegistered: (next, nextTarget, metadata) => {
1293
+ const retainedDisplayTitle = next && registered &&
1294
+ generation === metadata?.generation &&
1295
+ target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
1296
+ ? displayTitle : undefined;
1172
1297
  const availabilityChanged = registered !== next;
1173
1298
  registered = next;
1174
1299
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
1175
1300
  slot = next ? metadata?.slot : undefined;
1176
1301
  threadName = next ? metadata?.threadName : undefined;
1302
+ displayTitle = next && nextTarget && metadata?.generation &&
1303
+ metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
1304
+ ? metadata.displayTitle : retainedDisplayTitle;
1177
1305
  generation = next ? metadata?.generation : undefined;
1178
1306
  leaderProtocol =
1179
1307
  next && metadata?.leaderProtocol
@@ -1530,6 +1658,15 @@ export function createTelegramBusFollowerRegistrationRuntime<
1530
1658
  )
1531
1659
  : [];
1532
1660
  deps.registrationState?.setEligibleElectionSlots(slots);
1661
+ if (typeof heartbeatResult?.displayTitle === "string" &&
1662
+ deps.registrationState?.setDisplayTitle(heartbeatResult.displayTitle, registrationGeneration) &&
1663
+ heartbeatContext) {
1664
+ try {
1665
+ deps.onDisplayTitleChanged?.(heartbeatContext);
1666
+ } catch {
1667
+ // Display refresh failure must not invalidate a successful heartbeat.
1668
+ }
1669
+ }
1533
1670
  }
1534
1671
  if (response?.kind === "bus.ack" && !response.ok) {
1535
1672
  throw new Error(
@@ -1596,7 +1733,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
1596
1733
  previousInstanceId: pendingHandoff.instanceId,
1597
1734
  }
1598
1735
  : undefined;
1599
- const registrationOptions = options ?? pendingHandoffOptions;
1736
+ const registrationOptions: {
1737
+ target?: TelegramTarget;
1738
+ previousInstanceId?: string;
1739
+ restoreWorkspace?: boolean;
1740
+ } | undefined = options ?? pendingHandoffOptions;
1600
1741
  const leaderSocketPath =
1601
1742
  leader.busSocketPath ??
1602
1743
  deps.getLeaderSocketPath?.() ??
@@ -1607,11 +1748,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
1607
1748
  : leader?.busSecret;
1608
1749
  deps.setActiveAuthSecret?.(activeAuthSecret);
1609
1750
  const registrationGeneration = deps.createRequestId();
1610
- const registrationEnvelope: Extract<
1611
- TelegramBusEnvelope,
1612
- { kind: "follower.register" }
1613
- > = {
1614
- kind: "follower.register",
1751
+ const registrationEnvelope: TelegramBusEnvelope = {
1752
+ kind: registrationOptions?.restoreWorkspace
1753
+ ? "follower.restoreWorkspace"
1754
+ : "follower.register",
1615
1755
  requestId: registrationGeneration,
1616
1756
  auth: activeAuthSecret,
1617
1757
  registration: {
@@ -1705,6 +1845,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
1705
1845
  deps.registrationState?.setRegistered(false);
1706
1846
  deps.setActiveAuthSecret?.(undefined);
1707
1847
  await deps.stopReceiving?.();
1848
+ if (
1849
+ registrationOptions?.restoreWorkspace &&
1850
+ response.error?.code === "workspace-binding-unavailable"
1851
+ ) return false;
1708
1852
  throw new Error(
1709
1853
  response.message ??
1710
1854
  "Telegram bus follower registration was rejected.",
@@ -1759,6 +1903,168 @@ export function createTelegramBusFollowerRegistrationRuntime<
1759
1903
  setContext(ctx) {
1760
1904
  activeContext = ctx;
1761
1905
  },
1906
+ async setThreadDisplayMode(mode) {
1907
+ const socketPath = activeLeaderSocketPath;
1908
+ const generation = activeRegistrationGeneration;
1909
+ const auth = activeAuthSecret;
1910
+ if (!socketPath || !generation || !deps.registrationState?.isRegistered()) {
1911
+ throw new Error("Telegram follower is not registered with the leader.");
1912
+ }
1913
+ if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) ||
1914
+ !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)) {
1915
+ throw new Error("The Telegram peers do not support Thread display settings. Update or restart both instances.");
1916
+ }
1917
+ const requestId = deps.createRequestId();
1918
+ const response = await sendTelegramBusLocalEnvelope({
1919
+ socketPath, timeoutMs: registrationTimeoutMs,
1920
+ envelope: {
1921
+ kind: "follower.setThreadDisplayMode", requestId, auth,
1922
+ instanceId: deps.instanceId, registrationGeneration: generation, mode,
1923
+ },
1924
+ });
1925
+ if (activeLeaderSocketPath !== socketPath || activeRegistrationGeneration !== generation ||
1926
+ activeAuthSecret !== auth || deps.registrationState.getGeneration() !== generation) {
1927
+ throw new Error("Telegram Thread display setting completed for a stale registration.");
1928
+ }
1929
+ if (response?.kind !== "bus.ack" || !response.ok || response.requestId !== requestId ||
1930
+ !isRecord(response.result) || response.result.mode !== mode) {
1931
+ throw new Error(response?.kind === "bus.ack"
1932
+ ? response.message ?? "Telegram Thread display setting was rejected."
1933
+ : "Telegram Thread display setting was not acknowledged.");
1934
+ }
1935
+ },
1936
+ async renameThread(target, threadName) {
1937
+ if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
1938
+ throw new Error("Telegram follower is not registered with the leader.");
1939
+ }
1940
+ if (
1941
+ deps.registrationState &&
1942
+ !hasTelegramBusCapability(
1943
+ deps.registrationState.getLeaderProtocol(),
1944
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
1945
+ )
1946
+ ) {
1947
+ throw new Error(
1948
+ "The active Telegram leader does not support Workspace Thread rename. Update or restart that Pi instance.",
1949
+ );
1950
+ }
1951
+ const expectedLeaderSocketPath = activeLeaderSocketPath;
1952
+ const expectedRegistrationGeneration = activeRegistrationGeneration;
1953
+ const expectedAuthSecret = activeAuthSecret;
1954
+ const response = await sendTelegramBusLocalEnvelope({
1955
+ socketPath: expectedLeaderSocketPath,
1956
+ timeoutMs: registrationTimeoutMs,
1957
+ retry: getTelegramBusTransportRetryPolicy({
1958
+ endpoint: expectedLeaderSocketPath,
1959
+ operation: "operation",
1960
+ }),
1961
+ envelope: {
1962
+ kind: "follower.renameThread",
1963
+ requestId: deps.createRequestId(),
1964
+ auth: expectedAuthSecret,
1965
+ instanceId: deps.instanceId,
1966
+ registrationGeneration: expectedRegistrationGeneration,
1967
+ target,
1968
+ threadName,
1969
+ sentAtMs: getNowMs(),
1970
+ },
1971
+ });
1972
+ if (response?.kind !== "bus.ack" || !response.ok) {
1973
+ throw new Error(
1974
+ response?.kind === "bus.ack"
1975
+ ? (response.message ?? "Telegram Workspace Thread rename was rejected.")
1976
+ : "Telegram Workspace Thread rename was not acknowledged.",
1977
+ );
1978
+ }
1979
+ if (
1980
+ activeLeaderSocketPath !== expectedLeaderSocketPath ||
1981
+ activeRegistrationGeneration !== expectedRegistrationGeneration ||
1982
+ (deps.registrationState &&
1983
+ deps.registrationState.getGeneration() !==
1984
+ expectedRegistrationGeneration)
1985
+ ) {
1986
+ throw new Error(
1987
+ "Telegram Workspace Thread rename completed for a stale follower registration.",
1988
+ );
1989
+ }
1990
+ const renamedThreadName =
1991
+ response.result &&
1992
+ typeof response.result === "object" &&
1993
+ "threadName" in response.result &&
1994
+ typeof response.result.threadName === "string"
1995
+ ? response.result.threadName
1996
+ : threadName;
1997
+ const registrationTarget = deps.registrationState?.getTarget();
1998
+ if (deps.registrationState && registrationTarget) {
1999
+ deps.registrationState.setRegistered(true, registrationTarget, {
2000
+ slot: deps.registrationState.getSlot(),
2001
+ threadName: renamedThreadName,
2002
+ generation: expectedRegistrationGeneration,
2003
+ leaderProtocol: deps.registrationState.getLeaderProtocol(),
2004
+ });
2005
+ }
2006
+ return renamedThreadName;
2007
+ },
2008
+ async resetThreadName(target) {
2009
+ if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
2010
+ throw new Error("Telegram follower is not registered with the leader.");
2011
+ }
2012
+ if (deps.registrationState && !hasTelegramBusCapability(
2013
+ deps.registrationState.getLeaderProtocol(),
2014
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2015
+ )) {
2016
+ throw new Error(
2017
+ "The active Telegram leader does not support Workspace Thread reset. Update or restart that Pi instance.",
2018
+ );
2019
+ }
2020
+ const expectedLeaderSocketPath = activeLeaderSocketPath;
2021
+ const expectedRegistrationGeneration = activeRegistrationGeneration;
2022
+ const response = await sendTelegramBusLocalEnvelope({
2023
+ socketPath: expectedLeaderSocketPath,
2024
+ timeoutMs: registrationTimeoutMs,
2025
+ retry: getTelegramBusTransportRetryPolicy({
2026
+ endpoint: expectedLeaderSocketPath,
2027
+ operation: "operation",
2028
+ }),
2029
+ envelope: {
2030
+ kind: "follower.resetThreadName",
2031
+ requestId: deps.createRequestId(),
2032
+ auth: activeAuthSecret,
2033
+ instanceId: deps.instanceId,
2034
+ registrationGeneration: expectedRegistrationGeneration,
2035
+ target,
2036
+ sentAtMs: getNowMs(),
2037
+ },
2038
+ });
2039
+ const resetName = response?.kind === "bus.ack" && response.ok &&
2040
+ response.result && typeof response.result === "object" &&
2041
+ "threadName" in response.result &&
2042
+ typeof response.result.threadName === "string"
2043
+ ? response.result.threadName : undefined;
2044
+ if (!resetName) {
2045
+ throw new Error(response?.kind === "bus.ack"
2046
+ ? response.message ?? "Telegram Workspace Thread reset was rejected."
2047
+ : "Telegram Workspace Thread reset was not acknowledged.");
2048
+ }
2049
+ if (activeLeaderSocketPath !== expectedLeaderSocketPath ||
2050
+ activeRegistrationGeneration !== expectedRegistrationGeneration ||
2051
+ (deps.registrationState && deps.registrationState.getGeneration() !==
2052
+ expectedRegistrationGeneration)) {
2053
+ throw new Error(
2054
+ "Telegram Workspace Thread reset completed for a stale follower registration.",
2055
+ );
2056
+ }
2057
+ const registrationTarget = deps.registrationState?.getTarget();
2058
+ if (deps.registrationState && registrationTarget) {
2059
+ deps.registrationState.setRegistered(true, registrationTarget, {
2060
+ slot: deps.registrationState.getSlot(),
2061
+ threadName: resetName,
2062
+ generation: expectedRegistrationGeneration,
2063
+ leaderProtocol: deps.registrationState.getLeaderProtocol(),
2064
+ });
2065
+ }
2066
+ return resetName;
2067
+ },
1762
2068
  async disconnectFromLeader() {
1763
2069
  if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
1764
2070
  return false;
@@ -1793,6 +2099,38 @@ export function createTelegramBusFollowerRegistrationRuntime<
1793
2099
  const TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD =
1794
2100
  "pi_telegram_forward_comment_batch_position";
1795
2101
 
2102
+ /** Bind source-owned sender checks to the journal's already-admitted synchronous v1 hook. */
2103
+ export function createTelegramBusFollowerPairedAdmission(deps: {
2104
+ profileName: string;
2105
+ tokenSha256: string;
2106
+ configStore: Pick<TelegramConfigStore, "withPairedUserAdmission">;
2107
+ assertExecutionCurrent: () => void;
2108
+ }): NonNullable<TelegramUpdateJournalStoreOptions["withPairedAdmission"]> {
2109
+ return (updates, publish) => {
2110
+ if (updates.length !== 1) return { admitted: false };
2111
+ const update = updates[0]!;
2112
+ const keys = Object.keys(update).filter((key) =>
2113
+ key !== "update_id" && key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD);
2114
+ if (keys.length !== 1) return { admitted: false };
2115
+ const kind = keys[0];
2116
+ if (kind !== "message" && kind !== "edited_message" &&
2117
+ kind !== "callback_query" && kind !== "message_reaction") return { admitted: false };
2118
+ const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
2119
+ if (position !== undefined && (kind !== "message" ||
2120
+ (position !== "comment" && position !== "forward"))) return { admitted: false };
2121
+ const carrier = update[kind];
2122
+ if (!Number.isSafeInteger(update.update_id) || update.update_id < 0 ||
2123
+ !isRecord(carrier) || carrier.pi_telegram_source_update_id !== update.update_id ||
2124
+ carrier.sender_chat !== undefined || carrier.actor_chat !== undefined) return { admitted: false };
2125
+ const sender = kind === "message_reaction" ? carrier.user : carrier.from;
2126
+ if (!isRecord(sender) || typeof sender.id !== "number" || !Number.isSafeInteger(sender.id) ||
2127
+ sender.id <= 0 || sender.is_bot !== false) return { admitted: false };
2128
+ return deps.configStore.withPairedUserAdmission(
2129
+ deps.profileName, deps.tokenSha256, sender.id, publish, deps.assertExecutionCurrent,
2130
+ );
2131
+ };
2132
+ }
2133
+
1796
2134
  export function prepareTelegramBusFollowerJournaledUpdateForExecution<
1797
2135
  TUpdate extends { message?: unknown } & Record<string, unknown>,
1798
2136
  >(
@@ -1832,6 +2170,9 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
1832
2170
  if (!delivery) {
1833
2171
  throw new Error("Telegram follower durable admission requires delivery identity.");
1834
2172
  }
2173
+ if (envelope.kind === "leader.wakeInputCustody") throw new Error(
2174
+ "Telegram follower custody wake cannot use executable-copy admission.",
2175
+ );
1835
2176
  const expected = createTelegramBusFollowerDeliveryIdentity({
1836
2177
  kind: envelope.kind,
1837
2178
  recipientBindingKey: delivery.recipientBindingKey,
@@ -1879,6 +2220,94 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
1879
2220
  };
1880
2221
  }
1881
2222
 
2223
+ export interface TelegramBusFollowerInputCustodyBundle<TContext> {
2224
+ acceptHandoff(input: {
2225
+ sourceRecoveryKey: string;
2226
+ recipientBindingKey: string;
2227
+ source: { journalBindingKey: string; tokenSha256: string; updateId: number };
2228
+ handoffId: string;
2229
+ }, ctx: TContext): unknown;
2230
+ wakeSource(input: TelegramBusFollowerDurableAdmissionResult & {
2231
+ recipientBindingKey: string;
2232
+ sourceRecoveryKey: string;
2233
+ sourceClaim: { acquisitionId: string; handoffId: string };
2234
+ }, ctx: TContext): void;
2235
+ resolveForwardReference(input: {
2236
+ sourceUpdateId: number; recipientBindingKey: string;
2237
+ }): { sourceRecoveryKey: string; source: { updateId: number; owner: {
2238
+ acquisitionId: string; handoffId: string } } } | undefined;
2239
+ }
2240
+
2241
+ export function createTelegramBusFollowerInputCustodyPorts<TContext>(deps: {
2242
+ getInputCustodyBus(): TelegramBusFollowerInputCustodyBundle<TContext> | undefined;
2243
+ }) {
2244
+ const getRequired = (): TelegramBusFollowerInputCustodyBundle<TContext> => {
2245
+ const bundle = deps.getInputCustodyBus();
2246
+ if (!bundle) throw new Error("Telegram input custody bus binding is unavailable.");
2247
+ return bundle;
2248
+ };
2249
+ return {
2250
+ isSourceReferenceAdmissionEnabled: () => Boolean(deps.getInputCustodyBus()),
2251
+ handleInputCustodyHandoff(
2252
+ envelope: Extract<TelegramBusEnvelope, { kind: "leader.offerInputCustodyHandoff" }>,
2253
+ ctx: TContext,
2254
+ ) {
2255
+ return getRequired().acceptHandoff({ sourceRecoveryKey: envelope.sourceRecoveryKey,
2256
+ recipientBindingKey: envelope.recipientBindingKey,
2257
+ source: envelope.source, handoffId: envelope.handoffId }, ctx);
2258
+ },
2259
+ sourceReferenceAdmission: createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>({
2260
+ wakeSource(input, ctx) { getRequired().wakeSource(input, ctx); },
2261
+ }),
2262
+ resolveInputCustodyReference(input: {
2263
+ sourceUpdateId: number; recipientBindingKey: string;
2264
+ }) {
2265
+ return deps.getInputCustodyBus()?.resolveForwardReference(input);
2266
+ },
2267
+ };
2268
+ }
2269
+
2270
+ export function createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>(deps: {
2271
+ wakeSource: (input: TelegramBusFollowerDurableAdmissionResult & {
2272
+ recipientBindingKey: string;
2273
+ sourceRecoveryKey: string;
2274
+ sourceClaim: { acquisitionId: string; handoffId: string };
2275
+ }, ctx: TContext) => Promise<void> | void;
2276
+ }): TelegramBusFollowerDurableAdmissionPort<TContext> {
2277
+ return {
2278
+ async admit(envelope, ctx) {
2279
+ const delivery = envelope.delivery;
2280
+ if (!delivery) throw new Error(
2281
+ "Telegram follower source-reference admission requires delivery identity.",
2282
+ );
2283
+ if (!delivery.sourceRecoveryKey) throw new Error(
2284
+ "Telegram follower source-reference admission requires a recovery key.",
2285
+ );
2286
+ if (!delivery.sourceClaim) throw new Error(
2287
+ "Telegram follower source-reference admission requires exact claim evidence.",
2288
+ );
2289
+ const expected = createTelegramBusFollowerDeliveryIdentity({ kind: envelope.kind,
2290
+ recipientBindingKey: delivery.recipientBindingKey,
2291
+ sourceUpdateId: delivery.sourceUpdateId });
2292
+ if (expected.deliveryId !== delivery.deliveryId) throw new Error(
2293
+ "Invalid Telegram follower source-reference delivery id.",
2294
+ );
2295
+ const carrier = envelope.kind === "leader.wakeInputCustody" ? undefined
2296
+ : envelope.kind === "leader.forwardCallback" ? envelope.query
2297
+ : envelope.kind === "leader.forwardReaction" ? envelope.reactionUpdate : envelope.message;
2298
+ if (envelope.kind !== "leader.wakeInputCustody" &&
2299
+ (!isRecord(carrier) || carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId))
2300
+ throw new Error("Telegram follower source-reference update id mismatch.");
2301
+ await deps.wakeSource({ deliveryId: delivery.deliveryId,
2302
+ sourceUpdateId: delivery.sourceUpdateId,
2303
+ recipientBindingKey: delivery.recipientBindingKey,
2304
+ sourceRecoveryKey: delivery.sourceRecoveryKey,
2305
+ sourceClaim: { ...delivery.sourceClaim } }, ctx);
2306
+ return { deliveryId: delivery.deliveryId, sourceUpdateId: delivery.sourceUpdateId };
2307
+ },
2308
+ };
2309
+ }
2310
+
1882
2311
  export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
1883
2312
  deps: TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
1884
2313
  ): TelegramBusForwardedUpdateReceiverRuntime {
@@ -1903,6 +2332,8 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
1903
2332
  envelope.kind !== "leader.forwardReaction" &&
1904
2333
  envelope.kind !== "leader.forwardMessage" &&
1905
2334
  envelope.kind !== "leader.forwardEditedMessage" &&
2335
+ envelope.kind !== "leader.wakeInputCustody" &&
2336
+ envelope.kind !== "leader.offerInputCustodyHandoff" &&
1906
2337
  envelope.kind !== "leader.replaceFollowerTarget" &&
1907
2338
  envelope.kind !== "leader.offerQueueHandoff") ||
1908
2339
  envelope.recipientInstanceId !== deps.instanceId
@@ -1926,9 +2357,15 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
1926
2357
  message: "Stale Telegram bus follower registration generation.",
1927
2358
  };
1928
2359
  }
2360
+ if (envelope.kind === "leader.offerInputCustodyHandoff" &&
2361
+ envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
2362
+ kind: "bus.ack", requestId: envelope.requestId, ok: false,
2363
+ message: "Mismatched Telegram follower handoff recipient identity.",
2364
+ };
1929
2365
  if (
1930
2366
  envelope.kind !== "leader.replaceFollowerTarget" &&
1931
2367
  envelope.kind !== "leader.offerQueueHandoff" &&
2368
+ envelope.kind !== "leader.offerInputCustodyHandoff" &&
1932
2369
  (!envelope.delivery ||
1933
2370
  envelope.delivery.recipientBindingKey !==
1934
2371
  deps.getRecipientBindingKey())
@@ -1950,11 +2387,35 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
1950
2387
  };
1951
2388
  }
1952
2389
  try {
2390
+ if (envelope.kind === "leader.offerInputCustodyHandoff") {
2391
+ if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false)) throw new Error(
2392
+ "Telegram input custody handoff capability is not enabled.",
2393
+ );
2394
+ if (!deps.hasAuthenticatedSourceReferenceTransport?.()) throw new Error(
2395
+ "Telegram input custody handoff requires authenticated transport.",
2396
+ );
2397
+ if (!deps.handleInputCustodyHandoff) throw new Error(
2398
+ "Telegram input custody handoff acceptance is unavailable.",
2399
+ );
2400
+ const result = await deps.handleInputCustodyHandoff(envelope, ctx);
2401
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
2402
+ }
1953
2403
  if (
1954
2404
  envelope.kind !== "leader.replaceFollowerTarget" &&
1955
2405
  envelope.kind !== "leader.offerQueueHandoff"
1956
2406
  ) {
1957
- const receipt = await deps.durableAdmission.admit(envelope, ctx);
2407
+ const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
2408
+ (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
2409
+ if (sourceReferenceEnabled &&
2410
+ !deps.hasAuthenticatedSourceReferenceTransport?.()) throw new Error(
2411
+ "Telegram follower source-reference admission requires authenticated transport.",
2412
+ );
2413
+ const admission = sourceReferenceEnabled
2414
+ ? deps.sourceReferenceAdmission : deps.durableAdmission;
2415
+ if (!admission) throw new Error(
2416
+ "Telegram follower source-reference admission is enabled without a wake authority.",
2417
+ );
2418
+ const receipt = await admission.admit(envelope, ctx);
1958
2419
  return {
1959
2420
  kind: "bus.ack",
1960
2421
  requestId: envelope.requestId,
@@ -2029,6 +2490,7 @@ function parseRegistrationResult(value: unknown): {
2029
2490
  target?: TelegramTarget;
2030
2491
  slot?: string;
2031
2492
  threadName?: string;
2493
+ displayTitle?: string;
2032
2494
  } {
2033
2495
  if (!isRecord(value)) return {};
2034
2496
  const target = parseTarget(isRecord(value.target) ? value.target : value);
@@ -2038,6 +2500,9 @@ function parseRegistrationResult(value: unknown): {
2038
2500
  ...(typeof value.threadName === "string"
2039
2501
  ? { threadName: value.threadName }
2040
2502
  : {}),
2503
+ ...(typeof value.displayTitle === "string"
2504
+ ? { displayTitle: value.displayTitle }
2505
+ : {}),
2041
2506
  };
2042
2507
  }
2043
2508