@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/paths.ts CHANGED
@@ -2,16 +2,20 @@
2
2
  * Telegram bridge path resolution for Pi-compatible runtimes
3
3
  * Zones: telemetry paths, filesystem, runtime identity
4
4
  * Owns agent-dir detection and extension-local path derivation
5
- *
6
- * This domain is path-only: it resolves directories and file paths from
7
- * environment and runtime identity. Its only filesystem read canonicalizes the
8
- * existing agent-dir prefix; it does not read config, manage state, or import
9
- * broader Telegram domains.
10
5
  */
6
+
11
7
  import { createHash } from "node:crypto";
12
8
  import { realpathSync } from "node:fs";
13
9
  import { homedir } from "node:os";
14
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
+ import {
11
+ basename,
12
+ dirname,
13
+ isAbsolute,
14
+ join,
15
+ relative,
16
+ resolve,
17
+ sep,
18
+ } from "node:path";
15
19
 
16
20
  export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
17
21
 
@@ -50,11 +54,22 @@ export function resolveAgentDir(
50
54
  * Reject aliases; never repair relative historical references or redirect storage.
51
55
  * Equality proves spelling only, not file identity, consumer closure or migration readiness.
52
56
  */
53
- export function requireTelegramStoragePathReference(path: string, expectedPath: string): string {
54
- if (typeof path !== "string" || typeof expectedPath !== "string" ||
55
- !isAbsolute(path) || resolve(path) !== path ||
56
- !isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
57
- throw new Error("Telegram storage reference does not match its approved absolute path.");
57
+ export function requireTelegramStoragePathReference(
58
+ path: string,
59
+ expectedPath: string,
60
+ ): string {
61
+ if (
62
+ typeof path !== "string" ||
63
+ typeof expectedPath !== "string" ||
64
+ !isAbsolute(path) ||
65
+ resolve(path) !== path ||
66
+ !isAbsolute(expectedPath) ||
67
+ resolve(expectedPath) !== expectedPath ||
68
+ path !== expectedPath
69
+ ) {
70
+ throw new Error(
71
+ "Telegram storage reference does not match its approved absolute path.",
72
+ );
58
73
  }
59
74
  return path;
60
75
  }
@@ -67,17 +82,26 @@ export function resolveTelegramConfigPath(): string {
67
82
  /**
68
83
  * Resolve symlinks in the longest existing prefix of `path` and keep the missing suffix. Strict journal reads require
69
84
  * canonical anchors, so a symlinked agent directory (for example macOS `/var` → `/private/var`) must not leak in.
70
- * Relative input is returned unchanged so callers' exact-absolute-path guards still reject it. Windows keeps its
71
- * spelling: strict journal reads are POSIX-only there, and resolving a drive-less root would add a drive letter.
85
+ * Relative input is returned unchanged so callers' exact-absolute-path guards still reject it. On Windows only
86
+ * drive-letter and UNC paths resolve; resolving a drive-less root would add the current drive.
72
87
  */
73
88
  function canonicalizeExistingPrefix(path: string): string {
74
- if (!isAbsolute(path) || process.platform === "win32") return path;
89
+ if (
90
+ !isAbsolute(path) ||
91
+ (process.platform === "win32" && !/^(?:[a-zA-Z]:[\\/]|\\\\)/u.test(path))
92
+ )
93
+ return path;
75
94
  const absolute = resolve(path);
76
95
  const missing: string[] = [];
77
96
  for (let current = absolute; ; current = dirname(current)) {
78
- try { return join(realpathSync(current), ...missing.reverse()); }
79
- catch (error) {
80
- if ((error as { code?: unknown }).code !== "ENOENT" || dirname(current) === current) return absolute;
97
+ try {
98
+ return join(realpathSync(current), ...missing.reverse());
99
+ } catch (error) {
100
+ if (
101
+ (error as { code?: unknown }).code !== "ENOENT" ||
102
+ dirname(current) === current
103
+ )
104
+ return absolute;
81
105
  missing.push(basename(current));
82
106
  }
83
107
  }
@@ -89,12 +113,16 @@ export function resolveTelegramTempDir(agentDir = resolveAgentDir()): string {
89
113
  }
90
114
 
91
115
  /** Consolidated-root service artifacts (transaction guards, staging and IPC), never session custody. */
92
- export function resolveTelegramRuntimeDir(agentDir = resolveAgentDir()): string {
116
+ export function resolveTelegramRuntimeDir(
117
+ agentDir = resolveAgentDir(),
118
+ ): string {
93
119
  return join(resolveTelegramTempDir(agentDir), "runtime");
94
120
  }
95
121
 
96
122
  /** Rotated shared runtime event segments, outside the persistent root-file census. */
97
- export function resolveTelegramRuntimeLogsDir(agentDir = resolveAgentDir()): string {
123
+ export function resolveTelegramRuntimeLogsDir(
124
+ agentDir = resolveAgentDir(),
125
+ ): string {
98
126
  return join(resolveTelegramTempDir(agentDir), "logs");
99
127
  }
100
128
 
@@ -104,12 +132,16 @@ export function resolveTelegramStatePath(agentDir = resolveAgentDir()): string {
104
132
  }
105
133
 
106
134
  /** Flat attachment scratch directory (<agentDir>/tmp/pi-telegram/attachments). */
107
- export function resolveTelegramAttachmentsDir(agentDir = resolveAgentDir()): string {
135
+ export function resolveTelegramAttachmentsDir(
136
+ agentDir = resolveAgentDir(),
137
+ ): string {
108
138
  return join(resolveTelegramTempDir(agentDir), "attachments");
109
139
  }
110
140
 
111
141
  /** Read-only pre-0.52.0 ownership file, consulted only to refuse a second poller beside a live older leader. */
112
- export function resolveLegacyTelegramOwnersPath(agentDir = resolveAgentDir()): string {
142
+ export function resolveLegacyTelegramOwnersPath(
143
+ agentDir = resolveAgentDir(),
144
+ ): string {
113
145
  return join(agentDir, "tmp", "telegram", "owners.json");
114
146
  }
115
147
 
@@ -173,21 +205,38 @@ export function resolveTelegramServiceJournalStorage(
173
205
  ): { path: string; runtimeDir: string } {
174
206
  const profileHash = createHash("sha256").update(profileName).digest("hex");
175
207
  return {
176
- path: join(resolveTelegramTempDir(agentDir), "journals", `${kind}.${profileHash}.json`),
208
+ path: join(
209
+ resolveTelegramTempDir(agentDir),
210
+ "journals",
211
+ `${kind}.${profileHash}.json`,
212
+ ),
177
213
  runtimeDir: resolveTelegramRuntimeDir(agentDir),
178
214
  };
179
215
  }
180
216
 
181
217
  /** Bind one non-session journal to its exact colocated-root service namespace, never a format fallback. */
182
- export function getTelegramJournalPublicationPaths(path: string, runtimeDir?: string): {
218
+ export function getTelegramJournalPublicationPaths(
219
+ path: string,
220
+ runtimeDir?: string,
221
+ ): {
183
222
  transactionPath: string;
184
223
  temporaryBasePath: string;
185
224
  } {
186
- if (runtimeDir === undefined) return { transactionPath: `${path}.transaction`, temporaryBasePath: path };
225
+ if (runtimeDir === undefined)
226
+ return { transactionPath: `${path}.transaction`, temporaryBasePath: path };
187
227
  requireTelegramStoragePathReference(path, resolve(path));
188
- requireTelegramStoragePathReference(runtimeDir, join(dirname(dirname(path)), "runtime"));
189
- if (basename(dirname(path)) !== "journals") throw new Error("Telegram service journal must use its journals namespace.");
190
- return { transactionPath: join(runtimeDir, `${basename(path)}.transaction`), temporaryBasePath: join(runtimeDir, basename(path)) };
228
+ requireTelegramStoragePathReference(
229
+ runtimeDir,
230
+ join(dirname(dirname(path)), "runtime"),
231
+ );
232
+ if (basename(dirname(path)) !== "journals")
233
+ throw new Error(
234
+ "Telegram service journal must use its journals namespace.",
235
+ );
236
+ return {
237
+ transactionPath: join(runtimeDir, `${basename(path)}.transaction`),
238
+ temporaryBasePath: join(runtimeDir, basename(path)),
239
+ };
191
240
  }
192
241
 
193
242
  /** Durable inactive Thread cleanup work-set journal. */
@@ -195,7 +244,12 @@ export function resolveTelegramThreadCleanupWorkPath(
195
244
  agentDir = resolveAgentDir(),
196
245
  profileName?: string,
197
246
  ): string {
198
- return resolveTelegramProfileTempFilePath("thread-cleanup", "json", agentDir, profileName);
247
+ return resolveTelegramProfileTempFilePath(
248
+ "thread-cleanup",
249
+ "json",
250
+ agentDir,
251
+ profileName,
252
+ );
199
253
  }
200
254
 
201
255
  /** Durable agent-authored channel post journal. */
@@ -203,7 +257,12 @@ export function resolveTelegramChannelPostJournalPath(
203
257
  agentDir = resolveAgentDir(),
204
258
  profileName?: string,
205
259
  ): string {
206
- return resolveTelegramProfileTempFilePath("channel-posts", "json", agentDir, profileName);
260
+ return resolveTelegramProfileTempFilePath(
261
+ "channel-posts",
262
+ "json",
263
+ agentDir,
264
+ profileName,
265
+ );
207
266
  }
208
267
 
209
268
  /** Durable inbound update journal (<agentDir>/tmp/pi-telegram/inbox[.<profile>].json). */
@@ -248,7 +307,8 @@ export function resolveTelegramFollowerJournalPath(
248
307
  }
249
308
 
250
309
  const TELEGRAM_SESSION_DIRECTORY_MAX_LENGTH = 200;
251
- const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\..*)?$/u;
310
+ const WINDOWS_RESERVED_NAME =
311
+ /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\..*)?$/u;
252
312
 
253
313
  /**
254
314
  * Directory name for a session id. Lowercase letters, digits, `_`, `-` and inner `.` stay verbatim, so every real
@@ -256,22 +316,33 @@ const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])(?:\..*)?$/
256
316
  * filesystems never merge two sessions; a leading or trailing dot and Windows reserved names are encoded so the name
257
317
  * can never be `.`/`..`, hidden, or a device. The encoding is reversible; an empty or over-long result is refused.
258
318
  */
259
- export function encodeTelegramSessionDirectoryName(sessionId: string): string | undefined {
319
+ export function encodeTelegramSessionDirectoryName(
320
+ sessionId: string,
321
+ ): string | undefined {
260
322
  if (typeof sessionId !== "string" || sessionId.length === 0) return undefined;
261
323
  const bytes = Buffer.from(sessionId, "utf8");
262
324
  if (bytes.toString("utf8") !== sessionId) return undefined;
263
325
  let name = "";
264
326
  bytes.forEach((byte, index) => {
265
327
  const char = String.fromCharCode(byte);
266
- const safe = /[a-z0-9_-]/u.test(char) || (char === "." && index > 0 && index < bytes.length - 1);
267
- name += safe ? char : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
328
+ const safe =
329
+ /[a-z0-9_-]/u.test(char) ||
330
+ (char === "." && index > 0 && index < bytes.length - 1);
331
+ name += safe
332
+ ? char
333
+ : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
268
334
  });
269
- if (WINDOWS_RESERVED_NAME.test(name)) name = `%${name.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}${name.slice(1)}`;
270
- return name.length <= TELEGRAM_SESSION_DIRECTORY_MAX_LENGTH ? name : undefined;
335
+ if (WINDOWS_RESERVED_NAME.test(name))
336
+ name = `%${name.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}${name.slice(1)}`;
337
+ return name.length <= TELEGRAM_SESSION_DIRECTORY_MAX_LENGTH
338
+ ? name
339
+ : undefined;
271
340
  }
272
341
 
273
342
  /** Inverse of `encodeTelegramSessionDirectoryName`; undefined unless the name is exactly canonical. */
274
- export function decodeTelegramSessionDirectoryName(name: string): string | undefined {
343
+ export function decodeTelegramSessionDirectoryName(
344
+ name: string,
345
+ ): string | undefined {
275
346
  if (!/^[a-z0-9_.%A-F-]+$/u.test(name)) return undefined;
276
347
  const bytes: number[] = [];
277
348
  for (let index = 0; index < name.length; index++) {
@@ -283,25 +354,39 @@ export function decodeTelegramSessionDirectoryName(name: string): string | undef
283
354
  } else bytes.push(name.charCodeAt(index));
284
355
  }
285
356
  const decoded = Buffer.from(bytes).toString("utf8");
286
- return encodeTelegramSessionDirectoryName(decoded) === name ? decoded : undefined;
357
+ return encodeTelegramSessionDirectoryName(decoded) === name
358
+ ? decoded
359
+ : undefined;
287
360
  }
288
361
 
289
362
  /** Root of every session folder (<agentDir>/tmp/pi-telegram/sessions). */
290
- export function resolveTelegramSessionsDir(agentDir = resolveAgentDir()): string {
363
+ export function resolveTelegramSessionsDir(
364
+ agentDir = resolveAgentDir(),
365
+ ): string {
291
366
  return join(resolveTelegramTempDir(agentDir), "sessions");
292
367
  }
293
368
 
294
369
  /** One session's folder (<agentDir>/tmp/pi-telegram/sessions/<session id>). */
295
- export function resolveTelegramSessionDir(sessionId: string, agentDir = resolveAgentDir()): string {
370
+ export function resolveTelegramSessionDir(
371
+ sessionId: string,
372
+ agentDir = resolveAgentDir(),
373
+ ): string {
296
374
  const name = encodeTelegramSessionDirectoryName(sessionId);
297
- if (!name) throw new Error("Telegram session directory requires a usable session id.");
375
+ if (!name)
376
+ throw new Error("Telegram session directory requires a usable session id.");
298
377
  return join(resolveTelegramSessionsDir(agentDir), name);
299
378
  }
300
379
 
301
380
  /** Hash naming one recipient incarnation inside its session folder; unchanged from the former follower inbox. */
302
- export function getTelegramRecipientJournalHash(recipientBindingKey: string): string {
303
- if (!recipientBindingKey) throw new Error("Telegram session journal binding key is required.");
304
- return createHash("sha256").update(recipientBindingKey).digest("hex").slice(0, 16);
381
+ export function getTelegramRecipientJournalHash(
382
+ recipientBindingKey: string,
383
+ ): string {
384
+ if (!recipientBindingKey)
385
+ throw new Error("Telegram session journal binding key is required.");
386
+ return createHash("sha256")
387
+ .update(recipientBindingKey)
388
+ .digest("hex")
389
+ .slice(0, 16);
305
390
  }
306
391
 
307
392
  /** Durable per-session journal (`sessions/<session id>/journal.<hash>[.<profile>].json`); the name never reveals a role. */
@@ -323,22 +408,36 @@ export function resolveTelegramSessionPollingJournalPath(
323
408
  agentDir = resolveAgentDir(),
324
409
  profileName?: string,
325
410
  ): string {
326
- return join(resolveTelegramSessionDir(sessionId, agentDir), `inbox${getTelegramProfilePathSuffix(profileName)}.json`);
411
+ return join(
412
+ resolveTelegramSessionDir(sessionId, agentDir),
413
+ `inbox${getTelegramProfilePathSuffix(profileName)}.json`,
414
+ );
327
415
  }
328
416
 
329
417
  /** Accept only a canonical `sessions/<id>/inbox[.<profile>].json` beneath this agent's runtime. */
330
- export function isTelegramSessionPollingJournalPath(path: string, agentDir = resolveAgentDir()): boolean {
418
+ export function isTelegramSessionPollingJournalPath(
419
+ path: string,
420
+ agentDir = resolveAgentDir(),
421
+ ): boolean {
331
422
  const parts = relative(resolveTelegramSessionsDir(agentDir), path).split(sep);
332
- return isAbsolute(path) && parts.length === 2 && decodeTelegramSessionDirectoryName(parts[0]!) !== undefined &&
333
- /^inbox(?:\.[a-zA-Z0-9._-]+)?\.json$/u.test(parts[1]!);
423
+ return (
424
+ isAbsolute(path) &&
425
+ parts.length === 2 &&
426
+ decodeTelegramSessionDirectoryName(parts[0]!) !== undefined &&
427
+ /^inbox(?:\.[a-zA-Z0-9._-]+)?\.json$/u.test(parts[1]!)
428
+ );
334
429
  }
335
430
 
336
431
  /** Runtime event log (<agentDir>/tmp/pi-telegram/logs.jsonl). */
337
- export function resolveTelegramRuntimeLogPath(agentDir = resolveAgentDir()): string {
432
+ export function resolveTelegramRuntimeLogPath(
433
+ agentDir = resolveAgentDir(),
434
+ ): string {
338
435
  return resolveTelegramProfileTempFilePath("logs", "jsonl", agentDir);
339
436
  }
340
437
 
341
438
  /** Bounded previous shared log segment below `logs/`. */
342
- export function resolveTelegramPreviousSharedRuntimeLogPath(agentDir = resolveAgentDir()): string {
439
+ export function resolveTelegramPreviousSharedRuntimeLogPath(
440
+ agentDir = resolveAgentDir(),
441
+ ): string {
343
442
  return join(resolveTelegramRuntimeLogsDir(agentDir), "logs._prev.jsonl");
344
443
  }
package/lib/pi.ts CHANGED
@@ -93,6 +93,15 @@ function isPiRunMode(value: unknown): value is PiRunMode {
93
93
  );
94
94
  }
95
95
 
96
+ /** Pi rejects a context captured before session replacement with a stale-context error; this is not a domain failure. */
97
+ export function isPiStaleContextError(error: unknown): boolean {
98
+ return (
99
+ error instanceof Error &&
100
+ (error.message.includes("stale after session") ||
101
+ error.message.includes("stale ctx"))
102
+ );
103
+ }
104
+
96
105
  export function getExtensionContextMode(ctx: unknown): PiRunMode | undefined {
97
106
  const mode =
98
107
  typeof ctx === "object" && ctx !== null
@@ -184,10 +193,15 @@ type HostSettingsManager = {
184
193
 
185
194
  function readEnabledModels(value: unknown): string[] | undefined {
186
195
  if (value === undefined) return undefined;
187
- if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
196
+ if (
197
+ Array.isArray(value) &&
198
+ value.every((entry) => typeof entry === "string")
199
+ ) {
188
200
  return [...value];
189
201
  }
190
- throw new TypeError("Host settings enabledModels must be a string array or undefined.");
202
+ throw new TypeError(
203
+ "Host settings enabledModels must be a string array or undefined.",
204
+ );
191
205
  }
192
206
 
193
207
  export function normalizeSettingsManager(manager: unknown): PiSettingsManager {
@@ -198,18 +212,20 @@ export function normalizeSettingsManager(manager: unknown): PiSettingsManager {
198
212
  if (typeof host.flush !== "function") {
199
213
  throw new TypeError("Host settings manager must provide flush().");
200
214
  }
201
- const read = typeof host.getEnabledModels === "function"
202
- ? () => host.getEnabledModels!.call(host)
203
- : typeof host.get === "function"
204
- ? () => host.get!.call(host, "enabledModels")
205
- : undefined;
206
- const write = typeof host.setEnabledModels === "function"
207
- ? (patterns: string[] | undefined) =>
208
- host.setEnabledModels!.call(host, patterns)
209
- : typeof host.set === "function"
215
+ const read =
216
+ typeof host.getEnabledModels === "function"
217
+ ? () => host.getEnabledModels!.call(host)
218
+ : typeof host.get === "function"
219
+ ? () => host.get!.call(host, "enabledModels")
220
+ : undefined;
221
+ const write =
222
+ typeof host.setEnabledModels === "function"
210
223
  ? (patterns: string[] | undefined) =>
211
- host.set!.call(host, "enabledModels", patterns ?? [])
212
- : undefined;
224
+ host.setEnabledModels!.call(host, patterns)
225
+ : typeof host.set === "function"
226
+ ? (patterns: string[] | undefined) =>
227
+ host.set!.call(host, "enabledModels", patterns ?? [])
228
+ : undefined;
213
229
  if (!read || !write) {
214
230
  throw new TypeError(
215
231
  "Host settings manager must provide enabled-model read and write capabilities.",
@@ -264,8 +280,12 @@ export function getExtensionContextCwd(ctx: ExtensionContext): string {
264
280
  }
265
281
 
266
282
  export function getExtensionContextSessionId(ctx: ExtensionContext): string;
267
- export function getExtensionContextSessionId(ctx: ExtensionContext | undefined): string | undefined;
268
- export function getExtensionContextSessionId(ctx: ExtensionContext | undefined): string | undefined {
283
+ export function getExtensionContextSessionId(
284
+ ctx: ExtensionContext | undefined,
285
+ ): string | undefined;
286
+ export function getExtensionContextSessionId(
287
+ ctx: ExtensionContext | undefined,
288
+ ): string | undefined {
269
289
  // Minimal hosts and stale contexts may omit a session manager; absence is not an invented ID.
270
290
  return ctx?.sessionManager?.getSessionId?.();
271
291
  }