@llblab/pi-telegram 0.52.2 → 0.53.1

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 +15 -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 +65 -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 +252 -127
  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 +128 -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 +516 -301
  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/dist/lib/locks.js CHANGED
@@ -3,12 +3,13 @@
3
3
  * Zones: telegram ownership, filesystem, transport authority
4
4
  * Owns filesystem authority, atomic runtime-section publication and Telegram bridge ownership semantics
5
5
  */
6
- import { chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
7
6
  import { randomUUID } from "node:crypto";
8
- import { isDeepStrictEqual } from "node:util";
7
+ import { chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
9
8
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
10
- import { isWireRecord as runtimeStateRecord } from "./wire.js";
9
+ import { isDeepStrictEqual } from "node:util";
11
10
  import { isTelegramSessionPollingJournalPath, resolveTelegramOwnersPath, resolveTelegramSessionPollingJournalPath, resolveTelegramUpdateJournalPathForProfile, } from "./paths.js";
11
+ import { isProcessAlive } from "./process-identity.js";
12
+ import { isWireRecord as runtimeStateRecord } from "./wire.js";
12
13
  export const TELEGRAM_LOCK_KEY = "default";
13
14
  export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
14
15
  export const TELEGRAM_OWNERSHIP_CHECK_MS = 1_000;
@@ -56,7 +57,9 @@ export function createTelegramOwnedStateAuthorityCapture(lock, session) {
56
57
  const epoch = lock.getOwnedLeaderEpoch();
57
58
  if (epoch === undefined)
58
59
  return undefined;
59
- const current = () => session.isCurrent(ctx, generation) && lock.owns(ctx) && lock.getOwnedLeaderEpoch() === epoch;
60
+ const current = () => session.isCurrent(ctx, generation) &&
61
+ lock.owns(ctx) &&
62
+ lock.getOwnedLeaderEpoch() === epoch;
60
63
  return current() ? current : undefined;
61
64
  };
62
65
  }
@@ -71,16 +74,20 @@ export function createTelegramLeaderJournalPathResolver(deps) {
71
74
  if (existsSync(root))
72
75
  return root;
73
76
  const sessionId = deps.getSessionId();
74
- return sessionId === undefined ? undefined : resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
77
+ return sessionId === undefined
78
+ ? undefined
79
+ : resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
75
80
  };
76
81
  return {
77
82
  createJournalPath: () => createOwn(),
78
83
  resolve(profileName) {
79
84
  const named = deps.getNamedJournalPath();
80
- if (named && (isTelegramSessionPollingJournalPath(named) ||
81
- named === resolveTelegramUpdateJournalPathForProfile(profileName)))
85
+ if (named &&
86
+ (isTelegramSessionPollingJournalPath(named) ||
87
+ named === resolveTelegramUpdateJournalPathForProfile(profileName)))
82
88
  return named;
83
- return createOwn(profileName) ?? resolveTelegramUpdateJournalPathForProfile(profileName);
89
+ return (createOwn(profileName) ??
90
+ resolveTelegramUpdateJournalPathForProfile(profileName));
84
91
  },
85
92
  };
86
93
  }
@@ -128,6 +135,93 @@ function sleepSync(ms) {
128
135
  const buffer = new SharedArrayBuffer(4);
129
136
  Atomics.wait(new Int32Array(buffer), 0, 0, ms);
130
137
  }
138
+ /** Typed private-file refusal; owners map the failure into their own error vocabulary. */
139
+ export class TelegramPrivateFileError extends Error {
140
+ failure;
141
+ constructor(failure, message) {
142
+ super(message);
143
+ this.failure = failure;
144
+ this.name = "TelegramPrivateFileError";
145
+ }
146
+ }
147
+ /**
148
+ * Read flags for private runtime files. Where the platform offers them, no-follow and non-blocking opens refuse a
149
+ * swapped link or FIFO at open time. Windows has neither flag and no FIFOs in the file namespace; there the
150
+ * lstat-before/fstat-after identity binding every strict reader performs is the guard, so a link swapped in after
151
+ * inspection opens a different file and is refused as changed.
152
+ */
153
+ export const TELEGRAM_STRICT_READ_FLAGS = constants.O_RDONLY |
154
+ (constants.O_NOFOLLOW ?? 0) |
155
+ (constants.O_NONBLOCK ?? 0);
156
+ /** POSIX owner and permission bits; Windows has no uid, so its ACL-protected agent directory carries privacy. */
157
+ export function isTelegramOwnerPrivate(stat) {
158
+ const uid = process.getuid?.();
159
+ return (uid === undefined ||
160
+ (stat.uid === BigInt(uid) && (stat.mode & 63n) === 0n));
161
+ }
162
+ /**
163
+ * Read one owner-private, single-link, bounded regular file without following links. Absence returns undefined;
164
+ * an inode or metadata change between inspection and open refuses as `changed`.
165
+ */
166
+ export function readTelegramPrivateFile(path, maxBytes) {
167
+ let before;
168
+ try {
169
+ before = lstatSync(path, { bigint: true });
170
+ }
171
+ catch (error) {
172
+ if (error?.code === "ENOENT")
173
+ return undefined;
174
+ throw error;
175
+ }
176
+ if (!before.isFile() ||
177
+ before.isSymbolicLink() ||
178
+ !isTelegramOwnerPrivate(before) ||
179
+ before.nlink !== 1n ||
180
+ before.size > BigInt(maxBytes)) {
181
+ throw new TelegramPrivateFileError(before.size > BigInt(maxBytes) ? "capacity" : "unsafe", "Telegram private file is not a bounded no-follow regular file.");
182
+ }
183
+ const fd = openSync(path, TELEGRAM_STRICT_READ_FLAGS);
184
+ try {
185
+ const opened = fstatSync(fd, { bigint: true });
186
+ if (!opened.isFile() ||
187
+ opened.dev !== before.dev ||
188
+ opened.ino !== before.ino ||
189
+ opened.uid !== before.uid ||
190
+ opened.nlink !== 1n ||
191
+ !isTelegramOwnerPrivate(opened) ||
192
+ opened.size !== before.size ||
193
+ opened.mtimeNs !== before.mtimeNs) {
194
+ throw new TelegramPrivateFileError("changed", "Telegram private file changed during inspection.");
195
+ }
196
+ return readFileSync(fd, "utf8");
197
+ }
198
+ finally {
199
+ closeSync(fd);
200
+ }
201
+ }
202
+ /** Atomically publish owner-private contents through a unique staging file; the rename consumes staging. */
203
+ export function publishTelegramPrivateFile(path, temporaryBasePath, contents, label, onBoundary) {
204
+ const temporaryPath = `${temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
205
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
206
+ mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
207
+ try {
208
+ writeFileSync(temporaryPath, contents, { encoding: "utf8", mode: 0o600 });
209
+ chmodSync(temporaryPath, 0o600);
210
+ onBoundary?.("after-write-before-rename");
211
+ if (!renameTelegramPathWithRetry(temporaryPath, path))
212
+ throw new Error(`${label} staging file disappeared before publication.`);
213
+ chmodSync(path, 0o600);
214
+ onBoundary?.("after-rename");
215
+ }
216
+ finally {
217
+ try {
218
+ unlinkSync(temporaryPath);
219
+ }
220
+ catch {
221
+ /* Atomic rename consumes the temporary path. */
222
+ }
223
+ }
224
+ }
131
225
  /** Rename one Telegram runtime artifact with bounded Windows sharing retries. */
132
226
  export function renameTelegramPathWithRetry(sourcePath, destinationPath, options = {}) {
133
227
  const rename = options.rename ?? renameSync;
@@ -543,7 +637,10 @@ function requireRuntimeStatePath(path) {
543
637
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state path must be canonical and absolute.");
544
638
  }
545
639
  function requireRuntimeStateProfile(profile) {
546
- if (typeof profile !== "string" || !profile.length || profile.length > 512 || /[\x00-\x1f]/u.test(profile))
640
+ if (typeof profile !== "string" ||
641
+ !profile.length ||
642
+ profile.length > 512 ||
643
+ /[\x00-\x1f]/u.test(profile))
547
644
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state profile is invalid.");
548
645
  }
549
646
  /** Private single-link regular file read with identity continuity; initial absence only is positive. */
@@ -554,13 +651,17 @@ function readTelegramRuntimeSource(path) {
554
651
  try {
555
652
  const stat = lstatSync(path);
556
653
  observed = true;
557
- if (!stat.isFile() || stat.nlink !== 1 ||
654
+ if (!stat.isFile() ||
655
+ stat.nlink !== 1 ||
558
656
  (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
559
657
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state must be a private regular file.");
560
658
  const fd = openSync(path, "r");
561
659
  try {
562
660
  const opened = fstatSync(fd);
563
- if (opened.dev !== stat.dev || opened.ino !== stat.ino || opened.nlink !== 1 || !opened.isFile() ||
661
+ if (opened.dev !== stat.dev ||
662
+ opened.ino !== stat.ino ||
663
+ opened.nlink !== 1 ||
664
+ !opened.isFile() ||
564
665
  (process.platform !== "win32" && (opened.mode & 0o077) !== 0))
565
666
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state identity changed before reading.");
566
667
  source = readFileSync(fd).toString("utf8");
@@ -569,7 +670,11 @@ function readTelegramRuntimeSource(path) {
569
670
  closeSync(fd);
570
671
  }
571
672
  const after = lstatSync(path);
572
- if (after.dev !== stat.dev || after.ino !== stat.ino || after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs)
673
+ if (after.dev !== stat.dev ||
674
+ after.ino !== stat.ino ||
675
+ after.size !== stat.size ||
676
+ after.mtimeMs !== stat.mtimeMs ||
677
+ after.ctimeMs !== stat.ctimeMs)
573
678
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state changed during observation.");
574
679
  }
575
680
  catch (error) {
@@ -591,12 +696,15 @@ export function readTelegramRuntimeState(path) {
591
696
  catch (error) {
592
697
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state is unreadable.", { cause: error });
593
698
  }
594
- if (!runtimeStateRecord(value) || value.version !== 2 || !runtimeStateRecord(value.profiles) ||
595
- Object.keys(value).some(key => key !== "version" && key !== "profiles"))
699
+ if (!runtimeStateRecord(value) ||
700
+ value.version !== 2 ||
701
+ !runtimeStateRecord(value.profiles) ||
702
+ Object.keys(value).some((key) => key !== "version" && key !== "profiles"))
596
703
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state envelope is unsupported or malformed.");
597
704
  for (const [profile, sections] of Object.entries(value.profiles)) {
598
705
  requireRuntimeStateProfile(profile);
599
- if (!runtimeStateRecord(sections) || Object.keys(sections).some(key => !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(key)))
706
+ if (!runtimeStateRecord(sections) ||
707
+ Object.keys(sections).some((key) => !TELEGRAM_RUNTIME_STATE_SECTIONS.includes(key)))
600
708
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state sections are malformed.");
601
709
  }
602
710
  return value;
@@ -672,12 +780,19 @@ export function mutateTelegramRuntimeStateSection(path, profile, section, mutate
672
780
  return withTelegramFileTransaction(join(runtimeDir, `${basename(path)}.transaction`), () => {
673
781
  assertCurrent();
674
782
  const file = readTelegramRuntimeState(path);
675
- const previous = Object.hasOwn(file.profiles, profile) ? file.profiles[profile] : {};
783
+ const previous = Object.hasOwn(file.profiles, profile)
784
+ ? file.profiles[profile]
785
+ : {};
676
786
  const outcome = mutate(structuredClone(previous[section]), structuredClone(previous));
677
- if (!runtimeStateRecord(outcome) || !Object.hasOwn(outcome, "value") || !Object.hasOwn(outcome, "result") || "then" in outcome)
787
+ if (!runtimeStateRecord(outcome) ||
788
+ !Object.hasOwn(outcome, "value") ||
789
+ !Object.hasOwn(outcome, "result") ||
790
+ "then" in outcome)
678
791
  throw new TelegramRuntimeStateError("invalid", "Telegram runtime state mutations must return a synchronous value and result.");
679
792
  // Normalize intentional optional properties to their wire representation before comparison/publication.
680
- const next = outcome.value === undefined ? undefined : JSON.parse(JSON.stringify(outcome.value));
793
+ const next = outcome.value === undefined
794
+ ? undefined
795
+ : JSON.parse(JSON.stringify(outcome.value));
681
796
  assertCurrent();
682
797
  if (isDeepStrictEqual(previous[section], next))
683
798
  return outcome.result;
@@ -687,18 +802,32 @@ export function mutateTelegramRuntimeStateSection(path, profile, section, mutate
687
802
  else
688
803
  updated[section] = next;
689
804
  if (Object.keys(updated).length)
690
- Object.defineProperty(file.profiles, profile, { value: updated, enumerable: true, configurable: true, writable: true });
805
+ Object.defineProperty(file.profiles, profile, {
806
+ value: updated,
807
+ enumerable: true,
808
+ configurable: true,
809
+ writable: true,
810
+ });
691
811
  else
692
812
  delete file.profiles[profile];
693
813
  const tempPath = join(runtimeDir, `${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
694
814
  try {
695
815
  options.onPublicationBoundary?.("before-write");
696
816
  assertCurrent();
697
- writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
817
+ writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, {
818
+ encoding: "utf8",
819
+ flag: "wx",
820
+ mode: 0o600,
821
+ });
698
822
  options.onPublicationBoundary?.("after-write-before-rename");
699
823
  assertCurrent();
700
824
  try {
701
- if (!renameTelegramPathWithRetry(tempPath, path, { rename: options.publishRename }))
825
+ if (!renameTelegramPathWithRetry(tempPath, path, {
826
+ rename(from, to) {
827
+ assertCurrent();
828
+ (options.publishRename ?? renameSync)(from, to);
829
+ },
830
+ }))
702
831
  throw new Error("Telegram runtime state temporary publication disappeared.");
703
832
  options.onPublicationBoundary?.("after-rename");
704
833
  assertCurrent();
@@ -785,21 +914,11 @@ export function parseTelegramLockEntry(value) {
785
914
  ? record.busSocketPath
786
915
  : undefined,
787
916
  busSecret: typeof record.busSecret === "string" ? record.busSecret : undefined,
788
- ...(readTelegramLockJournalPath(record) ? { journalPath: readTelegramLockJournalPath(record) } : {}),
917
+ ...(readTelegramLockJournalPath(record)
918
+ ? { journalPath: readTelegramLockJournalPath(record) }
919
+ : {}),
789
920
  };
790
921
  }
791
- export function isProcessAlive(pid) {
792
- if (!Number.isInteger(pid) || pid <= 0)
793
- return false;
794
- try {
795
- process.kill(pid, 0);
796
- return true;
797
- }
798
- catch (error) {
799
- // Only an absent PID proves death; permission and unexpected failures do not.
800
- return error.code !== "ESRCH";
801
- }
802
- }
803
922
  function formatTelegramLockEntry(lock) {
804
923
  return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
805
924
  }
@@ -883,14 +1002,29 @@ function assertTelegramStateTransport(value) {
883
1002
  return;
884
1003
  if (!runtimeStateRecord(value))
885
1004
  throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
886
- const keys = ["pid", "cwd", "instanceId", "heartbeatMs", "leaderEpoch", "runtimeGeneration", "busSocketPath", "busSecret", "journalPath"];
887
- if (Object.keys(value).some(key => !keys.includes(key)) ||
888
- (value.pid === undefined ? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value) :
889
- !Number.isSafeInteger(value.pid) || value.pid <= 0) ||
890
- ["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some(key => value[key] !== undefined && typeof value[key] !== "string") ||
891
- ["heartbeatMs", "runtimeGeneration"].some(key => value[key] !== undefined && (!Number.isSafeInteger(value[key]) || value[key] < 0)) ||
1005
+ const keys = [
1006
+ "pid",
1007
+ "cwd",
1008
+ "instanceId",
1009
+ "heartbeatMs",
1010
+ "leaderEpoch",
1011
+ "runtimeGeneration",
1012
+ "busSocketPath",
1013
+ "busSecret",
1014
+ "journalPath",
1015
+ ];
1016
+ if (Object.keys(value).some((key) => !keys.includes(key)) ||
1017
+ (value.pid === undefined
1018
+ ? Object.keys(value).length !== 1 || !readTelegramLockJournalPath(value)
1019
+ : !Number.isSafeInteger(value.pid) || value.pid <= 0) ||
1020
+ ["cwd", "instanceId", "busSocketPath", "busSecret", "journalPath"].some((key) => value[key] !== undefined && typeof value[key] !== "string") ||
1021
+ ["heartbeatMs", "runtimeGeneration"].some((key) => value[key] !== undefined &&
1022
+ (!Number.isSafeInteger(value[key]) || value[key] < 0)) ||
892
1023
  (value.journalPath !== undefined && !readTelegramLockJournalPath(value)) ||
893
- (value.leaderEpoch !== undefined && (typeof value.leaderEpoch === "string" ? value.leaderEpoch.length === 0 : !Number.isSafeInteger(value.leaderEpoch))))
1024
+ (value.leaderEpoch !== undefined &&
1025
+ (typeof value.leaderEpoch === "string"
1026
+ ? value.leaderEpoch.length === 0
1027
+ : !Number.isSafeInteger(value.leaderEpoch))))
894
1028
  throw new TelegramRuntimeStateError("invalid", "Telegram state transport is malformed.");
895
1029
  }
896
1030
  export function createTelegramLockRuntime(options = {}) {
@@ -921,7 +1055,8 @@ export function createTelegramLockRuntime(options = {}) {
921
1055
  // Read-only ownership queries fail closed (no owner); acquisition/publication still refuse malformed state.
922
1056
  try {
923
1057
  const transports = Object.fromEntries(Object.entries(readTelegramRuntimeState(statePath).profiles)
924
- .filter(([, value]) => Object.hasOwn(value, "transport")).map(([profile, value]) => [profile, value.transport]));
1058
+ .filter(([, value]) => Object.hasOwn(value, "transport"))
1059
+ .map(([profile, value]) => [profile, value.transport]));
925
1060
  assertTelegramStateTransport(transports[resolveEffectiveKey()]);
926
1061
  return transports;
927
1062
  }
@@ -933,14 +1068,17 @@ export function createTelegramLockRuntime(options = {}) {
933
1068
  if (!statePath)
934
1069
  return withLockTransaction(locksPath, mutate);
935
1070
  const profile = resolveEffectiveKey();
936
- return mutateTelegramRuntimeStateSection(statePath, profile, "transport", current => {
1071
+ return mutateTelegramRuntimeStateSection(statePath, profile, "transport", (current) => {
937
1072
  assertTelegramStateTransport(current);
938
1073
  const locks = { [profile]: current };
939
1074
  const outcome = mutate(locks);
940
1075
  const value = outcome.changed ? locks[profile] : current;
941
1076
  assertTelegramStateTransport(value);
942
1077
  return { value, result: outcome.result };
943
- }, { ...options.statePublication, isCurrent: () => resolveEffectiveKey() === profile });
1078
+ }, {
1079
+ ...options.statePublication,
1080
+ isCurrent: () => resolveEffectiveKey() === profile,
1081
+ });
944
1082
  };
945
1083
  const readLock = () => {
946
1084
  const effectiveKey = resolveEffectiveKey();
@@ -949,19 +1087,30 @@ export function createTelegramLockRuntime(options = {}) {
949
1087
  // A live older-release leader polls the same bot from its own directory. Only identity is exposed: its bus
950
1088
  // endpoint, secret and epoch belong to another protocol and must never become a follower target.
951
1089
  const readLegacyOwner = (effectiveKey) => {
952
- if (!options.legacyLocksPath || options.legacyLocksPath === (statePath ?? locksPath))
1090
+ if (!options.legacyLocksPath ||
1091
+ options.legacyLocksPath === (statePath ?? locksPath))
953
1092
  return undefined;
954
1093
  try {
955
- const records = statePath ? readLocksForTransaction(options.legacyLocksPath) : readLocks(options.legacyLocksPath);
1094
+ const records = statePath
1095
+ ? readLocksForTransaction(options.legacyLocksPath)
1096
+ : readLocks(options.legacyLocksPath);
956
1097
  if (statePath)
957
1098
  for (const value of Object.values(records))
958
1099
  assertTelegramStateTransport(value);
959
1100
  const raw = records[effectiveKey];
960
1101
  const legacy = parseTelegramLockEntry(raw);
961
- if (!legacy || getLockState(legacy, -1, isAlive, stateOptions()).kind !== "active-elsewhere")
1102
+ if (!legacy ||
1103
+ getLockState(legacy, -1, isAlive, stateOptions()).kind !==
1104
+ "active-elsewhere")
962
1105
  return undefined;
963
- return { pid: legacy.pid, ...(legacy.cwd ? { cwd: legacy.cwd } : {}), ...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
964
- ...(legacy.heartbeatMs !== undefined ? { heartbeatMs: legacy.heartbeatMs } : {}) };
1106
+ return {
1107
+ pid: legacy.pid,
1108
+ ...(legacy.cwd ? { cwd: legacy.cwd } : {}),
1109
+ ...(legacy.instanceId ? { instanceId: legacy.instanceId } : {}),
1110
+ ...(legacy.heartbeatMs !== undefined
1111
+ ? { heartbeatMs: legacy.heartbeatMs }
1112
+ : {}),
1113
+ };
965
1114
  }
966
1115
  catch (error) {
967
1116
  if (statePath)
@@ -990,7 +1139,10 @@ export function createTelegramLockRuntime(options = {}) {
990
1139
  const effectiveKey = resolveEffectiveKey();
991
1140
  const legacy = readLegacyOwner(effectiveKey);
992
1141
  if (legacy)
993
- return { result: { ok: false, lock: legacy }, changed: false };
1142
+ return {
1143
+ result: { ok: false, lock: legacy },
1144
+ changed: false,
1145
+ };
994
1146
  const current = parseTelegramLockEntry(locks[effectiveKey]);
995
1147
  const state = getLockState(current, pid, isAlive, stateOptions());
996
1148
  const expectedOwned = adoptCompatibleOwnedLock(effectiveKey, current, ctx);
@@ -1032,7 +1184,8 @@ export function createTelegramLockRuntime(options = {}) {
1032
1184
  changed: false,
1033
1185
  };
1034
1186
  }
1035
- const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ?? options.createJournalPath?.(ctx);
1187
+ const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ??
1188
+ options.createJournalPath?.(ctx);
1036
1189
  const lock = createLockEntry(pid, ctx, {
1037
1190
  journalPath,
1038
1191
  instanceId: options.instanceId,
@@ -1093,34 +1246,45 @@ export function createTelegramLockRuntime(options = {}) {
1093
1246
  const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
1094
1247
  return hasSameLockOwner(lock, adoptCompatibleOwnedLock(effectiveKey, lock, ctx));
1095
1248
  },
1096
- commitIfOwned: (commit) => !deliveryRevoked && transactOwners((locks) => {
1097
- const effectiveKey = resolveEffectiveKey();
1098
- const lock = parseTelegramLockEntry(locks[effectiveKey]);
1099
- const exactOwner = ownedLockKey === effectiveKey && hasSameLockOwner(lock, ownedLock);
1100
- if (!exactOwner) {
1101
- if (ownedLockKey === effectiveKey) {
1102
- ownedLockKey = undefined;
1103
- ownedLock = undefined;
1249
+ commitIfOwned: (commit) => !deliveryRevoked &&
1250
+ transactOwners((locks) => {
1251
+ const effectiveKey = resolveEffectiveKey();
1252
+ const lock = parseTelegramLockEntry(locks[effectiveKey]);
1253
+ const exactOwner = ownedLockKey === effectiveKey && hasSameLockOwner(lock, ownedLock);
1254
+ if (!exactOwner) {
1255
+ if (ownedLockKey === effectiveKey) {
1256
+ ownedLockKey = undefined;
1257
+ ownedLock = undefined;
1258
+ }
1259
+ return { result: false, changed: false };
1104
1260
  }
1105
- return { result: false, changed: false };
1106
- }
1107
- commit();
1108
- // External-file commits may retain an independent effect; never acknowledge under a changed unified owner.
1109
- const current = !statePath || (!deliveryRevoked && hasSameLockOwner(parseTelegramLockEntry(readOwners()[effectiveKey]), ownedLock));
1110
- return { result: current, changed: false };
1111
- }),
1261
+ commit();
1262
+ // External-file commits may retain an independent effect; never acknowledge under a changed unified owner.
1263
+ const current = !statePath ||
1264
+ (!deliveryRevoked &&
1265
+ hasSameLockOwner(parseTelegramLockEntry(readOwners()[effectiveKey]), ownedLock));
1266
+ return { result: current, changed: false };
1267
+ }),
1112
1268
  publishStateSectionIfOwned(section, mutate, publication) {
1113
1269
  if (section !== "workspace" && section !== "runtime")
1114
1270
  throw new TelegramRuntimeStateError("invalid", "Telegram owner section publication is restricted to Workspace and runtime observations.");
1115
1271
  if (!statePath)
1116
1272
  throw new TelegramRuntimeStateError("invalid", "Consolidated transport storage is not selected.");
1117
1273
  const profile = resolveEffectiveKey(), expected = ownedLock ? { ...ownedLock } : undefined;
1118
- if (publication.expectedScope && (publication.expectedScope.path !== statePath || publication.expectedScope.profile !== profile))
1274
+ if (publication.expectedScope &&
1275
+ (publication.expectedScope.path !== statePath ||
1276
+ publication.expectedScope.profile !== profile))
1119
1277
  return { committed: false };
1120
- if (deliveryRevoked || ownedLockKey !== profile || !expected || !publication.isCurrent())
1278
+ if (deliveryRevoked ||
1279
+ ownedLockKey !== profile ||
1280
+ !expected ||
1281
+ !publication.isCurrent())
1121
1282
  return { committed: false };
1122
- const isCurrent = () => !deliveryRevoked && resolveEffectiveKey() === profile && ownedLockKey === profile &&
1123
- hasSameLockOwner(ownedLock, expected) && publication.isCurrent() &&
1283
+ const isCurrent = () => !deliveryRevoked &&
1284
+ resolveEffectiveKey() === profile &&
1285
+ ownedLockKey === profile &&
1286
+ hasSameLockOwner(ownedLock, expected) &&
1287
+ publication.isCurrent() &&
1124
1288
  hasSameLockOwner(parseTelegramLockEntry(readOwners()[profile]), expected);
1125
1289
  if (!isCurrent())
1126
1290
  return { committed: false };
@@ -1129,40 +1293,44 @@ export function createTelegramLockRuntime(options = {}) {
1129
1293
  if (!hasSameLockOwner(parseTelegramLockEntry(observed.transport), expected))
1130
1294
  return { value: current, result: { committed: false } };
1131
1295
  const outcome = mutate(current, observed);
1132
- return { value: outcome.value, result: { committed: true, result: outcome.result } };
1296
+ return {
1297
+ value: outcome.value,
1298
+ result: { committed: true, result: outcome.result },
1299
+ };
1133
1300
  }, { ...publication, isCurrent });
1134
1301
  },
1135
- refresh: (ctx) => !deliveryRevoked && transactOwners((locks) => {
1136
- const effectiveKey = resolveEffectiveKey();
1137
- const lock = parseTelegramLockEntry(locks[effectiveKey]);
1138
- const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
1139
- if (!lock || !hasSameLockOwner(lock, expectedOwner)) {
1140
- if (ownedLockKey === effectiveKey) {
1141
- ownedLockKey = undefined;
1142
- ownedLock = undefined;
1302
+ refresh: (ctx) => !deliveryRevoked &&
1303
+ transactOwners((locks) => {
1304
+ const effectiveKey = resolveEffectiveKey();
1305
+ const lock = parseTelegramLockEntry(locks[effectiveKey]);
1306
+ const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
1307
+ if (!lock || !hasSameLockOwner(lock, expectedOwner)) {
1308
+ if (ownedLockKey === effectiveKey) {
1309
+ ownedLockKey = undefined;
1310
+ ownedLock = undefined;
1311
+ }
1312
+ return { result: false, changed: false };
1143
1313
  }
1144
- return { result: false, changed: false };
1145
- }
1146
- if (!options.instanceId)
1147
- return { result: true, changed: false };
1148
- const refreshedLock = {
1149
- pid: lock.pid,
1150
- ...(lock.cwd ? { cwd: lock.cwd } : {}),
1151
- instanceId: options.instanceId,
1152
- heartbeatMs: getNowMs(),
1153
- leaderEpoch: lock.leaderEpoch ?? options.mintLeaderEpoch?.() ?? randomUUID(),
1154
- runtimeGeneration: lock.runtimeGeneration ?? runtimeGeneration,
1155
- ...(options.busSocketPath
1156
- ? { busSocketPath: options.busSocketPath }
1157
- : {}),
1158
- busSecret: options.busSecret ?? lock.busSecret,
1159
- ...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
1160
- };
1161
- locks[effectiveKey] = refreshedLock;
1162
- ownedLockKey = effectiveKey;
1163
- ownedLock = refreshedLock;
1164
- return { result: true, changed: true };
1165
- }),
1314
+ if (!options.instanceId)
1315
+ return { result: true, changed: false };
1316
+ const refreshedLock = {
1317
+ pid: lock.pid,
1318
+ ...(lock.cwd ? { cwd: lock.cwd } : {}),
1319
+ instanceId: options.instanceId,
1320
+ heartbeatMs: getNowMs(),
1321
+ leaderEpoch: lock.leaderEpoch ?? options.mintLeaderEpoch?.() ?? randomUUID(),
1322
+ runtimeGeneration: lock.runtimeGeneration ?? runtimeGeneration,
1323
+ ...(options.busSocketPath
1324
+ ? { busSocketPath: options.busSocketPath }
1325
+ : {}),
1326
+ busSecret: options.busSecret ?? lock.busSecret,
1327
+ ...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
1328
+ };
1329
+ locks[effectiveKey] = refreshedLock;
1330
+ ownedLockKey = effectiveKey;
1331
+ ownedLock = refreshedLock;
1332
+ return { result: true, changed: true };
1333
+ }),
1166
1334
  };
1167
1335
  }
1168
1336
  export function createTelegramLockOwnershipGuard(lock) {
@@ -1220,7 +1388,9 @@ export function createTelegramLockedPollingRuntime(deps) {
1220
1388
  }
1221
1389
  await deps.stopPolling();
1222
1390
  // Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
1223
- if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
1391
+ if (generation === pollingGeneration &&
1392
+ suspensionsInFlight === 1 &&
1393
+ startupsInFlight === 0) {
1224
1394
  suspendedGeneration = generation;
1225
1395
  }
1226
1396
  }
@@ -1473,12 +1643,18 @@ export function createTelegramLockedPollingRuntime(deps) {
1473
1643
  suspend: suspendPolling,
1474
1644
  captureTransportAuthority(ctx) {
1475
1645
  const generation = pollingGeneration;
1476
- const current = () => generation === pollingGeneration && activeContext === ctx && !ownershipStop &&
1477
- (deps.isContextCurrent?.(ctx) ?? true) && deps.lock.owns(snapshotLockContext(ctx));
1646
+ const current = () => generation === pollingGeneration &&
1647
+ activeContext === ctx &&
1648
+ !ownershipStop &&
1649
+ (deps.isContextCurrent?.(ctx) ?? true) &&
1650
+ deps.lock.owns(snapshotLockContext(ctx));
1478
1651
  return current() ? current : undefined;
1479
1652
  },
1480
1653
  isSuspended: () => suspendedGeneration === pollingGeneration &&
1481
- suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
1654
+ suspensionsInFlight === 0 &&
1655
+ startupsInFlight === 0 &&
1656
+ !sessionAutoStartRun &&
1657
+ !ownershipStop,
1482
1658
  onPersistentConflict: async (ctx, count) => {
1483
1659
  if (activeContext === undefined || ownershipStop)
1484
1660
  return;
@@ -1505,13 +1681,17 @@ export function createTelegramLockedPollingRuntime(deps) {
1505
1681
  }
1506
1682
  ownershipStop = Promise.resolve()
1507
1683
  .then(() => deps.stopPolling())
1508
- .catch((error) => { cleanupErrors.push(String(error)); })
1684
+ .catch((error) => {
1685
+ cleanupErrors.push(String(error));
1686
+ })
1509
1687
  .finally(() => {
1510
1688
  ownershipStop = undefined;
1511
1689
  deps.recordRuntimeEvent?.("polling", ownership === "lost"
1512
1690
  ? "Telegram transport stopped: local ownership lost; check for another Pi instance."
1513
1691
  : "Telegram transport stopped: competing getUpdates client or ownership mismatch.", {
1514
- phase: "persistent-conflict", count, ownership,
1692
+ phase: "persistent-conflict",
1693
+ count,
1694
+ ownership,
1515
1695
  ...(cleanupErrors.length ? { cleanupErrors } : {}),
1516
1696
  });
1517
1697
  deps.updateStatus(ctx);
@@ -1553,7 +1733,8 @@ export function createTelegramLockedPollingRuntime(deps) {
1553
1733
  await ownershipStop;
1554
1734
  if (!isCurrent())
1555
1735
  return;
1556
- if (canRestoreRememberedFollower && state?.kind === "active-elsewhere") {
1736
+ if (canRestoreRememberedFollower &&
1737
+ state?.kind === "active-elsewhere") {
1557
1738
  const restored = await deps.restoreFollowerWithOwner?.(ctx, state.lock);
1558
1739
  if (!isCurrent())
1559
1740
  return;