@llblab/pi-kit 0.35.0 → 0.35.2

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 (68) hide show
  1. package/BACKLOG.md +2 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +1 -1
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -6
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
  7. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +11 -3
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
  10. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +22 -0
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +75 -16
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +1 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +11 -1
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +30 -19
  15. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
  16. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
  17. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +61 -46
  18. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
  19. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
  20. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +36 -0
  21. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +80 -0
  22. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
  23. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
  24. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +2 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +18 -3
  26. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +13 -0
  27. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +33 -12
  28. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
  30. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
  31. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
  33. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
  34. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +11 -0
  35. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +31 -8
  36. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +12 -5
  37. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +21 -3
  38. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +5 -1
  39. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +5 -1
  40. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +26 -13
  41. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +6 -2
  42. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +57 -12
  43. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  44. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
  45. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +6 -2
  46. package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
  47. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +13 -2
  48. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
  49. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +111 -15
  50. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -0
  51. package/node_modules/@llblab/pi-telegram/lib/bus.ts +30 -21
  52. package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
  53. package/node_modules/@llblab/pi-telegram/lib/extension.ts +64 -56
  54. package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
  55. package/node_modules/@llblab/pi-telegram/lib/logging.ts +122 -0
  56. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
  57. package/node_modules/@llblab/pi-telegram/lib/paths.ts +22 -3
  58. package/node_modules/@llblab/pi-telegram/lib/polling.ts +44 -10
  59. package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
  60. package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
  61. package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
  62. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +40 -7
  63. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +30 -10
  64. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +6 -1
  65. package/node_modules/@llblab/pi-telegram/lib/threads.ts +37 -20
  66. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +69 -17
  67. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  68. package/package.json +2 -2
@@ -111,6 +111,7 @@ import * as WorkspaceIdentity from "./workspace-identity.ts";
111
111
  const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
112
112
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
113
113
  const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
114
+ const TELEGRAM_BUS_FOLLOWER_API_SLOW_CALL_MS = 1_500;
114
115
  const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
115
116
  export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
116
117
  TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
@@ -326,6 +327,20 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
326
327
  timeoutMs?: number;
327
328
  }
328
329
 
330
+ /** Structural port for run-collapsing diagnostics, implemented by the logging domain. */
331
+ export interface TelegramFailureRunLogPort {
332
+ record: (error: unknown, details?: Record<string, unknown>) => boolean;
333
+ settled: () => TelegramFailureRunSummaryPort | undefined;
334
+ reset: () => void;
335
+ }
336
+
337
+ export interface TelegramFailureRunSummaryPort {
338
+ signature: string;
339
+ failedAttempts: number;
340
+ suppressedFailures: number;
341
+ failedForMs: number;
342
+ }
343
+
329
344
  export interface TelegramBusFollowerApiCallerDeps {
330
345
  socketPath: TelegramBusSocketPathSource;
331
346
  instanceId: string;
@@ -337,11 +352,23 @@ export interface TelegramBusFollowerApiCallerDeps {
337
352
  ) => Promise<string | undefined>;
338
353
  getNowMs?: () => number;
339
354
  timeoutMs?: number;
355
+ /** Receives one diagnostic per call slower than `slowCallMs`; observation only. */
356
+ recordRuntimeEvent?: (
357
+ category: string,
358
+ error: unknown,
359
+ details?: Record<string, unknown>,
360
+ ) => void;
361
+ slowCallMs?: number;
340
362
  }
341
363
 
342
364
  export interface TelegramBusFollowerRegistrationRuntimeDeps<
343
365
  TContext extends { cwd?: string },
344
366
  > {
367
+ /**
368
+ * Collapses one sustained heartbeat failure run. Optional at this unit boundary, where an absent port records
369
+ * every failure; the runtime assembly requires it so production always supplies the logging domain's owner.
370
+ */
371
+ failureLog?: TelegramFailureRunLogPort;
345
372
  instanceId: string;
346
373
  createRequestId: () => string;
347
374
  protocolIdentity: TelegramBusProtocolIdentity;
@@ -673,6 +700,8 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
673
700
  TContext extends { cwd?: string },
674
701
  > {
675
702
  instanceId: string;
703
+ /** Collapses one sustained heartbeat failure run; the composition root supplies the logging domain's owner. */
704
+ failureLog: TelegramFailureRunLogPort;
676
705
  registrationState: TelegramBusFollowerRegistrationState;
677
706
  recordRuntimeEvent: (
678
707
  category: string,
@@ -789,6 +818,7 @@ export function createTelegramBusFollowerRuntimeAssembly<
789
818
  });
790
819
  registration = createTelegramBusFollowerRegistrationRuntime({
791
820
  ...ports.registration,
821
+ failureLog: ports.failureLog,
792
822
  ...sharedRuntimeDeps,
793
823
  startReceiving: receiver.start,
794
824
  stopReceiving: receiver.stop,
@@ -1495,6 +1525,7 @@ export function createTelegramBusFollowerClientRuntime<
1495
1525
  instanceId: deps.instanceId,
1496
1526
  getAuthSecret: deps.getApiAuthSecret,
1497
1527
  getRegistrationGeneration: deps.getRegistrationGeneration,
1528
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1498
1529
  }),
1499
1530
  agentMessages: createTelegramBusAgentMessageClient({
1500
1531
  ...sharedClientDeps,
@@ -1881,7 +1912,13 @@ export function createTelegramBusFollowerApiCaller(
1881
1912
  ): (method: string, args: unknown[], localOptions?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<unknown> {
1882
1913
  const getNowMs = deps.getNowMs ?? Date.now;
1883
1914
  const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1884
- return async (method, args, localOptions) => {
1915
+ const slowCallMs = deps.slowCallMs ?? TELEGRAM_BUS_FOLLOWER_API_SLOW_CALL_MS;
1916
+ const caller = async (
1917
+ method: string,
1918
+ args: unknown[],
1919
+ localOptions: Pick<TelegramApiCallOptions, "assertAuthority"> | undefined,
1920
+ timing: { registeredAtMs?: number },
1921
+ ): Promise<unknown> => {
1885
1922
  const assertion = localOptions?.assertAuthority;
1886
1923
  const assertCurrent = () => assertTelegramApiCallAuthority(assertion, false);
1887
1924
  assertCurrent();
@@ -1889,6 +1926,7 @@ export function createTelegramBusFollowerApiCaller(
1889
1926
  deps,
1890
1927
  timeoutMs,
1891
1928
  );
1929
+ timing.registeredAtMs = getNowMs();
1892
1930
  assertCurrent();
1893
1931
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
1894
1932
  let response: TelegramBusEnvelope | undefined;
@@ -1950,6 +1988,45 @@ export function createTelegramBusFollowerApiCaller(
1950
1988
  }
1951
1989
  throw new Error(message ?? "Telegram bus API call failed.");
1952
1990
  };
1991
+ return async (method, args, localOptions) => {
1992
+ const startedAtMs = getNowMs();
1993
+ const timing: { registeredAtMs?: number } = {};
1994
+ let outcome = "ok";
1995
+ try {
1996
+ return await caller(method, args, localOptions, timing);
1997
+ } catch (error) {
1998
+ outcome = "error";
1999
+ throw error;
2000
+ } finally {
2001
+ const elapsedMs = getNowMs() - startedAtMs;
2002
+ if (elapsedMs >= slowCallMs) {
2003
+ const apiMethod =
2004
+ (method === "call" || method === "callMultipart") &&
2005
+ typeof args[0] === "string"
2006
+ ? args[0]
2007
+ : method;
2008
+ try {
2009
+ deps.recordRuntimeEvent?.(
2010
+ "bus",
2011
+ `Telegram bus follower API call was slow (${elapsedMs}ms).`,
2012
+ {
2013
+ phase: "follower-api-slow",
2014
+ method: apiMethod,
2015
+ elapsedMs,
2016
+ registrationWaitMs:
2017
+ (timing.registeredAtMs ?? getNowMs()) - startedAtMs,
2018
+ ...(timing.registeredAtMs === undefined
2019
+ ? {}
2020
+ : { transportMs: getNowMs() - timing.registeredAtMs }),
2021
+ outcome,
2022
+ },
2023
+ );
2024
+ } catch {
2025
+ // Diagnostics cannot change the API call outcome.
2026
+ }
2027
+ }
2028
+ }
2029
+ };
1953
2030
  }
1954
2031
 
1955
2032
  async function resolveTelegramBusFollowerRegistration(
@@ -2521,10 +2598,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
2521
2598
  election: TelegramBusFollowerElection,
2522
2599
  ) => {
2523
2600
  if (!isCurrent()) return;
2524
- const activeCtx = deps.getActiveContext?.();
2525
- if (deps.getActiveContext && activeCtx !== ctx) {
2526
- scheduleRecovery(reason, ctx, binding);
2527
- return;
2601
+ if (deps.getActiveContext) {
2602
+ // Pi hands every event and command a fresh context object, so a manually connected follower never holds
2603
+ // the stored session object. Promote through the live session context instead of rescheduling: heartbeat
2604
+ // failures re-request recovery faster than the retry grace, so a reschedule-only path never fires.
2605
+ const activeCtx = deps.getActiveContext();
2606
+ if (!activeCtx) {
2607
+ scheduleRecovery(reason, ctx, binding);
2608
+ return;
2609
+ }
2610
+ ctx = activeCtx;
2528
2611
  }
2529
2612
  deps.setLifecyclePhase("electing");
2530
2613
  safeUpdateStatus(ctx);
@@ -2765,6 +2848,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
2765
2848
  (!deps.registrationState ||
2766
2849
  deps.registrationState.getGeneration() === generation);
2767
2850
  const stopHeartbeat = () => {
2851
+ // A stopped heartbeat ends the current failure run, so a later run logs its own first failure.
2852
+ heartbeatFailureLog.reset();
2768
2853
  if (!heartbeatInterval) return;
2769
2854
  clearInterval(heartbeatInterval);
2770
2855
  heartbeatInterval = undefined;
@@ -2793,6 +2878,22 @@ export function createTelegramBusFollowerRegistrationRuntime<
2793
2878
  }
2794
2879
  });
2795
2880
  };
2881
+ /** A sustained leader outage repeats one identical failure every interval; the port collapses the run. */
2882
+ const heartbeatFailureLog: TelegramFailureRunLogPort =
2883
+ deps.failureLog ?? {
2884
+ record(error) {
2885
+ try {
2886
+ deps.recordRuntimeEvent?.("bus", error, {
2887
+ phase: "follower-heartbeat",
2888
+ });
2889
+ } catch {
2890
+ // Diagnostics cannot replace heartbeat recovery.
2891
+ }
2892
+ return true;
2893
+ },
2894
+ settled: () => undefined,
2895
+ reset: () => undefined,
2896
+ };
2796
2897
  const sendHeartbeat = async () => {
2797
2898
  const leaderSocketPath = activeLeaderSocketPath;
2798
2899
  const registrationGeneration = activeRegistrationGeneration;
@@ -2818,6 +2919,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
2818
2919
  );
2819
2920
  if (!isCurrentHeartbeat()) return;
2820
2921
  if (response?.kind === "bus.ack" && response.ok) {
2922
+ heartbeatFailureLog.settled();
2821
2923
  const heartbeatResult = isRecord(response.result)
2822
2924
  ? response.result
2823
2925
  : undefined;
@@ -2850,13 +2952,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
2850
2952
  }
2851
2953
  } catch (error) {
2852
2954
  if (!isCurrentHeartbeat()) return;
2853
- try {
2854
- deps.recordRuntimeEvent?.("bus", error, {
2855
- phase: "follower-heartbeat",
2856
- });
2857
- } catch {
2858
- // Diagnostics cannot replace heartbeat recovery.
2859
- }
2955
+ heartbeatFailureLog.record(error);
2860
2956
  if (!heartbeatContext) return;
2861
2957
  try {
2862
2958
  await deps.onHeartbeatFailure?.(error, heartbeatContext);
@@ -3120,11 +3216,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
3120
3216
  return false;
3121
3217
  },
3122
3218
  setContext(ctx) {
3219
+ // Pi hands every event a fresh context object. Only a replaced session drops an in-flight attempt.
3123
3220
  if (
3124
3221
  registrationAttempt &&
3125
- (registrationAttempt.ctx !== ctx ||
3126
- registrationAttempt.sessionGeneration !==
3127
- deps.getSessionGeneration?.() ||
3222
+ (registrationAttempt.sessionGeneration !==
3223
+ deps.getSessionGeneration?.() ||
3128
3224
  registrationAttempt.sessionId !== deps.getSessionId?.(ctx))
3129
3225
  )
3130
3226
  registrationAttempt = undefined;
@@ -167,6 +167,7 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
167
167
  callApi: <TResponse>(
168
168
  method: string,
169
169
  body: Record<string, unknown>,
170
+ options?: TelegramApiCallOptions,
170
171
  ) => Promise<TResponse>;
171
172
  getCurrentLeaderEpoch?: () => number | string | undefined;
172
173
  getSyncState: () => Sync.TelegramSyncState;
@@ -1925,6 +1926,8 @@ export function createTelegramBusFollowerTargetProvisioner(
1925
1926
  text: connectedAnnouncement.text,
1926
1927
  parse_mode: connectedAnnouncement.parseMode,
1927
1928
  },
1929
+ // A deleted Thread is this probe's expected answer; the recovery below records it.
1930
+ { isExpectedError: Threads.isTelegramTopicTargetStaleError },
1928
1931
  );
1929
1932
  assertRestoreRegistration();
1930
1933
  if (recoverableTarget || probeRequiredRecord) {
@@ -1968,7 +1971,19 @@ export function createTelegramBusFollowerTargetProvisioner(
1968
1971
  error instanceof Error ? error.message : String(error),
1969
1972
  );
1970
1973
  await deps.topicTargetStore.persist();
1974
+ const staleTarget = result.target;
1971
1975
  result = await provisionTarget();
1976
+ deps.recordRuntimeEvent(
1977
+ "telegram",
1978
+ "Telegram follower restore probe found its Thread deleted; re-created it.",
1979
+ {
1980
+ phase: "topic-lifecycle-restore-probe-recreated",
1981
+ instanceId: registration.instanceId,
1982
+ chatId: staleTarget.chatId,
1983
+ staleThreadId: staleTarget.threadId,
1984
+ threadId: result.target.threadId,
1985
+ },
1986
+ );
1972
1987
  connectedAnnouncement =
1973
1988
  createTelegramBusInstanceLifecycleAnnouncement({
1974
1989
  target: result.target,
@@ -3297,33 +3297,42 @@ export async function sendTelegramBusLocalEnvelope(
3297
3297
  };
3298
3298
  const attempts = Math.max(1, resolvedOptions.retry?.attempts ?? 1);
3299
3299
  const delayMs = Math.max(0, resolvedOptions.retry?.delayMs ?? 0);
3300
+ // Log the first failure, then only the outcome: a reloading leader would otherwise log every attempt.
3300
3301
  for (let attempt = 1; ; attempt += 1) {
3301
3302
  try {
3302
- return await sendTelegramBusLocalEnvelopeOnce(resolvedOptions);
3303
+ const response = await sendTelegramBusLocalEnvelopeOnce(resolvedOptions);
3304
+ if (attempt > 1)
3305
+ resolvedOptions.recordTransportEvent?.("client-recovered", {
3306
+ ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
3307
+ ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
3308
+ attempt,
3309
+ attempts,
3310
+ });
3311
+ return response;
3303
3312
  } catch (error) {
3304
3313
  const info = classifyTelegramBusTransportError(error);
3305
- resolvedOptions.recordTransportEvent?.("client-failed", {
3306
- ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
3307
- ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
3308
- attempt,
3309
- attempts,
3310
- ...info,
3311
- });
3312
- if (
3314
+ const final =
3313
3315
  error instanceof TelegramBusLocalAuthorityError ||
3314
3316
  attempt >= attempts ||
3315
- !isRetryableTelegramBusTransportError(error)
3316
- ) {
3317
- throw error;
3318
- }
3319
- resolvedOptions.recordTransportEvent?.("client-retry", {
3320
- ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
3321
- ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
3322
- attempt,
3323
- attempts,
3324
- delayMs,
3325
- ...info,
3326
- });
3317
+ !isRetryableTelegramBusTransportError(error);
3318
+ if (attempt === 1 || final)
3319
+ resolvedOptions.recordTransportEvent?.("client-failed", {
3320
+ ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
3321
+ ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
3322
+ attempt,
3323
+ attempts,
3324
+ ...info,
3325
+ });
3326
+ if (final) throw error;
3327
+ if (attempt === 1)
3328
+ resolvedOptions.recordTransportEvent?.("client-retry", {
3329
+ ...getTelegramBusEndpointDiagnostics(resolvedOptions.socketPath),
3330
+ ...getTelegramBusEnvelopeDiagnostics(resolvedOptions.envelope),
3331
+ attempt,
3332
+ attempts,
3333
+ delayMs,
3334
+ ...info,
3335
+ });
3327
3336
  await delayTelegramBusTransportRetry(delayMs);
3328
3337
  }
3329
3338
  }
@@ -2281,6 +2281,8 @@ export interface TelegramSelectedCommandAdmission {
2281
2281
  /** Independent exact recipient/target lifetime, including inside captured menu transport adapters. */
2282
2282
  assertRecipientCurrent(): void;
2283
2283
  reportCompleted(): boolean;
2284
+ /** Optional recipient release barrier after acceptance; rejection suppresses effects without replay. */
2285
+ beforeEffects?(): Promise<void>;
2284
2286
  }
2285
2287
 
2286
2288
  /**
@@ -3068,7 +3070,8 @@ export function createTelegramCommandHandler<
3068
3070
  ? copySelectedMenuCarrier(original)
3069
3071
  : copySelectedReplyAnchor(original);
3070
3072
  const assertRecipientCurrent = admission.assertRecipientCurrent,
3071
- report = admission.reportCompleted;
3073
+ report = admission.reportCompleted,
3074
+ beforeEffects = admission.beforeEffects;
3072
3075
  const effects: TelegramHeldCommandEffects<TContext> | undefined =
3073
3076
  parsed.name === "status"
3074
3077
  ? undefined
@@ -3100,8 +3103,11 @@ export function createTelegramCommandHandler<
3100
3103
  throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
3101
3104
  };
3102
3105
  return issueSelectedOnce(assertCurrent, async () => {
3103
- if (plan.completion === "before-effect" && !report.call(admission))
3104
- return false;
3106
+ if (plan.completion === "before-effect") {
3107
+ if (!report.call(admission)) return false;
3108
+ if (beforeEffects) await beforeEffects();
3109
+ assertRecipientCurrent();
3110
+ }
3105
3111
  return prepared.run({
3106
3112
  message,
3107
3113
  command: parsed,
@@ -3137,7 +3143,8 @@ export function createTelegramCommandHandler<
3137
3143
  from: { ...original.from },
3138
3144
  };
3139
3145
  const assertRecipientCurrent = admission.assertRecipientCurrent,
3140
- reportCompleted = admission.reportCompleted;
3146
+ reportCompleted = admission.reportCompleted,
3147
+ beforeEffects = admission.beforeEffects;
3141
3148
  const assertAdmissionCurrent = createSelectedSemanticGuard(
3142
3149
  admission,
3143
3150
  () => captured.assertExecutionCurrent?.(original),
@@ -3163,7 +3170,7 @@ export function createTelegramCommandHandler<
3163
3170
  );
3164
3171
  },
3165
3172
  },
3166
- { assertSemanticCurrent, assertRecipientCurrent, reportCompleted },
3173
+ { assertSemanticCurrent, assertRecipientCurrent, reportCompleted, beforeEffects },
3167
3174
  ),
3168
3175
  );
3169
3176
  };
@@ -3607,6 +3614,7 @@ export function createTelegramCommandHandler<
3607
3614
  const assertSourceCurrent = admission.assertSourceCurrent,
3608
3615
  assertRecipientCurrent = admission.assertRecipientCurrent;
3609
3616
  const reportCompleted = admission.reportCompleted,
3617
+ beforeEffects = admission.beforeEffects,
3610
3618
  assertExecutionCurrent = deps.assertExecutionCurrent;
3611
3619
  const assertIssuanceCurrent = () => {
3612
3620
  assertSourceCurrent.call(admission);
@@ -3639,9 +3647,12 @@ export function createTelegramCommandHandler<
3639
3647
  };
3640
3648
  return issueSelectedOnce(
3641
3649
  assertIssuanceCurrent,
3642
- async () =>
3643
- reportCompleted.call(admission) &&
3644
- handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args),
3650
+ async () => {
3651
+ if (!reportCompleted.call(admission)) return false;
3652
+ if (beforeEffects) await beforeEffects();
3653
+ assertRecipientCurrent();
3654
+ return handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args);
3655
+ },
3645
3656
  );
3646
3657
  },
3647
3658
  });
@@ -3766,6 +3777,7 @@ async function handleTelegramHelpCommand<
3766
3777
  assertSemanticCurrent(): void;
3767
3778
  assertRecipientCurrent(): void;
3768
3779
  reportCompleted(): boolean;
3780
+ beforeEffects?(): Promise<void>;
3769
3781
  },
3770
3782
  ): Promise<boolean> {
3771
3783
  const assertSemanticCurrent =
@@ -3788,6 +3800,7 @@ async function handleTelegramHelpCommand<
3788
3800
  if (admission) {
3789
3801
  assertSemanticCurrent();
3790
3802
  if (!admission.reportCompleted()) return false;
3803
+ if (admission.beforeEffects) await admission.beforeEffects();
3791
3804
  admission.assertRecipientCurrent();
3792
3805
  }
3793
3806
  const assertEffectCurrent =
@@ -861,6 +861,12 @@ export default function (pi: Pi.ExtensionAPI) {
861
861
  const promptDispatchRuntime = Runtime.createTelegramPromptDispatchRuntime({
862
862
  lifecycle,
863
863
  typing,
864
+ failureLog: runtimeDiagnostics.createFailureLog({
865
+ category: "typing",
866
+ failurePhase: "typing-presence",
867
+ outcomePhase: "typing-presence-recovered",
868
+ label: "typing presence",
869
+ }),
864
870
  getDefaultChatId: proactivePushChatIdGetter,
865
871
  isThreadedMode() {
866
872
  return deliveryTargetPolicyRuntime.getTargetPolicyView().threaded === true;
@@ -1302,6 +1308,32 @@ export default function (pi: Pi.ExtensionAPI) {
1302
1308
  delivery: deliveryLifecycleRuntime,
1303
1309
  api: telegramApiTargetActivityRuntime,
1304
1310
  });
1311
+ const prepareTemporaryThreadJournalInspection = function (
1312
+ target: Targets.TelegramTarget,
1313
+ requiredJournalBindingKeys: readonly string[],
1314
+ ownInputs?: readonly Threads.TelegramTemporaryThreadInput[],
1315
+ ): Parameters<typeof Journal.isTelegramThreadCleanupJournalNamespaceClear>[0] | undefined {
1316
+ const binding = resolveTelegramUpdateJournalBinding();
1317
+ const botToken = configStore.getBotToken();
1318
+ if (!binding || !botToken) return undefined;
1319
+ const profile = configStore.getActiveProfileName() ?? "default";
1320
+ const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({ botToken, botId: getTelegramBotId() });
1321
+ return {
1322
+ directory: Paths.resolveTelegramTempDir(), profile, botIdentity,
1323
+ pollingPath: telegramLeaderJournalPath.resolve(configStore.getActiveProfileName()),
1324
+ requiredJournalBindingKeys: [binding.recoveryKey, ...requiredJournalBindingKeys],
1325
+ ...(target.threadId !== undefined && ownInputs
1326
+ ? { cleanup: { target: { chatId: target.chatId, threadId: target.threadId }, ownInputs } } : {}),
1327
+ limits: { maxDirectoryEntries: 10_000, maxFiles: 4096, maxBytes: 64 * 1024 * 1024,
1328
+ maxEntries: 10_000, maxWork: 100_000 },
1329
+ withSourceReference(path, operation) {
1330
+ return telegramJournalReferenceRegistry.withReference({
1331
+ referenceClass: "operator-disposition",
1332
+ recoveryKey: Journal.createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity }),
1333
+ }, operation);
1334
+ },
1335
+ };
1336
+ };
1305
1337
  const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime({
1306
1338
  configStore,
1307
1339
  callApi: callTelegramApi,
@@ -1407,64 +1439,25 @@ export default function (pi: Pi.ExtensionAPI) {
1407
1439
  },
1408
1440
  );
1409
1441
  },
1410
- inspectTemporaryThreadSources(
1411
- target,
1412
- requiredJournalBindingKeys,
1413
- ownInputs,
1414
- ) {
1415
- const binding = resolveTelegramUpdateJournalBinding();
1416
- const botToken = configStore.getBotToken();
1417
- if (!binding || !botToken) return undefined;
1418
- const profile = configStore.getActiveProfileName() ?? "default";
1419
- const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({
1420
- botToken,
1421
- botId: getTelegramBotId(),
1422
- });
1442
+ inspectTemporaryThreadSources(target, requiredJournalBindingKeys, ownInputs) {
1443
+ const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
1444
+ if (!input) return undefined;
1423
1445
  return withTelegramJournalSourceSerialization(function () {
1424
- return Journal.isTelegramThreadCleanupJournalNamespaceClear({
1425
- directory: Paths.resolveTelegramTempDir(),
1426
- profile,
1427
- botIdentity,
1428
- pollingPath: telegramLeaderJournalPath.resolve(
1429
- configStore.getActiveProfileName(),
1430
- ),
1431
- requiredJournalBindingKeys: [
1432
- binding.recoveryKey,
1433
- ...requiredJournalBindingKeys,
1434
- ],
1435
- ...(target.threadId !== undefined && ownInputs
1436
- ? {
1437
- cleanup: {
1438
- target: { chatId: target.chatId, threadId: target.threadId },
1439
- ownInputs,
1440
- },
1441
- }
1442
- : {}),
1443
- limits: {
1444
- maxDirectoryEntries: 10_000,
1445
- maxFiles: 4096,
1446
- maxBytes: 64 * 1024 * 1024,
1447
- maxEntries: 10_000,
1448
- maxWork: 100_000,
1449
- },
1450
- withSourceReference(path, operation) {
1451
- return telegramJournalReferenceRegistry.withReference(
1452
- {
1453
- referenceClass: "operator-disposition",
1454
- recoveryKey: Journal.createTelegramUpdateJournalBindingKey({
1455
- path,
1456
- profileName: profile,
1457
- botIdentity,
1458
- }),
1459
- },
1460
- operation,
1461
- );
1462
- },
1463
- })
1464
- ? []
1465
- : undefined;
1446
+ return Journal.isTelegramThreadCleanupJournalNamespaceClear(input) ? [] : undefined;
1466
1447
  });
1467
1448
  },
1449
+ recoverTemporaryThreadDeadQueueOwners(target, requiredJournalBindingKeys, ownInputs, isCurrent) {
1450
+ const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
1451
+ if (!input || !isCurrent()) return 0;
1452
+ return Journal.recoverTelegramThreadCleanupDeadQueueOwners({
1453
+ ...input, isCurrent, withSourceSerialization: withTelegramJournalSourceSerialization,
1454
+ recoveryOwner: {
1455
+ instanceId: telegramInstanceId, processId: telegramProcessId,
1456
+ processBirthId: telegramQueueProcessBirthId,
1457
+ sessionGeneration: telegramSessionContextStore.getGeneration(),
1458
+ },
1459
+ });
1460
+ },
1468
1461
  hasWorkspaceRestoreAuthority() {
1469
1462
  return (
1470
1463
  lockRuntime.owns() &&
@@ -1589,12 +1582,14 @@ export default function (pi: Pi.ExtensionAPI) {
1589
1582
  target.threadId === undefined
1590
1583
  )
1591
1584
  return undefined;
1585
+ // Pi creates a fresh context object per event and command, so authority follows the session, not the object.
1586
+ const sessionGeneration = telegramSessionContextStore.getGeneration();
1592
1587
  return Threads.createTelegramWorkspaceThreadRenameRecipient({
1593
1588
  store: threadStore,
1594
1589
  instanceId: telegramInstanceId,
1595
1590
  target,
1596
1591
  assertAuthority() {
1597
- if (telegramSessionContextStore.get() !== ctx)
1592
+ if (!telegramSessionContextStore.isCurrent(ctx, sessionGeneration))
1598
1593
  throw new Error("Telegram Thread name dialog lost Pi context.");
1599
1594
  },
1600
1595
  getAuthority: threadNameRecipientAuthority.getAuthority,
@@ -1981,6 +1976,12 @@ export default function (pi: Pi.ExtensionAPI) {
1981
1976
  const telegramBusFollowerAssembly: BusFollower.TelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext> =
1982
1977
  BusFollower.createTelegramBusFollowerRuntimeAssembly<Pi.ExtensionContext>({
1983
1978
  instanceId: telegramInstanceId,
1979
+ failureLog: runtimeDiagnostics.createFailureLog({
1980
+ category: "bus",
1981
+ failurePhase: "follower-heartbeat",
1982
+ outcomePhase: "follower-heartbeat-recovered",
1983
+ label: "follower heartbeat",
1984
+ }),
1984
1985
  registrationState: telegramBusFollowerRegistrationState,
1985
1986
  recordRuntimeEvent,
1986
1987
  receiver: {
@@ -2383,6 +2384,12 @@ export default function (pi: Pi.ExtensionAPI) {
2383
2384
  Locks.TelegramLockEntry
2384
2385
  >({
2385
2386
  state: telegramThreadCapabilityState,
2387
+ failureLog: runtimeDiagnostics.createFailureLog({
2388
+ category: "bus",
2389
+ failurePhase: "capability-monitor",
2390
+ outcomePhase: "capability-monitor-recovered",
2391
+ label: "Threaded Mode capability probe",
2392
+ }),
2386
2393
  getAllowedUserId: configStore.getAllowedUserId,
2387
2394
  callApi: callTelegramApi,
2388
2395
  topicTargetStore: threadStore,
@@ -3071,6 +3078,7 @@ export default function (pi: Pi.ExtensionAPI) {
3071
3078
  deleteMessage: deleteTelegramMessage,
3072
3079
  sendGuestReply,
3073
3080
  editGuestReply,
3081
+ editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
3074
3082
  stopGuestPlaceholder: guestPlaceholderRuntime.stop,
3075
3083
  finalizeMarkdownPreview,
3076
3084
  preparePreviewDelivery,