@llblab/pi-telegram 0.51.5 → 0.52.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 (181) hide show
  1. package/AGENTS.md +10 -8
  2. package/BACKLOG.md +1 -8
  3. package/CHANGELOG.md +23 -0
  4. package/LICENSE +21 -0
  5. package/README.md +16 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +9 -2
  9. package/dist/lib/bindings.js +22 -13
  10. package/dist/lib/bus-api.js +4 -2
  11. package/dist/lib/bus-follower.d.ts +55 -57
  12. package/dist/lib/bus-follower.js +213 -140
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +12 -26
  24. package/dist/lib/commands.js +152 -98
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +281 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +177 -16
  34. package/dist/lib/journal.js +1115 -249
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.d.ts +57 -2
  38. package/dist/lib/lifecycle.js +110 -5
  39. package/dist/lib/locks.d.ts +100 -12
  40. package/dist/lib/locks.js +380 -14
  41. package/dist/lib/logging.d.ts +30 -12
  42. package/dist/lib/logging.js +129 -72
  43. package/dist/lib/media.d.ts +28 -5
  44. package/dist/lib/media.js +26 -8
  45. package/dist/lib/menu-model.d.ts +0 -11
  46. package/dist/lib/menu-model.js +8 -8
  47. package/dist/lib/menu-settings.d.ts +0 -18
  48. package/dist/lib/menu-settings.js +18 -18
  49. package/dist/lib/menu-status.d.ts +4 -4
  50. package/dist/lib/menu-status.js +12 -7
  51. package/dist/lib/menu-thinking.d.ts +1 -1
  52. package/dist/lib/menu-thinking.js +1 -1
  53. package/dist/lib/menu.d.ts +1 -0
  54. package/dist/lib/menu.js +4 -3
  55. package/dist/lib/model.js +2 -4
  56. package/dist/lib/outbound-attachments.d.ts +0 -1
  57. package/dist/lib/outbound-attachments.js +1 -1
  58. package/dist/lib/outbound-buttons.js +1 -8
  59. package/dist/lib/outbound-markup.d.ts +1 -6
  60. package/dist/lib/outbound-markup.js +4 -21
  61. package/dist/lib/outbound.d.ts +0 -8
  62. package/dist/lib/outbound.js +5 -5
  63. package/dist/lib/paths.d.ts +50 -7
  64. package/dist/lib/paths.js +155 -17
  65. package/dist/lib/pi.d.ts +1 -0
  66. package/dist/lib/pi.js +2 -1
  67. package/dist/lib/polling.d.ts +0 -5
  68. package/dist/lib/polling.js +5 -5
  69. package/dist/lib/preview.d.ts +0 -3
  70. package/dist/lib/preview.js +3 -3
  71. package/dist/lib/prompt-templates.d.ts +0 -1
  72. package/dist/lib/prompt-templates.js +1 -1
  73. package/dist/lib/queue.d.ts +3 -15
  74. package/dist/lib/queue.js +69 -8
  75. package/dist/lib/recovery.d.ts +29 -9
  76. package/dist/lib/recovery.js +104 -33
  77. package/dist/lib/replies.d.ts +0 -4
  78. package/dist/lib/replies.js +1 -1
  79. package/dist/lib/routing.d.ts +73 -8
  80. package/dist/lib/routing.js +2206 -307
  81. package/dist/lib/sections.d.ts +0 -2
  82. package/dist/lib/sections.js +1 -1
  83. package/dist/lib/status.d.ts +46 -9
  84. package/dist/lib/status.js +179 -27
  85. package/dist/lib/sync.d.ts +0 -5
  86. package/dist/lib/sync.js +9 -8
  87. package/dist/lib/telegram-api.d.ts +11 -3
  88. package/dist/lib/telegram-api.js +34 -23
  89. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  90. package/dist/lib/thread-cleanup-manager.js +18 -21
  91. package/dist/lib/thread-naming.d.ts +32 -5
  92. package/dist/lib/thread-naming.js +191 -7
  93. package/dist/lib/thread-reconciler.d.ts +2 -0
  94. package/dist/lib/thread-reconciler.js +1 -1
  95. package/dist/lib/threads.d.ts +292 -59
  96. package/dist/lib/threads.js +1664 -373
  97. package/dist/lib/turns.d.ts +0 -1
  98. package/dist/lib/turns.js +1 -1
  99. package/dist/lib/updates.d.ts +184 -28
  100. package/dist/lib/updates.js +1032 -114
  101. package/dist/lib/wire.d.ts +12 -0
  102. package/dist/lib/wire.js +21 -0
  103. package/dist/lib/workspace-admission.d.ts +25 -17
  104. package/dist/lib/workspace-admission.js +95 -20
  105. package/dist/lib/workspace-identity.d.ts +20 -0
  106. package/dist/lib/workspace-identity.js +103 -0
  107. package/dist/lib/workspace-retirement.d.ts +45 -10
  108. package/dist/lib/workspace-retirement.js +196 -64
  109. package/dist/package.json +6 -6
  110. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  111. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  112. package/docs/README.md +1 -1
  113. package/docs/activity.md +1 -1
  114. package/docs/architecture.md +158 -39
  115. package/docs/callback-namespaces.md +1 -1
  116. package/docs/delivery.md +1 -1
  117. package/docs/generative-apps.md +2 -2
  118. package/docs/inbound.md +1 -1
  119. package/docs/multi-instance-bus.md +127 -19
  120. package/docs/public-api.md +5 -3
  121. package/docs/ui-style.md +26 -8
  122. package/docs/updates.md +1 -1
  123. package/lib/activity-verbosity.ts +5 -9
  124. package/lib/bindings.ts +23 -10
  125. package/lib/bus-api.ts +5 -2
  126. package/lib/bus-follower.ts +251 -226
  127. package/lib/bus-leader.ts +178 -31
  128. package/lib/bus-transport.ts +30 -8
  129. package/lib/bus.ts +110 -334
  130. package/lib/channel-posts.ts +29 -29
  131. package/lib/command-templates.ts +5 -5
  132. package/lib/commands.ts +150 -112
  133. package/lib/config.ts +12 -17
  134. package/lib/delivery.ts +2 -8
  135. package/lib/extension.ts +282 -72
  136. package/lib/generative-apps.ts +0 -22
  137. package/lib/inbound.ts +2 -2
  138. package/lib/journal.ts +1182 -326
  139. package/lib/keyboard.ts +2 -2
  140. package/lib/lifecycle.ts +141 -5
  141. package/lib/locks.ts +402 -23
  142. package/lib/logging.ts +151 -101
  143. package/lib/media.ts +51 -9
  144. package/lib/menu-model.ts +8 -8
  145. package/lib/menu-settings.ts +18 -18
  146. package/lib/menu-status.ts +11 -0
  147. package/lib/menu-thinking.ts +2 -2
  148. package/lib/menu.ts +5 -1
  149. package/lib/model.ts +2 -4
  150. package/lib/outbound-attachments.ts +1 -1
  151. package/lib/outbound-buttons.ts +1 -10
  152. package/lib/outbound-markup.ts +5 -24
  153. package/lib/outbound.ts +5 -5
  154. package/lib/paths.ts +177 -17
  155. package/lib/pi.ts +5 -2
  156. package/lib/polling.ts +5 -5
  157. package/lib/preview.ts +3 -3
  158. package/lib/prompt-templates.ts +1 -1
  159. package/lib/queue.ts +67 -9
  160. package/lib/recovery.ts +104 -44
  161. package/lib/replies.ts +1 -1
  162. package/lib/routing.ts +1955 -375
  163. package/lib/sections.ts +1 -1
  164. package/lib/status.ts +185 -36
  165. package/lib/sync.ts +8 -8
  166. package/lib/telegram-api.ts +48 -33
  167. package/lib/thread-cleanup-manager.ts +24 -21
  168. package/lib/thread-naming.ts +267 -9
  169. package/lib/thread-reconciler.ts +3 -1
  170. package/lib/threads.ts +1718 -491
  171. package/lib/turns.ts +1 -1
  172. package/lib/updates.ts +1072 -151
  173. package/lib/wire.ts +28 -0
  174. package/lib/workspace-admission.ts +97 -47
  175. package/lib/workspace-identity.ts +147 -0
  176. package/lib/workspace-retirement.ts +234 -102
  177. package/package.json +6 -6
  178. package/scripts/audit-exports.mjs +100 -0
  179. package/scripts/check-downgrade.mjs +80 -43
  180. package/skills/generated-control-surface/SKILL.md +3 -3
  181. package/skills/telegram-bridge/references/diagnosis.md +3 -3
@@ -121,8 +121,6 @@ export interface TelegramSectionRuntimeDeps {
121
121
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
122
122
  }
123
123
  /** @internal */
124
- export declare function setGlobalTelegramSectionRegistry(registry: TelegramSectionRegistry): void;
125
- /** @internal */
126
124
  export declare function createAndBindTelegramSectionRegistry(): TelegramSectionRegistry;
127
125
  /**
128
126
  * Register a Telegram Extension Section from any pi extension.
@@ -51,7 +51,7 @@ function buildTelegramSectionCallbackContext(sectionId, token, chatId, messageId
51
51
  }
52
52
  // --- GlobalThis Bridge ---
53
53
  /** @internal */
54
- export function setGlobalTelegramSectionRegistry(registry) {
54
+ function setGlobalTelegramSectionRegistry(registry) {
55
55
  globalThis[SECTION_REGISTRY_KEY] = registry;
56
56
  }
57
57
  /** @internal */
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * Telegram status rendering helpers
3
3
  * Zones: telegram ui, pi agent diagnostics, tui
4
- * Builds usage, cost, and context summaries for the interactive Telegram status view
4
+ * Owns status summaries, redacted diagnostics, projection lifecycle and compact connection-failure copy
5
+ * Excludes canonical state, transport admission and filesystem policy
5
6
  */
7
+ /** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
8
+ export declare function formatTelegramConnectionFailure(error: unknown): string;
6
9
  export type TelegramStatusQueueLane = "control" | "priority" | "default";
7
10
  export interface TelegramUsageStats {
8
11
  totalInput: number;
@@ -376,7 +379,6 @@ export interface TelegramStatusRuntime<TContext extends TelegramStatusRuntimeCon
376
379
  getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
377
380
  getStatusState: () => TelegramBridgeStatusLineState;
378
381
  }
379
- export declare function redactTelegramRuntimeMessage(message: string, botToken: string | undefined): string;
380
382
  export declare function recordStructuredTelegramRuntimeEvent(events: TelegramRuntimeEvent[], input: TelegramRuntimeEventInput, options: {
381
383
  botToken?: string;
382
384
  maxEvents: number;
@@ -392,7 +394,6 @@ export declare function recordStructuredTelegramRuntimeEvent(events: TelegramRun
392
394
  export declare function registerTelegramStatusLineProvider(provider: TelegramStatusLineProvider, options: {
393
395
  id: string;
394
396
  }): () => void;
395
- export declare function getTelegramStatusLineProviderResults(ctx: TelegramStatusLineProviderContext): TelegramStatusLineProviderResult[];
396
397
  export declare function clearTelegramStatusLineProviders(): void;
397
398
  export declare function createTelegramRuntimeEventRecorder(options: TelegramRuntimeEventRecorderOptions): TelegramRuntimeEventRecorder;
398
399
  export declare function buildTelegramRuntimeEventLines(events: TelegramRuntimeEvent[]): string[];
@@ -416,18 +417,56 @@ export declare function createTelegramRuntimeLogScope(input: {
416
417
  state: TelegramBridgeStatusLineState;
417
418
  instanceId: string;
418
419
  }): TelegramRuntimeLogScope;
419
- export declare function createTelegramStatusSnapshot(state: TelegramBridgeStatusLineState): {
420
+ export interface TelegramStatusSnapshot {
420
421
  runtime: Record<string, unknown>;
421
422
  liveRoster: Record<string, unknown>;
422
423
  diagnostics: Record<string, unknown>;
424
+ }
425
+ export interface TelegramStoredRuntimeProjection extends TelegramStatusSnapshot {
426
+ version: 1;
427
+ source: "snapshot";
428
+ writtenAtMs: number;
429
+ }
430
+ export declare function createTelegramStatusSnapshot(state: TelegramBridgeStatusLineState): TelegramStatusSnapshot;
431
+ export interface TelegramRuntimeProjectionScope {
432
+ path: string;
433
+ profile: string;
434
+ }
435
+ export interface TelegramRuntimeProjectionStorage {
436
+ read: (scope: TelegramRuntimeProjectionScope) => unknown;
437
+ /** The adapter must enforce exact owner plus the supplied physical/logical scope in one transaction. */
438
+ publish: (scope: TelegramRuntimeProjectionScope, mutate: (current: unknown) => {
439
+ value: unknown;
440
+ changed: boolean;
441
+ }, isCurrent: () => boolean) => boolean;
442
+ }
443
+ export interface TelegramRuntimeProjectionStoreOptions {
444
+ getPath: () => string;
445
+ getProfile: () => string | undefined;
446
+ /** Capture exact owner/context/session authority at submission, never renew it after the queued await. */
447
+ captureAuthority: () => (() => boolean) | undefined;
448
+ storage: TelegramRuntimeProjectionStorage;
449
+ getNowMs?: () => number;
450
+ }
451
+ /** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
452
+ export declare function createTelegramRuntimeProjectionStore(options: TelegramRuntimeProjectionStoreOptions): {
453
+ read: () => TelegramStoredRuntimeProjection | undefined;
454
+ persist: (snapshot: TelegramStatusSnapshot) => Promise<boolean>;
423
455
  };
424
456
  export declare function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
425
- persistSnapshot: () => Promise<void>;
457
+ persistSnapshot: (isCurrent: () => boolean) => Promise<void>;
426
458
  recordError: (error: unknown) => void;
459
+ captureScope?: () => (() => boolean) | undefined;
427
460
  setTimer?: (callback: () => void, ms: number) => {
428
461
  unref?: () => void;
429
- };
430
- }): () => void;
462
+ } | number;
463
+ clearTimer?: (timer: {
464
+ unref?: () => void;
465
+ } | number) => void;
466
+ }): (() => void) & {
467
+ resume(): void;
468
+ suspend(): Promise<void>;
469
+ };
431
470
  export declare function getTelegramStatusBarProcessingStatus(state: {
432
471
  hasActiveTurn: boolean;
433
472
  hasPendingDispatch: boolean;
@@ -437,6 +476,4 @@ export declare function getTelegramStatusBarProcessingStatus(state: {
437
476
  }): string | undefined;
438
477
  export declare function buildTelegramStatusBarText(theme: TelegramStatusBarTheme, state: TelegramStatusBarState): string;
439
478
  export declare function buildTelegramBridgeStatusLines(state: TelegramBridgeStatusLineState, options?: TelegramBridgeStatusLineOptions): string[];
440
- export declare function buildTelegramBridgeDiagnosticStatusLines(state: TelegramBridgeStatusLineState): string[];
441
- export declare function buildStatusHtml(ctx: TelegramStatusContext, activeModel: TelegramStatusActiveModel | undefined, bridgeStatus?: TelegramBridgeStatusLineState): string;
442
479
  export {};
@@ -1,8 +1,40 @@
1
1
  /**
2
2
  * Telegram status rendering helpers
3
3
  * Zones: telegram ui, pi agent diagnostics, tui
4
- * Builds usage, cost, and context summaries for the interactive Telegram status view
4
+ * Owns status summaries, redacted diagnostics, projection lifecycle and compact connection-failure copy
5
+ * Excludes canonical state, transport admission and filesystem policy
5
6
  */
7
+ import { isDeepStrictEqual } from "node:util";
8
+ /** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
9
+ export function formatTelegramConnectionFailure(error) {
10
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
11
+ const status = error && typeof error === "object" && "status" in error ? error.status : undefined;
12
+ const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
13
+ if (status === 401)
14
+ return "Telegram token rejected. Run /telegram-setup.";
15
+ if (status === 403)
16
+ 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)) {
19
+ return "Telegram network unavailable. Retry /telegram-connect.";
20
+ }
21
+ if (/Workspace slots?.*(?:unavailable|exhausted)|no free.*slot/i.test(message)) {
22
+ return "No Telegram slot available. Check /telegram-status --debug.";
23
+ }
24
+ if (code === "incompatible-protocol" || /protocol.*(?:incompatible|mismatch)|incompatible.*protocol/i.test(message)) {
25
+ return "Telegram instances are incompatible. Update them together.";
26
+ }
27
+ if (/unsupported.*(?:journal|version)|(?:journal|version).*unsupported/i.test(message)) {
28
+ return "Telegram state version unsupported. Use a compatible runtime.";
29
+ }
30
+ if (/follower registration failed|active in another Pi instance/i.test(message)) {
31
+ return "Telegram leader is active but unavailable. Check /telegram-status --debug.";
32
+ }
33
+ if (/unfinished Thread creation does not match/i.test(message)) {
34
+ return "Telegram Thread creation is unresolved. Check /telegram-status --debug.";
35
+ }
36
+ return "Telegram connection failed. Check /telegram-status --debug.";
37
+ }
6
38
  const TELEGRAM_STATUS_DEFAULT_PROFILE_NAME = "default";
7
39
  const TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY = "__piTelegramStatusLineProviders__";
8
40
  const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
@@ -29,7 +61,7 @@ function truncateTelegramRuntimeEventText(text, maxLength) {
29
61
  return text;
30
62
  return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
31
63
  }
32
- export function redactTelegramRuntimeMessage(message, botToken) {
64
+ function redactTelegramRuntimeMessage(message, botToken) {
33
65
  const redacted = botToken
34
66
  ? message.split(botToken).join("<redacted-token>")
35
67
  : message;
@@ -106,7 +138,7 @@ export function registerTelegramStatusLineProvider(provider, options) {
106
138
  registry.delete(options.id);
107
139
  };
108
140
  }
109
- export function getTelegramStatusLineProviderResults(ctx) {
141
+ function getTelegramStatusLineProviderResults(ctx) {
110
142
  const results = [];
111
143
  const registry = getOrCreateTelegramStatusLineProviderRegistry();
112
144
  for (const provider of registry.values()) {
@@ -329,7 +361,6 @@ export function createTelegramStatusSnapshot(state) {
329
361
  ...(state.localBus ? { localBus: state.localBus } : {}),
330
362
  topicTargets: state.topicTargets ?? [],
331
363
  reservations: state.threadReservations ?? [],
332
- syncObservations: state.topicSyncObservations ?? [],
333
364
  },
334
365
  diagnostics: {
335
366
  pendingDispatch: state.pendingDispatch,
@@ -342,12 +373,71 @@ export function createTelegramStatusSnapshot(state) {
342
373
  },
343
374
  };
344
375
  }
376
+ function isRuntimeProjection(value) {
377
+ if (!value || typeof value !== "object" || Array.isArray(value))
378
+ return false;
379
+ const record = value;
380
+ return record.version === 1 && record.source === "snapshot" && Number.isSafeInteger(record.writtenAtMs) && record.writtenAtMs >= 0 &&
381
+ Object.keys(record).every(key => ["version", "source", "writtenAtMs", "runtime", "liveRoster", "diagnostics"].includes(key)) &&
382
+ ["runtime", "liveRoster", "diagnostics"].every(key => record[key] !== null && typeof record[key] === "object" && !Array.isArray(record[key]));
383
+ }
384
+ /** Observational runtime storage only; Workspace, admission, transport, logs and recovery remain with their owners. */
385
+ export function createTelegramRuntimeProjectionStore(options) {
386
+ let publicationQueue = Promise.resolve();
387
+ const { getPath, getProfile: readProfile, captureAuthority } = options;
388
+ const { read, publish } = options.storage;
389
+ const getNowMs = options.getNowMs ?? Date.now;
390
+ const getProfile = () => readProfile() || TELEGRAM_STATUS_DEFAULT_PROFILE_NAME;
391
+ return {
392
+ read() {
393
+ try {
394
+ const path = getPath(), profile = getProfile(), value = read({ path, profile });
395
+ return getPath() === path && getProfile() === profile && isRuntimeProjection(value) ? value : undefined;
396
+ }
397
+ catch {
398
+ return undefined;
399
+ }
400
+ },
401
+ async persist(snapshot) {
402
+ const path = getPath(), profile = getProfile(), authority = captureAuthority();
403
+ if (!authority)
404
+ return false;
405
+ // Freeze wire content at submission, but do not duplicate the event log in persistent runtime state.
406
+ const semantic = JSON.parse(JSON.stringify({ version: 1, source: "snapshot", runtime: snapshot.runtime,
407
+ liveRoster: snapshot.liveRoster, diagnostics: snapshot.diagnostics }));
408
+ if (!isRuntimeProjection({ ...semantic, writtenAtMs: 0 }))
409
+ throw new Error("Telegram runtime projection is malformed.");
410
+ delete semantic.diagnostics.recentRuntimeEvents;
411
+ const isCurrent = () => getPath() === path && getProfile() === profile && authority() === true;
412
+ const publication = publicationQueue.then(() => {
413
+ if (!isCurrent())
414
+ return false;
415
+ return publish({ path, profile }, current => {
416
+ if (isRuntimeProjection(current)) {
417
+ const { writtenAtMs: _writtenAtMs, ...existing } = current;
418
+ if (isDeepStrictEqual(existing, semantic))
419
+ return { value: current, changed: false };
420
+ }
421
+ const writtenAtMs = getNowMs();
422
+ if (!Number.isSafeInteger(writtenAtMs) || writtenAtMs < 0)
423
+ throw new Error("Telegram runtime projection timestamp is invalid.");
424
+ return { value: { ...semantic, writtenAtMs }, changed: true };
425
+ }, isCurrent) === true;
426
+ });
427
+ publicationQueue = publication.then(() => undefined, () => undefined);
428
+ return publication;
429
+ },
430
+ };
431
+ }
345
432
  const TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS = 100;
346
433
  export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps) {
347
434
  const setTimer = deps.setTimer ?? setTimeout;
435
+ const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
436
+ let generation = 0;
437
+ let enabled = true;
348
438
  let timer;
349
439
  let persistPromise;
350
- let pending = false;
440
+ let pending;
351
441
  const recordError = (error) => {
352
442
  try {
353
443
  deps.recordError(error);
@@ -356,32 +446,100 @@ export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps) {
356
446
  // Snapshot diagnostics cannot create an unhandled scheduler rejection.
357
447
  }
358
448
  };
359
- const request = () => {
360
- if (timer || persistPromise) {
361
- pending = true;
449
+ const cancelTimer = () => {
450
+ const previous = timer;
451
+ timer = undefined;
452
+ if (previous) {
453
+ try {
454
+ clearTimer(previous.handle);
455
+ }
456
+ catch (error) {
457
+ recordError(error);
458
+ }
459
+ }
460
+ };
461
+ const capture = () => {
462
+ if (!enabled)
463
+ return undefined;
464
+ const expectedGeneration = generation;
465
+ let scope;
466
+ try {
467
+ scope = deps.captureScope ? deps.captureScope() : () => true;
468
+ }
469
+ catch (error) {
470
+ recordError(error);
471
+ return undefined;
472
+ }
473
+ if (!scope)
474
+ return undefined;
475
+ return () => {
476
+ if (!enabled || generation !== expectedGeneration)
477
+ return false;
478
+ try {
479
+ return scope();
480
+ }
481
+ catch (error) {
482
+ recordError(error);
483
+ return false;
484
+ }
485
+ };
486
+ };
487
+ const arm = () => {
488
+ if (timer || persistPromise || !pending)
489
+ return;
490
+ const authority = pending;
491
+ if (!authority()) {
492
+ pending = undefined;
362
493
  return;
363
494
  }
364
- timer = setTimer(() => {
495
+ const token = {};
496
+ const handle = setTimer(() => {
497
+ if (timer?.token !== token)
498
+ return;
365
499
  timer = undefined;
500
+ const publicationAuthority = pending;
501
+ pending = undefined;
502
+ if (!authority() || !publicationAuthority?.())
503
+ return;
366
504
  let tracked;
367
505
  tracked = Promise.resolve()
368
- .then(deps.persistSnapshot)
506
+ .then(() => publicationAuthority() ? deps.persistSnapshot(publicationAuthority) : undefined)
369
507
  .catch(recordError)
370
508
  .finally(() => {
371
509
  if (persistPromise !== tracked)
372
510
  return;
373
511
  persistPromise = undefined;
374
- if (pending) {
375
- pending = false;
376
- request();
377
- }
512
+ // Only an explicitly captured new request can arm a successor publication.
513
+ arm();
378
514
  });
379
515
  persistPromise = tracked;
380
516
  }, TELEGRAM_DIAGNOSTICS_SNAPSHOT_COALESCE_MS);
381
- if (typeof timer !== "number")
382
- timer?.unref?.();
517
+ timer = { token, handle };
518
+ if (typeof handle !== "number")
519
+ handle.unref?.();
520
+ };
521
+ const request = () => {
522
+ const authority = capture();
523
+ if (!authority)
524
+ return;
525
+ pending = authority;
526
+ arm();
383
527
  };
384
- return request;
528
+ return Object.assign(request, {
529
+ resume() {
530
+ generation += 1;
531
+ cancelTimer();
532
+ pending = undefined;
533
+ enabled = true;
534
+ },
535
+ suspend() {
536
+ enabled = false;
537
+ generation += 1;
538
+ cancelTimer();
539
+ pending = undefined;
540
+ return persistPromise ?? Promise.resolve();
541
+ },
542
+ });
385
543
  }
386
544
  export function getTelegramStatusBarProcessingStatus(state) {
387
545
  if (state.hasPendingModelSwitch)
@@ -757,15 +915,9 @@ function buildTelegramBridgeCompactStatusLines(state) {
757
915
  : state.activeSourceMessageIds?.length
758
916
  ? "active"
759
917
  : "idle";
760
- const diagnosticsProfileName = state.activeProfileName === TELEGRAM_STATUS_DEFAULT_PROFILE_NAME
761
- ? undefined
762
- : state.activeProfileName;
763
- const profileSuffix = diagnosticsProfileName
764
- ? `.${diagnosticsProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
765
- : "";
766
918
  const diagnosticsPaths = state.diagnosticPaths ?? {
767
- state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
768
- logs: `~/.pi/agent/tmp/telegram/logs${profileSuffix}.jsonl`,
919
+ state: "~/.pi/agent/tmp/pi-telegram/state.json",
920
+ logs: "~/.pi/agent/tmp/pi-telegram/logs.jsonl",
769
921
  };
770
922
  return [
771
923
  "connection:",
@@ -813,7 +965,7 @@ export function buildTelegramBridgeStatusLines(state, options = {}) {
813
965
  return buildTelegramBridgeDiagnosticStatusLines(state);
814
966
  return buildTelegramBridgeCompactStatusLines(state);
815
967
  }
816
- export function buildTelegramBridgeDiagnosticStatusLines(state) {
968
+ function buildTelegramBridgeDiagnosticStatusLines(state) {
817
969
  const controlQueueCount = state.queuedItems.filter((item) => item.queueLane === "control").length;
818
970
  const priorityQueueCount = state.queuedItems.filter((item) => item.queueLane === "priority").length;
819
971
  const defaultQueueCount = state.queuedItems.filter((item) => item.queueLane === "default").length;
@@ -972,7 +1124,7 @@ function buildTelegramStatusRoleSuffix(state) {
972
1124
  return "";
973
1125
  return ` @${state.busRole}`;
974
1126
  }
975
- export function buildStatusHtml(ctx, activeModel, bridgeStatus) {
1127
+ function buildStatusHtml(ctx, activeModel, bridgeStatus) {
976
1128
  const stats = collectUsageStats(ctx);
977
1129
  const usesSubscription = activeModel
978
1130
  ? ctx.modelRegistry.isUsingOAuth(activeModel)
@@ -94,9 +94,6 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
94
94
  workspaceOperationKind?: string;
95
95
  getNowMs?: () => number;
96
96
  }
97
- export declare function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(state: TSyncState, action: string, options?: {
98
- nowMs?: number;
99
- }): TSyncState;
100
97
  export declare function createTelegramPreservedLeaderQuitHandler(deps: {
101
98
  instanceId: string;
102
99
  topicTargetStore: Pick<TelegramTopicTargetStore, "load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
@@ -109,7 +106,6 @@ export declare function createTelegramPreservedLeaderQuitHandler(deps: {
109
106
  export interface TelegramSessionRestartThreadCleanupDeps<TSyncState extends TelegramSyncState> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
110
107
  suspendPolling: () => Promise<void>;
111
108
  }
112
- export declare function createTelegramSessionRestartThreadCleanupHandler<TSyncState extends TelegramSyncState>(deps: TelegramSessionRestartThreadCleanupDeps<TSyncState>): () => Promise<string>;
113
109
  export interface TelegramThreadDisconnectAssembly {
114
110
  disconnect: () => Promise<string>;
115
111
  cleanupForSessionRestart: () => Promise<string>;
@@ -141,7 +137,6 @@ export declare function createTelegramStaleTopicApiErrorRecoveryRuntime<TSyncSta
141
137
  export declare function settleStaleTelegramTopicExecutionFailure<TSyncState extends TelegramSyncState>(error: unknown, deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>): Promise<boolean>;
142
138
  export declare function recoverStaleTelegramTopicApiError<TSyncState extends TelegramSyncState>(apiBody: unknown, error: unknown, deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>): Promise<boolean>;
143
139
  export declare function ensureTelegramLeaderThreadBinding(deps: TelegramLeaderThreadSyncDeps): Promise<TelegramOwnTopicProvisionResult | undefined>;
144
- export declare const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
145
140
  export declare const TELEGRAM_SYNC_SLICES: readonly ["bot-identity", "bot-capabilities", "pairing", "allowed-user", "topic-capability", "topic-state", "target-bindings", "reservations", "transport-health"];
146
141
  export type TelegramSyncSlice = (typeof TELEGRAM_SYNC_SLICES)[number];
147
142
  export type TelegramSyncTrigger = "startup" | "reload" | "topic-lifecycle" | "stale-api-error" | "setup-change" | "pairing-change" | "follower-register" | "follower-prune" | "status-request" | "leader-health-tick" | "ordinary-message" | "ordinary-send";
package/dist/lib/sync.js CHANGED
@@ -7,9 +7,10 @@ import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from
7
7
  import { getTelegramTargetKey } from "./target.js";
8
8
  import * as ThreadReconciler from "./thread-reconciler.js";
9
9
  import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
10
+ import { normalizeTelegramWorkspacePath } from "./workspace-identity.js";
10
11
  import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
11
- import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, normalizeTelegramWorkspacePath, provisionOwnBusTopic, } from "./threads.js";
12
- export function markTelegramConfigSyncChange(state, action, options) {
12
+ import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, provisionOwnBusTopic, } from "./threads.js";
13
+ function markTelegramConfigSyncChange(state, action, options) {
13
14
  const nowMs = options?.nowMs ?? Date.now();
14
15
  let nextState = markTelegramSyncSliceFresh(state, "pairing", {
15
16
  nowMs,
@@ -61,7 +62,7 @@ export function createTelegramPreservedLeaderQuitHandler(deps) {
61
62
  };
62
63
  };
63
64
  }
64
- export function createTelegramSessionRestartThreadCleanupHandler(deps) {
65
+ function createTelegramSessionRestartThreadCleanupHandler(deps) {
65
66
  return createTelegramManualThreadDisconnectHandler({
66
67
  ...deps,
67
68
  workspaceOperationKind: "workspace.cleanup-session-restart",
@@ -459,6 +460,7 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
459
460
  ? deps.topicTargetStore.getWorkspaceBinding(workspaceIdentity.cwd, workspaceIdentity.instanceSlot, workspaceIdentity.sessionId)
460
461
  : undefined;
461
462
  const legacyWorkspaceBinding = workspaceIdentity?.instanceSlot === "a" &&
463
+ deps.sessionId === undefined &&
462
464
  !persistedWorkspaceBinding &&
463
465
  typeof legacyLeaderRecord?.target.threadId === "number"
464
466
  ? {
@@ -553,10 +555,9 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
553
555
  return (record.instanceId === deps.instanceId &&
554
556
  (record.status === "active" || record.status === "starting"));
555
557
  });
556
- // Short-circuit: when the instance already has an active thread and we are not
557
- // force-freshing, reuse it without re-provisioning. A thread belongs to the
558
- // live instance binding, not to one transient Pi session lifecycle.
559
- if (!deps.forceFreshUnnamed && priorTargets.length > 0) {
558
+ // Legacy callers without session identity may reuse the active instance target.
559
+ // Session-aware callers must resolve their exact Workspace binding instead.
560
+ if (deps.sessionId === undefined && !deps.forceFreshUnnamed && priorTargets.length > 0) {
560
561
  const record = priorTargets[0];
561
562
  deps.recordEvent("telegram", "Leader thread preserved after session lifecycle change", {
562
563
  phase: "leader-thread-reused",
@@ -655,7 +656,7 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
655
656
  deps.topicTargetStore.releaseWorkspaceClaim(deps.instanceId);
656
657
  }
657
658
  }
658
- export const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
659
+ const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
659
660
  export const TELEGRAM_SYNC_SLICES = [
660
661
  "bot-identity",
661
662
  "bot-capabilities",
@@ -5,7 +5,7 @@
5
5
  * Wraps bot API calls, file uploads/downloads (including voice messages),
6
6
  * multipart sending, runtime transport binding, and Telegram temp-file lifecycle.
7
7
  */
8
- export declare const TELEGRAM_API_BASE = "https://api.telegram.org";
8
+ import type { TelegramAttachmentSource } from "./media.ts";
9
9
  export declare const TELEGRAM_FILE_MAX_BYTES: number;
10
10
  export declare function getTelegramInboundFileByteLimitFromEnv(env: NodeJS.ProcessEnv, names: string[], defaultValue?: number): number;
11
11
  export type TelegramNetworkFamilyPolicy = "auto" | "ipv4" | "ipv6" | "ipv4-fallback";
@@ -415,6 +415,8 @@ export interface TelegramBridgeApiRuntimeDeps {
415
415
  tempDir: string;
416
416
  maxFileSizeBytes: number;
417
417
  tempFileMaxAgeMs: number;
418
+ /** Bot `@username` (or numeric id) naming private-chat attachments. */
419
+ getBotScope?: () => string | undefined;
418
420
  recordRuntimeEvent: (kind: "api" | "multipart" | "download", error: unknown, details?: Record<string, unknown>) => void;
419
421
  now?: () => number;
420
422
  chatActionMinIntervalMs?: number;
@@ -423,7 +425,7 @@ export interface TelegramBridgeApiRuntimeDeps {
423
425
  export interface TelegramBridgeApiRuntime {
424
426
  call: <TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<TResponse>;
425
427
  callMultipart: <TResponse>(method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<TResponse>;
426
- downloadFile: (fileId: string, suggestedName: string) => Promise<string>;
428
+ downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<string>;
427
429
  deleteWebhook: (signal?: AbortSignal) => Promise<boolean>;
428
430
  getUpdates: (body: Record<string, unknown>, signal?: AbortSignal) => Promise<TelegramUpdate[]>;
429
431
  setMyCommands: (commands: readonly {
@@ -462,6 +464,12 @@ export interface TelegramBridgeApiRuntime {
462
464
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
463
465
  prepareTempDir: () => Promise<number>;
464
466
  }
467
+ /** Names follow `<kind>-<scope>-<messageId>[-<index>][-<name>|.<ext>]`. */
468
+ /**
469
+ * `scope` is the bot `@username` (id fallback) in a private chat and the public chat `@username` (id fallback) elsewhere.
470
+ * A negative chat id loses its sign, so the name never contains two adjacent dashes.
471
+ */
472
+ export declare function createTelegramAttachmentFileName(source: TelegramAttachmentSource, generatedName: string, botScope?: string): string;
465
473
  export declare class TelegramApiCommitUnknownError extends Error {
466
474
  readonly kind: "commit-unknown";
467
475
  readonly method: string;
@@ -507,7 +515,6 @@ export declare function fetchTelegramBotIdentity(botToken: string, fetchImpl?: t
507
515
  export declare function callTelegramMultipart<TResponse>(botToken: string | undefined, method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions): Promise<TResponse>;
508
516
  export declare function downloadTelegramFile(botToken: string | undefined, fileId: string, suggestedName: string, tempDir: string, options?: TelegramFileDownloadOptions): Promise<string>;
509
517
  export declare function answerTelegramCallbackQuery(botToken: string | undefined, callbackQueryId: string, text?: string, options?: TelegramAnswerCallbackQueryOptions): Promise<void>;
510
- export declare function deleteTelegramMessage(botToken: string | undefined, chatId: number, messageId: number): Promise<void>;
511
518
  export declare function createTelegramChatActionSender<TAction extends string>(sendChatAction: (chatId: number, action: TAction, options?: {
512
519
  message_thread_id?: number;
513
520
  }) => Promise<unknown>, action: TAction): (chatId: number, options?: {
@@ -530,6 +537,7 @@ export type TelegramWorkspaceThreadDeletionTransport = (authorize: () => {
530
537
  }) => Promise<void>;
531
538
  export declare function createDefaultTelegramBridgeApiRuntime(deps: {
532
539
  getBotToken: () => string | undefined;
540
+ getBotScope?: () => string | undefined;
533
541
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
534
542
  captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
535
543
  targetActivity?: TelegramApiTargetActivityRuntime;