@llblab/pi-kit 0.27.6 → 0.28.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 (182) hide show
  1. package/BACKLOG.md +2 -5
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  22. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  24. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  27. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  28. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  29. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  33. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  34. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  36. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  37. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  38. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  39. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  40. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  42. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  43. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  44. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  45. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  46. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  48. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  49. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  50. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  60. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  61. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  62. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  63. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  64. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  66. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  68. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  69. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  70. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  72. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  73. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  75. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  77. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  78. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  79. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  82. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  85. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  86. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  87. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  88. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  89. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  90. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  94. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  97. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  98. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  99. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  102. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  104. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  105. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  106. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  108. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  110. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  111. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  112. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  113. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  114. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  115. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  116. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  119. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  120. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  121. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  122. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  123. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  124. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  125. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  126. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  127. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  128. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  129. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  130. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  131. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  132. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  133. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  134. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  135. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  136. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  137. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  138. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  139. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  140. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  141. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  142. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  143. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  144. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  145. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  146. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  147. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  148. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  149. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  150. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  151. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  152. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  153. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  154. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  155. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  156. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  157. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  158. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  159. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  160. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  161. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  162. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  163. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  164. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  165. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  166. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  167. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  168. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  169. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  170. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  171. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  172. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  173. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  174. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  175. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  176. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  177. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  178. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  179. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  180. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  181. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  182. package/package.json +2 -2
@@ -1,15 +1,18 @@
1
1
  /**
2
2
  * Telegram singleton lock helpers
3
3
  * Zones: telegram ownership, filesystem, transport authority
4
- * Owns extension-local owners.json access and Telegram bridge ownership semantics
4
+ * Owns filesystem authority, atomic runtime-section publication and Telegram bridge ownership semantics
5
5
  */
6
6
 
7
7
  import {
8
8
  chmodSync,
9
+ closeSync,
9
10
  existsSync,
11
+ fstatSync,
10
12
  lstatSync,
11
13
  mkdirSync,
12
14
  mkdtempSync,
15
+ openSync,
13
16
  readFileSync,
14
17
  readdirSync,
15
18
  renameSync,
@@ -18,8 +21,15 @@ import {
18
21
  writeFileSync,
19
22
  } from "node:fs";
20
23
  import { randomUUID } from "node:crypto";
21
- import { basename, dirname, join } from "node:path";
22
- import { resolveTelegramOwnersPath } from "./paths.ts";
24
+ import { isDeepStrictEqual } from "node:util";
25
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
26
+ import { isWireRecord as runtimeStateRecord } from "./wire.ts";
27
+ import {
28
+ isTelegramSessionPollingJournalPath,
29
+ resolveTelegramOwnersPath,
30
+ resolveTelegramSessionPollingJournalPath,
31
+ resolveTelegramUpdateJournalPathForProfile,
32
+ } from "./paths.ts";
23
33
 
24
34
  export const TELEGRAM_LOCK_KEY = "default";
25
35
  export const TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS = 8_000;
@@ -57,7 +67,7 @@ export function resolveTelegramLockKey(activeProfile?: string): string {
57
67
  return activeProfile || TELEGRAM_LOCK_KEY;
58
68
  }
59
69
 
60
- export interface TelegramActiveProfileGetter {
70
+ interface TelegramActiveProfileGetter {
61
71
  getActiveProfileName: () => string | undefined;
62
72
  }
63
73
 
@@ -69,6 +79,31 @@ export function createTelegramLockKeyResolver(
69
79
  };
70
80
  }
71
81
 
82
+ /** Structural session view; Locks never imports the lifecycle owner. */
83
+ interface TelegramOwnedStateSessionPort<TContext extends TelegramLockContext> {
84
+ get: () => TContext | undefined;
85
+ getGeneration: () => number;
86
+ isCurrent: (ctx: TContext, generation?: number) => boolean;
87
+ }
88
+
89
+ /**
90
+ * Captures exact context, session generation and owned leader epoch before awaits.
91
+ * Session replacement, release or re-election revokes the grant; a successor never renews it.
92
+ */
93
+ export function createTelegramOwnedStateAuthorityCapture<TContext extends TelegramLockContext>(
94
+ lock: Pick<TelegramLockRuntime<TContext>, "owns" | "getOwnedLeaderEpoch">,
95
+ session: TelegramOwnedStateSessionPort<TContext>,
96
+ ): () => (() => boolean) | undefined {
97
+ return () => {
98
+ const ctx = session.get(), generation = session.getGeneration();
99
+ if (ctx === undefined || !session.isCurrent(ctx, generation)) return undefined;
100
+ const epoch = lock.getOwnedLeaderEpoch();
101
+ if (epoch === undefined) return undefined;
102
+ const current = (): boolean => session.isCurrent(ctx, generation) && lock.owns(ctx) && lock.getOwnedLeaderEpoch() === epoch;
103
+ return current() ? current : undefined;
104
+ };
105
+ }
106
+
72
107
  export interface TelegramLockEntry {
73
108
  pid: number;
74
109
  cwd?: string;
@@ -78,6 +113,8 @@ export interface TelegramLockEntry {
78
113
  runtimeGeneration?: number;
79
114
  busSocketPath?: string;
80
115
  busSecret?: string;
116
+ /** Polling journal custody; inherited by every successor and kept after release. */
117
+ journalPath?: string;
81
118
  }
82
119
 
83
120
  export interface TelegramLockContext {
@@ -90,16 +127,18 @@ export type TelegramLockState =
90
127
  | { kind: "active-elsewhere"; lock: TelegramLockEntry }
91
128
  | { kind: "stale"; lock: TelegramLockEntry };
92
129
 
93
- export interface TelegramLockAcquireOptions {
130
+ interface TelegramLockAcquireOptions {
94
131
  force?: boolean;
95
132
  expectedOwner?: TelegramLockEntry;
96
133
  election?: boolean;
97
134
  }
98
135
 
99
- export type TelegramLockAcquireResult =
136
+ type TelegramLockAcquireResult =
100
137
  | { ok: true; lock: TelegramLockEntry; replacedStale: boolean }
101
138
  | { ok: false; lock: TelegramLockEntry };
102
139
 
140
+ export type TelegramOwnedStatePublicationResult<T> = { committed: false } | { committed: true; result: T };
141
+
103
142
  export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
104
143
  acquire: (
105
144
  ctx: TContext,
@@ -111,24 +150,37 @@ export interface TelegramLockRuntime<TContext extends TelegramLockContext> {
111
150
  getOwnedLeaderEpoch: () => number | string | undefined;
112
151
  owns: (ctx?: TelegramLockContext) => boolean;
113
152
  commitIfOwned: (commit: () => void) => boolean;
153
+ /** Consolidated-store publication; domain reducers retain their own exact payload/CAS rules. */
154
+ publishStateSectionIfOwned?: <T>(
155
+ section: "workspace" | "runtime",
156
+ mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
157
+ options: TelegramOwnedStatePublicationOptions,
158
+ ) => TelegramOwnedStatePublicationResult<T>;
114
159
  refresh: (ctx?: TelegramLockContext) => boolean;
160
+ /** The polling journal named by owners.json for this key, owned or not. */
161
+ getJournalPath: () => string | undefined;
115
162
  }
116
163
 
117
- export interface TelegramLockOwnershipGuard<
164
+ interface TelegramLockOwnershipGuard<
118
165
  TContext extends TelegramLockContext,
119
166
  > {
120
167
  ownsContext: (ctx: TContext) => boolean;
121
168
  }
122
169
 
123
- export interface TelegramLockContextStore<
170
+ interface TelegramLockContextStore<
124
171
  TContext extends TelegramLockContext,
125
172
  > {
126
173
  get: () => TContext | undefined;
127
174
  }
128
175
 
129
- export interface TelegramLockRuntimeOptions {
176
+ interface TelegramLockRuntimeOptions {
130
177
  key?: string | (() => string | undefined);
131
178
  locksPath?: string;
179
+ /** Consolidated version-2 state envelope; exclusive with `locksPath`. */
180
+ statePath?: string;
181
+ statePublication?: Pick<TelegramRuntimeStatePublicationOptions, "onPublicationBoundary" | "publishRename">;
182
+ /** Read-only ownership file of releases that used another directory; a live fresh owner there blocks acquisition. */
183
+ legacyLocksPath?: string;
132
184
  pid?: number;
133
185
  isProcessAlive?: (pid: number) => boolean;
134
186
  instanceId?: string;
@@ -138,6 +190,42 @@ export interface TelegramLockRuntimeOptions {
138
190
  mintLeaderEpoch?: () => number | string;
139
191
  runtimeGeneration?: number;
140
192
  staleHeartbeatMs?: number;
193
+ /** First leader without an inherited pointer names the polling journal it creates. */
194
+ createJournalPath?: (ctx: TelegramLockContext) => string | undefined;
195
+ }
196
+
197
+ /**
198
+ * Leader polling journal: the path owners.json names, else the session that would host it on acquisition.
199
+ * Without a session identity, the flat root `inbox` remains the compatibility fallback.
200
+ */
201
+ export function createTelegramLeaderJournalPathResolver(deps: {
202
+ getNamedJournalPath: () => string | undefined;
203
+ getSessionId: () => string | undefined;
204
+ getProfileName: () => string | undefined;
205
+ }) {
206
+ // An existing flat root journal keeps its custody; new installations never create one.
207
+ const createOwn = (profileName = deps.getProfileName()): string | undefined => {
208
+ const root = resolveTelegramUpdateJournalPathForProfile(profileName);
209
+ if (existsSync(root)) return root;
210
+ const sessionId = deps.getSessionId();
211
+ return sessionId === undefined ? undefined : resolveTelegramSessionPollingJournalPath(sessionId, undefined, profileName);
212
+ };
213
+ return {
214
+ createJournalPath: () => createOwn(),
215
+ resolve(profileName?: string): string {
216
+ const named = deps.getNamedJournalPath();
217
+ if (named && (isTelegramSessionPollingJournalPath(named) ||
218
+ named === resolveTelegramUpdateJournalPathForProfile(profileName))) return named;
219
+ return createOwn(profileName) ?? resolveTelegramUpdateJournalPathForProfile(profileName);
220
+ },
221
+ };
222
+ }
223
+
224
+ /** 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;
227
+ const path = (value as Record<string, unknown>).journalPath;
228
+ return typeof path === "string" && path ? path : undefined;
141
229
  }
142
230
 
143
231
  export function readLocks(path = getOwnersPath()): Record<string, unknown> {
@@ -177,7 +265,7 @@ function sleepSync(ms: number): void {
177
265
  Atomics.wait(new Int32Array(buffer), 0, 0, ms);
178
266
  }
179
267
 
180
- export interface TelegramRenameRetryOptions {
268
+ interface TelegramRenameRetryOptions {
181
269
  rename?: typeof renameSync;
182
270
  attempts?: number;
183
271
  retryDelayMs?: number;
@@ -650,6 +738,188 @@ export function withTelegramFileTransaction<T>(
650
738
  }
651
739
  }
652
740
 
741
+ export type TelegramRuntimeStateSection = "transport" | "workspace" | "admission" | "runtime";
742
+ type TelegramRuntimeStateProfile = Partial<Record<TelegramRuntimeStateSection, unknown>>;
743
+ interface TelegramRuntimeStateFile {
744
+ version: 2;
745
+ profiles: Record<string, TelegramRuntimeStateProfile>;
746
+ }
747
+ export class TelegramRuntimeStateError extends Error {
748
+ readonly code: "invalid" | "authority-changed" | "publication-unknown";
749
+ constructor(code: TelegramRuntimeStateError["code"], message: string, options?: ErrorOptions) {
750
+ super(message, options);
751
+ this.name = "TelegramRuntimeStateError";
752
+ this.code = code;
753
+ }
754
+ }
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> };
758
+ function requireRuntimeStatePath(path: string): void {
759
+ if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path)
760
+ throw new TelegramRuntimeStateError("invalid", "Telegram runtime state path must be canonical and absolute.");
761
+ }
762
+ 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.");
765
+ }
766
+ /** Private single-link regular file read with identity continuity; initial absence only is positive. */
767
+ function readTelegramRuntimeSource(path: string): string | undefined {
768
+ requireRuntimeStatePath(path);
769
+ let source: string;
770
+ let observed = false;
771
+ try {
772
+ const stat = lstatSync(path);
773
+ 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.");
777
+ const fd = openSync(path, "r");
778
+ try {
779
+ 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.");
783
+ source = readFileSync(fd).toString("utf8");
784
+ } finally { closeSync(fd); }
785
+ 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;
791
+ throw error;
792
+ }
793
+ return source;
794
+ }
795
+
796
+ /** Strict, observational envelope read; section owners validate their own payloads. Legacy state is never adopted here. */
797
+ export function readTelegramRuntimeState(path: string): TelegramRuntimeStateFile {
798
+ const observed = readTelegramRuntimeSource(path);
799
+ if (observed === undefined) return { version: 2, profiles: {} };
800
+ 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.");
806
+ for (const [profile, sections] of Object.entries(value.profiles)) {
807
+ 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.");
810
+ }
811
+ return value as unknown as TelegramRuntimeStateFile;
812
+ }
813
+ /**
814
+ * Operator-approved optimistic recovery before leader acquisition: when the shared envelope, any transport section or a
815
+ * caller-validated section is damaged, publish a fresh empty envelope instead of refusing. All profiles lose runtime
816
+ * continuity. Filesystem access errors are not damage and still throw. Returns whether a reset was published.
817
+ */
818
+ export function resetDamagedTelegramRuntimeState(path: string,
819
+ validateProfile?: (profile: string, sections: Readonly<TelegramRuntimeStateProfile>) => void): boolean {
820
+ requireRuntimeStatePath(path);
821
+ const runtimeDir = join(dirname(path), "runtime");
822
+ 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; }
829
+ }
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
+ });
845
+ }
846
+ export interface TelegramRuntimeStateMutation<T> {
847
+ value: unknown;
848
+ result: T;
849
+ }
850
+ interface TelegramRuntimeStatePublicationOptions {
851
+ /** Exact domain authority, recaptured by the caller before invoking this synchronous transaction. */
852
+ isCurrent: () => boolean;
853
+ onPublicationBoundary?: (boundary: "before-write" | "after-write-before-rename" | "after-rename") => void;
854
+ publishRename?: typeof renameSync;
855
+ }
856
+ export interface TelegramOwnedStatePublicationOptions extends TelegramRuntimeStatePublicationOptions {
857
+ /** Optional caller-bound physical/logical identity; a mismatch cannot redirect an owner grant. */
858
+ expectedScope?: { path: string; profile: string };
859
+ }
860
+ /**
861
+ * One physical read/check/write transaction for one named section. The caller supplies domain policy; copies expose
862
+ * current sibling facts without granting writes to them. No await, nested transaction, repair or legacy import.
863
+ */
864
+ export function mutateTelegramRuntimeStateSection<T>(
865
+ path: string, profile: string, section: TelegramRuntimeStateSection,
866
+ mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
867
+ options: TelegramRuntimeStatePublicationOptions,
868
+ ): T {
869
+ requireRuntimeStatePath(path);
870
+ requireRuntimeStateProfile(profile);
871
+ 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.");
875
+ const assertCurrent = (): void => {
876
+ if (!options.isCurrent()) throw new TelegramRuntimeStateError("authority-changed", "Telegram runtime state publication authority changed.");
877
+ };
878
+ assertCurrent();
879
+ active.add(path);
880
+ try {
881
+ const runtimeDir = join(dirname(path), "runtime");
882
+ 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");
902
+ assertCurrent();
903
+ writeFileSync(tempPath, `${JSON.stringify(file, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
904
+ options.onPublicationBoundary?.("after-write-before-rename");
905
+ assertCurrent();
906
+ try {
907
+ if (!renameTelegramPathWithRetry(tempPath, path, { rename: options.publishRename }))
908
+ throw new Error("Telegram runtime state temporary publication disappeared.");
909
+ options.onPublicationBoundary?.("after-rename");
910
+ assertCurrent();
911
+ } catch (error) {
912
+ throw new TelegramRuntimeStateError("publication-unknown", "Telegram runtime state publication outcome is unknown.", { cause: error });
913
+ }
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); }
921
+ }
922
+
653
923
  function withLockTransaction<T>(
654
924
  locksPath: string,
655
925
  mutate: (locks: Record<string, unknown>) => {
@@ -730,6 +1000,7 @@ export function parseTelegramLockEntry(
730
1000
  : undefined,
731
1001
  busSecret:
732
1002
  typeof record.busSecret === "string" ? record.busSecret : undefined,
1003
+ ...(readTelegramLockJournalPath(record) ? { journalPath: readTelegramLockJournalPath(record) } : {}),
733
1004
  };
734
1005
  }
735
1006
 
@@ -744,7 +1015,7 @@ export function isProcessAlive(pid: number): boolean {
744
1015
  }
745
1016
  }
746
1017
 
747
- export function formatTelegramLockEntry(lock: TelegramLockEntry): string {
1018
+ function formatTelegramLockEntry(lock: TelegramLockEntry): string {
748
1019
  return lock.cwd ? `pid ${lock.pid}, cwd ${lock.cwd}` : `pid ${lock.pid}`;
749
1020
  }
750
1021
 
@@ -832,9 +1103,11 @@ function createLockEntry(
832
1103
  getNowMs?: () => number;
833
1104
  mintLeaderEpoch?: () => number | string;
834
1105
  runtimeGeneration?: number;
1106
+ journalPath?: string;
835
1107
  },
836
1108
  ): TelegramLockEntry {
837
1109
  const lock: TelegramLockEntry = { pid, cwd: ctx.cwd };
1110
+ if (options.journalPath) lock.journalPath = options.journalPath;
838
1111
  if (options.instanceId) {
839
1112
  const nowMs = options.getNowMs?.();
840
1113
  lock.instanceId = options.instanceId;
@@ -860,10 +1133,27 @@ function formatLockState(state: TelegramLockState): string {
860
1133
  }
861
1134
  }
862
1135
 
1136
+ function assertTelegramStateTransport(value: unknown): void {
1137
+ 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.");
1148
+ }
1149
+
863
1150
  export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
864
1151
  options: TelegramLockRuntimeOptions = {},
865
1152
  ): TelegramLockRuntime<TContext> {
866
1153
  const key = options.key ?? TELEGRAM_LOCK_KEY;
1154
+ if (options.statePath && options.locksPath)
1155
+ throw new TelegramRuntimeStateError("invalid", "Telegram transport must select one storage identity.");
1156
+ const statePath = options.statePath;
867
1157
  const locksPath = options.locksPath ?? getOwnersPath();
868
1158
  const pid = options.pid ?? process.pid;
869
1159
  const isAlive = options.isProcessAlive ?? isProcessAlive;
@@ -881,9 +1171,48 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
881
1171
  if (typeof key === "function") return key() || TELEGRAM_LOCK_KEY;
882
1172
  return key;
883
1173
  };
1174
+ const readOwners = (): Record<string, unknown> => {
1175
+ if (!statePath) return readLocks(locksPath);
1176
+ // Read-only ownership queries fail closed (no owner); acquisition/publication still refuse malformed state.
1177
+ try {
1178
+ const transports = Object.fromEntries(Object.entries(readTelegramRuntimeState(statePath).profiles)
1179
+ .filter(([, value]) => Object.hasOwn(value, "transport")).map(([profile, value]) => [profile, value.transport]));
1180
+ assertTelegramStateTransport(transports[resolveEffectiveKey()]);
1181
+ return transports;
1182
+ } catch { return {}; }
1183
+ };
1184
+ const transactOwners = <T>(mutate: (locks: Record<string, unknown>) => { result: T; changed: boolean }): T => {
1185
+ if (!statePath) return withLockTransaction(locksPath, mutate);
1186
+ 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 });
1195
+ };
884
1196
  const readLock = () => {
885
1197
  const effectiveKey = resolveEffectiveKey();
886
- return parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
1198
+ return parseTelegramLockEntry(readOwners()[effectiveKey]);
1199
+ };
1200
+ // A live older-release leader polls the same bot from its own directory. Only identity is exposed: its bus
1201
+ // 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;
1204
+ try {
1205
+ const records = statePath ? readLocksForTransaction(options.legacyLocksPath) : readLocks(options.legacyLocksPath);
1206
+ if (statePath) for (const value of Object.values(records)) assertTelegramStateTransport(value);
1207
+ const raw = records[effectiveKey];
1208
+ 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 } : {}) };
1212
+ } catch (error) {
1213
+ if (statePath) throw error;
1214
+ return undefined;
1215
+ }
887
1216
  };
888
1217
  const adoptCompatibleOwnedLock = (
889
1218
  effectiveKey: string,
@@ -908,8 +1237,10 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
908
1237
  };
909
1238
  return {
910
1239
  acquire: (ctx, acquireOptions = {}) =>
911
- withLockTransaction<TelegramLockAcquireResult>(locksPath, (locks) => {
1240
+ transactOwners<TelegramLockAcquireResult>((locks) => {
912
1241
  const effectiveKey = resolveEffectiveKey();
1242
+ const legacy = readLegacyOwner(effectiveKey);
1243
+ if (legacy) return { result: { ok: false, lock: legacy } as const, changed: false };
913
1244
  const current = parseTelegramLockEntry(locks[effectiveKey]);
914
1245
  const state = getLockState(current, pid, isAlive, stateOptions());
915
1246
  const expectedOwned = adoptCompatibleOwnedLock(
@@ -971,7 +1302,9 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
971
1302
  changed: false,
972
1303
  };
973
1304
  }
1305
+ const journalPath = readTelegramLockJournalPath(locks[effectiveKey]) ?? options.createJournalPath?.(ctx);
974
1306
  const lock = createLockEntry(pid, ctx, {
1307
+ journalPath,
975
1308
  instanceId: options.instanceId,
976
1309
  busSocketPath: options.busSocketPath,
977
1310
  busSecret: options.busSecret,
@@ -995,7 +1328,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
995
1328
  release: () => {
996
1329
  // Withdraw local send authority even if the durable release fails.
997
1330
  deliveryRevoked = true;
998
- return withLockTransaction(locksPath, (locks) => {
1331
+ return transactOwners((locks) => {
999
1332
  const effectiveKey = resolveEffectiveKey();
1000
1333
  const state = getLockState(
1001
1334
  parseTelegramLockEntry(locks[effectiveKey]),
@@ -1010,7 +1343,9 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1010
1343
  ownedLock,
1011
1344
  );
1012
1345
  if (changed) {
1013
- delete locks[effectiveKey];
1346
+ const journalPath = readTelegramLockJournalPath(locks[effectiveKey]);
1347
+ if (journalPath) locks[effectiveKey] = { journalPath };
1348
+ else delete locks[effectiveKey];
1014
1349
  ownedLockKey = undefined;
1015
1350
  ownedLock = undefined;
1016
1351
  }
@@ -1018,26 +1353,27 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1018
1353
  });
1019
1354
  },
1020
1355
  getState: () => getLockState(readLock(), pid, isAlive, stateOptions()),
1356
+ getJournalPath: () => readTelegramLockJournalPath(readOwners()[resolveEffectiveKey()]),
1021
1357
  getStatusLabel: () =>
1022
1358
  formatLockState(getLockState(readLock(), pid, isAlive, stateOptions())),
1023
1359
  getOwnedLeaderEpoch: () => {
1024
1360
  if (deliveryRevoked) return undefined;
1025
1361
  const effectiveKey = resolveEffectiveKey();
1026
- const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
1362
+ const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
1027
1363
  const exactOwner = adoptCompatibleOwnedLock(effectiveKey, lock);
1028
1364
  return hasSameLockOwner(lock, exactOwner) ? lock?.leaderEpoch : undefined;
1029
1365
  },
1030
1366
  owns: (ctx) => {
1031
1367
  if (deliveryRevoked) return false;
1032
1368
  const effectiveKey = resolveEffectiveKey();
1033
- const lock = parseTelegramLockEntry(readLocks(locksPath)[effectiveKey]);
1369
+ const lock = parseTelegramLockEntry(readOwners()[effectiveKey]);
1034
1370
  return hasSameLockOwner(
1035
1371
  lock,
1036
1372
  adoptCompatibleOwnedLock(effectiveKey, lock, ctx),
1037
1373
  );
1038
1374
  },
1039
1375
  commitIfOwned: (commit) =>
1040
- !deliveryRevoked && withLockTransaction(locksPath, (locks) => {
1376
+ !deliveryRevoked && transactOwners((locks) => {
1041
1377
  const effectiveKey = resolveEffectiveKey();
1042
1378
  const lock = parseTelegramLockEntry(locks[effectiveKey]);
1043
1379
  const exactOwner =
@@ -1050,10 +1386,36 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1050
1386
  return { result: false, changed: false };
1051
1387
  }
1052
1388
  commit();
1053
- return { result: true, changed: false };
1389
+ // 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));
1391
+ return { result: current, changed: false };
1054
1392
  }),
1393
+ publishStateSectionIfOwned<T>(
1394
+ section: "workspace" | "runtime",
1395
+ mutate: (current: unknown, observed: Readonly<TelegramRuntimeStateProfile>) => TelegramRuntimeStateMutation<T>,
1396
+ publication: TelegramOwnedStatePublicationOptions,
1397
+ ): TelegramOwnedStatePublicationResult<T> {
1398
+ 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))
1403
+ 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);
1408
+ 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 });
1416
+ },
1055
1417
  refresh: (ctx) =>
1056
- !deliveryRevoked && withLockTransaction(locksPath, (locks) => {
1418
+ !deliveryRevoked && transactOwners((locks) => {
1057
1419
  const effectiveKey = resolveEffectiveKey();
1058
1420
  const lock = parseTelegramLockEntry(locks[effectiveKey]);
1059
1421
  const expectedOwner = adoptCompatibleOwnedLock(effectiveKey, lock, ctx);
@@ -1077,6 +1439,7 @@ export function createTelegramLockRuntime<TContext extends TelegramLockContext>(
1077
1439
  ? { busSocketPath: options.busSocketPath }
1078
1440
  : {}),
1079
1441
  busSecret: options.busSecret ?? lock.busSecret,
1442
+ ...(lock.journalPath ? { journalPath: lock.journalPath } : {}),
1080
1443
  };
1081
1444
  locks[effectiveKey] = refreshedLock;
1082
1445
  ownedLockKey = effectiveKey;
@@ -1106,7 +1469,7 @@ export function createTelegramDirectDeliveryOwnershipChecker<
1106
1469
  };
1107
1470
  }
1108
1471
 
1109
- export interface TelegramLockedPollingStartOptions {
1472
+ interface TelegramLockedPollingStartOptions {
1110
1473
  force?: boolean;
1111
1474
  forceFreshLeaderThread?: boolean;
1112
1475
  requestedThreadName?: string;
@@ -1114,7 +1477,7 @@ export interface TelegramLockedPollingStartOptions {
1114
1477
  onAcquired?: () => Promise<void> | void;
1115
1478
  }
1116
1479
 
1117
- export type TelegramLockedPollingStartResult =
1480
+ type TelegramLockedPollingStartResult =
1118
1481
  | { ok: true; message?: string; canTakeover?: false }
1119
1482
  | { ok: false; message: string; canTakeover?: boolean; owner?: string };
1120
1483
 
@@ -1128,6 +1491,8 @@ export interface TelegramLockedPollingRuntime<
1128
1491
  stop: () => Promise<string>;
1129
1492
  suspend: () => Promise<void>;
1130
1493
  isSuspended: () => boolean;
1494
+ /** Fence one owned polling generation; suspension, restart, conflict or lock loss revokes it. */
1495
+ captureTransportAuthority: (ctx: TContext) => (() => boolean) | undefined;
1131
1496
  onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
1132
1497
  onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
1133
1498
  registerFollowerWithOwner?: (
@@ -1141,10 +1506,12 @@ export interface TelegramLockedPollingRuntime<
1141
1506
  stopFollowerRegistration?: () => void;
1142
1507
  }
1143
1508
 
1144
- export interface TelegramLockedPollingRuntimeDeps<
1509
+ interface TelegramLockedPollingRuntimeDeps<
1145
1510
  TContext extends TelegramLockContext,
1146
1511
  > {
1147
1512
  lock: TelegramLockRuntime<TContext>;
1513
+ /** Optimistically replaces a damaged shared runtime state before a non-election acquisition. */
1514
+ resetDamagedState?: () => boolean;
1148
1515
  hasBotToken: () => boolean;
1149
1516
  getBotTokenDiagnostic?: () => string | undefined;
1150
1517
  canStartPolling?: (ctx: TContext) => boolean;
@@ -1343,6 +1710,12 @@ export function createTelegramLockedPollingRuntime<
1343
1710
  (deps.isContextCurrent?.(ctx) ?? true);
1344
1711
  if (ownershipStop) await ownershipStop;
1345
1712
  if (!isCurrent()) return cancelled;
1713
+ if (!options.election && deps.resetDamagedState) {
1714
+ try {
1715
+ if (deps.resetDamagedState()) deps.recordRuntimeEvent?.("lock",
1716
+ new Error("Damaged Telegram runtime state was reset; previous runtime continuity was discarded."), { phase: "state-reset" });
1717
+ } catch (error) { deps.recordRuntimeEvent?.("lock", error, { phase: "state-reset" }); }
1718
+ }
1346
1719
  let acquired = deps.lock.acquire(ctx, {
1347
1720
  force: options.force,
1348
1721
  expectedOwner:
@@ -1438,6 +1811,12 @@ export function createTelegramLockedPollingRuntime<
1438
1811
  return "Telegram bridge disconnected.";
1439
1812
  },
1440
1813
  suspend: suspendPolling,
1814
+ captureTransportAuthority(ctx) {
1815
+ const generation = pollingGeneration;
1816
+ const current = () => generation === pollingGeneration && activeContext === ctx && !ownershipStop &&
1817
+ (deps.isContextCurrent?.(ctx) ?? true) && deps.lock.owns(snapshotLockContext(ctx));
1818
+ return current() ? current : undefined;
1819
+ },
1441
1820
  isSuspended: () => suspendedGeneration === pollingGeneration &&
1442
1821
  suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
1443
1822
  onPersistentConflict: async (ctx, count) => {