@llblab/pi-kit 0.19.1 → 0.20.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 (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +5 -0
  5. package/node_modules/@llblab/pi-state-flow/README.md +3 -1
  6. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +45 -5
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +9 -9
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +19 -2
  9. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +52 -11
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -1
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +0 -2
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +2 -2
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +36 -15
  14. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  15. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +1 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +1 -1
  17. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +5 -5
  18. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +2 -1
  19. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -2
  20. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +48 -5
  21. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +9 -9
  22. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +65 -11
  23. package/node_modules/@llblab/pi-state-flow/lib/status.ts +1 -2
  24. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +2 -2
  25. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +35 -15
  26. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  27. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +1 -1
  28. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +1 -1
  29. package/node_modules/@llblab/pi-telegram/AGENTS.md +9 -7
  30. package/node_modules/@llblab/pi-telegram/BACKLOG.md +13 -32
  31. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +11 -0
  32. package/node_modules/@llblab/pi-telegram/README.md +4 -3
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +2 -2
  34. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +3 -5
  35. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
  36. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +93 -9
  37. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +9 -3
  38. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +231 -71
  39. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +4 -0
  40. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +34 -7
  41. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +63 -2
  42. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +3 -0
  43. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +13 -0
  44. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -0
  45. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +25 -4
  46. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +1 -0
  47. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +32 -12
  48. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +10 -0
  49. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +22 -1
  50. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +2 -2
  51. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +1 -1
  52. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +1 -2
  53. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +9 -0
  54. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +41 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +9 -1
  56. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +23 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +7 -2
  58. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +2 -0
  59. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +68 -37
  60. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +6 -4
  61. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +134 -61
  62. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +9 -0
  63. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +49 -3
  64. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +77 -6
  65. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +384 -8
  66. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +3 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +6 -0
  68. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  69. package/node_modules/@llblab/pi-telegram/docs/architecture.md +21 -14
  70. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +31 -10
  71. package/node_modules/@llblab/pi-telegram/docs/updates.md +2 -0
  72. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +5 -7
  73. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +69 -12
  74. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +236 -94
  75. package/node_modules/@llblab/pi-telegram/lib/bus.ts +39 -9
  76. package/node_modules/@llblab/pi-telegram/lib/extension.ts +69 -2
  77. package/node_modules/@llblab/pi-telegram/lib/journal.ts +14 -0
  78. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +23 -4
  79. package/node_modules/@llblab/pi-telegram/lib/locks.ts +32 -13
  80. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -1
  81. package/node_modules/@llblab/pi-telegram/lib/polling.ts +2 -2
  82. package/node_modules/@llblab/pi-telegram/lib/queue.ts +2 -3
  83. package/node_modules/@llblab/pi-telegram/lib/sync.ts +45 -1
  84. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -2
  85. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +11 -3
  86. package/node_modules/@llblab/pi-telegram/lib/threads.ts +73 -36
  87. package/node_modules/@llblab/pi-telegram/lib/updates.ts +145 -78
  88. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +67 -4
  89. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +465 -7
  90. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +7 -0
  91. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  92. package/package.json +3 -3
@@ -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>;
@@ -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). */
@@ -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",
@@ -474,7 +474,7 @@ export interface TelegramAgentEndHookRuntimeDeps<TTurn extends PendingTelegramTu
474
474
  getActiveTurn: () => TTurn | undefined;
475
475
  loadConfig?: () => Promise<void>;
476
476
  extractAssistant: (messages: readonly TMessage[]) => TelegramAgentEndAssistantResult;
477
- isRecoveredAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
477
+ isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
478
478
  getFoldQueuedPromptsIntoHistory: () => boolean;
479
479
  resetRuntimeState: () => void;
480
480
  isSessionActive?: (ctx: TContext) => boolean;
@@ -869,8 +869,7 @@ export function createTelegramAgentEndHook(deps) {
869
869
  return;
870
870
  const turn = deps.getActiveTurn();
871
871
  const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
872
- const assistant = extractedAssistant.recoveredFromEarlier &&
873
- deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
872
+ const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
874
873
  ? { stopReason: extractedAssistant.stopReason }
875
874
  : extractedAssistant;
876
875
  const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
@@ -97,6 +97,15 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
97
97
  export declare function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(state: TSyncState, action: string, options?: {
98
98
  nowMs?: number;
99
99
  }): TSyncState;
100
+ export declare function createTelegramPreservedLeaderQuitHandler(deps: {
101
+ instanceId: string;
102
+ topicTargetStore: Pick<TelegramTopicTargetStore, "load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
103
+ getCurrentLeaderEpoch: () => number | string | undefined;
104
+ getProfileName: () => string | undefined;
105
+ isPollingSuspended: () => boolean;
106
+ resolveAutomaticThreadCleanupEnabled: () => boolean | Promise<boolean>;
107
+ runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
108
+ }): (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
100
109
  export interface TelegramSessionRestartThreadCleanupDeps<TSyncState extends TelegramSyncState> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
101
110
  suspendPolling: () => Promise<void>;
102
111
  }
@@ -6,6 +6,7 @@
6
6
  import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.js";
7
7
  import { getTelegramTargetKey } from "./target.js";
8
8
  import * as ThreadReconciler from "./thread-reconciler.js";
9
+ import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
9
10
  import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
10
11
  import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, normalizeTelegramWorkspacePath, provisionOwnBusTopic, } from "./threads.js";
11
12
  export function markTelegramConfigSyncChange(state, action, options) {
@@ -24,6 +25,42 @@ export function markTelegramConfigSyncChange(state, action, options) {
24
25
  });
25
26
  return nextState;
26
27
  }
28
+ export function createTelegramPreservedLeaderQuitHandler(deps) {
29
+ return (isSessionCurrent) => {
30
+ const epoch = deps.getCurrentLeaderEpoch();
31
+ const profile = deps.getProfileName();
32
+ if (epoch === undefined || !isSessionCurrent())
33
+ return undefined;
34
+ const isCurrent = () => isSessionCurrent() && deps.getCurrentLeaderEpoch() === epoch &&
35
+ deps.getProfileName() === profile && deps.isPollingSuspended();
36
+ return async () => {
37
+ if (!isCurrent() || await deps.resolveAutomaticThreadCleanupEnabled() !== false)
38
+ return;
39
+ if (!isCurrent())
40
+ return;
41
+ await deps.runWorkspaceOperation({
42
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
43
+ operationKind: "workspace.preserve-leader-quit",
44
+ scopes: [{ kind: "profile" }],
45
+ }, async () => {
46
+ if (!isCurrent())
47
+ return;
48
+ await deps.topicTargetStore.load();
49
+ if (!isCurrent())
50
+ return;
51
+ const records = deps.topicTargetStore.list().filter((record) => record.instanceId === deps.instanceId && (record.status === "active" || record.status === "starting"));
52
+ if (records.length !== 1)
53
+ return;
54
+ const record = records[0];
55
+ if (deps.topicTargetStore.listPendingCleanups().some((intent) => intent.target.chatId === record.target.chatId && intent.target.threadId === record.target.threadId))
56
+ return;
57
+ if (!await deps.topicTargetStore.detachTargetOwner(record, isCurrent) && isCurrent()) {
58
+ throw new Error("Telegram preserved leader detachment was not committed.");
59
+ }
60
+ });
61
+ };
62
+ };
63
+ }
27
64
  export function createTelegramSessionRestartThreadCleanupHandler(deps) {
28
65
  return createTelegramManualThreadDisconnectHandler({
29
66
  ...deps,
@@ -353,10 +390,13 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
353
390
  };
354
391
  const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
355
392
  const legacyLeaderRecord = deps.topicTargetStore.getByProfileKey(leaderProfileKey);
393
+ let capacityUnavailable = false;
356
394
  const workspaceIdentity = normalizedLeaderCwd
357
- ? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId })
395
+ ? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId, onCapacityUnavailable() { capacityUnavailable = true; } })
358
396
  : undefined;
359
397
  if (deps.cwd && !workspaceIdentity) {
398
+ if (capacityUnavailable)
399
+ throw new TelegramWorkspaceSlotUnavailableError();
360
400
  throw new Error("Telegram Workspace identity is already claimed.");
361
401
  }
362
402
  const commitWorkspaceBinding = async (result) => {
@@ -484,6 +484,8 @@ export declare function getTelegramApiErrorRequestTarget(error: unknown): {
484
484
  threadId: number;
485
485
  } | undefined;
486
486
  export declare function isTelegramStaleTargetHttpError(error: unknown): boolean;
487
+ /** Only a parsed Telegram rejection of this method proves a request had no effect. */
488
+ export declare function isTelegramApiRequestRejected(error: unknown, method: string): boolean;
487
489
  export declare function isTelegramMessageNotModifiedError(error: unknown): boolean;
488
490
  export declare function isTelegramApiMethodRetrySafe(method: string): boolean;
489
491
  export declare function isRetryableTelegramApiError(error: unknown): boolean;
@@ -522,13 +524,19 @@ export declare function createTelegramAssistantDraftSender(deps: {
522
524
  sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
523
525
  }): TelegramBridgeApiRuntime["sendMessageDraft"];
524
526
  export declare function buildTelegramAnswerGuestQueryBody(guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions): Record<string, unknown>;
527
+ export type TelegramWorkspaceThreadDeletionTransport = (authorize: () => {
528
+ chatId: number;
529
+ threadId: number;
530
+ }) => Promise<void>;
525
531
  export declare function createDefaultTelegramBridgeApiRuntime(deps: {
526
532
  getBotToken: () => string | undefined;
527
533
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
528
534
  captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
529
535
  targetActivity?: TelegramApiTargetActivityRuntime;
530
536
  workspaceAdmission?: TelegramApiWorkspaceAdmissionPort | (() => TelegramApiWorkspaceAdmissionPort | undefined);
531
- }): TelegramBridgeApiRuntime;
537
+ }): TelegramBridgeApiRuntime & {
538
+ deleteWorkspaceThread: TelegramWorkspaceThreadDeletionTransport;
539
+ };
532
540
  export declare function createTelegramBridgeApiRuntime(deps: TelegramBridgeApiRuntimeDeps): TelegramBridgeApiRuntime;
533
541
  /**
534
542
  * Creates a low-level Telegram Bot API client.
@@ -231,11 +231,13 @@ export function isTelegramApiCommitUnknownError(error) {
231
231
  class TelegramApiHttpError extends Error {
232
232
  status;
233
233
  retryAfterSeconds;
234
+ rejectedRequestMethod;
234
235
  requestTarget;
235
- constructor(message, status, retryAfterSeconds) {
236
+ constructor(message, status, retryAfterSeconds, rejectedRequestMethod) {
236
237
  super(message);
237
238
  this.status = status;
238
239
  this.retryAfterSeconds = retryAfterSeconds;
240
+ this.rejectedRequestMethod = rejectedRequestMethod;
239
241
  }
240
242
  }
241
243
  function attachTelegramApiRequestTarget(error, body) {
@@ -267,6 +269,11 @@ export function isTelegramStaleTargetHttpError(error) {
267
269
  return false;
268
270
  return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(error.message);
269
271
  }
272
+ /** Only a parsed Telegram rejection of this method proves a request had no effect. */
273
+ export function isTelegramApiRequestRejected(error, method) {
274
+ return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
275
+ error.rejectedRequestMethod === method;
276
+ }
270
277
  export function isTelegramMessageNotModifiedError(error) {
271
278
  return (error instanceof Error && error.message.includes("message is not modified"));
272
279
  }
@@ -398,7 +405,8 @@ async function parseTelegramApiResponse(response, method) {
398
405
  const retryAfterHeader = response.headers?.get("retry-after");
399
406
  const retryAfterSeconds = data?.parameters?.retry_after ??
400
407
  (retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
401
- throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined);
408
+ throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false && data.error_code === response.status &&
409
+ [400, 401, 403, 404, 429].includes(response.status) ? method : undefined);
402
410
  }
403
411
  if (!data) {
404
412
  throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
@@ -856,7 +864,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
856
864
  },
857
865
  })
858
866
  : client;
859
- return createTelegramBridgeApiRuntime({
867
+ const runtime = createTelegramBridgeApiRuntime({
860
868
  client: deps.targetActivity
861
869
  ? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
862
870
  : admittedClient,
@@ -866,6 +874,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
866
874
  recordRuntimeEvent: deps.recordRuntimeEvent,
867
875
  captureRequestErrorHandler: deps.captureRequestErrorHandler,
868
876
  });
877
+ return {
878
+ ...runtime,
879
+ async deleteWorkspaceThread(authorize) {
880
+ // The exclusive deletion permit replaces ordinary target admission.
881
+ const target = authorize();
882
+ const deleted = await client.call("deleteForumTopic", {
883
+ chat_id: target.chatId, message_thread_id: target.threadId,
884
+ }, { maxAttempts: 1, retrySafety: "non-idempotent" });
885
+ if (deleted !== true)
886
+ throw new Error("Telegram Workspace Thread deletion was not confirmed.");
887
+ },
888
+ };
869
889
  }
870
890
  export function createTelegramBridgeApiRuntime(deps) {
871
891
  const recoverRequestError = async (handler, error) => {
@@ -342,7 +342,9 @@ export function createTelegramThreadCleanupWorkStore(options) {
342
342
  return { confirmed: true, entry: structuredClone(entry) };
343
343
  });
344
344
  },
345
- list() { return structuredClone(read().workSets); },
345
+ list() {
346
+ return structuredClone(withTelegramFileTransaction(`${options.path}.transaction`, () => read().workSets));
347
+ },
346
348
  };
347
349
  }
348
350
  export async function commitTelegramInactiveThreadCleanup(input) {
@@ -401,7 +403,10 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
401
403
  if (!deps.canAdoptFence(fence))
402
404
  return undefined;
403
405
  try {
404
- fence = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
406
+ const adopted = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
407
+ if (adopted.destructiveKind !== "manual-thread-cleanup")
408
+ return undefined;
409
+ fence = adopted;
405
410
  }
406
411
  catch {
407
412
  return undefined;
@@ -212,6 +212,8 @@ export interface TelegramTopicTargetStore {
212
212
  refresh?: () => Promise<void>;
213
213
  persist: () => Promise<void>;
214
214
  invalidateTarget: (target: TelegramTarget, isCurrent: () => boolean, lastSyncError: string) => Promise<boolean>;
215
+ /** Caller proves owner detachment; this does not assert Telegram Thread absence. */
216
+ detachTargetOwner: (expected: TelegramTopicTargetRecord, isCurrent: () => boolean) => Promise<boolean>;
215
217
  list: () => TelegramTopicTargetRecord[];
216
218
  getFollowerRecoveryHintByTarget?: (target: TelegramTarget) => {
217
219
  slot?: string;