@llblab/pi-telegram 0.50.1 → 0.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/AGENTS.md +10 -8
  2. package/BACKLOG.md +13 -32
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +4 -3
  5. package/dist/lib/bindings.d.ts +4 -2
  6. package/dist/lib/bindings.js +5 -5
  7. package/dist/lib/bus-follower.d.ts +4 -2
  8. package/dist/lib/bus-follower.js +99 -10
  9. package/dist/lib/bus-leader.d.ts +9 -3
  10. package/dist/lib/bus-leader.js +231 -71
  11. package/dist/lib/bus.d.ts +4 -0
  12. package/dist/lib/bus.js +34 -7
  13. package/dist/lib/commands.d.ts +5 -0
  14. package/dist/lib/commands.js +9 -0
  15. package/dist/lib/extension.js +68 -3
  16. package/dist/lib/journal.d.ts +3 -0
  17. package/dist/lib/journal.js +13 -0
  18. package/dist/lib/lifecycle.d.ts +2 -0
  19. package/dist/lib/lifecycle.js +25 -4
  20. package/dist/lib/locks.d.ts +1 -0
  21. package/dist/lib/locks.js +32 -12
  22. package/dist/lib/paths.d.ts +10 -0
  23. package/dist/lib/paths.js +22 -1
  24. package/dist/lib/polling.js +2 -2
  25. package/dist/lib/queue.d.ts +9 -1
  26. package/dist/lib/queue.js +64 -13
  27. package/dist/lib/replies.d.ts +4 -0
  28. package/dist/lib/replies.js +14 -0
  29. package/dist/lib/routing.d.ts +1 -0
  30. package/dist/lib/routing.js +5 -0
  31. package/dist/lib/sync.d.ts +9 -0
  32. package/dist/lib/sync.js +41 -1
  33. package/dist/lib/telegram-api.d.ts +9 -1
  34. package/dist/lib/telegram-api.js +23 -3
  35. package/dist/lib/thread-cleanup-manager.js +7 -2
  36. package/dist/lib/threads.d.ts +2 -0
  37. package/dist/lib/threads.js +68 -37
  38. package/dist/lib/updates.d.ts +6 -4
  39. package/dist/lib/updates.js +134 -61
  40. package/dist/lib/workspace-admission.d.ts +9 -0
  41. package/dist/lib/workspace-admission.js +49 -3
  42. package/dist/lib/workspace-retirement.d.ts +77 -6
  43. package/dist/lib/workspace-retirement.js +384 -8
  44. package/dist/lib/workspace-slots.d.ts +3 -0
  45. package/dist/lib/workspace-slots.js +6 -0
  46. package/dist/package.json +1 -1
  47. package/docs/architecture.md +22 -15
  48. package/docs/multi-instance-bus.md +32 -11
  49. package/docs/public-api.md +1 -1
  50. package/docs/updates.md +2 -0
  51. package/lib/bindings.ts +12 -7
  52. package/lib/bus-follower.ts +82 -14
  53. package/lib/bus-leader.ts +236 -94
  54. package/lib/bus.ts +39 -9
  55. package/lib/commands.ts +19 -0
  56. package/lib/extension.ts +78 -2
  57. package/lib/journal.ts +14 -0
  58. package/lib/lifecycle.ts +23 -4
  59. package/lib/locks.ts +32 -13
  60. package/lib/paths.ts +29 -1
  61. package/lib/polling.ts +2 -2
  62. package/lib/queue.ts +77 -19
  63. package/lib/replies.ts +18 -0
  64. package/lib/routing.ts +7 -0
  65. package/lib/sync.ts +45 -1
  66. package/lib/telegram-api.ts +30 -2
  67. package/lib/thread-cleanup-manager.ts +11 -3
  68. package/lib/threads.ts +73 -36
  69. package/lib/updates.ts +145 -78
  70. package/lib/workspace-admission.ts +67 -4
  71. package/lib/workspace-retirement.ts +465 -7
  72. package/lib/workspace-slots.ts +7 -0
  73. package/package.json +1 -1
@@ -79,7 +79,7 @@ export default function (pi) {
79
79
  const workspaceAdmissionRuntime = WorkspaceAdmission.createTelegramWorkspaceAdmissionRuntimeBinding({
80
80
  getProfileName: configStore.getActiveProfileName,
81
81
  getBotToken: configStore.getBotToken,
82
- getPath: Paths.resolveTelegramWorkspaceAdmissionPath,
82
+ getPath: Paths.resolveTelegramWorkspaceAdmissionPathForProfile,
83
83
  owner: {
84
84
  processId: telegramProcessId,
85
85
  processBirthId: telegramQueueProcessBirthId,
@@ -146,7 +146,7 @@ export default function (pi) {
146
146
  },
147
147
  getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
148
148
  },
149
- getLeaderJournalPath: Paths.resolveTelegramUpdateJournalPath,
149
+ getLeaderJournalPath: Paths.resolveTelegramUpdateJournalPathForProfile,
150
150
  getFollowerJournalPath(bindingKey, profileName) {
151
151
  return Paths.resolveTelegramFollowerJournalPath(bindingKey, undefined, profileName);
152
152
  },
@@ -295,6 +295,45 @@ export default function (pi) {
295
295
  : "clear";
296
296
  },
297
297
  });
298
+ const reclaimTelegramWorkspaceDeadOwnerQueue = WorkspaceRetirement.createTelegramWorkspaceDeadOwnerQueueReclaimer({
299
+ getExternalProtection: captureWorkspaceExternalProtection,
300
+ getActiveTurnTarget: activeTurnRuntime.getTarget,
301
+ getQueuedItems: telegramQueueStore.getQueuedItems,
302
+ resolveLeaderJournal: resolveTelegramUpdateJournalBinding,
303
+ createFollowerJournalResolver: telegramJournalBindingRuntime.createRecipientResolver,
304
+ discoverFollowerJournals() {
305
+ return Journal.discoverTelegramFollowerJournalPaths({
306
+ directory: Paths.resolveTelegramTempDir(),
307
+ profileName: configStore.getActiveProfileName(),
308
+ });
309
+ },
310
+ createJournalPathResolver: telegramJournalBindingRuntime.createPathResolver,
311
+ withJournalReference(binding, operation) {
312
+ if (!binding.recoveryKey)
313
+ throw new Error("Telegram workspace journal reference identity is unavailable.");
314
+ return telegramJournalReferenceRegistry.withReference({
315
+ referenceClass: "workspace-retirement",
316
+ recoveryKey: binding.recoveryKey,
317
+ }, operation);
318
+ },
319
+ getRecoveryOwner() {
320
+ return {
321
+ instanceId: telegramInstanceId,
322
+ processId: telegramProcessId,
323
+ processBirthId: telegramQueueProcessBirthId,
324
+ sessionGeneration: telegramSessionContextStore.getGeneration(),
325
+ };
326
+ },
327
+ getQueueOwnerLiveness: Bus.getTelegramProcessLiveness,
328
+ onMutationError(error) {
329
+ recordRuntimeEvent("Telegram dead-owner queue reclamation was refused.", {
330
+ error: error instanceof Error ? error.message : String(error),
331
+ });
332
+ },
333
+ isBindingCurrent(binding) {
334
+ return WorkspaceRetirement.isCurrentTelegramWorkspaceBinding(threadStore, binding);
335
+ },
336
+ });
298
337
  const inactiveThreadCleanupReviewRuntime = ThreadCleanupManager.createTelegramInactiveThreadCleanupReviewRuntime({
299
338
  getProfileName() { return configStore.getActiveProfileName() ?? "default"; },
300
339
  listBindings: threadStore.listWorkspaceBindings,
@@ -603,7 +642,7 @@ export default function (pi) {
603
642
  editMessageText: editTelegramMessageText,
604
643
  },
605
644
  });
606
- const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
645
+ const { mutation: queueMutationRuntime, dispatchNext: dispatchNextQueuedTelegramTurn, requestNextDispatchAnnouncement, cancelNextDispatchAnnouncement, watchdog: queueDispatchWatchdogRuntime, } = Bindings.createTelegramQueueBindingRuntime({
607
646
  store: telegramQueueStore,
608
647
  queue,
609
648
  lifecycle,
@@ -617,6 +656,9 @@ export default function (pi) {
617
656
  updateStatus,
618
657
  sendTextReply,
619
658
  sendUserMessage,
659
+ reconcileNextDispatchAnnouncementReplyOwnership(item) {
660
+ Replies.preserveTransportReplyDedupOnNextReset(item.chatId, item.replyToMessageId, item.target);
661
+ },
620
662
  recordRuntimeEvent,
621
663
  });
622
664
  const { finalizeMarkdownPreview, preparePreviewDelivery } = Outbound.createTelegramOutboundTextPreviewRuntime({
@@ -848,6 +890,7 @@ export default function (pi) {
848
890
  isContextActive: telegramSessionContextStore.isCurrent,
849
891
  dispatchNextQueuedTelegramTurn,
850
892
  requestNextDispatchAnnouncement,
893
+ cancelNextDispatchAnnouncement,
851
894
  requestDeferredDispatchNextQueuedTelegramTurn: deferredQueueDispatchRuntime.request,
852
895
  hasDeferredDispatchContext: deferredQueueDispatchRuntime.isBound,
853
896
  startTypingLoop: promptDispatchRuntime.startTypingLoop,
@@ -1169,6 +1212,18 @@ export default function (pi) {
1169
1212
  getWorkspaceAdmission: workspaceAdmissionRuntime.resolve,
1170
1213
  runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
1171
1214
  captureWorkspaceExternalProtection,
1215
+ workspaceRotation: {
1216
+ getAdmission: workspaceAdmissionRuntime.resolve,
1217
+ runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
1218
+ deleteThread: directTelegramApiRuntime.deleteWorkspaceThread,
1219
+ reclaimDeadOwnerQueuedWork(binding, isCurrent) {
1220
+ return telegramWorkspaceOperationRuntime.run({
1221
+ operationId: WorkspaceAdmission.createTelegramWorkspaceAdmissionOperationId(),
1222
+ operationKind: "workspace.reclaim-dead-owner-queue",
1223
+ scopes: [{ kind: "target", target: binding.target }],
1224
+ }, reclaimTelegramWorkspaceDeadOwnerQueue.bind(undefined, binding, isCurrent));
1225
+ },
1226
+ },
1172
1227
  callApi(method, body, options) {
1173
1228
  return directTelegramApiRuntime.call(method, body, options);
1174
1229
  },
@@ -1282,6 +1337,15 @@ export default function (pi) {
1282
1337
  recordRuntimeEvent,
1283
1338
  runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
1284
1339
  });
1340
+ const prepareThreadPreservationOnQuit = Sync.createTelegramPreservedLeaderQuitHandler({
1341
+ instanceId: telegramInstanceId,
1342
+ topicTargetStore: threadStore,
1343
+ getCurrentLeaderEpoch,
1344
+ getProfileName: configStore.getActiveProfileName,
1345
+ isPollingSuspended: lockedPollingRuntime.isSuspended,
1346
+ resolveAutomaticThreadCleanupEnabled: configControls.resolveAutomaticThreadCleanupEnabled,
1347
+ runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
1348
+ });
1285
1349
  const telegramBridgeSessionLifecycleDeps = Lifecycle.createTelegramBridgeSessionLifecycleDeps({
1286
1350
  contextStore: telegramSessionContextStore,
1287
1351
  queue: {
@@ -1335,6 +1399,7 @@ export default function (pi) {
1335
1399
  capabilityMonitor: telegramThreadCapabilityMonitor,
1336
1400
  queueWatchdog: queueDispatchWatchdogRuntime,
1337
1401
  guestPlaceholder: { stopAll: guestPlaceholderRuntime.stopAll },
1402
+ prepareThreadPreservationOnQuit,
1338
1403
  },
1339
1404
  });
1340
1405
  const sessionLifecycleRuntime = Lifecycle.createTelegramBridgeSessionLifecycleAssembly(telegramBridgeSessionLifecycleDeps);
@@ -610,6 +610,9 @@ export interface TelegramUpdateJournalRuntimeBinding {
610
610
  runtimeKey: string;
611
611
  recoveryKey: string;
612
612
  journal: TelegramUpdateJournalStore;
613
+ readForProtection?: () => {
614
+ entries: readonly TelegramUpdateJournalEntry[];
615
+ };
613
616
  }
614
617
  export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
615
618
  getProfileName: () => string | undefined;
@@ -1443,6 +1443,19 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
1443
1443
  profileName,
1444
1444
  botIdentity,
1445
1445
  }),
1446
+ readForProtection() {
1447
+ // Protection must never turn corruption/recovery into empty-work evidence.
1448
+ const evidence = inspectTelegramUpdateJournalFamily({
1449
+ directory: dirname(path), path, profile: profileName, botIdentity,
1450
+ limits: {
1451
+ maxFiles: 1024,
1452
+ maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
1453
+ maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
1454
+ maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
1455
+ },
1456
+ });
1457
+ return { entries: evidence.kind === "present" ? evidence.file.entries : [] };
1458
+ },
1446
1459
  journal: createTelegramUpdateJournalStore({
1447
1460
  path,
1448
1461
  profileName,
@@ -85,6 +85,7 @@ export interface TelegramBridgeSessionServiceRuntime {
85
85
  guestPlaceholder?: {
86
86
  stopAll(): void;
87
87
  };
88
+ prepareThreadPreservationOnQuit?: (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
88
89
  }
89
90
  export interface TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel = unknown> {
90
91
  contextStore: TelegramSessionContextStore<ExtensionContext>;
@@ -111,6 +112,7 @@ export interface TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel = unknow
111
112
  capabilityMonitor: TelegramBridgeSessionServiceRuntime["capabilityMonitor"];
112
113
  queueWatchdog: TelegramBridgeSessionServiceRuntime["queueWatchdog"];
113
114
  guestPlaceholder?: TelegramBridgeSessionServiceRuntime["guestPlaceholder"];
115
+ prepareThreadPreservationOnQuit?: TelegramBridgeSessionServiceRuntime["prepareThreadPreservationOnQuit"];
114
116
  };
115
117
  }
116
118
  export declare function createTelegramBridgeSessionLifecycleDeps<TQueueItem, TModel = unknown>(ports: TelegramBridgeSessionLifecyclePorts<TQueueItem, TModel>): TelegramBridgeSessionLifecycleAssemblyDeps<TQueueItem, TModel>;
@@ -91,7 +91,8 @@ export function createTelegramSessionGenerationFence(store, hooks) {
91
91
  if (!store.isCurrent(ctx, generation))
92
92
  return;
93
93
  await hooks.onSessionShutdown(event, ctx);
94
- store.clear(ctx);
94
+ if (store.isCurrent(ctx, generation))
95
+ store.clear(ctx);
95
96
  },
96
97
  };
97
98
  }
@@ -115,6 +116,7 @@ export function createTelegramBridgeSessionLifecycleDeps(ports) {
115
116
  capabilityMonitor: ports.services.capabilityMonitor,
116
117
  queueWatchdog: ports.services.queueWatchdog,
117
118
  guestPlaceholder: ports.services.guestPlaceholder,
119
+ prepareThreadPreservationOnQuit: ports.services.prepareThreadPreservationOnQuit,
118
120
  },
119
121
  };
120
122
  }
@@ -144,18 +146,37 @@ export function createTelegramBridgeSessionLifecycleAssembly(deps) {
144
146
  deps.services.queueWatchdog.start(ctx);
145
147
  },
146
148
  async onSessionShutdown(event, ctx) {
147
- if (!isSessionActive(ctx))
149
+ const generation = deps.contextStore.getGeneration();
150
+ const isCurrent = () => deps.contextStore.isCurrent(ctx, generation);
151
+ if (!isCurrent())
148
152
  return;
153
+ let preserveThread;
154
+ if (event.reason === "quit") {
155
+ try {
156
+ preserveThread = deps.services.prepareThreadPreservationOnQuit?.(isCurrent);
157
+ }
158
+ catch (error) {
159
+ deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
160
+ }
161
+ }
149
162
  deps.services.guestPlaceholder?.stopAll();
150
163
  await deps.services.delivery.onSessionShutdown();
151
- if (!isSessionActive(ctx))
164
+ if (!isCurrent())
152
165
  return;
153
166
  deps.services.queueWatchdog.stop();
154
167
  deps.services.capabilityMonitor.stop();
155
168
  await queueLifecycle.onSessionShutdown(event, ctx);
156
- if (!isSessionActive(ctx))
169
+ if (!isCurrent())
157
170
  return;
158
171
  await deps.services.inboundWorker.onSessionShutdown();
172
+ if (!isCurrent())
173
+ return;
174
+ try {
175
+ await preserveThread?.();
176
+ }
177
+ catch (error) {
178
+ deps.follower.recordRuntimeEvent("session", error, { phase: "preserve-thread-on-quit" });
179
+ }
159
180
  },
160
181
  };
161
182
  const followerLifecycle = appendTelegramLifecycleHooks(servicesLifecycle, {
@@ -133,6 +133,7 @@ export interface TelegramLockedPollingRuntime<TContext extends TelegramLockConte
133
133
  start: (ctx: TContext, options?: TelegramLockedPollingStartOptions) => Promise<TelegramLockedPollingStartResult>;
134
134
  stop: () => Promise<string>;
135
135
  suspend: () => Promise<void>;
136
+ isSuspended: () => boolean;
136
137
  onPersistentConflict: (ctx: TContext, count: number) => Promise<void>;
137
138
  onSessionStart: (_event: unknown, ctx: TContext) => Promise<void>;
138
139
  registerFollowerWithOwner?: (ctx: TContext, owner: TelegramLockEntry) => boolean | undefined | Promise<boolean | undefined>;
package/dist/lib/locks.js CHANGED
@@ -551,7 +551,8 @@ export function isProcessAlive(pid) {
551
551
  return true;
552
552
  }
553
553
  catch (error) {
554
- return error.code === "EPERM";
554
+ // Only an absent PID proves death; permission and unexpected failures do not.
555
+ return error.code !== "ESRCH";
555
556
  }
556
557
  }
557
558
  export function formatTelegramLockEntry(lock) {
@@ -835,6 +836,9 @@ export function createTelegramLockedPollingRuntime(deps) {
835
836
  let takeoverCandidate;
836
837
  let sessionAutoStartRun;
837
838
  let pollingGeneration = 0;
839
+ let suspendedGeneration;
840
+ let suspensionsInFlight = 0;
841
+ let startupsInFlight = 0;
838
842
  const ownershipCheckMs = deps.ownershipCheckMs ?? TELEGRAM_OWNERSHIP_CHECK_MS;
839
843
  const ownershipRefreshMs = deps.ownershipRefreshMs ?? TELEGRAM_OWNERSHIP_REFRESH_MS;
840
844
  const stopOwnershipWatcher = () => {
@@ -846,20 +850,30 @@ export function createTelegramLockedPollingRuntime(deps) {
846
850
  ownershipRefreshInterval = undefined;
847
851
  };
848
852
  const suspendPolling = async () => {
849
- pollingGeneration += 1;
850
- activeContext = undefined;
851
- deps.transportMonitor?.stop();
852
- deps.stopFollowerRegistration?.();
853
- stopOwnershipWatcher();
854
- if (sessionAutoStartRun) {
855
- await sessionAutoStartRun;
853
+ const generation = ++pollingGeneration;
854
+ suspensionsInFlight += 1;
855
+ try {
856
+ activeContext = undefined;
857
+ deps.transportMonitor?.stop();
856
858
  deps.stopFollowerRegistration?.();
859
+ stopOwnershipWatcher();
860
+ if (sessionAutoStartRun) {
861
+ await sessionAutoStartRun;
862
+ deps.stopFollowerRegistration?.();
863
+ }
864
+ if (ownershipStop) {
865
+ await ownershipStop;
866
+ return;
867
+ }
868
+ await deps.stopPolling();
869
+ // Unsettled starts, overlapping stops or stale completion cannot certify quiescence.
870
+ if (generation === pollingGeneration && suspensionsInFlight === 1 && startupsInFlight === 0) {
871
+ suspendedGeneration = generation;
872
+ }
857
873
  }
858
- if (ownershipStop) {
859
- await ownershipStop;
860
- return;
874
+ finally {
875
+ suspensionsInFlight -= 1;
861
876
  }
862
- await deps.stopPolling();
863
877
  };
864
878
  const stopAfterOwnershipLoss = () => {
865
879
  if (ownershipStop)
@@ -906,6 +920,7 @@ export function createTelegramLockedPollingRuntime(deps) {
906
920
  return false;
907
921
  activeContext = ctx;
908
922
  startOwnershipWatcher(ctx);
923
+ startupsInFlight += 1;
909
924
  try {
910
925
  if (!deps.lock.refresh(snapshotLockContext(ctx))) {
911
926
  stopOwnershipWatcher();
@@ -934,6 +949,9 @@ export function createTelegramLockedPollingRuntime(deps) {
934
949
  deps.onTransportAvailabilityChanged?.();
935
950
  throw error;
936
951
  }
952
+ finally {
953
+ startupsInFlight -= 1;
954
+ }
937
955
  if (!isCurrent())
938
956
  return false;
939
957
  if (deps.lock.owns(ctx)) {
@@ -1075,6 +1093,8 @@ export function createTelegramLockedPollingRuntime(deps) {
1075
1093
  return "Telegram bridge disconnected.";
1076
1094
  },
1077
1095
  suspend: suspendPolling,
1096
+ isSuspended: () => suspendedGeneration === pollingGeneration &&
1097
+ suspensionsInFlight === 0 && startupsInFlight === 0 && !sessionAutoStartRun && !ownershipStop,
1078
1098
  onPersistentConflict: async (ctx, count) => {
1079
1099
  if (activeContext === undefined || ownershipStop)
1080
1100
  return;
@@ -14,6 +14,12 @@ export interface TelegramAgentDirResolutionInput {
14
14
  * 3. Fallback: `~/.pi/agent`.
15
15
  */
16
16
  export declare function resolveAgentDir(input?: TelegramAgentDirResolutionInput): string;
17
+ /**
18
+ * Pure reference preflight against an independently approved resource path.
19
+ * Reject aliases; never repair relative historical references or redirect storage.
20
+ * Equality proves spelling only, not file identity, consumer closure or migration readiness.
21
+ */
22
+ export declare function requireTelegramStoragePathReference(path: string, expectedPath: string): string;
17
23
  /** Telegram bridge configuration file (<agentDir>/telegram.json). */
18
24
  export declare function resolveTelegramConfigPath(): string;
19
25
  /** Telegram bridge temporary directory (<agentDir>/tmp/telegram). */
@@ -28,12 +34,16 @@ export declare function getTelegramDiagnosticsDisplayPaths(profileName?: string)
28
34
  };
29
35
  /** Durable Workspace admission ledger (<agentDir>/tmp/telegram/workspace-admission[.<profile>].json). */
30
36
  export declare function resolveTelegramWorkspaceAdmissionPath(agentDir?: string, profileName?: string): string;
37
+ /** Profile-only callback shape; binds storage to the configured agent directory. */
38
+ export declare function resolveTelegramWorkspaceAdmissionPathForProfile(profileName?: string): string;
31
39
  /** Durable inactive Thread cleanup work-set journal. */
32
40
  export declare function resolveTelegramThreadCleanupWorkPath(agentDir?: string, profileName?: string): string;
33
41
  /** Durable agent-authored channel post journal. */
34
42
  export declare function resolveTelegramChannelPostJournalPath(agentDir?: string, profileName?: string): string;
35
43
  /** Durable inbound update journal (<agentDir>/tmp/telegram/inbox[.<profile>].json). */
36
44
  export declare function resolveTelegramUpdateJournalPath(agentDir?: string, profileName?: string): string;
45
+ /** Profile-only callback shape; binds storage to the configured agent directory. */
46
+ export declare function resolveTelegramUpdateJournalPathForProfile(profileName?: string): string;
37
47
  /** Durable follower delivery journal, isolated by stable recipient binding. */
38
48
  export declare function resolveTelegramFollowerJournalPath(recipientBindingKey: string, agentDir?: string, profileName?: string): string;
39
49
  /** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
package/dist/lib/paths.js CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { createHash } from "node:crypto";
11
11
  import { homedir } from "node:os";
12
- import { join, resolve } from "node:path";
12
+ import { isAbsolute, join, resolve } from "node:path";
13
13
  export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
14
14
  /**
15
15
  * Resolve the agent data directory for the current Pi-compatible runtime.
@@ -33,6 +33,19 @@ export function resolveAgentDir(input = {}) {
33
33
  }
34
34
  return join(homedir(), ".pi", "agent");
35
35
  }
36
+ /**
37
+ * Pure reference preflight against an independently approved resource path.
38
+ * Reject aliases; never repair relative historical references or redirect storage.
39
+ * Equality proves spelling only, not file identity, consumer closure or migration readiness.
40
+ */
41
+ export function requireTelegramStoragePathReference(path, expectedPath) {
42
+ if (typeof path !== "string" || typeof expectedPath !== "string" ||
43
+ !isAbsolute(path) || resolve(path) !== path ||
44
+ !isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
45
+ throw new Error("Telegram storage reference does not match its approved absolute path.");
46
+ }
47
+ return path;
48
+ }
36
49
  /** Telegram bridge configuration file (<agentDir>/telegram.json). */
37
50
  export function resolveTelegramConfigPath() {
38
51
  return join(resolveAgentDir(), "telegram.json");
@@ -65,6 +78,10 @@ export function getTelegramDiagnosticsDisplayPaths(profileName) {
65
78
  export function resolveTelegramWorkspaceAdmissionPath(agentDir = resolveAgentDir(), profileName) {
66
79
  return resolveTelegramProfileTempFilePath("workspace-admission", "json", agentDir, profileName);
67
80
  }
81
+ /** Profile-only callback shape; binds storage to the configured agent directory. */
82
+ export function resolveTelegramWorkspaceAdmissionPathForProfile(profileName) {
83
+ return resolveTelegramWorkspaceAdmissionPath(resolveAgentDir(), profileName);
84
+ }
68
85
  /** Durable inactive Thread cleanup work-set journal. */
69
86
  export function resolveTelegramThreadCleanupWorkPath(agentDir = resolveAgentDir(), profileName) {
70
87
  return resolveTelegramProfileTempFilePath("thread-cleanup", "json", agentDir, profileName);
@@ -77,6 +94,10 @@ export function resolveTelegramChannelPostJournalPath(agentDir = resolveAgentDir
77
94
  export function resolveTelegramUpdateJournalPath(agentDir = resolveAgentDir(), profileName) {
78
95
  return resolveTelegramProfileTempFilePath("inbox", "json", agentDir, profileName);
79
96
  }
97
+ /** Profile-only callback shape; binds storage to the configured agent directory. */
98
+ export function resolveTelegramUpdateJournalPathForProfile(profileName) {
99
+ return resolveTelegramUpdateJournalPath(resolveAgentDir(), profileName);
100
+ }
80
101
  /** Durable follower delivery journal, isolated by stable recipient binding. */
81
102
  export function resolveTelegramFollowerJournalPath(recipientBindingKey, agentDir = resolveAgentDir(), profileName) {
82
103
  if (!recipientBindingKey) {
@@ -16,8 +16,8 @@ const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
16
16
  const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
17
17
  const TELEGRAM_POLLING_RETRY_MS = 3_000;
18
18
  export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
19
- // Standard Telegram DM polling does not expose ordinary message-deletion events,
20
- // so queue removal stays reaction-driven while delete-like business updates remain defensive-only.
19
+ // Standard Telegram DM polling does not expose ordinary message-deletion events.
20
+ // Business deletions belong to a separate namespace and default routing ignores them.
21
21
  export const TELEGRAM_ALLOWED_UPDATES = [
22
22
  "message",
23
23
  "edited_message",
@@ -84,6 +84,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
84
84
  historyText: string;
85
85
  priorityEmoji?: string;
86
86
  reactionSuppressionEmoji?: string;
87
+ /** Emit the explicit aborted-turn notice when /next settles this active turn. */
88
+ announceNextAbortOnEnd?: boolean;
87
89
  /** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
88
90
  voiceReplyPreferred?: boolean;
89
91
  /** Turn must be delivered as voice (voice mode) */
@@ -165,6 +167,8 @@ export interface TelegramActiveTurnStore<TTurn extends PendingTelegramTurn = Pen
165
167
  has: () => boolean;
166
168
  set: (turn: TTurn) => void;
167
169
  clear: () => void;
170
+ markNextAbortAnnouncement: () => boolean;
171
+ clearNextAbortAnnouncement: () => boolean;
168
172
  getChatId: () => number | undefined;
169
173
  getTarget: () => TelegramQueueTarget | undefined;
170
174
  getReplyToMessageId: () => number | undefined;
@@ -391,6 +395,7 @@ export interface TelegramAgentEndPlan {
391
395
  kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
392
396
  shouldClearPreview: boolean;
393
397
  shouldDispatchNext: boolean;
398
+ shouldSendAbortMessage: boolean;
394
399
  shouldSendErrorMessage: boolean;
395
400
  shouldSendAttachmentNotice: boolean;
396
401
  }
@@ -474,7 +479,7 @@ export interface TelegramAgentEndHookRuntimeDeps<TTurn extends PendingTelegramTu
474
479
  getActiveTurn: () => TTurn | undefined;
475
480
  loadConfig?: () => Promise<void>;
476
481
  extractAssistant: (messages: readonly TMessage[]) => TelegramAgentEndAssistantResult;
477
- isRecoveredAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
482
+ isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
478
483
  getFoldQueuedPromptsIntoHistory: () => boolean;
479
484
  resetRuntimeState: () => void;
480
485
  isSessionActive?: (ctx: TContext) => boolean;
@@ -517,6 +522,7 @@ export declare function buildTelegramAgentEndPlan(options: {
517
522
  hasFinalText: boolean;
518
523
  hasQueuedAttachments: boolean;
519
524
  foldQueuedPromptsIntoHistory: boolean;
525
+ announceNextAbortOnEnd?: boolean;
520
526
  }): TelegramAgentEndPlan;
521
527
  export declare function createTelegramAgentEndHook<TTurn extends PendingTelegramTurn, TContext, TMessage, TReplyMarkup = unknown>(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup>): (event: TelegramAgentEndHookEvent<TMessage>, ctx: TContext, assistantOverride?: TelegramAgentEndAssistantResult) => Promise<void>;
522
528
  export declare function handleTelegramAgentEndRuntime<TTurn extends PendingTelegramTurn, TReplyMarkup = unknown>(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void>;
@@ -759,6 +765,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
759
765
  }>["item"], ctx: TContext) => boolean;
760
766
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
761
767
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
768
+ reconcileNextDispatchAnnouncementReplyOwnership?: (item: PendingTelegramTurn) => void;
762
769
  isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
763
770
  hasPendingInboundQueueMutationForItem?: (item: TelegramQueueItem<TContext>) => boolean;
764
771
  isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
@@ -768,6 +775,7 @@ export interface TelegramQueueDispatchControllerDeps<TContext = unknown> extends
768
775
  export interface TelegramQueueDispatchController<TContext = unknown> {
769
776
  dispatchNext: (ctx: TContext) => void;
770
777
  requestNextDispatchAnnouncement: () => void;
778
+ cancelNextDispatchAnnouncement: () => void;
771
779
  }
772
780
  export declare function executeTelegramQueueDispatchPlan<TContext = unknown>(plan: TelegramQueueDispatchAction<TContext>, deps: TelegramDispatchRuntimeDeps<TContext>): void;
773
781
  export type TelegramQueueDispatchRuntimeDeps<TContext = unknown> = Omit<TelegramQueueDispatchControllerDeps<TContext>, "canDispatch"> & TelegramDispatchReadinessDeps<TContext>;