@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/status.ts CHANGED
@@ -9,25 +9,53 @@ import { isDeepStrictEqual } from "node:util";
9
9
 
10
10
  /** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
11
11
  export function formatTelegramConnectionFailure(error: unknown): string {
12
- const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
13
- const status = error && typeof error === "object" && "status" in error ? error.status : undefined;
14
- const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
12
+ const message =
13
+ error instanceof Error
14
+ ? error.message
15
+ : typeof error === "string"
16
+ ? error
17
+ : "";
18
+ const status =
19
+ error && typeof error === "object" && "status" in error
20
+ ? error.status
21
+ : undefined;
22
+ const code =
23
+ error && typeof error === "object" && "code" in error
24
+ ? error.code
25
+ : undefined;
15
26
  if (status === 401) return "Telegram token rejected. Run /telegram-setup.";
16
- if (status === 403) return "Telegram access denied. Check /telegram-status --debug.";
17
- if ((typeof code === "string" && ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ECONNRESET"].includes(code)) ||
18
- /network unavailable|fetch failed/i.test(message)) {
27
+ if (status === 403)
28
+ return "Telegram access denied. Check /telegram-status --debug.";
29
+ if (
30
+ (typeof code === "string" &&
31
+ ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ECONNRESET"].includes(
32
+ code,
33
+ )) ||
34
+ /network unavailable|fetch failed/i.test(message)
35
+ ) {
19
36
  return "Telegram network unavailable. Retry /telegram-connect.";
20
37
  }
21
- if (/Workspace slots?.*(?:unavailable|exhausted)|no free.*slot/i.test(message)) {
38
+ if (
39
+ /Workspace slots?.*(?:unavailable|exhausted)|no free.*slot/i.test(message)
40
+ ) {
22
41
  return "No Telegram slot available. Check /telegram-status --debug.";
23
42
  }
24
- if (code === "incompatible-protocol" || /protocol.*(?:incompatible|mismatch)|incompatible.*protocol/i.test(message)) {
43
+ if (
44
+ code === "incompatible-protocol" ||
45
+ /protocol.*(?:incompatible|mismatch)|incompatible.*protocol/i.test(message)
46
+ ) {
25
47
  return "Telegram instances are incompatible. Update them together.";
26
48
  }
27
- if (/unsupported.*(?:journal|version)|(?:journal|version).*unsupported/i.test(message)) {
49
+ if (
50
+ /unsupported.*(?:journal|version)|(?:journal|version).*unsupported/i.test(
51
+ message,
52
+ )
53
+ ) {
28
54
  return "Telegram state version unsupported. Use a compatible runtime.";
29
55
  }
30
- if (/follower registration failed|active in another Pi instance/i.test(message)) {
56
+ if (
57
+ /follower registration failed|active in another Pi instance/i.test(message)
58
+ ) {
31
59
  return "Telegram leader is active but unavailable. Check /telegram-status --debug.";
32
60
  }
33
61
  if (/unfinished Thread creation does not match/i.test(message)) {
@@ -466,30 +494,15 @@ function truncateTelegramRuntimeEventText(
466
494
  return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
467
495
  }
468
496
 
469
- function redactTelegramRuntimeMessage(
470
- message: string,
471
- botToken: string | undefined,
472
- ): string {
473
- const redacted = botToken
474
- ? message.split(botToken).join("<redacted-token>")
475
- : message;
476
- return truncateTelegramRuntimeEventText(
477
- redacted,
478
- MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH,
479
- );
480
- }
481
-
482
- function redactTelegramRuntimeDetail(
483
- message: string,
497
+ function redactTelegramRuntimeText(
498
+ text: string,
484
499
  botToken: string | undefined,
500
+ maxLength: number,
485
501
  ): string {
486
502
  const redacted = botToken
487
- ? message.split(botToken).join("<redacted-token>")
488
- : message;
489
- return truncateTelegramRuntimeEventText(
490
- redacted,
491
- MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH,
492
- );
503
+ ? text.split(botToken).join("<redacted-token>")
504
+ : text;
505
+ return truncateTelegramRuntimeEventText(redacted, maxLength);
493
506
  }
494
507
 
495
508
  function normalizeTelegramRuntimeEventDetails(
@@ -501,7 +514,11 @@ function normalizeTelegramRuntimeEventDetails(
501
514
  for (const [key, value] of Object.entries(details)) {
502
515
  if (value === undefined) continue;
503
516
  if (typeof value === "string") {
504
- normalized[key] = redactTelegramRuntimeDetail(value, botToken);
517
+ normalized[key] = redactTelegramRuntimeText(
518
+ value,
519
+ botToken,
520
+ MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH,
521
+ );
505
522
  continue;
506
523
  }
507
524
  if (typeof value === "number" || typeof value === "boolean") {
@@ -512,7 +529,11 @@ function normalizeTelegramRuntimeEventDetails(
512
529
  normalized[key] = null;
513
530
  continue;
514
531
  }
515
- normalized[key] = redactTelegramRuntimeDetail(String(value), botToken);
532
+ normalized[key] = redactTelegramRuntimeText(
533
+ String(value),
534
+ botToken,
535
+ MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH,
536
+ );
516
537
  }
517
538
  return Object.keys(normalized).length > 0 ? normalized : undefined;
518
539
  }
@@ -537,9 +558,10 @@ export function recordStructuredTelegramRuntimeEvent(
537
558
  events.push({
538
559
  at: options.now ?? Date.now(),
539
560
  category: input.category,
540
- message: redactTelegramRuntimeMessage(
561
+ message: redactTelegramRuntimeText(
541
562
  getTelegramRuntimeEventMessage(input),
542
563
  options.botToken,
564
+ MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH,
543
565
  ),
544
566
  ...(details ? { details } : {}),
545
567
  });
@@ -738,7 +760,9 @@ export function createTelegramBridgeStatusRuntime<
738
760
  const config = deps.getConfig();
739
761
  const queuedItems = deps.getQueuedItems();
740
762
  const hasPendingDispatch = deps.hasDispatchPending();
741
- const waitingItems = hasPendingDispatch ? queuedItems.slice(1) : queuedItems;
763
+ const waitingItems = hasPendingDispatch
764
+ ? queuedItems.slice(1)
765
+ : queuedItems;
742
766
  const queuedItemCount =
743
767
  deps.getQueuedItemCount?.(waitingItems) ?? waitingItems.length;
744
768
  const hasActiveTurn = deps.hasActiveTurn();
@@ -797,9 +821,7 @@ export function createTelegramBridgeStatusRuntime<
797
821
  instanceThreadName: deps.getInstanceThreadName?.(),
798
822
  lockState: deps.getRuntimeLockState?.(),
799
823
  pollingActive: deps.isPollingActive(),
800
- ...(deps.getPollingState
801
- ? { polling: deps.getPollingState() }
802
- : {}),
824
+ ...(deps.getPollingState ? { polling: deps.getPollingState() } : {}),
803
825
  ...(deps.getInboundWorkerState
804
826
  ? { inboundWorker: deps.getInboundWorkerState() }
805
827
  : {}),
@@ -891,12 +913,33 @@ export function createTelegramStatusSnapshot(
891
913
  };
892
914
  }
893
915
 
894
- function isRuntimeProjection(value: unknown): value is TelegramStoredRuntimeProjection {
916
+ function isRuntimeProjection(
917
+ value: unknown,
918
+ ): value is TelegramStoredRuntimeProjection {
895
919
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
896
920
  const record = value as Record<string, unknown>;
897
- return record.version === 1 && record.source === "snapshot" && Number.isSafeInteger(record.writtenAtMs) && (record.writtenAtMs as number) >= 0 &&
898
- Object.keys(record).every(key => ["version", "source", "writtenAtMs", "runtime", "liveRoster", "diagnostics"].includes(key)) &&
899
- ["runtime", "liveRoster", "diagnostics"].every(key => record[key] !== null && typeof record[key] === "object" && !Array.isArray(record[key]));
921
+ return (
922
+ record.version === 1 &&
923
+ record.source === "snapshot" &&
924
+ Number.isSafeInteger(record.writtenAtMs) &&
925
+ (record.writtenAtMs as number) >= 0 &&
926
+ Object.keys(record).every((key) =>
927
+ [
928
+ "version",
929
+ "source",
930
+ "writtenAtMs",
931
+ "runtime",
932
+ "liveRoster",
933
+ "diagnostics",
934
+ ].includes(key),
935
+ ) &&
936
+ ["runtime", "liveRoster", "diagnostics"].every(
937
+ (key) =>
938
+ record[key] !== null &&
939
+ typeof record[key] === "object" &&
940
+ !Array.isArray(record[key]),
941
+ )
942
+ );
900
943
  }
901
944
  export interface TelegramRuntimeProjectionScope {
902
945
  path: string;
@@ -920,7 +963,9 @@ export interface TelegramRuntimeProjectionStoreOptions {
920
963
  getNowMs?: () => number;
921
964
  }
922
965
  /** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
923
- export function createTelegramRuntimeProjectionStore(options: TelegramRuntimeProjectionStoreOptions): {
966
+ export function createTelegramRuntimeProjectionStore(
967
+ options: TelegramRuntimeProjectionStoreOptions,
968
+ ): {
924
969
  read: () => TelegramStoredRuntimeProjection | undefined;
925
970
  persist: (snapshot: TelegramStatusSnapshot) => Promise<boolean>;
926
971
  } {
@@ -928,36 +973,69 @@ export function createTelegramRuntimeProjectionStore(options: TelegramRuntimePro
928
973
  const { getPath, getProfile: readProfile, captureAuthority } = options;
929
974
  const { read, publish } = options.storage;
930
975
  const getNowMs = options.getNowMs ?? Date.now;
931
- const getProfile = (): string => readProfile() || TELEGRAM_STATUS_DEFAULT_PROFILE_NAME;
976
+ const getProfile = (): string =>
977
+ readProfile() || TELEGRAM_STATUS_DEFAULT_PROFILE_NAME;
932
978
  return {
933
979
  read() {
934
980
  try {
935
- const path = getPath(), profile = getProfile(), value = read({ path, profile });
936
- return getPath() === path && getProfile() === profile && isRuntimeProjection(value) ? value : undefined;
937
- } catch { return undefined; }
981
+ const path = getPath(),
982
+ profile = getProfile(),
983
+ value = read({ path, profile });
984
+ return getPath() === path &&
985
+ getProfile() === profile &&
986
+ isRuntimeProjection(value)
987
+ ? value
988
+ : undefined;
989
+ } catch {
990
+ return undefined;
991
+ }
938
992
  },
939
993
  async persist(snapshot) {
940
- const path = getPath(), profile = getProfile(), authority = captureAuthority();
994
+ const path = getPath(),
995
+ profile = getProfile(),
996
+ authority = captureAuthority();
941
997
  if (!authority) return false;
942
998
  // Freeze wire content at submission, but do not duplicate the event log in persistent runtime state.
943
- const semantic = JSON.parse(JSON.stringify({ version: 1, source: "snapshot", runtime: snapshot.runtime,
944
- liveRoster: snapshot.liveRoster, diagnostics: snapshot.diagnostics })) as Omit<TelegramStoredRuntimeProjection, "writtenAtMs">;
945
- if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 })) throw new Error("Telegram runtime projection is malformed.");
999
+ const semantic = JSON.parse(
1000
+ JSON.stringify({
1001
+ version: 1,
1002
+ source: "snapshot",
1003
+ runtime: snapshot.runtime,
1004
+ liveRoster: snapshot.liveRoster,
1005
+ diagnostics: snapshot.diagnostics,
1006
+ }),
1007
+ ) as Omit<TelegramStoredRuntimeProjection, "writtenAtMs">;
1008
+ if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 }))
1009
+ throw new Error("Telegram runtime projection is malformed.");
946
1010
  delete semantic.diagnostics.recentRuntimeEvents;
947
- const isCurrent = (): boolean => getPath() === path && getProfile() === profile && authority() === true;
1011
+ const isCurrent = (): boolean =>
1012
+ getPath() === path && getProfile() === profile && authority() === true;
948
1013
  const publication = publicationQueue.then(() => {
949
1014
  if (!isCurrent()) return false;
950
- return publish({ path, profile }, current => {
951
- if (isRuntimeProjection(current)) {
952
- const { writtenAtMs: _writtenAtMs, ...existing } = current;
953
- if (isDeepStrictEqual(existing, semantic)) return { value: current, changed: false };
954
- }
955
- const writtenAtMs = getNowMs();
956
- if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0) throw new Error("Telegram runtime projection timestamp is invalid.");
957
- return { value: { ...semantic, writtenAtMs }, changed: true };
958
- }, isCurrent) === true;
1015
+ return (
1016
+ publish(
1017
+ { path, profile },
1018
+ (current) => {
1019
+ if (isRuntimeProjection(current)) {
1020
+ const { writtenAtMs: _writtenAtMs, ...existing } = current;
1021
+ if (isDeepStrictEqual(existing, semantic))
1022
+ return { value: current, changed: false };
1023
+ }
1024
+ const writtenAtMs = getNowMs();
1025
+ if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0)
1026
+ throw new Error(
1027
+ "Telegram runtime projection timestamp is invalid.",
1028
+ );
1029
+ return { value: { ...semantic, writtenAtMs }, changed: true };
1030
+ },
1031
+ isCurrent,
1032
+ ) === true
1033
+ );
959
1034
  });
960
- publicationQueue = publication.then(() => undefined, () => undefined);
1035
+ publicationQueue = publication.then(
1036
+ () => undefined,
1037
+ () => undefined,
1038
+ );
961
1039
  return publication;
962
1040
  },
963
1041
  };
@@ -969,14 +1047,20 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
969
1047
  persistSnapshot: (isCurrent: () => boolean) => Promise<void>;
970
1048
  recordError: (error: unknown) => void;
971
1049
  captureScope?: () => (() => boolean) | undefined;
972
- setTimer?: (callback: () => void, ms: number) => { unref?: () => void } | number;
1050
+ setTimer?: (
1051
+ callback: () => void,
1052
+ ms: number,
1053
+ ) => { unref?: () => void } | number;
973
1054
  clearTimer?: (timer: { unref?: () => void } | number) => void;
974
1055
  }): (() => void) & { resume(): void; suspend(): Promise<void> } {
975
1056
  const setTimer = deps.setTimer ?? setTimeout;
976
- const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle as ReturnType<typeof setTimeout>));
1057
+ const clearTimer =
1058
+ deps.clearTimer ??
1059
+ ((handle) => clearTimeout(handle as ReturnType<typeof setTimeout>));
977
1060
  let generation = 0;
978
1061
  let enabled = true;
979
- let timer: { token: object; handle: { unref?: () => void } | number } | undefined;
1062
+ let timer:
1063
+ { token: object; handle: { unref?: () => void } | number } | undefined;
980
1064
  let persistPromise: Promise<void> | undefined;
981
1065
  let pending: (() => boolean) | undefined;
982
1066
  const recordError = (error: unknown): void => {
@@ -990,25 +1074,41 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
990
1074
  const previous = timer;
991
1075
  timer = undefined;
992
1076
  if (previous) {
993
- try { clearTimer(previous.handle); } catch (error) { recordError(error); }
1077
+ try {
1078
+ clearTimer(previous.handle);
1079
+ } catch (error) {
1080
+ recordError(error);
1081
+ }
994
1082
  }
995
1083
  };
996
1084
  const capture = (): (() => boolean) | undefined => {
997
1085
  if (!enabled) return undefined;
998
1086
  const expectedGeneration = generation;
999
1087
  let scope: (() => boolean) | undefined;
1000
- try { scope = deps.captureScope ? deps.captureScope() : () => true; }
1001
- catch (error) { recordError(error); return undefined; }
1088
+ try {
1089
+ scope = deps.captureScope ? deps.captureScope() : () => true;
1090
+ } catch (error) {
1091
+ recordError(error);
1092
+ return undefined;
1093
+ }
1002
1094
  if (!scope) return undefined;
1003
1095
  return () => {
1004
1096
  if (!enabled || generation !== expectedGeneration) return false;
1005
- try { return scope!(); } catch (error) { recordError(error); return false; }
1097
+ try {
1098
+ return scope!();
1099
+ } catch (error) {
1100
+ recordError(error);
1101
+ return false;
1102
+ }
1006
1103
  };
1007
1104
  };
1008
1105
  const arm = (): void => {
1009
1106
  if (timer || persistPromise || !pending) return;
1010
1107
  const authority = pending;
1011
- if (!authority()) { pending = undefined; return; }
1108
+ if (!authority()) {
1109
+ pending = undefined;
1110
+ return;
1111
+ }
1012
1112
  const token = {};
1013
1113
  const handle = setTimer(() => {
1014
1114
  if (timer?.token !== token) return;
@@ -1018,7 +1118,11 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
1018
1118
  if (!authority() || !publicationAuthority?.()) return;
1019
1119
  let tracked: Promise<void>;
1020
1120
  tracked = Promise.resolve()
1021
- .then(() => publicationAuthority() ? deps.persistSnapshot(publicationAuthority) : undefined)
1121
+ .then(() =>
1122
+ publicationAuthority()
1123
+ ? deps.persistSnapshot(publicationAuthority)
1124
+ : undefined,
1125
+ )
1022
1126
  .catch(recordError)
1023
1127
  .finally(() => {
1024
1128
  if (persistPromise !== tracked) return;
@@ -1069,11 +1173,7 @@ export function getTelegramStatusBarProcessingStatus(state: {
1069
1173
  }
1070
1174
 
1071
1175
  function getTelegramStatusBarLabel(state: TelegramStatusBarState): string {
1072
- const threadName = state.instanceThreadName?.trim();
1073
- if (!threadName) return "telegram";
1074
- const genericLabels = new Set(["telegram", "leader", "follower"]);
1075
- if (genericLabels.has(threadName.toLowerCase())) return "telegram";
1076
- return threadName;
1176
+ return state.instanceThreadName?.trim() || "telegram";
1077
1177
  }
1078
1178
 
1079
1179
  export function buildTelegramStatusBarText(
@@ -1086,14 +1186,17 @@ export function buildTelegramStatusBarText(
1086
1186
  : "";
1087
1187
  if (!state.hasBotToken)
1088
1188
  return `${label} ${theme.fg("muted", "not configured")}${queued}`;
1089
- if (state.pollingStopReason === "persistent-conflict" && state.busRole !== "follower")
1189
+ if (
1190
+ state.pollingStopReason === "persistent-conflict" &&
1191
+ state.busRole !== "follower"
1192
+ )
1090
1193
  return `${label} ${theme.fg("error", "error")}`;
1091
1194
  if (!state.paired)
1092
1195
  return `${label} ${theme.fg("warning", "awaiting pairing")}${queued}`;
1093
1196
  if (state.busLifecyclePhase === "electing")
1094
1197
  return `${label} ${theme.fg("warning", "electing")}${queued}`;
1095
1198
  if (!state.pollingActive && state.busRole !== "follower")
1096
- return `${theme.fg("accent", "telegram")} ${theme.fg("dim", "disconnected")}${queued}`;
1199
+ return `${label} ${theme.fg("dim", "disconnected")}${queued}`;
1097
1200
  if (state.error) {
1098
1201
  return `${label} ${theme.fg("error", "error")}${queued}`;
1099
1202
  }
@@ -1190,9 +1293,7 @@ function buildTelegramLocalBusLines(
1190
1293
  slot: localBus.followerSlot,
1191
1294
  threadName: localBus.followerThreadName,
1192
1295
  });
1193
- const protocol = formatTelegramBusProtocolIdentity(
1194
- localBus.leaderProtocol,
1195
- );
1296
+ const protocol = formatTelegramBusProtocolIdentity(localBus.leaderProtocol);
1196
1297
  const followerLine = `- follower registered: ${localBus.followerRegistered ? "yes" : "no"}${label ? ` ${label}` : ""}${target}${protocol}`;
1197
1298
  const lines = ["", "local bus:", followerLine];
1198
1299
  if (options.verbose) {
@@ -1399,9 +1500,7 @@ function buildTelegramPollingDiagnosticLines(
1399
1500
  return [
1400
1501
  `- phase: ${polling.phase}`,
1401
1502
  ...(polling.phaseStartedAtMs !== undefined
1402
- ? [
1403
- `- phase started: ${new Date(polling.phaseStartedAtMs).toISOString()}`,
1404
- ]
1503
+ ? [`- phase started: ${new Date(polling.phaseStartedAtMs).toISOString()}`]
1405
1504
  : []),
1406
1505
  ...(polling.currentUpdateId !== undefined
1407
1506
  ? [`- current update id: ${polling.currentUpdateId}`]
@@ -1472,7 +1571,9 @@ function buildTelegramInboundWorkerDiagnosticLines(
1472
1571
  ? [`- blocked reason: ${worker.blockedReason}`]
1473
1572
  : []),
1474
1573
  ...(worker.blockedInputCustody
1475
- ? [`- blocked input custody: update=${worker.blockedInputCustody.updateId}, kind=${worker.blockedInputCustody.kind}`]
1574
+ ? [
1575
+ `- blocked input custody: update=${worker.blockedInputCustody.updateId}, kind=${worker.blockedInputCustody.kind}`,
1576
+ ]
1476
1577
  : []),
1477
1578
  ...(worker.lastCompletedUpdateId !== undefined
1478
1579
  ? [
@@ -1487,18 +1588,26 @@ function buildTelegramInboundWorkerDiagnosticLines(
1487
1588
  ];
1488
1589
  }
1489
1590
 
1591
+ function countTelegramQueueLanes(
1592
+ items: TelegramBridgeStatusLineState["queuedItems"],
1593
+ ): Record<TelegramStatusQueueLane, number> {
1594
+ const counts: Record<TelegramStatusQueueLane, number> = {
1595
+ control: 0,
1596
+ priority: 0,
1597
+ default: 0,
1598
+ };
1599
+ for (const item of items) counts[item.queueLane]++;
1600
+ return counts;
1601
+ }
1602
+
1490
1603
  function buildTelegramBridgeCompactStatusLines(
1491
1604
  state: TelegramBridgeStatusLineState,
1492
1605
  ): string[] {
1493
- const controlQueueCount = state.queuedItems.filter(
1494
- (item) => item.queueLane === "control",
1495
- ).length;
1496
- const priorityQueueCount = state.queuedItems.filter(
1497
- (item) => item.queueLane === "priority",
1498
- ).length;
1499
- const defaultQueueCount = state.queuedItems.filter(
1500
- (item) => item.queueLane === "default",
1501
- ).length;
1606
+ const {
1607
+ control: controlQueueCount,
1608
+ priority: priorityQueueCount,
1609
+ default: defaultQueueCount,
1610
+ } = countTelegramQueueLanes(state.queuedItems);
1502
1611
  const queueLine = `- queued turns: ${state.queuedItems.length}${
1503
1612
  state.queuedItems.length > 0
1504
1613
  ? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
@@ -1566,15 +1675,11 @@ export function buildTelegramBridgeStatusLines(
1566
1675
  function buildTelegramBridgeDiagnosticStatusLines(
1567
1676
  state: TelegramBridgeStatusLineState,
1568
1677
  ): string[] {
1569
- const controlQueueCount = state.queuedItems.filter(
1570
- (item) => item.queueLane === "control",
1571
- ).length;
1572
- const priorityQueueCount = state.queuedItems.filter(
1573
- (item) => item.queueLane === "priority",
1574
- ).length;
1575
- const defaultQueueCount = state.queuedItems.filter(
1576
- (item) => item.queueLane === "default",
1577
- ).length;
1678
+ const {
1679
+ control: controlQueueCount,
1680
+ priority: priorityQueueCount,
1681
+ default: defaultQueueCount,
1682
+ } = countTelegramQueueLanes(state.queuedItems);
1578
1683
  return [
1579
1684
  "connection:",
1580
1685
  `- bot: ${formatTelegramBridgeBotStatus(state)}`,
@@ -1631,13 +1736,6 @@ function buildTelegramBridgeDiagnosticStatusLines(
1631
1736
  ];
1632
1737
  }
1633
1738
 
1634
- function escapeHtml(text: string): string {
1635
- return text
1636
- .replace(/&/g, "&amp;")
1637
- .replace(/</g, "&lt;")
1638
- .replace(/>/g, "&gt;");
1639
- }
1640
-
1641
1739
  function formatTokens(count: number): string {
1642
1740
  if (count < 1000) return count.toString();
1643
1741
  if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
@@ -1773,3 +1871,10 @@ function buildStatusHtml(
1773
1871
  }
1774
1872
  return lines.join("\n");
1775
1873
  }
1874
+
1875
+ function escapeHtml(text: string): string {
1876
+ return text
1877
+ .replace(/&/g, "&amp;")
1878
+ .replace(/</g, "&lt;")
1879
+ .replace(/>/g, "&gt;");
1880
+ }