@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
@@ -4,7 +4,7 @@
4
4
  * Owns persisted bot/session pairing state, local config storage, live config controls, authorization policy, and first-user pairing side effects
5
5
  */
6
6
 
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import {
9
9
  chmodSync,
10
10
  existsSync,
@@ -83,6 +83,35 @@ export interface ResolvedTelegramTimeConfig {
83
83
  timezone: string;
84
84
  }
85
85
 
86
+ export type TelegramThreadDisplayMode = "letters" | "names" | "directories";
87
+
88
+ export function resolveTelegramThreadDisplayMode(
89
+ config: Pick<TelegramConfig, "threadDisplayMode">,
90
+ ): TelegramThreadDisplayMode {
91
+ return config.threadDisplayMode === "directories"
92
+ ? "directories"
93
+ : "letters";
94
+ }
95
+
96
+ export async function setTelegramThreadDisplayMode(
97
+ store: TelegramConfigStore,
98
+ mode: TelegramThreadDisplayMode,
99
+ isCurrent: () => boolean,
100
+ ): Promise<void> {
101
+ if (!["letters", "directories"].includes(mode)) {
102
+ throw new Error("Invalid Telegram Thread display mode.");
103
+ }
104
+ const profile = store.getActiveProfileName();
105
+ const current = () => isCurrent() && store.getActiveProfileName() === profile;
106
+ if (!current()) throw new Error("Telegram Thread display setting lost authority.");
107
+ await store.load();
108
+ if (!current() || !store.hasBotToken()) {
109
+ throw new Error("Telegram Thread display setting lost its configured profile.");
110
+ }
111
+ await store.persist({ ...store.get(), threadDisplayMode: mode }, { isCurrent: current });
112
+ if (!current()) throw new Error("Telegram Thread display setting changed during persistence.");
113
+ }
114
+
86
115
  export type TelegramAssistantRenderingMode = "rich" | "html";
87
116
  export type TelegramActivityVerbosity =
88
117
  | "quiet"
@@ -99,6 +128,8 @@ export interface TelegramConfig {
99
128
  botId?: number;
100
129
  /** @deprecated persisted identity belongs in profiles.default; retained for effective/legacy views */
101
130
  allowedUserId?: number;
131
+ /** Effective view; persisted under profiles.<name>. */
132
+ threadDisplayMode?: TelegramThreadDisplayMode;
102
133
  inboundHandlers?: TelegramInboundHandlerConfig[];
103
134
  attachmentHandlers?: TelegramInboundHandlerConfig[];
104
135
  outboundHandlers?: TelegramOutboundHandlerConfig[];
@@ -130,7 +161,7 @@ export interface TelegramConfig {
130
161
  }
131
162
 
132
163
  /**
133
- * Per-profile bot/session identity fields.
164
+ * Per-profile bot/session identity and Thread display preference.
134
165
  * Stored under `profiles.<name>` in telegram.json.
135
166
  * Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
136
167
  * assistant) stay at the top level.
@@ -140,6 +171,7 @@ export interface TelegramBotProfile {
140
171
  botUsername?: string;
141
172
  botId?: number;
142
173
  allowedUserId?: number;
174
+ threadDisplayMode?: TelegramThreadDisplayMode;
143
175
  }
144
176
 
145
177
  interface TelegramLegacyCursorCarrier {
@@ -182,9 +214,35 @@ export interface TelegramConfigStore {
182
214
  getAttachmentHandlers: () => TelegramInboundHandlerConfig[] | undefined;
183
215
  getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
184
216
  setAllowedUserId: (userId: number) => void;
217
+ /** Publish an unpaired profile owner atomically; true only for the resulting exact owner. */
218
+ persistAllowedUserId: (
219
+ userId: number,
220
+ assertExecutionCurrent?: () => void,
221
+ commitIfOwned?: (commit: () => void) => boolean,
222
+ ) => Promise<boolean>;
223
+ /** Lock-only serialization for trusted synchronous source operations, not authorization.
224
+ * Does not read/adopt config. Acquire required Workspace admission first; never
225
+ * acquire owners or nest config admission here. Do not pass async callbacks:
226
+ * returned promises are not protected after their synchronous prefix.
227
+ */
228
+ withSourceSerialization: <T>(operation: () => T) => T;
229
+ /** Trusted synchronous publication only; caller acquires Workspace admission before this config transaction. */
230
+ withPairingAdmission: <T>(
231
+ profileName: string,
232
+ tokenSha256: string,
233
+ publish: (preApprovalExcluded: boolean) => T,
234
+ ) => T;
235
+ /** Observe an existing exact owner and refresh an unpaired cache; never create an owner. Callback must be synchronous. */
236
+ withPairedUserAdmission: <T>(
237
+ profileName: string,
238
+ tokenSha256: string,
239
+ userId: number,
240
+ publish: () => T,
241
+ assertExecutionCurrent?: () => void,
242
+ ) => { admitted: false } | { admitted: true; value: T };
185
243
  load: () => Promise<void>;
186
244
  didLastLoadRecoverInvalidConfig: () => boolean;
187
- persist: (config?: TelegramConfig) => Promise<void>;
245
+ persist: (config?: TelegramConfig, options?: { isCurrent?: () => boolean }) => Promise<void>;
188
246
  }
189
247
 
190
248
  export function createTelegramConfigBotIdGetter(
@@ -437,6 +495,9 @@ export function getTelegramProfileFields(
437
495
  ...(config.allowedUserId !== undefined
438
496
  ? { allowedUserId: config.allowedUserId }
439
497
  : {}),
498
+ ...(config.threadDisplayMode !== undefined
499
+ ? { threadDisplayMode: config.threadDisplayMode }
500
+ : {}),
440
501
  ...(legacyCursor !== undefined ? { lastUpdateId: legacyCursor } : {}),
441
502
  };
442
503
  }
@@ -447,6 +508,7 @@ function omitTelegramRootProfileFields(config: TelegramConfig): TelegramConfig {
447
508
  botUsername: _botUsername,
448
509
  botId: _botId,
449
510
  allowedUserId: _allowedUserId,
511
+ threadDisplayMode: _threadDisplayMode,
450
512
  lastUpdateId: _lastUpdateId,
451
513
  ...sharedConfig
452
514
  } = config as TelegramConfig & TelegramLegacyCursorCarrier;
@@ -484,6 +546,7 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
484
546
  "botUsername",
485
547
  "botId",
486
548
  "allowedUserId",
549
+ "threadDisplayMode",
487
550
  "lastUpdateId",
488
551
  ].some((field) => Object.hasOwn(config, field));
489
552
  if (!hasLegacyRootProfile) {
@@ -504,6 +567,9 @@ export function normalizeTelegramDefaultProfileConfig(config: TelegramConfig): {
504
567
  ...(config.allowedUserId !== undefined
505
568
  ? { allowedUserId: config.allowedUserId }
506
569
  : {}),
570
+ ...(config.threadDisplayMode !== undefined
571
+ ? { threadDisplayMode: config.threadDisplayMode }
572
+ : {}),
507
573
  ...((config as TelegramConfig & TelegramLegacyCursorCarrier)
508
574
  .lastUpdateId !== undefined
509
575
  ? {
@@ -597,6 +663,35 @@ export function createTelegramConfigStore(
597
663
  );
598
664
  mutationVersion += 1;
599
665
  };
666
+ const adoptPersistedConfig = (merged: TelegramConfig, preserveLocalChanges: boolean) => {
667
+ // Local edits are relative to the latest observation, not a queued write's older request baseline.
668
+ const nextConfig = preserveLocalChanges
669
+ ? mergeTelegramConfigDelta(persistedConfig as Record<string, unknown>, config as Record<string, unknown>,
670
+ merged as Record<string, unknown>) as TelegramConfig
671
+ : cloneTelegramConfig(merged);
672
+ persistedConfig = cloneTelegramConfig(merged);
673
+ config = nextConfig;
674
+ };
675
+ const withPersistedPairingProfile = <T>(
676
+ profileName: string, tokenSha256: string,
677
+ observe: (latest: TelegramConfig, profile: TelegramBotProfile) => T,
678
+ ): T => {
679
+ if ((profileName !== TELEGRAM_DEFAULT_PROFILE_NAME && !isValidTelegramProfileName(profileName)) ||
680
+ !/^[a-f0-9]{64}$/u.test(tokenSha256)) {
681
+ throw new Error("Invalid Telegram pairing admission identity.");
682
+ }
683
+ return withTelegramFileTransaction(`${configPath}.transaction`, () => {
684
+ const latest = readTelegramConfigForTransaction(configPath);
685
+ const profile = latest.profiles?.[profileName];
686
+ if (typeof profile?.botToken !== "string" || !profile.botToken ||
687
+ createHash("sha256").update(profile.botToken).digest("hex") !== tokenSha256 ||
688
+ (profile.allowedUserId !== undefined &&
689
+ (!Number.isSafeInteger(profile.allowedUserId) || profile.allowedUserId <= 0))) {
690
+ throw new Error("Telegram pairing admission authority is unavailable or changed.");
691
+ }
692
+ return observe(latest, profile);
693
+ });
694
+ };
600
695
  return {
601
696
  get: getEffectiveConfig,
602
697
  getStoredConfig: () => config,
@@ -652,6 +747,73 @@ export function createTelegramConfigStore(
652
747
  nextConfig.allowedUserId = userId;
653
748
  setEffectiveConfig(nextConfig);
654
749
  },
750
+ withSourceSerialization: (operation) =>
751
+ withTelegramFileTransaction(`${configPath}.transaction`, operation),
752
+ withPairingAdmission: (profileName, tokenSha256, publish) =>
753
+ withPersistedPairingProfile(profileName, tokenSha256, (_latest, profile) =>
754
+ publish(profile.allowedUserId === undefined)),
755
+ withPairedUserAdmission: (profileName, tokenSha256, userId, publish, assertExecutionCurrent) => {
756
+ if (!Number.isSafeInteger(userId) || userId <= 0) return { admitted: false };
757
+ assertExecutionCurrent?.();
758
+ return withPersistedPairingProfile(profileName, tokenSha256, (latest, profile) => {
759
+ if (profile.allowedUserId !== userId) return { admitted: false };
760
+ assertExecutionCurrent?.();
761
+ const current = getEffectiveConfig();
762
+ const previousOwner = persistedConfig.profiles?.[profileName]?.allowedUserId;
763
+ if ((activeProfileName ?? TELEGRAM_DEFAULT_PROFILE_NAME) !== profileName ||
764
+ current.botToken !== profile.botToken ||
765
+ (current.allowedUserId !== undefined && current.allowedUserId !== userId) ||
766
+ (current.allowedUserId === undefined && previousOwner !== undefined)) {
767
+ throw new Error("Telegram paired admission lost local profile authority.");
768
+ }
769
+ // Observation is not a local edit; queued persistence still adopts its own fresh disk result.
770
+ adoptPersistedConfig(latest, true);
771
+ return { admitted: true, value: publish() };
772
+ });
773
+ },
774
+ persistAllowedUserId: (userId, assertExecutionCurrent, commitIfOwned) => {
775
+ const profileName = activeProfileName;
776
+ const profileKey = profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
777
+ const botToken = getEffectiveConfig().botToken;
778
+ const previousOwner = getEffectiveConfig().allowedUserId;
779
+ const assertCurrent = () => {
780
+ assertExecutionCurrent?.();
781
+ if (activeProfileName !== profileName || getEffectiveConfig().botToken !== botToken ||
782
+ getEffectiveConfig().allowedUserId !== previousOwner) {
783
+ throw new Error("Telegram pairing lost its originating profile authority.");
784
+ }
785
+ };
786
+ const pairing = persistQueue.then(() => {
787
+ assertCurrent();
788
+ if (!Number.isSafeInteger(userId) || userId <= 0) throw new Error("Invalid Telegram pairing user ID.");
789
+ let merged: TelegramConfig | undefined;
790
+ const publish = () => {
791
+ merged = withTelegramFileTransaction(`${configPath}.transaction`, () => {
792
+ const latest = readTelegramConfigForTransaction(configPath);
793
+ const profile = latest.profiles?.[profileKey];
794
+ if (!botToken || profile?.botToken !== botToken) {
795
+ throw new Error("Telegram pairing profile is unavailable or changed.");
796
+ }
797
+ assertCurrent();
798
+ if (profile.allowedUserId !== undefined) return latest;
799
+ const next = { ...latest, profiles: { ...latest.profiles,
800
+ [profileKey]: { ...profile, allowedUserId: userId } } };
801
+ writeTelegramConfigInTransaction(agentDir, configPath, next);
802
+ return next;
803
+ });
804
+ };
805
+ if (commitIfOwned) {
806
+ if (!commitIfOwned(publish)) throw new Error("Telegram pairing lost transport ownership before publication.");
807
+ } else {
808
+ publish();
809
+ }
810
+ if (!merged) throw new Error("Telegram pairing publication did not execute.");
811
+ adoptPersistedConfig(merged, true);
812
+ return merged.profiles?.[profileKey]?.allowedUserId === userId;
813
+ });
814
+ persistQueue = pairing.then(() => undefined, () => undefined);
815
+ return pairing;
816
+ },
655
817
  load: async () => {
656
818
  lastLoadRecoveredInvalidConfig = false;
657
819
  const loadedConfig = await readTelegramConfig(configPath, {
@@ -693,7 +855,7 @@ export function createTelegramConfigStore(
693
855
  mutationVersion += 1;
694
856
  },
695
857
  didLastLoadRecoverInvalidConfig: () => lastLoadRecoveredInvalidConfig,
696
- persist: (nextConfig = getEffectiveConfig()) => {
858
+ persist: (nextConfig = getEffectiveConfig(), options) => {
697
859
  const profileName = activeProfileName;
698
860
  const desiredConfig = storeTelegramEffectiveConfig(
699
861
  config,
@@ -706,6 +868,9 @@ export function createTelegramConfigStore(
706
868
  const mergedConfig = withTelegramFileTransaction(
707
869
  `${configPath}.transaction`,
708
870
  () => {
871
+ if (options?.isCurrent && !options.isCurrent()) {
872
+ throw new Error("Telegram config update lost its originating authority.");
873
+ }
709
874
  const latestConfig = readTelegramConfigForTransaction(configPath);
710
875
  const merged = mergeTelegramConfigDelta(
711
876
  baseConfig as Record<string, unknown>,
@@ -718,16 +883,7 @@ export function createTelegramConfigStore(
718
883
  return merged;
719
884
  },
720
885
  );
721
- persistedConfig = cloneTelegramConfig(mergedConfig);
722
- if (mutationVersion === capturedMutationVersion) {
723
- config = cloneTelegramConfig(mergedConfig);
724
- } else {
725
- config = mergeTelegramConfigDelta(
726
- baseConfig as Record<string, unknown>,
727
- config as Record<string, unknown>,
728
- mergedConfig as Record<string, unknown>,
729
- ) as TelegramConfig;
730
- }
886
+ adoptPersistedConfig(mergedConfig, mutationVersion !== capturedMutationVersion);
731
887
  });
732
888
  persistQueue = persist.catch(() => undefined);
733
889
  return persist;
@@ -744,7 +900,7 @@ export function createTelegramDraftPreviewsChecker(
744
900
  config.assistant?.draftPreviews ??
745
901
  config.draftPreviews ??
746
902
  config.richDraftPreviews ??
747
- false
903
+ true
748
904
  );
749
905
  };
750
906
  }
@@ -1054,20 +1210,19 @@ export type TelegramAuthorizationState =
1054
1210
  export interface TelegramUserPairingDeps<TContext> {
1055
1211
  allowedUserId?: number;
1056
1212
  ctx: TContext;
1057
- setAllowedUserId: (userId: number) => void;
1058
- persistConfig: () => Promise<void>;
1213
+ persistAllowedUserId: TelegramConfigStore["persistAllowedUserId"];
1059
1214
  updateStatus: (ctx: TContext) => void;
1060
1215
  assertExecutionCurrent?: () => void;
1061
1216
  }
1062
1217
 
1063
1218
  export interface TelegramUserPairingRuntimeDeps<TContext> {
1064
1219
  getAllowedUserId: () => number | undefined;
1065
- setAllowedUserId: (userId: number) => void;
1066
- persistConfig: () => Promise<void>;
1220
+ persistAllowedUserId: TelegramConfigStore["persistAllowedUserId"];
1067
1221
  updateStatus: (ctx: TContext) => void;
1068
1222
  }
1069
1223
 
1070
1224
  export interface TelegramUserPairingRuntime<TContext> {
1225
+ /** True means this user is authorized, whether newly paired or already configured. */
1071
1226
  pairIfNeeded: (
1072
1227
  userId: number,
1073
1228
  ctx: TContext,
@@ -1104,12 +1259,11 @@ export async function pairTelegramUserIfNeeded<TContext>(
1104
1259
  userId,
1105
1260
  deps.allowedUserId,
1106
1261
  );
1107
- if (authorization.kind !== "pair") return false;
1108
- deps.assertExecutionCurrent?.();
1109
- deps.setAllowedUserId(authorization.userId);
1262
+ if (authorization.kind !== "pair") return authorization.kind === "allow";
1110
1263
  deps.assertExecutionCurrent?.();
1111
- await deps.persistConfig();
1264
+ const allowed = await deps.persistAllowedUserId(authorization.userId, deps.assertExecutionCurrent);
1112
1265
  deps.assertExecutionCurrent?.();
1266
+ if (!allowed) return false;
1113
1267
  try {
1114
1268
  deps.updateStatus(deps.ctx);
1115
1269
  } catch (error) {
@@ -1126,8 +1280,7 @@ export function createTelegramUserPairingRuntime<TContext>(
1126
1280
  pairTelegramUserIfNeeded(userId, {
1127
1281
  allowedUserId: deps.getAllowedUserId(),
1128
1282
  ctx,
1129
- setAllowedUserId: deps.setAllowedUserId,
1130
- persistConfig: deps.persistConfig,
1283
+ persistAllowedUserId: deps.persistAllowedUserId,
1131
1284
  updateStatus: deps.updateStatus,
1132
1285
  assertExecutionCurrent,
1133
1286
  }),
@@ -10,7 +10,7 @@ import {
10
10
  type TelegramInlineKeyboardMarkup,
11
11
  } from "./keyboard.ts";
12
12
  import {
13
- buildTelegramReplyParameters,
13
+ withTelegramReplyParameters,
14
14
  renderTelegramMessage,
15
15
  } from "./replies.ts";
16
16
  import {
@@ -724,23 +724,23 @@ export function createTelegramBridgeDeliveryRuntime(
724
724
  },
725
725
  async sendChunk(target, chunk, options) {
726
726
  assertTransportActive();
727
- const replyParameters = buildTelegramReplyParameters(
728
- target.chatId,
729
- options.replyToMessageId,
730
- target,
731
- );
732
- const body = {
733
- chat_id: target.chatId,
734
- text: chunk.text,
735
- ...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
736
- ...getTelegramTargetThreadParams(target),
737
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
738
- ...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
739
- };
740
- const sent = await deps.api.sendMessage(
741
- target.threadId === undefined
742
- ? markTelegramBusAggregateDelivery(body)
743
- : body,
727
+ const sent = await withTelegramReplyParameters(
728
+ target.chatId, options.replyToMessageId, target,
729
+ (replyParameters) => {
730
+ const body = {
731
+ chat_id: target.chatId,
732
+ text: chunk.text,
733
+ ...(chunk.parseMode === "html" ? { parse_mode: "HTML" as const } : {}),
734
+ ...getTelegramTargetThreadParams(target),
735
+ ...(replyParameters ? { reply_parameters: replyParameters } : {}),
736
+ ...(options.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
737
+ };
738
+ return deps.api.sendMessage(
739
+ target.threadId === undefined
740
+ ? markTelegramBusAggregateDelivery(body)
741
+ : body,
742
+ );
743
+ },
744
744
  );
745
745
  assertTransportActive();
746
746
  deps.recordOwnership({