@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
package/lib/locks.ts CHANGED
@@ -4,9 +4,11 @@
4
4
  * Owns filesystem authority, atomic runtime-section publication and Telegram bridge ownership semantics
5
5
  */
6
6
 
7
+ import { randomUUID } from "node:crypto";
7
8
  import {
8
9
  chmodSync,
9
10
  closeSync,
11
+ constants,
10
12
  existsSync,
11
13
  fstatSync,
12
14
  lstatSync,
@@ -20,16 +22,17 @@ import {
20
22
  unlinkSync,
21
23
  writeFileSync,
22
24
  } from "node:fs";
23
- import { randomUUID } from "node:crypto";
24
- import { isDeepStrictEqual } from "node:util";
25
25
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
26
- import { isWireRecord as runtimeStateRecord } from "./wire.ts";
26
+ import { isDeepStrictEqual } from "node:util";
27
+
27
28
  import {
28
29
  isTelegramSessionPollingJournalPath,
29
30
  resolveTelegramOwnersPath,
30
31
  resolveTelegramSessionPollingJournalPath,
31
32
  resolveTelegramUpdateJournalPathForProfile,
32
33
  } from "./paths.ts";
34
+ import { isProcessAlive } from "./process-identity.ts";
35
+ import { isWireRecord as runtimeStateRecord } from "./wire.ts";
33
36
 
34
37
  export const TELEGRAM_LOCK_KEY = "default";
35
38
  export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
@@ -90,16 +93,23 @@ interface TelegramOwnedStateSessionPort<TContext extends TelegramLockContext> {
90
93
  * Captures exact context, session generation and owned leader epoch before awaits.
91
94
  * Session replacement, release or re-election revokes the grant; a successor never renews it.
92
95
  */
93
- export function createTelegramOwnedStateAuthorityCapture<TContext extends TelegramLockContext>(
96
+ export function createTelegramOwnedStateAuthorityCapture<
97
+ TContext extends TelegramLockContext,
98
+ >(
94
99
  lock: Pick<TelegramLockRuntime<TContext>, "owns" | "getOwnedLeaderEpoch">,
95
100
  session: TelegramOwnedStateSessionPort<TContext>,
96
101
  ): () => (() => boolean) | undefined {
97
102
  return () => {
98
- const ctx = session.get(), generation = session.getGeneration();
99
- if (ctx === undefined || !session.isCurrent(ctx, generation)) return undefined;
103
+ const ctx = session.get(),
104
+ generation = session.getGeneration();
105
+ if (ctx === undefined || !session.isCurrent(ctx, generation))
106
+ return undefined;
100
107
  const epoch = lock.getOwnedLeaderEpoch();
101
108
  if (epoch === undefined) return undefined;
102
- const current = (): boolean => session.isCurrent(ctx, generation) && lock.owns(ctx) && lock.getOwnedLeaderEpoch() === epoch;
109
+ const current = (): boolean =>
110
+ session.isCurrent(ctx, generation) &&
111
+ lock.owns(ctx) &&
112
+ lock.getOwnedLeaderEpoch() === epoch;
103
113
  return current() ? current : undefined;
104
114
  };
105
115
  }
@@ -137,7 +147,8 @@ type TelegramLockAcquireResult =
137
147
  | { ok: true; lock: TelegramLockEntry; replacedStale: boolean }
138
148
  | { ok: false; lock: TelegramLockEntry };
139
149
 
140
- export type TelegramOwnedStatePublicationResult<T> = { committed: false } | { committed: true; result: T };
150
+ export type TelegramOwnedStatePublicationResult<T> =
151
+ { committed: false } | { committed: true; result: T };
141
152
 
142
153
  export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
143
154
  acquire: (
@@ -153,7 +164,7 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
153
164
  /** Consolidated-store publication; domain reducers retain their own exact payload/CAS rules. */
154
165
  publishStateSectionIfOwned?: <T>(
155
166
  section: "workspace" | "runtime",
156
- mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
167
+ mutate: TelegramRuntimeStateSectionReducer<T>,
157
168
  options: TelegramOwnedStatePublicationOptions,
158
169
  ) => TelegramOwnedStatePublicationResult<T>;
159
170
  refresh: (ctx?: TelegramLockContext) => boolean;
@@ -161,15 +172,11 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
161
172
  getJournalPath: () => string | undefined;
162
173
  }
163
174
 
164
- interface TelegramLockOwnershipGuard<
165
- TContext extends TelegramLockContext,
166
- > {
175
+ interface TelegramLockOwnershipGuard<TContext extends TelegramLockContext> {
167
176
  ownsContext: (ctx: TContext) => boolean;
168
177
  }
169
178
 
170
- interface TelegramLockContextStore<
171
- TContext extends TelegramLockContext,
172
- > {
179
+ interface TelegramLockContextStore<TContext extends TelegramLockContext> {
173
180
  get: () => TContext | undefined;
174
181
  }
175
182
 
@@ -178,7 +185,10 @@ interface TelegramLockRuntimeOptions {
178
185
  locksPath?: string;
179
186
  /** Consolidated version-2 state envelope; exclusive with `locksPath`. */
180
187
  statePath?: string;
181
- statePublication?: Pick<TelegramRuntimeStatePublicationOptions, "onPublicationBoundary" | "publishRename">;
188
+ statePublication?: Pick<
189
+ TelegramRuntimeStatePublicationOptions,
190
+ "onPublicationBoundary" | "publishRename"
191
+ >;
182
192
  /** Read-only ownership file of releases that used another directory; a live fresh owner there blocks acquisition. */
183
193
  legacyLocksPath?: string;
184
194
  pid?: number;
@@ -204,26 +214,44 @@ export function createTelegramLeaderJournalPathResolver(deps: {
204
214
  getProfileName: () => string | undefined;
205
215
  }) {
206
216
  // An existing flat root journal keeps its custody; new installations never create one.
207
- const createOwn = (profileName = deps.getProfileName()): string | undefined => {
217
+ const createOwn = (
218
+ profileName = deps.getProfileName(),
219
+ ): string | undefined => {
208
220
  const root = resolveTelegramUpdateJournalPathForProfile(profileName);
209
221
  if (existsSync(root)) return root;
210
222
  const sessionId = deps.getSessionId();
211
- return sessionId === undefined ? undefined : resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
223
+ return sessionId === undefined
224
+ ? undefined
225
+ : resolveTelegramSessionPollingJournalPath(
226
+ sessionId,
227
+ undefined,
228
+ profileName,
229
+ );
212
230
  };
213
231
  return {
214
232
  createJournalPath: () => createOwn(),
215
233
  resolve(profileName?: string): string {
216
234
  const named = deps.getNamedJournalPath();
217
- if (named && (isTelegramSessionPollingJournalPath(named) ||
218
- named === resolveTelegramUpdateJournalPathForProfile(profileName))) return named;
219
- return createOwn(profileName) ?? resolveTelegramUpdateJournalPathForProfile(profileName);
235
+ if (
236
+ named &&
237
+ (isTelegramSessionPollingJournalPath(named) ||
238
+ named === resolveTelegramUpdateJournalPathForProfile(profileName))
239
+ )
240
+ return named;
241
+ return (
242
+ createOwn(profileName) ??
243
+ resolveTelegramUpdateJournalPathForProfile(profileName)
244
+ );
220
245
  },
221
246
  };
222
247
  }
223
248
 
224
249
  /** A released key keeps only `{ journalPath }`: no owner, but the successor's polling custody. */
225
- export function readTelegramLockJournalPath(value: unknown): string | undefined {
226
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
250
+ export function readTelegramLockJournalPath(
251
+ value: unknown,
252
+ ): string | undefined {
253
+ if (!value || typeof value !== "object" || Array.isArray(value))
254
+ return undefined;
227
255
  const path = (value as Record<string, unknown>).journalPath;
228
256
  return typeof path === "string" && path ? path : undefined;
229
257
  }
@@ -271,6 +299,120 @@ interface TelegramRenameRetryOptions {
271
299
  retryDelayMs?: number;
272
300
  }
273
301
 
302
+ export type TelegramPrivateFileFailure = "capacity" | "unsafe" | "changed";
303
+
304
+ /** Typed private-file refusal; owners map the failure into their own error vocabulary. */
305
+ export class TelegramPrivateFileError extends Error {
306
+ readonly failure: TelegramPrivateFileFailure;
307
+ constructor(failure: TelegramPrivateFileFailure, message: string) {
308
+ super(message);
309
+ this.failure = failure;
310
+ this.name = "TelegramPrivateFileError";
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Read flags for private runtime files. Where the platform offers them, no-follow and non-blocking opens refuse a
316
+ * swapped link or FIFO at open time. Windows has neither flag and no FIFOs in the file namespace; there the
317
+ * lstat-before/fstat-after identity binding every strict reader performs is the guard, so a link swapped in after
318
+ * inspection opens a different file and is refused as changed.
319
+ */
320
+ export const TELEGRAM_STRICT_READ_FLAGS =
321
+ constants.O_RDONLY |
322
+ (constants.O_NOFOLLOW ?? 0) |
323
+ (constants.O_NONBLOCK ?? 0);
324
+
325
+ /** POSIX owner and permission bits; Windows has no uid, so its ACL-protected agent directory carries privacy. */
326
+ export function isTelegramOwnerPrivate(stat: {
327
+ uid: bigint;
328
+ mode: bigint;
329
+ }): boolean {
330
+ const uid = process.getuid?.();
331
+ return (
332
+ uid === undefined ||
333
+ (stat.uid === BigInt(uid) && (stat.mode & 0o077n) === 0n)
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Read one owner-private, single-link, bounded regular file without following links. Absence returns undefined;
339
+ * an inode or metadata change between inspection and open refuses as `changed`.
340
+ */
341
+ export function readTelegramPrivateFile(
342
+ path: string,
343
+ maxBytes: number,
344
+ ): string | undefined {
345
+ let before;
346
+ try {
347
+ before = lstatSync(path, { bigint: true });
348
+ } catch (error) {
349
+ if ((error as { code?: unknown })?.code === "ENOENT") return undefined;
350
+ throw error;
351
+ }
352
+ if (
353
+ !before.isFile() ||
354
+ before.isSymbolicLink() ||
355
+ !isTelegramOwnerPrivate(before) ||
356
+ before.nlink !== 1n ||
357
+ before.size > BigInt(maxBytes)
358
+ ) {
359
+ throw new TelegramPrivateFileError(
360
+ before.size > BigInt(maxBytes) ? "capacity" : "unsafe",
361
+ "Telegram private file is not a bounded no-follow regular file.",
362
+ );
363
+ }
364
+ const fd = openSync(path, TELEGRAM_STRICT_READ_FLAGS);
365
+ try {
366
+ const opened = fstatSync(fd, { bigint: true });
367
+ if (
368
+ !opened.isFile() ||
369
+ opened.dev !== before.dev ||
370
+ opened.ino !== before.ino ||
371
+ opened.uid !== before.uid ||
372
+ opened.nlink !== 1n ||
373
+ !isTelegramOwnerPrivate(opened) ||
374
+ opened.size !== before.size ||
375
+ opened.mtimeNs !== before.mtimeNs
376
+ ) {
377
+ throw new TelegramPrivateFileError(
378
+ "changed",
379
+ "Telegram private file changed during inspection.",
380
+ );
381
+ }
382
+ return readFileSync(fd, "utf8");
383
+ } finally {
384
+ closeSync(fd);
385
+ }
386
+ }
387
+
388
+ /** Atomically publish owner-private contents through a unique staging file; the rename consumes staging. */
389
+ export function publishTelegramPrivateFile(
390
+ path: string,
391
+ temporaryBasePath: string,
392
+ contents: string,
393
+ label: string,
394
+ onBoundary?: (boundary: "after-write-before-rename" | "after-rename") => void,
395
+ ): void {
396
+ const temporaryPath = `${temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
397
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
398
+ mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
399
+ try {
400
+ writeFileSync(temporaryPath, contents, { encoding: "utf8", mode: 0o600 });
401
+ chmodSync(temporaryPath, 0o600);
402
+ onBoundary?.("after-write-before-rename");
403
+ if (!renameTelegramPathWithRetry(temporaryPath, path))
404
+ throw new Error(`${label} staging file disappeared before publication.`);
405
+ chmodSync(path, 0o600);
406
+ onBoundary?.("after-rename");
407
+ } finally {
408
+ try {
409
+ unlinkSync(temporaryPath);
410
+ } catch {
411
+ /* Atomic rename consumes the temporary path. */
412
+ }
413
+ }
414
+ }
415
+
274
416
  /** Rename one Telegram runtime artifact with bounded Windows sharing retries. */
275
417
  export function renameTelegramPathWithRetry(
276
418
  sourcePath: string,
@@ -738,30 +880,53 @@ export function withTelegramFileTransaction<T>(
738
880
  }
739
881
  }
740
882
 
741
- export type TelegramRuntimeStateSection = "transport" | "workspace" | "admission" | "runtime";
742
- type TelegramRuntimeStateProfile = Partial<Record<TelegramRuntimeStateSection, unknown>>;
883
+ export type TelegramRuntimeStateSection =
884
+ "transport" | "workspace" | "admission" | "runtime";
885
+ type TelegramRuntimeStateProfile = Partial<
886
+ Record<TelegramRuntimeStateSection, unknown>
887
+ >;
743
888
  interface TelegramRuntimeStateFile {
744
889
  version: 2;
745
890
  profiles: Record<string, TelegramRuntimeStateProfile>;
746
891
  }
747
892
  export class TelegramRuntimeStateError extends Error {
748
893
  readonly code: "invalid" | "authority-changed" | "publication-unknown";
749
- constructor(code: TelegramRuntimeStateError["code"], message: string, options?: ErrorOptions) {
894
+ constructor(
895
+ code: TelegramRuntimeStateError["code"],
896
+ message: string,
897
+ options?: ErrorOptions,
898
+ ) {
750
899
  super(message, options);
751
900
  this.name = "TelegramRuntimeStateError";
752
901
  this.code = code;
753
902
  }
754
903
  }
755
- const TELEGRAM_RUNTIME_STATE_SECTIONS: readonly TelegramRuntimeStateSection[] = ["transport", "workspace", "admission", "runtime"];
756
- const TELEGRAM_ACTIVE_STATE_TRANSACTIONS = Symbol.for("@llblab/pi-telegram/active-state-transactions");
757
- type TelegramStateGlobal = typeof globalThis & { [TELEGRAM_ACTIVE_STATE_TRANSACTIONS]?: Set<string> };
904
+ const TELEGRAM_RUNTIME_STATE_SECTIONS: readonly TelegramRuntimeStateSection[] =
905
+ ["transport", "workspace", "admission", "runtime"];
906
+ const TELEGRAM_ACTIVE_STATE_TRANSACTIONS = Symbol.for(
907
+ "@llblab/pi-telegram/active-state-transactions",
908
+ );
909
+ type TelegramStateGlobal = typeof globalThis & {
910
+ [TELEGRAM_ACTIVE_STATE_TRANSACTIONS]?: Set<string>;
911
+ };
758
912
  function requireRuntimeStatePath(path: string): void {
759
913
  if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path)
760
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state path must be canonical and absolute.");
914
+ throw new TelegramRuntimeStateError(
915
+ "invalid",
916
+ "Telegram runtime state path must be canonical and absolute.",
917
+ );
761
918
  }
762
919
  function requireRuntimeStateProfile(profile: string): void {
763
- if (typeof profile !== "string" || !profile.length || profile.length > 512 || /[\x00-\x1f]/u.test(profile))
764
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state profile is invalid.");
920
+ if (
921
+ typeof profile !== "string" ||
922
+ !profile.length ||
923
+ profile.length > 512 ||
924
+ /[\x00-\x1f]/u.test(profile)
925
+ )
926
+ throw new TelegramRuntimeStateError(
927
+ "invalid",
928
+ "Telegram runtime state profile is invalid.",
929
+ );
765
930
  }
766
931
  /** Private single-link regular file read with identity continuity; initial absence only is positive. */
767
932
  function readTelegramRuntimeSource(path: string): string | undefined {
@@ -771,42 +936,94 @@ function readTelegramRuntimeSource(path: string): string | undefined {
771
936
  try {
772
937
  const stat = lstatSync(path);
773
938
  observed = true;
774
- if (!stat.isFile() || stat.nlink !== 1 ||
775
- (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
776
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state must be a private regular file.");
939
+ if (
940
+ !stat.isFile() ||
941
+ stat.nlink !== 1 ||
942
+ (process.platform !== "win32" && (stat.mode & 0o077) !== 0)
943
+ )
944
+ throw new TelegramRuntimeStateError(
945
+ "invalid",
946
+ "Telegram runtime state must be a private regular file.",
947
+ );
777
948
  const fd = openSync(path, "r");
778
949
  try {
779
950
  const opened = fstatSync(fd);
780
- if (opened.dev !== stat.dev || opened.ino !== stat.ino || opened.nlink !== 1 || !opened.isFile() ||
781
- (process.platform !== "win32" && (opened.mode & 0o077) !== 0))
782
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state identity changed before reading.");
951
+ if (
952
+ opened.dev !== stat.dev ||
953
+ opened.ino !== stat.ino ||
954
+ opened.nlink !== 1 ||
955
+ !opened.isFile() ||
956
+ (process.platform !== "win32" && (opened.mode & 0o077) !== 0)
957
+ )
958
+ throw new TelegramRuntimeStateError(
959
+ "invalid",
960
+ "Telegram runtime state identity changed before reading.",
961
+ );
783
962
  source = readFileSync(fd).toString("utf8");
784
- } finally { closeSync(fd); }
963
+ } finally {
964
+ closeSync(fd);
965
+ }
785
966
  const after = lstatSync(path);
786
- if (after.dev !== stat.dev || after.ino !== stat.ino || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs)
787
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state changed during observation.");
788
- }
789
- catch (error) {
790
- if (!observed && (error as { code?: unknown }).code === "ENOENT") return undefined;
967
+ if (
968
+ after.dev !== stat.dev ||
969
+ after.ino !== stat.ino ||
970
+ after.size !== stat.size ||
971
+ after.mtimeMs !== stat.mtimeMs ||
972
+ after.ctimeMs !== stat.ctimeMs
973
+ )
974
+ throw new TelegramRuntimeStateError(
975
+ "invalid",
976
+ "Telegram runtime state changed during observation.",
977
+ );
978
+ } catch (error) {
979
+ if (!observed && (error as { code?: unknown }).code === "ENOENT")
980
+ return undefined;
791
981
  throw error;
792
982
  }
793
983
  return source;
794
984
  }
795
985
 
796
986
  /** Strict, observational envelope read; section owners validate their own payloads. Legacy state is never adopted here. */
797
- export function readTelegramRuntimeState(path: string): TelegramRuntimeStateFile {
987
+ export function readTelegramRuntimeState(
988
+ path: string,
989
+ ): TelegramRuntimeStateFile {
798
990
  const observed = readTelegramRuntimeSource(path);
799
991
  if (observed === undefined) return { version: 2, profiles: {} };
800
992
  let value: unknown;
801
- try { value = JSON.parse(observed); }
802
- catch (error) { throw new TelegramRuntimeStateError("invalid", "Telegram runtime state is unreadable.", { cause: error }); }
803
- if (!runtimeStateRecord(value) || value.version !== 2 || !runtimeStateRecord(value.profiles) ||
804
- Object.keys(value).some(key => key !== "version" && key !== "profiles"))
805
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state envelope is unsupported or malformed.");
993
+ try {
994
+ value = JSON.parse(observed);
995
+ } catch (error) {
996
+ throw new TelegramRuntimeStateError(
997
+ "invalid",
998
+ "Telegram runtime state is unreadable.",
999
+ { cause: error },
1000
+ );
1001
+ }
1002
+ if (
1003
+ !runtimeStateRecord(value) ||
1004
+ value.version !== 2 ||
1005
+ !runtimeStateRecord(value.profiles) ||
1006
+ Object.keys(value).some((key) => key !== "version" && key !== "profiles")
1007
+ )
1008
+ throw new TelegramRuntimeStateError(
1009
+ "invalid",
1010
+ "Telegram runtime state envelope is unsupported or malformed.",
1011
+ );
806
1012
  for (const [profile, sections] of Object.entries(value.profiles)) {
807
1013
  requireRuntimeStateProfile(profile);
808
- if (!runtimeStateRecord(sections) || Object.keys(sections).some(key => !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(key as TelegramRuntimeStateSection)))
809
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state sections are malformed.");
1014
+ if (
1015
+ !runtimeStateRecord(sections) ||
1016
+ Object.keys(sections).some(
1017
+ (key) =>
1018
+ !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(
1019
+ key as TelegramRuntimeStateSection,
1020
+ ),
1021
+ )
1022
+ )
1023
+ throw new TelegramRuntimeStateError(
1024
+ "invalid",
1025
+ "Telegram runtime state sections are malformed.",
1026
+ );
810
1027
  }
811
1028
  return value as unknown as TelegramRuntimeStateFile;
812
1029
  }
@@ -815,34 +1032,67 @@ export function readTelegramRuntimeState(path: string): TelegramRuntimeStateFile
815
1032
  * caller-validated section is damaged, publish a fresh empty envelope instead of refusing. All profiles lose runtime
816
1033
  * continuity. Filesystem access errors are not damage and still throw. Returns whether a reset was published.
817
1034
  */
818
- export function resetDamagedTelegramRuntimeState(path: string,
819
- validateProfile?: (profile: string, sections: Readonly<TelegramRuntimeStateProfile>) => void): boolean {
1035
+ export function resetDamagedTelegramRuntimeState(
1036
+ path: string,
1037
+ validateProfile?: (
1038
+ profile: string,
1039
+ sections: Readonly<TelegramRuntimeStateProfile>,
1040
+ ) => void,
1041
+ ): boolean {
820
1042
  requireRuntimeStatePath(path);
821
1043
  const runtimeDir = join(dirname(path), "runtime");
822
1044
  mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
823
- return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
824
- let damaged = false;
825
- try {
826
- for (const [profile, sections] of Object.entries(readTelegramRuntimeState(path).profiles)) {
827
- assertTelegramStateTransport(sections.transport);
828
- try { validateProfile?.(profile, sections); } catch { damaged = true; }
1045
+ return withTelegramFileTransaction(
1046
+ join(runtimeDir, `${basename(path)}.transaction`),
1047
+ () => {
1048
+ let damaged = false;
1049
+ try {
1050
+ for (const [profile, sections] of Object.entries(
1051
+ readTelegramRuntimeState(path).profiles,
1052
+ )) {
1053
+ assertTelegramStateTransport(sections.transport);
1054
+ try {
1055
+ validateProfile?.(profile, sections);
1056
+ } catch {
1057
+ damaged = true;
1058
+ }
1059
+ }
1060
+ } catch (error) {
1061
+ if (!(error instanceof TelegramRuntimeStateError)) throw error;
1062
+ damaged = true;
829
1063
  }
830
- } catch (error) {
831
- if (!(error instanceof TelegramRuntimeStateError)) throw error;
832
- damaged = true;
833
- }
834
- if (!damaged) return false;
835
- const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
836
- try {
837
- writeFileSync(tempPath, `${JSON.stringify({ version: 2, profiles: {} }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
838
- if (!renameTelegramPathWithRetry(tempPath, path)) throw new Error("Telegram runtime state reset publication disappeared.");
839
- } finally {
840
- try { unlinkSync(tempPath); }
841
- catch (error) { if ((error as { code?: unknown }).code !== "ENOENT") throw error; }
842
- }
843
- return true;
844
- });
1064
+ if (!damaged) return false;
1065
+ const tempPath = join(
1066
+ runtimeDir,
1067
+ `${basename(path)}.${process.pid}.${randomUUID()}.tmp`,
1068
+ );
1069
+ try {
1070
+ writeFileSync(
1071
+ tempPath,
1072
+ `${JSON.stringify({ version: 2, profiles: {} }, null, 2)}\n`,
1073
+ { encoding: "utf8", flag: "wx", mode: 0o600 },
1074
+ );
1075
+ if (!renameTelegramPathWithRetry(tempPath, path))
1076
+ throw new Error(
1077
+ "Telegram runtime state reset publication disappeared.",
1078
+ );
1079
+ } finally {
1080
+ try {
1081
+ unlinkSync(tempPath);
1082
+ } catch (error) {
1083
+ if ((error as { code?: unknown }).code !== "ENOENT") throw error;
1084
+ }
1085
+ }
1086
+ return true;
1087
+ },
1088
+ );
845
1089
  }
1090
+ /** Pure section reducer: receives the stored section and its profile, returns the next section and result. */
1091
+ export type TelegramRuntimeStateSectionReducer<T> = (
1092
+ current: unknown,
1093
+ observed: Readonly<TelegramRuntimeStateProfile>,
1094
+ ) => TelegramRuntimeStateMutation<T>;
1095
+
846
1096
  export interface TelegramRuntimeStateMutation<T> {
847
1097
  value: unknown;
848
1098
  result: T;
@@ -850,7 +1100,9 @@ export interface TelegramRuntimeStateMutation<T> {
850
1100
  interface TelegramRuntimeStatePublicationOptions {
851
1101
  /** Exact domain authority, recaptured by the caller before invoking this synchronous transaction. */
852
1102
  isCurrent: () => boolean;
853
- onPublicationBoundary?: (boundary: "before-write" | "after-write-before-rename" | "after-rename") => void;
1103
+ onPublicationBoundary?: (
1104
+ boundary: "before-write" | "after-write-before-rename" | "after-rename",
1105
+ ) => void;
854
1106
  publishRename?: typeof renameSync;
855
1107
  }
856
1108
  export interface TelegramOwnedStatePublicationOptions extends TelegramRuntimeStatePublicationOptions {
@@ -862,62 +1114,127 @@ export interface TelegramOwnedStatePublicationOptions extends TelegramRuntimeSta
862
1114
  * current sibling facts without granting writes to them. No await, nested transaction, repair or legacy import.
863
1115
  */
864
1116
  export function mutateTelegramRuntimeStateSection<T>(
865
- path: string, profile: string, section: TelegramRuntimeStateSection,
866
- mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
1117
+ path: string,
1118
+ profile: string,
1119
+ section: TelegramRuntimeStateSection,
1120
+ mutate: TelegramRuntimeStateSectionReducer<T>,
867
1121
  options: TelegramRuntimeStatePublicationOptions,
868
1122
  ): T {
869
1123
  requireRuntimeStatePath(path);
870
1124
  requireRuntimeStateProfile(profile);
871
1125
  if (!TELEGRAM_RUNTIME_STATE_SECTIONS.includes(section))
872
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state section is invalid.");
873
- const active = ((globalThis as TelegramStateGlobal)[TELEGRAM_ACTIVE_STATE_TRANSACTIONS] ??= new Set<string>());
874
- if (active.has(path)) throw new TelegramRuntimeStateError("invalid", "Nested Telegram runtime state transaction is not allowed.");
1126
+ throw new TelegramRuntimeStateError(
1127
+ "invalid",
1128
+ "Telegram runtime state section is invalid.",
1129
+ );
1130
+ const active = ((globalThis as TelegramStateGlobal)[
1131
+ TELEGRAM_ACTIVE_STATE_TRANSACTIONS
1132
+ ] ??= new Set<string>());
1133
+ if (active.has(path))
1134
+ throw new TelegramRuntimeStateError(
1135
+ "invalid",
1136
+ "Nested Telegram runtime state transaction is not allowed.",
1137
+ );
875
1138
  const assertCurrent = (): void => {
876
- if (!options.isCurrent()) throw new TelegramRuntimeStateError("authority-changed", "Telegram runtime state publication authority changed.");
1139
+ if (!options.isCurrent())
1140
+ throw new TelegramRuntimeStateError(
1141
+ "authority-changed",
1142
+ "Telegram runtime state publication authority changed.",
1143
+ );
877
1144
  };
878
1145
  assertCurrent();
879
1146
  active.add(path);
880
1147
  try {
881
1148
  const runtimeDir = join(dirname(path), "runtime");
882
1149
  mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
883
- return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
884
- assertCurrent();
885
- const file = readTelegramRuntimeState(path);
886
- const previous = Object.hasOwn(file.profiles, profile) ? file.profiles[profile]! : {};
887
- const outcome = mutate(structuredClone(previous[section]), structuredClone(previous));
888
- if (!runtimeStateRecord(outcome) || !Object.hasOwn(outcome, "value") || !Object.hasOwn(outcome, "result") || "then" in outcome)
889
- throw new TelegramRuntimeStateError("invalid", "Telegram runtime state mutations must return a synchronous value and result.");
890
- // Normalize intentional optional properties to their wire representation before comparison/publication.
891
- const next = outcome.value === undefined ? undefined : JSON.parse(JSON.stringify(outcome.value)) as unknown;
892
- assertCurrent();
893
- if (isDeepStrictEqual(previous[section], next)) return outcome.result;
894
- const updated = { ...previous };
895
- if (next === undefined) delete updated[section];
896
- else updated[section] = next;
897
- if (Object.keys(updated).length) Object.defineProperty(file.profiles, profile, { value: updated, enumerable: true, configurable: true, writable: true });
898
- else delete file.profiles[profile];
899
- const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
900
- try {
901
- options.onPublicationBoundary?.("before-write");
1150
+ return withTelegramFileTransaction(
1151
+ join(runtimeDir, `${basename(path)}.transaction`),
1152
+ () => {
902
1153
  assertCurrent();
903
- writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
904
- options.onPublicationBoundary?.("after-write-before-rename");
1154
+ const file = readTelegramRuntimeState(path);
1155
+ const previous = Object.hasOwn(file.profiles, profile)
1156
+ ? file.profiles[profile]!
1157
+ : {};
1158
+ const outcome = mutate(
1159
+ structuredClone(previous[section]),
1160
+ structuredClone(previous),
1161
+ );
1162
+ if (
1163
+ !runtimeStateRecord(outcome) ||
1164
+ !Object.hasOwn(outcome, "value") ||
1165
+ !Object.hasOwn(outcome, "result") ||
1166
+ "then" in outcome
1167
+ )
1168
+ throw new TelegramRuntimeStateError(
1169
+ "invalid",
1170
+ "Telegram runtime state mutations must return a synchronous value and result.",
1171
+ );
1172
+ // Normalize intentional optional properties to their wire representation before comparison/publication.
1173
+ const next =
1174
+ outcome.value === undefined
1175
+ ? undefined
1176
+ : (JSON.parse(JSON.stringify(outcome.value)) as unknown);
905
1177
  assertCurrent();
1178
+ if (isDeepStrictEqual(previous[section], next)) return outcome.result;
1179
+ const updated = { ...previous };
1180
+ if (next === undefined) delete updated[section];
1181
+ else updated[section] = next;
1182
+ if (Object.keys(updated).length)
1183
+ Object.defineProperty(file.profiles, profile, {
1184
+ value: updated,
1185
+ enumerable: true,
1186
+ configurable: true,
1187
+ writable: true,
1188
+ });
1189
+ else delete file.profiles[profile];
1190
+ const tempPath = join(
1191
+ runtimeDir,
1192
+ `${basename(path)}.${process.pid}.${randomUUID()}.tmp`,
1193
+ );
906
1194
  try {
907
- if (!renameTelegramPathWithRetry(tempPath, path, { rename: options.publishRename }))
908
- throw new Error("Telegram runtime state temporary publication disappeared.");
909
- options.onPublicationBoundary?.("after-rename");
1195
+ options.onPublicationBoundary?.("before-write");
910
1196
  assertCurrent();
911
- } catch (error) {
912
- throw new TelegramRuntimeStateError("publication-unknown", "Telegram runtime state publication outcome is unknown.", { cause: error });
1197
+ writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, {
1198
+ encoding: "utf8",
1199
+ flag: "wx",
1200
+ mode: 0o600,
1201
+ });
1202
+ options.onPublicationBoundary?.("after-write-before-rename");
1203
+ assertCurrent();
1204
+ try {
1205
+ if (
1206
+ !renameTelegramPathWithRetry(tempPath, path, {
1207
+ rename(from, to) {
1208
+ assertCurrent();
1209
+ (options.publishRename ?? renameSync)(from, to);
1210
+ },
1211
+ })
1212
+ )
1213
+ throw new Error(
1214
+ "Telegram runtime state temporary publication disappeared.",
1215
+ );
1216
+ options.onPublicationBoundary?.("after-rename");
1217
+ assertCurrent();
1218
+ } catch (error) {
1219
+ throw new TelegramRuntimeStateError(
1220
+ "publication-unknown",
1221
+ "Telegram runtime state publication outcome is unknown.",
1222
+ { cause: error },
1223
+ );
1224
+ }
1225
+ return outcome.result;
1226
+ } finally {
1227
+ try {
1228
+ unlinkSync(tempPath);
1229
+ } catch (error) {
1230
+ if ((error as { code?: unknown }).code !== "ENOENT") throw error;
1231
+ }
913
1232
  }
914
- return outcome.result;
915
- } finally {
916
- try { unlinkSync(tempPath); }
917
- catch (error) { if ((error as { code?: unknown }).code !== "ENOENT") throw error; }
918
- }
919
- });
920
- } finally { active.delete(path); }
1233
+ },
1234
+ );
1235
+ } finally {
1236
+ active.delete(path);
1237
+ }
921
1238
  }
922
1239
 
923
1240
  function withLockTransaction<T>(
@@ -1000,21 +1317,12 @@ export function parseTelegramLockEntry(
1000
1317
  : undefined,
1001
1318
  busSecret:
1002
1319
  typeof record.busSecret === "string" ? record.busSecret : undefined,
1003
- ...(readTelegramLockJournalPath(record) ? { journalPath: readTelegramLockJournalPath(record) } : {}),
1320
+ ...(readTelegramLockJournalPath(record)
1321
+ ? { journalPath: readTelegramLockJournalPath(record) }
1322
+ : {}),
1004
1323
  };
1005
1324
  }
1006
1325
 
1007
- export function isProcessAlive(pid: number): boolean {
1008
- if (!Number.isInteger(pid) || pid <= 0) return false;
1009
- try {
1010
- process.kill(pid, 0);
1011
- return true;
1012
- } catch (error) {
1013
- // Only an absent PID proves death; permission and unexpected failures do not.
1014
- return (error as { code?: string }).code !== "ESRCH";
1015
- }
1016
- }
1017
-
1018
1326
  function formatTelegramLockEntry(lock: TelegramLockEntry): string {
1019
1327
  return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
1020
1328
  }
@@ -1135,16 +1443,45 @@ function formatLockState(state: TelegramLockState): string {
1135
1443
 
1136
1444
  function assertTelegramStateTransport(value: unknown): void {
1137
1445
  if (value === undefined) return;
1138
- if (!runtimeStateRecord(value)) throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
1139
- const keys = ["pid", "cwd", "instanceId", "heartbeatMs", "leaderEpoch", "runtimeGeneration", "busSocketPath", "busSecret", "journalPath"];
1140
- if (Object.keys(value).some(key => !keys.includes(key)) ||
1141
- (value.pid === undefined ? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value) :
1142
- !Number.isSafeInteger(value.pid) || (value.pid as number) <= 0) ||
1143
- ["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some(key => value[key] !== undefined && typeof value[key] !== "string") ||
1144
- ["heartbeatMs", "runtimeGeneration"].some(key => value[key] !== undefined && (!Number.isSafeInteger(value[key]) || (value[key] as number) < 0)) ||
1145
- (value.journalPath !== undefined && !readTelegramLockJournalPath(value)) ||
1146
- (value.leaderEpoch !== undefined && (typeof value.leaderEpoch === "string" ? value.leaderEpoch.length === 0 : !Number.isSafeInteger(value.leaderEpoch))))
1147
- throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
1446
+ if (!runtimeStateRecord(value))
1447
+ throw new TelegramRuntimeStateError(
1448
+ "invalid",
1449
+ "Telegram state transport is malformed.",
1450
+ );
1451
+ const keys = [
1452
+ "pid",
1453
+ "cwd",
1454
+ "instanceId",
1455
+ "heartbeatMs",
1456
+ "leaderEpoch",
1457
+ "runtimeGeneration",
1458
+ "busSocketPath",
1459
+ "busSecret",
1460
+ "journalPath",
1461
+ ];
1462
+ if (
1463
+ Object.keys(value).some((key) => !keys.includes(key)) ||
1464
+ (value.pid === undefined
1465
+ ? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value)
1466
+ : !Number.isSafeInteger(value.pid) || (value.pid as number) <= 0) ||
1467
+ ["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some(
1468
+ (key) => value[key] !== undefined && typeof value[key] !== "string",
1469
+ ) ||
1470
+ ["heartbeatMs", "runtimeGeneration"].some(
1471
+ (key) =>
1472
+ value[key] !== undefined &&
1473
+ (!Number.isSafeInteger(value[key]) || (value[key] as number) < 0),
1474
+ ) ||
1475
+ (value.journalPath !== undefined && !readTelegramLockJournalPath(value)) ||
1476
+ (value.leaderEpoch !== undefined &&
1477
+ (typeof value.leaderEpoch === "string"
1478
+ ? value.leaderEpoch.length === 0
1479
+ : !Number.isSafeInteger(value.leaderEpoch)))
1480
+ )
1481
+ throw new TelegramRuntimeStateError(
1482
+ "invalid",
1483
+ "Telegram state transport is malformed.",
1484
+ );
1148
1485
  }
1149
1486
 
1150
1487
  export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
@@ -1152,7 +1489,10 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1152
1489
  ): TelegramLockRuntime<TContext> {
1153
1490
  const key = options.key ?? TELEGRAM_LOCK_KEY;
1154
1491
  if (options.statePath && options.locksPath)
1155
- throw new TelegramRuntimeStateError("invalid", "Telegram transport must select one storage identity.");
1492
+ throw new TelegramRuntimeStateError(
1493
+ "invalid",
1494
+ "Telegram transport must select one storage identity.",
1495
+ );
1156
1496
  const statePath = options.statePath;
1157
1497
  const locksPath = options.locksPath ?? getOwnersPath();
1158
1498
  const pid = options.pid ?? process.pid;
@@ -1175,23 +1515,39 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1175
1515
  if (!statePath) return readLocks(locksPath);
1176
1516
  // Read-only ownership queries fail closed (no owner); acquisition/publication still refuse malformed state.
1177
1517
  try {
1178
- const transports = Object.fromEntries(Object.entries(readTelegramRuntimeState(statePath).profiles)
1179
- .filter(([, value]) => Object.hasOwn(value, "transport")).map(([profile, value]) => [profile, value.transport]));
1518
+ const transports = Object.fromEntries(
1519
+ Object.entries(readTelegramRuntimeState(statePath).profiles)
1520
+ .filter(([, value]) => Object.hasOwn(value, "transport"))
1521
+ .map(([profile, value]) => [profile, value.transport]),
1522
+ );
1180
1523
  assertTelegramStateTransport(transports[resolveEffectiveKey()]);
1181
1524
  return transports;
1182
- } catch { return {}; }
1525
+ } catch {
1526
+ return {};
1527
+ }
1183
1528
  };
1184
- const transactOwners = <T>(mutate: (locks: Record<string, unknown>) => { result: T; changed: boolean }): T => {
1529
+ const transactOwners = <T>(
1530
+ mutate: (locks: Record<string, unknown>) => { result: T; changed: boolean },
1531
+ ): T => {
1185
1532
  if (!statePath) return withLockTransaction(locksPath, mutate);
1186
1533
  const profile = resolveEffectiveKey();
1187
- return mutateTelegramRuntimeStateSection(statePath, profile, "transport", current => {
1188
- assertTelegramStateTransport(current);
1189
- const locks = { [profile]: current };
1190
- const outcome = mutate(locks);
1191
- const value = outcome.changed ? locks[profile] : current;
1192
- assertTelegramStateTransport(value);
1193
- return { value, result: outcome.result };
1194
- }, { ...options.statePublication, isCurrent: () => resolveEffectiveKey() === profile });
1534
+ return mutateTelegramRuntimeStateSection(
1535
+ statePath,
1536
+ profile,
1537
+ "transport",
1538
+ (current) => {
1539
+ assertTelegramStateTransport(current);
1540
+ const locks = { [profile]: current };
1541
+ const outcome = mutate(locks);
1542
+ const value = outcome.changed ? locks[profile] : current;
1543
+ assertTelegramStateTransport(value);
1544
+ return { value, result: outcome.result };
1545
+ },
1546
+ {
1547
+ ...options.statePublication,
1548
+ isCurrent: () => resolveEffectiveKey() === profile,
1549
+ },
1550
+ );
1195
1551
  };
1196
1552
  const readLock = () => {
1197
1553
  const effectiveKey = resolveEffectiveKey();
@@ -1199,16 +1555,37 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1199
1555
  };
1200
1556
  // A live older-release leader polls the same bot from its own directory. Only identity is exposed: its bus
1201
1557
  // endpoint, secret and epoch belong to another protocol and must never become a follower target.
1202
- const readLegacyOwner = (effectiveKey: string): TelegramLockEntry | undefined => {
1203
- if (!options.legacyLocksPath || options.legacyLocksPath === (statePath ?? locksPath)) return undefined;
1558
+ const readLegacyOwner = (
1559
+ effectiveKey: string,
1560
+ ): TelegramLockEntry | undefined => {
1561
+ if (
1562
+ !options.legacyLocksPath ||
1563
+ options.legacyLocksPath === (statePath ?? locksPath)
1564
+ )
1565
+ return undefined;
1204
1566
  try {
1205
- const records = statePath ? readLocksForTransaction(options.legacyLocksPath) : readLocks(options.legacyLocksPath);
1206
- if (statePath) for (const value of Object.values(records)) assertTelegramStateTransport(value);
1567
+ const records = statePath
1568
+ ? readLocksForTransaction(options.legacyLocksPath)
1569
+ : readLocks(options.legacyLocksPath);
1570
+ if (statePath)
1571
+ for (const value of Object.values(records))
1572
+ assertTelegramStateTransport(value);
1207
1573
  const raw = records[effectiveKey];
1208
1574
  const legacy = parseTelegramLockEntry(raw);
1209
- if (!legacy || getLockState(legacy, -1, isAlive, stateOptions()).kind !== "active-elsewhere") return undefined;
1210
- return { pid: legacy.pid, ...(legacy.cwd ? { cwd: legacy.cwd } : {}), ...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
1211
- ...(legacy.heartbeatMs !== undefined ? { heartbeatMs: legacy.heartbeatMs } : {}) };
1575
+ if (
1576
+ !legacy ||
1577
+ getLockState(legacy, -1, isAlive, stateOptions()).kind !==
1578
+ "active-elsewhere"
1579
+ )
1580
+ return undefined;
1581
+ return {
1582
+ pid: legacy.pid,
1583
+ ...(legacy.cwd ? { cwd: legacy.cwd } : {}),
1584
+ ...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
1585
+ ...(legacy.heartbeatMs !== undefined
1586
+ ? { heartbeatMs: legacy.heartbeatMs }
1587
+ : {}),
1588
+ };
1212
1589
  } catch (error) {
1213
1590
  if (statePath) throw error;
1214
1591
  return undefined;
@@ -1240,7 +1617,11 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1240
1617
  transactOwners<TelegramLockAcquireResult>((locks) => {
1241
1618
  const effectiveKey = resolveEffectiveKey();
1242
1619
  const legacy = readLegacyOwner(effectiveKey);
1243
- if (legacy) return { result: { ok: false, lock: legacy } as const, changed: false };
1620
+ if (legacy)
1621
+ return {
1622
+ result: { ok: false, lock: legacy } as const,
1623
+ changed: false,
1624
+ };
1244
1625
  const current = parseTelegramLockEntry(locks[effectiveKey]);
1245
1626
  const state = getLockState(current, pid, isAlive, stateOptions());
1246
1627
  const expectedOwned = adoptCompatibleOwnedLock(
@@ -1302,7 +1683,9 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1302
1683
  changed: false,
1303
1684
  };
1304
1685
  }
1305
- const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ?? options.createJournalPath?.(ctx);
1686
+ const journalPath =
1687
+ readTelegramLockJournalPath(locks[effectiveKey]) ??
1688
+ options.createJournalPath?.(ctx);
1306
1689
  const lock = createLockEntry(pid, ctx, {
1307
1690
  journalPath,
1308
1691
  instanceId: options.instanceId,
@@ -1353,7 +1736,8 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1353
1736
  });
1354
1737
  },
1355
1738
  getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
1356
- getJournalPath: () => readTelegramLockJournalPath(readOwners()[resolveEffectiveKey()]),
1739
+ getJournalPath: () =>
1740
+ readTelegramLockJournalPath(readOwners()[resolveEffectiveKey()]),
1357
1741
  getStatusLabel: () =>
1358
1742
  formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
1359
1743
  getOwnedLeaderEpoch: () => {
@@ -1373,7 +1757,8 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1373
1757
  );
1374
1758
  },
1375
1759
  commitIfOwned: (commit) =>
1376
- !deliveryRevoked && transactOwners((locks) => {
1760
+ !deliveryRevoked &&
1761
+ transactOwners((locks) => {
1377
1762
  const effectiveKey = resolveEffectiveKey();
1378
1763
  const lock = parseTelegramLockEntry(locks[effectiveKey]);
1379
1764
  const exactOwner =
@@ -1387,35 +1772,83 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1387
1772
  }
1388
1773
  commit();
1389
1774
  // External-file commits may retain an independent effect; never acknowledge under a changed unified owner.
1390
- const current = !statePath || (!deliveryRevoked && hasSameLockOwner(parseTelegramLockEntry(readOwners()[effectiveKey]), ownedLock));
1775
+ const current =
1776
+ !statePath ||
1777
+ (!deliveryRevoked &&
1778
+ hasSameLockOwner(
1779
+ parseTelegramLockEntry(readOwners()[effectiveKey]),
1780
+ ownedLock,
1781
+ ));
1391
1782
  return { result: current, changed: false };
1392
1783
  }),
1393
1784
  publishStateSectionIfOwned<T>(
1394
1785
  section: "workspace" | "runtime",
1395
- mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
1786
+ mutate: TelegramRuntimeStateSectionReducer<T>,
1396
1787
  publication: TelegramOwnedStatePublicationOptions,
1397
1788
  ): TelegramOwnedStatePublicationResult<T> {
1398
1789
  if (section !== "workspace" && section !== "runtime")
1399
- throw new TelegramRuntimeStateError("invalid", "Telegram owner section publication is restricted to Workspace and runtime observations.");
1400
- if (!statePath) throw new TelegramRuntimeStateError("invalid", "Consolidated transport storage is not selected.");
1401
- const profile = resolveEffectiveKey(), expected = ownedLock ? { ...ownedLock } : undefined;
1402
- if (publication.expectedScope && (publication.expectedScope.path !== statePath || publication.expectedScope.profile !== profile))
1790
+ throw new TelegramRuntimeStateError(
1791
+ "invalid",
1792
+ "Telegram owner section publication is restricted to Workspace and runtime observations.",
1793
+ );
1794
+ if (!statePath)
1795
+ throw new TelegramRuntimeStateError(
1796
+ "invalid",
1797
+ "Consolidated transport storage is not selected.",
1798
+ );
1799
+ const profile = resolveEffectiveKey(),
1800
+ expected = ownedLock ? { ...ownedLock } : undefined;
1801
+ if (
1802
+ publication.expectedScope &&
1803
+ (publication.expectedScope.path !== statePath ||
1804
+ publication.expectedScope.profile !== profile)
1805
+ )
1403
1806
  return { committed: false };
1404
- if (deliveryRevoked || ownedLockKey !== profile || !expected || !publication.isCurrent()) return { committed: false };
1405
- const isCurrent = (): boolean => !deliveryRevoked && resolveEffectiveKey() === profile && ownedLockKey === profile &&
1406
- hasSameLockOwner(ownedLock, expected) && publication.isCurrent() &&
1407
- hasSameLockOwner(parseTelegramLockEntry(readOwners()[profile]), expected);
1807
+ if (
1808
+ deliveryRevoked ||
1809
+ ownedLockKey !== profile ||
1810
+ !expected ||
1811
+ !publication.isCurrent()
1812
+ )
1813
+ return { committed: false };
1814
+ const isCurrent = (): boolean =>
1815
+ !deliveryRevoked &&
1816
+ resolveEffectiveKey() === profile &&
1817
+ ownedLockKey === profile &&
1818
+ hasSameLockOwner(ownedLock, expected) &&
1819
+ publication.isCurrent() &&
1820
+ hasSameLockOwner(
1821
+ parseTelegramLockEntry(readOwners()[profile]),
1822
+ expected,
1823
+ );
1408
1824
  if (!isCurrent()) return { committed: false };
1409
- return mutateTelegramRuntimeStateSection<TelegramOwnedStatePublicationResult<T>>(statePath, profile, section, (current, observed) => {
1410
- assertTelegramStateTransport(observed.transport);
1411
- if (!hasSameLockOwner(parseTelegramLockEntry(observed.transport), expected))
1412
- return { value: current, result: { committed: false } as const };
1413
- const outcome = mutate(current, observed);
1414
- return { value: outcome.value, result: { committed: true, result: outcome.result } as const };
1415
- }, { ...publication, isCurrent });
1825
+ return mutateTelegramRuntimeStateSection<
1826
+ TelegramOwnedStatePublicationResult<T>
1827
+ >(
1828
+ statePath,
1829
+ profile,
1830
+ section,
1831
+ (current, observed) => {
1832
+ assertTelegramStateTransport(observed.transport);
1833
+ if (
1834
+ !hasSameLockOwner(
1835
+ parseTelegramLockEntry(observed.transport),
1836
+ expected,
1837
+ )
1838
+ )
1839
+ return { value: current, result: { committed: false } as const };
1840
+ const outcome = mutate(current, observed);
1841
+ return {
1842
+ value: outcome.value,
1843
+ result: { committed: true, result: outcome.result } as const,
1844
+ };
1845
+ },
1846
+ { ...publication, isCurrent },
1847
+ );
1416
1848
  },
1417
1849
  refresh: (ctx) =>
1418
- !deliveryRevoked && transactOwners((locks) => {
1850
+ !deliveryRevoked &&
1851
+ transactOwners((locks) => {
1419
1852
  const effectiveKey = resolveEffectiveKey();
1420
1853
  const lock = parseTelegramLockEntry(locks[effectiveKey]);
1421
1854
  const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
@@ -1564,8 +1997,7 @@ export function createTelegramLockedPollingRuntime<
1564
1997
  let suspendedGeneration: number | undefined;
1565
1998
  let suspensionsInFlight = 0;
1566
1999
  let startupsInFlight = 0;
1567
- const ownershipCheckMs =
1568
- deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
2000
+ const ownershipCheckMs = deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
1569
2001
  const ownershipRefreshMs =
1570
2002
  deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
1571
2003
  const stopOwnershipWatcher = () => {
@@ -1593,7 +2025,11 @@ export function createTelegramLockedPollingRuntime<
1593
2025
  }
1594
2026
  await deps.stopPolling();
1595
2027
  // Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
1596
- if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
2028
+ if (
2029
+ generation === pollingGeneration &&
2030
+ suspensionsInFlight === 1 &&
2031
+ startupsInFlight === 0
2032
+ ) {
1597
2033
  suspendedGeneration = generation;
1598
2034
  }
1599
2035
  } finally {
@@ -1693,7 +2129,10 @@ export function createTelegramLockedPollingRuntime<
1693
2129
  const stop = async (): Promise<string> => {
1694
2130
  const generation = pollingGeneration + 1;
1695
2131
  await suspendPolling();
1696
- if (generation !== pollingGeneration) throw new Error("Telegram disconnect was superseded by a new connection.");
2132
+ if (generation !== pollingGeneration)
2133
+ throw new Error(
2134
+ "Telegram disconnect was superseded by a new connection.",
2135
+ );
1697
2136
  const state = deps.lock.release();
1698
2137
  deps.onTransportAvailabilityChanged?.();
1699
2138
  if (state.kind === "active-elsewhere") {
@@ -1723,15 +2162,24 @@ export function createTelegramLockedPollingRuntime<
1723
2162
  };
1724
2163
  if (deps.isContextCurrent?.(ctx) === false) return cancelled;
1725
2164
  const generation = ++pollingGeneration;
1726
- const isCurrent = () => generation === pollingGeneration &&
2165
+ const isCurrent = () =>
2166
+ generation === pollingGeneration &&
1727
2167
  (deps.isContextCurrent?.(ctx) ?? true);
1728
2168
  if (ownershipStop) await ownershipStop;
1729
2169
  if (!isCurrent()) return cancelled;
1730
2170
  if (!options.election && deps.resetDamagedState) {
1731
2171
  try {
1732
- if (deps.resetDamagedState()) deps.recordRuntimeEvent?.("lock",
1733
- new Error("Damaged Telegram runtime state was reset; previous runtime continuity was discarded."), { phase: "state-reset" });
1734
- } catch (error) { deps.recordRuntimeEvent?.("lock", error, { phase: "state-reset" }); }
2172
+ if (deps.resetDamagedState())
2173
+ deps.recordRuntimeEvent?.(
2174
+ "lock",
2175
+ new Error(
2176
+ "Damaged Telegram runtime state was reset; previous runtime continuity was discarded.",
2177
+ ),
2178
+ { phase: "state-reset" },
2179
+ );
2180
+ } catch (error) {
2181
+ deps.recordRuntimeEvent?.("lock", error, { phase: "state-reset" });
2182
+ }
1735
2183
  }
1736
2184
  let acquired = deps.lock.acquire(ctx, {
1737
2185
  force: options.force,
@@ -1822,7 +2270,10 @@ export function createTelegramLockedPollingRuntime<
1822
2270
  return {
1823
2271
  isCurrent,
1824
2272
  async stop() {
1825
- if (!isCurrent()) throw new Error("Telegram disconnect was superseded by a new connection.");
2273
+ if (!isCurrent())
2274
+ throw new Error(
2275
+ "Telegram disconnect was superseded by a new connection.",
2276
+ );
1826
2277
  return stop();
1827
2278
  },
1828
2279
  };
@@ -1830,12 +2281,20 @@ export function createTelegramLockedPollingRuntime<
1830
2281
  suspend: suspendPolling,
1831
2282
  captureTransportAuthority(ctx) {
1832
2283
  const generation = pollingGeneration;
1833
- const current = () => generation === pollingGeneration && activeContext === ctx && !ownershipStop &&
1834
- (deps.isContextCurrent?.(ctx) ?? true) && deps.lock.owns(snapshotLockContext(ctx));
2284
+ const current = () =>
2285
+ generation === pollingGeneration &&
2286
+ activeContext === ctx &&
2287
+ !ownershipStop &&
2288
+ (deps.isContextCurrent?.(ctx) ?? true) &&
2289
+ deps.lock.owns(snapshotLockContext(ctx));
1835
2290
  return current() ? current : undefined;
1836
2291
  },
1837
- isSuspended: () => suspendedGeneration === pollingGeneration &&
1838
- suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
2292
+ isSuspended: () =>
2293
+ suspendedGeneration === pollingGeneration &&
2294
+ suspensionsInFlight === 0 &&
2295
+ startupsInFlight === 0 &&
2296
+ !sessionAutoStartRun &&
2297
+ !ownershipStop,
1839
2298
  onPersistentConflict: async (ctx, count) => {
1840
2299
  if (activeContext === undefined || ownershipStop) return;
1841
2300
  if (!(deps.isContextCurrent?.(ctx) ?? activeContext === ctx)) return;
@@ -1858,15 +2317,23 @@ export function createTelegramLockedPollingRuntime<
1858
2317
  }
1859
2318
  ownershipStop = Promise.resolve()
1860
2319
  .then(() => deps.stopPolling())
1861
- .catch((error) => { cleanupErrors.push(String(error)); })
2320
+ .catch((error) => {
2321
+ cleanupErrors.push(String(error));
2322
+ })
1862
2323
  .finally(() => {
1863
2324
  ownershipStop = undefined;
1864
- deps.recordRuntimeEvent?.("polling", ownership === "lost"
1865
- ? "Telegram transport stopped: local ownership lost; check for another Pi instance."
1866
- : "Telegram transport stopped: competing getUpdates client or ownership mismatch.", {
1867
- phase: "persistent-conflict", count, ownership,
1868
- ...(cleanupErrors.length ? { cleanupErrors } : {}),
1869
- });
2325
+ deps.recordRuntimeEvent?.(
2326
+ "polling",
2327
+ ownership === "lost"
2328
+ ? "Telegram transport stopped: local ownership lost; check for another Pi instance."
2329
+ : "Telegram transport stopped: competing getUpdates client or ownership mismatch.",
2330
+ {
2331
+ phase: "persistent-conflict",
2332
+ count,
2333
+ ownership,
2334
+ ...(cleanupErrors.length ? { cleanupErrors } : {}),
2335
+ },
2336
+ );
1870
2337
  deps.updateStatus(ctx);
1871
2338
  });
1872
2339
  deps.onTransportAvailabilityChanged?.();
@@ -1895,7 +2362,8 @@ export function createTelegramLockedPollingRuntime<
1895
2362
  }
1896
2363
  if (deps.isContextCurrent?.(ctx) === false) return;
1897
2364
  const generation = ++pollingGeneration;
1898
- const isCurrent = () => generation === pollingGeneration &&
2365
+ const isCurrent = () =>
2366
+ generation === pollingGeneration &&
1899
2367
  (deps.isContextCurrent?.(ctx) ?? true);
1900
2368
  const startedAtMs = Date.now();
1901
2369
  deps.recordRuntimeEvent?.("lock", "Telegram auto-start scheduled", {
@@ -1906,15 +2374,21 @@ export function createTelegramLockedPollingRuntime<
1906
2374
  await new Promise((resolve) => setTimeout(resolve, 0));
1907
2375
  if (ownershipStop) await ownershipStop;
1908
2376
  if (!isCurrent()) return;
1909
- if (canRestoreRememberedFollower && state?.kind === "active-elsewhere") {
2377
+ if (
2378
+ canRestoreRememberedFollower &&
2379
+ state?.kind === "active-elsewhere"
2380
+ ) {
1910
2381
  const restored = await deps.restoreFollowerWithOwner?.(
1911
2382
  ctx,
1912
2383
  state.lock,
1913
2384
  );
1914
2385
  if (!isCurrent()) return;
1915
2386
  if (!restored) {
1916
- deps.recordRuntimeEvent?.("lock", "Telegram follower auto-connect did not restore this session.",
1917
- { phase: "follower-auto-connect-unavailable" });
2387
+ deps.recordRuntimeEvent?.(
2388
+ "lock",
2389
+ "Telegram follower auto-connect did not restore this session.",
2390
+ { phase: "follower-auto-connect-unavailable" },
2391
+ );
1918
2392
  return;
1919
2393
  }
1920
2394
  deps.onTransportAvailabilityChanged?.();