@llblab/pi-kit 0.36.1 → 0.36.3

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 (77) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-claude-usage/CHANGELOG.md +4 -0
  4. package/node_modules/@llblab/pi-claude-usage/README.md +2 -2
  5. package/node_modules/@llblab/pi-claude-usage/lib/status-format.ts +2 -3
  6. package/node_modules/@llblab/pi-claude-usage/package.json +1 -1
  7. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +5 -0
  8. package/node_modules/@llblab/pi-codex-usage/README.md +1 -1
  9. package/node_modules/@llblab/pi-codex-usage/lib/status-format.ts +2 -3
  10. package/node_modules/@llblab/pi-codex-usage/package.json +1 -1
  11. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
  12. package/node_modules/@llblab/pi-telegram/BACKLOG.md +43 -5
  13. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +45 -0
  14. package/node_modules/@llblab/pi-telegram/README.md +4 -2
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +17 -1
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +23 -11
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +24 -6
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +2 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +7 -0
  20. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +7 -4
  21. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +22 -4
  22. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +52 -10
  23. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +1 -0
  24. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +9 -0
  25. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -1
  26. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +1 -1
  27. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +3 -2
  28. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +4 -4
  29. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -2
  30. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +6 -3
  31. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +50 -9
  32. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +3 -2
  33. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +120 -14
  34. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +13 -0
  35. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +12 -0
  36. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +39 -3
  37. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +144 -16
  38. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -1
  39. package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +15 -8
  40. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +14 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +122 -8
  42. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +9 -6
  43. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +4 -3
  44. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +3 -3
  45. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -0
  46. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +52 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.d.ts +17 -0
  48. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +25 -0
  49. package/node_modules/@llblab/pi-telegram/dist/package.json +2 -1
  50. package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
  51. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +23 -3
  52. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -0
  53. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +1 -1
  54. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +24 -1
  55. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +30 -13
  56. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +25 -8
  57. package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -0
  58. package/node_modules/@llblab/pi-telegram/lib/commands.ts +34 -8
  59. package/node_modules/@llblab/pi-telegram/lib/extension.ts +54 -11
  60. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +12 -0
  61. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +2 -0
  62. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +4 -3
  63. package/node_modules/@llblab/pi-telegram/lib/menu.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/lib/pi.ts +2 -0
  65. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +79 -16
  66. package/node_modules/@llblab/pi-telegram/lib/routing.ts +142 -26
  67. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +18 -0
  68. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +173 -17
  69. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +14 -8
  70. package/node_modules/@llblab/pi-telegram/lib/threads.ts +166 -10
  71. package/node_modules/@llblab/pi-telegram/lib/updates.ts +9 -6
  72. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +8 -7
  73. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +56 -2
  74. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +44 -0
  75. package/node_modules/@llblab/pi-telegram/package.json +2 -1
  76. package/node_modules/@llblab/pi-telegram/scripts/run-macos-tests.mjs +43 -0
  77. package/package.json +4 -4
@@ -70,6 +70,7 @@ import {
70
70
  type TelegramBusSelectedMenuTextEffect,
71
71
  type TelegramBusSocketPathSource,
72
72
  type TelegramBusWorkspaceRestoreObservation,
73
+ TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE,
73
74
  } from "./bus.ts";
74
75
  import type {
75
76
  TelegramConfigStore,
@@ -96,6 +97,7 @@ import {
96
97
  assertTelegramApiCallAuthority,
97
98
  TelegramApiAuthorityError,
98
99
  TelegramApiCommitUnknownError,
100
+ TelegramApiFollowerNotAllowedError,
99
101
  TelegramApiStaleTargetError,
100
102
  type TelegramApiCallOptions,
101
103
  } from "./telegram-api.ts";
@@ -1917,11 +1919,16 @@ export function createTelegramBusFollowerApiCaller(
1917
1919
  method: string,
1918
1920
  args: unknown[],
1919
1921
  localOptions: Pick<TelegramApiCallOptions, "assertAuthority"> | undefined,
1920
- timing: { registeredAtMs?: number },
1922
+ timing: { registeredAtMs?: number; apiMethod?: string },
1921
1923
  ): Promise<unknown> => {
1922
1924
  const assertion = localOptions?.assertAuthority;
1923
1925
  const assertCurrent = () => assertTelegramApiCallAuthority(assertion, false);
1924
1926
  assertCurrent();
1927
+ // Match IPC's JSON semantics once, before awaits; send and refusal metadata share this identity.
1928
+ const requestArgs = JSON.parse(JSON.stringify(args)) as unknown[];
1929
+ const apiMethod = (method === "call" || method === "callMultipart") && typeof requestArgs[0] === "string"
1930
+ ? requestArgs[0] : method;
1931
+ timing.apiMethod = apiMethod;
1925
1932
  const registration = await resolveTelegramBusFollowerRegistration(
1926
1933
  deps,
1927
1934
  timeoutMs,
@@ -1941,18 +1948,13 @@ export function createTelegramBusFollowerApiCaller(
1941
1948
  instanceId: deps.instanceId,
1942
1949
  registrationGeneration: registration.generation,
1943
1950
  method,
1944
- args,
1951
+ args: requestArgs,
1945
1952
  sentAtMs: getNowMs(),
1946
1953
  },
1947
1954
  assertion ? assertCurrent : undefined,
1948
1955
  );
1949
1956
  } catch (error) {
1950
1957
  if (error instanceof TelegramBusLocalAuthorityError) throw new TelegramApiAuthorityError(error.requestIssued);
1951
- const apiMethod =
1952
- (method === "call" || method === "callMultipart") &&
1953
- typeof args[0] === "string"
1954
- ? args[0]
1955
- : method;
1956
1958
  if (!isTelegramApiMethodRetrySafe(apiMethod)) {
1957
1959
  throw new TelegramApiCommitUnknownError(apiMethod, error);
1958
1960
  }
@@ -1986,11 +1988,30 @@ export function createTelegramBusFollowerApiCaller(
1986
1988
  new Error(message ?? "Telegram bus API call result is ambiguous."),
1987
1989
  );
1988
1990
  }
1991
+ const refusedMethod = response?.kind === "bus.ack" && !response.ok &&
1992
+ response.error?.code === "follower-api-not-allowed" &&
1993
+ response.error.method === apiMethod
1994
+ ? apiMethod : undefined;
1995
+ if (refusedMethod || message === TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE) {
1996
+ const body =
1997
+ (method === "call" || method === "callMultipart") && isRecord(requestArgs[1])
1998
+ ? requestArgs[1]
1999
+ : undefined;
2000
+ const chatId = Number(body?.chat_id);
2001
+ const threadId = Number(body?.message_thread_id);
2002
+ throw new TelegramApiFollowerNotAllowedError(
2003
+ message ?? TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE,
2004
+ Number.isSafeInteger(chatId) && Number.isSafeInteger(threadId)
2005
+ ? { chatId, threadId }
2006
+ : undefined,
2007
+ refusedMethod,
2008
+ );
2009
+ }
1989
2010
  throw new Error(message ?? "Telegram bus API call failed.");
1990
2011
  };
1991
2012
  return async (method, args, localOptions) => {
1992
2013
  const startedAtMs = getNowMs();
1993
- const timing: { registeredAtMs?: number } = {};
2014
+ const timing: { registeredAtMs?: number; apiMethod?: string } = {};
1994
2015
  let outcome = "ok";
1995
2016
  try {
1996
2017
  return await caller(method, args, localOptions, timing);
@@ -2000,11 +2021,7 @@ export function createTelegramBusFollowerApiCaller(
2000
2021
  } finally {
2001
2022
  const elapsedMs = getNowMs() - startedAtMs;
2002
2023
  if (elapsedMs >= slowCallMs) {
2003
- const apiMethod =
2004
- (method === "call" || method === "callMultipart") &&
2005
- typeof args[0] === "string"
2006
- ? args[0]
2007
- : method;
2024
+ const apiMethod = timing.apiMethod ?? method;
2008
2025
  try {
2009
2026
  deps.recordRuntimeEvent?.(
2010
2027
  "bus",
@@ -40,6 +40,7 @@ import {
40
40
  type TelegramBusProtocolIdentity,
41
41
  type TelegramBusSelectedMenuDelivery,
42
42
  type TelegramBusSocketPathSource,
43
+ TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE,
43
44
  } from "./bus.ts";
44
45
  import type { TelegramThreadDisplayMode } from "./config.ts";
45
46
  import type { TelegramAttachmentSource } from "./media.ts";
@@ -464,6 +465,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
464
465
  store: deps.topicTargetStore,
465
466
  getLeaderEpoch: deps.getCurrentLeaderEpoch,
466
467
  getExternalProtection: deps.captureWorkspaceExternalProtection,
468
+ onSlotReused: (bindingKey, isCurrent) => display
469
+ ? reconcileThreadDisplay(bindingKey, isCurrent).then(() => undefined)
470
+ : Promise.resolve(),
467
471
  recordEvent(message, details) {
468
472
  deps.recordRuntimeEvent("bus", message, details);
469
473
  },
@@ -513,6 +517,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
513
517
  getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
514
518
  captureLiveBindingKeys,
515
519
  captureBindingAuthority(binding) {
520
+ // Slotless retained bindings need display repair even without a live owner.
521
+ // The reconciler still fences the exact binding snapshot, profile, mode and epoch.
522
+ if (binding.slot === undefined) return () => true;
516
523
  const matches = (target: TelegramTarget | undefined) =>
517
524
  target?.chatId === binding.target.chatId &&
518
525
  target?.threadId === binding.target.threadId;
@@ -544,19 +551,22 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
544
551
  callApi: deps.callApi,
545
552
  })
546
553
  : undefined;
547
- const reconcileThreadDisplayOperation = async () => {
548
- const result = await display!.reconcile();
554
+ const reconcileThreadDisplayOperation = async (bindingKey?: string) => {
555
+ const result = await display!.reconcile(bindingKey);
549
556
  deps.onThreadDisplayChanged?.();
550
557
  return result;
551
558
  };
552
- const reconcileThreadDisplay = () =>
559
+ const reconcileThreadDisplay = (bindingKey?: string, isCurrent: () => boolean = () => true) =>
553
560
  runWorkspaceOperation(
554
561
  {
555
562
  operationId: createTelegramWorkspaceAdmissionOperationId(),
556
563
  operationKind: "workspace.reconcile-display",
557
564
  scopes: [{ kind: "profile" }],
558
565
  },
559
- reconcileThreadDisplayOperation,
566
+ () => {
567
+ if (!isCurrent()) throw new Error("Telegram Thread display update lost allocation authority.");
568
+ return reconcileThreadDisplayOperation(bindingKey);
569
+ },
560
570
  );
561
571
  let startupDisplayTimer: ReturnType<typeof setTimeout> | undefined;
562
572
  let startupDisplayGeneration = 0;
@@ -4179,10 +4189,17 @@ async function handleFollowerApiCall(
4179
4189
  args: envelope.args,
4180
4190
  })
4181
4191
  ) {
4182
- return rejectTelegramBusRequest(
4183
- envelope.requestId,
4184
- "Telegram bus API call is not allowed for this follower.",
4185
- );
4192
+ const apiMethod = getFollowerApiMethodAndBody(envelope).apiMethod;
4193
+ return {
4194
+ ...rejectTelegramBusRequest(
4195
+ envelope.requestId,
4196
+ TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE,
4197
+ ),
4198
+ // Only this pre-dispatch authorizer refusal certifies that the API method was not called.
4199
+ ...(/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(apiMethod)
4200
+ ? { error: { code: "follower-api-not-allowed" as const, method: apiMethod } }
4201
+ : {}),
4202
+ };
4186
4203
  }
4187
4204
  if (!deps.callApi) {
4188
4205
  return rejectTelegramBusRequest(
@@ -1260,6 +1260,7 @@ export type TelegramBusEnvelope = (
1260
1260
  | "ledger-overloaded"
1261
1261
  | "incompatible-protocol"
1262
1262
  | "stale-target"
1263
+ | "follower-api-not-allowed"
1263
1264
  | "workspace-binding-unavailable";
1264
1265
  method?: string;
1265
1266
  chatId?: number;
@@ -2873,6 +2874,9 @@ export function isTelegramBusEnvelopeAuthorized(
2873
2874
  }
2874
2875
 
2875
2876
  /** Negative acknowledgement carrying only a diagnostic message. */
2877
+ export const TELEGRAM_BUS_FOLLOWER_API_NOT_ALLOWED_MESSAGE =
2878
+ "Telegram bus API call is not allowed for this follower.";
2879
+
2876
2880
  export function rejectTelegramBusRequest(
2877
2881
  requestId: string,
2878
2882
  message: string,
@@ -4145,10 +4149,17 @@ function parseAckEnvelope(
4145
4149
  code === "ledger-overloaded" ||
4146
4150
  code === "incompatible-protocol" ||
4147
4151
  code === "stale-target" ||
4152
+ code === "follower-api-not-allowed" ||
4148
4153
  code === "workspace-binding-unavailable"
4149
4154
  ) {
4150
4155
  const chatId = value.error.chatId;
4151
4156
  const threadId = value.error.threadId;
4157
+ if (
4158
+ code === "follower-api-not-allowed" &&
4159
+ (value.ok || Object.hasOwn(value, "result") ||
4160
+ typeof value.error.method !== "string" ||
4161
+ !/^[A-Za-z][A-Za-z0-9]{0,63}$/.test(value.error.method))
4162
+ ) return undefined;
4152
4163
  if (
4153
4164
  code === "stale-target" &&
4154
4165
  (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId))
@@ -36,6 +36,7 @@ import {
36
36
  TelegramApiAuthorityError,
37
37
  type TelegramApiCallOptions,
38
38
  } from "./telegram-api.ts";
39
+ import type { TelegramTarget } from "./target.ts";
39
40
  import type { TelegramSessionReplacementIntent } from "./threads.ts";
40
41
  import {
41
42
  acquireTelegramUpdateRouting,
@@ -1194,34 +1195,38 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
1194
1195
  ) => void;
1195
1196
  getAdmissionScope?: () => string | undefined;
1196
1197
  getAdmissionJournalBinding?: () => string | undefined;
1198
+ /** Ordinary reply destination selected before publication, not resend authority. */
1199
+ getReplyTarget?: (
1200
+ message: TelegramCommandRuntimeMessage,
1201
+ ) => TelegramTarget | undefined;
1197
1202
  onControlQueued?: (
1198
1203
  message: TelegramCommandRuntimeMessage,
1199
1204
  receipt: TelegramQueueAdmissionReceipt,
1200
1205
  ) => void;
1201
1206
  showStatus: (
1202
1207
  chatId: number,
1203
- replyToMessageId: number,
1208
+ replyToMessageId: number | undefined,
1204
1209
  ctx: TContext,
1205
1210
  threadId?: number,
1206
1211
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
1207
1212
  ) => Promise<void>;
1208
1213
  openModelMenu: (
1209
1214
  chatId: number,
1210
- replyToMessageId: number,
1215
+ replyToMessageId: number | undefined,
1211
1216
  ctx: TContext,
1212
1217
  threadId?: number,
1213
1218
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
1214
1219
  ) => Promise<void>;
1215
1220
  openSettingsMenu?: (
1216
1221
  chatId: number,
1217
- replyToMessageId: number,
1222
+ replyToMessageId: number | undefined,
1218
1223
  ctx: TContext,
1219
1224
  threadId?: number,
1220
1225
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
1221
1226
  ) => Promise<void>;
1222
1227
  sendTextReply: (
1223
1228
  chatId: number,
1224
- replyToMessageId: number,
1229
+ replyToMessageId: number | undefined,
1225
1230
  text: string,
1226
1231
  options?: {
1227
1232
  parseMode?: "HTML";
@@ -1336,6 +1341,7 @@ export function createTelegramCommandTargetQueueRuntime<
1336
1341
  createControlItem,
1337
1342
  enqueueControlItem: controlQueueController.enqueue,
1338
1343
  }),
1344
+ getReplyTarget: deps.getReplyTarget,
1339
1345
  getAdmissionScope: deps.getAdmissionScope,
1340
1346
  getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
1341
1347
  onControlQueued: deps.onControlQueued,
@@ -1352,6 +1358,25 @@ export function createTelegramCommandTargetRuntime<
1352
1358
  >(
1353
1359
  deps: TelegramCommandTargetRuntimeDeps<TContext>,
1354
1360
  ): TelegramCommandTargetRuntime<TMessage, TContext> {
1361
+ const getReplyTarget = (
1362
+ message: TMessage,
1363
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
1364
+ ) => {
1365
+ const source = getTelegramCommandMessageTarget(message);
1366
+ const recipient = options?.assertAuthority
1367
+ ? undefined
1368
+ : deps.getReplyTarget?.(message);
1369
+ if (!recipient) return source;
1370
+ if (recipient.chatId !== source.chatId)
1371
+ throw new Error("Telegram command reply cannot change its recipient chat.");
1372
+ if (source.threadId !== undefined && recipient.threadId === undefined)
1373
+ throw new Error("Telegram command cannot move a Thread reply to All.");
1374
+ return {
1375
+ ...recipient,
1376
+ replyToMessageId:
1377
+ recipient.threadId === source.threadId ? source.replyToMessageId : undefined,
1378
+ };
1379
+ };
1355
1380
  return {
1356
1381
  enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
1357
1382
  const sourceUpdateId = message.pi_telegram_source_update_id;
@@ -1381,7 +1406,7 @@ export function createTelegramCommandTargetRuntime<
1381
1406
  );
1382
1407
  },
1383
1408
  showStatus: (message, ctx, options) => {
1384
- const target = getTelegramCommandMessageTarget(message);
1409
+ const target = getReplyTarget(message, options);
1385
1410
  return deps.showStatus(
1386
1411
  target.chatId,
1387
1412
  target.replyToMessageId,
@@ -1391,7 +1416,7 @@ export function createTelegramCommandTargetRuntime<
1391
1416
  );
1392
1417
  },
1393
1418
  openModelMenu: (message, ctx, options) => {
1394
- const target = getTelegramCommandMessageTarget(message);
1419
+ const target = getReplyTarget(message, options);
1395
1420
  return deps.openModelMenu(
1396
1421
  target.chatId,
1397
1422
  target.replyToMessageId,
@@ -1401,7 +1426,7 @@ export function createTelegramCommandTargetRuntime<
1401
1426
  );
1402
1427
  },
1403
1428
  openSettingsMenu: async (message, ctx, options) => {
1404
- const target = getTelegramCommandMessageTarget(message);
1429
+ const target = getReplyTarget(message, options);
1405
1430
  if (!deps.openSettingsMenu) {
1406
1431
  await deps.sendTextReply(
1407
1432
  target.chatId,
@@ -1423,7 +1448,7 @@ export function createTelegramCommandTargetRuntime<
1423
1448
  );
1424
1449
  },
1425
1450
  sendTextReply: async (message, text, options) => {
1426
- const target = getTelegramCommandMessageTarget(message);
1451
+ const target = getReplyTarget(message, options);
1427
1452
  const assertAuthority = options?.assertAuthority,
1428
1453
  send = deps.sendTextReply;
1429
1454
  const capturedOptions = { ...options, target };
@@ -2217,6 +2242,7 @@ export function createTelegramCommandHandlerTargetRuntime<
2217
2242
  }),
2218
2243
  appendControlItem: deps.appendControlItem,
2219
2244
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
2245
+ getReplyTarget: deps.getReplyTarget,
2220
2246
  getAdmissionScope: deps.getAdmissionScope,
2221
2247
  getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
2222
2248
  onControlQueued: deps.onControlQueued,
@@ -590,18 +590,32 @@ export default function (pi: Pi.ExtensionAPI) {
590
590
  );
591
591
  },
592
592
  });
593
- const inactiveThreadCleanupReviewRuntime =
594
- ThreadCleanupManager.createTelegramInactiveThreadCleanupReviewRuntime({
595
- getProfileName() {
596
- return configStore.getActiveProfileName() ?? "default";
593
+ const inactiveThreadCleanupRuntime =
594
+ ThreadCleanupManager.createTelegramInactiveThreadCleanupRuntime({
595
+ store: threadStore,
596
+ captureAuthority() {
597
+ const ledger = workspaceAdmissionRuntime.resolve();
598
+ const leaderEpoch = getCurrentLeaderEpoch();
599
+ const operator = configStore.getAllowedUserId();
600
+ if (!ledger || leaderEpoch === undefined || operator === undefined) return undefined;
601
+ const stamp = telegramTransportStampRuntime.getStamp();
602
+ const generation = telegramSessionContextStore.getGeneration();
603
+ return {
604
+ profileName: configStore.getActiveProfileName() ?? "default",
605
+ leaderEpoch, ledger,
606
+ isCurrent() {
607
+ return getCurrentLeaderEpoch() === leaderEpoch &&
608
+ telegramTransportStampRuntime.isActive(stamp) &&
609
+ configStore.getAllowedUserId() === operator &&
610
+ telegramSessionContextStore.getGeneration() === generation;
611
+ },
612
+ };
597
613
  },
598
- listBindings: threadStore.listWorkspaceBindings,
599
614
  getProtection: captureWorkspaceExternalProtection,
600
- listReservations: threadStore.listReservations,
601
- listPendingProvisions: threadStore.listPendingProvisions,
602
- listPendingCleanups: threadStore.listPendingCleanups,
603
- getWorkStore() {
604
- const profileName = configStore.getActiveProfileName() ?? "default";
615
+ getProcessLiveness: ProcessIdentity.getTelegramProcessLiveness,
616
+ runExclusive: telegramWorkspaceOperationRuntime.runExclusive,
617
+ getWorkStore(authority) {
618
+ const profileName = authority.profileName;
605
619
  const botToken = configStore.getBotToken();
606
620
  if (!botToken)
607
621
  throw new Error(
@@ -617,9 +631,16 @@ export default function (pi: Pi.ExtensionAPI) {
617
631
  tokenSha256: Journal.createTelegramUpdateJournalBotIdentity({
618
632
  botToken,
619
633
  }).tokenSha256,
634
+ admissionProfileKey: authority.ledger.getProfileKey(),
620
635
  });
621
636
  },
622
637
  runWorkspaceOperation: telegramWorkspaceOperationRuntime.run,
638
+ deleteThread(authorize) {
639
+ return directTelegramApiRuntime.deleteWorkspaceThread(authorize);
640
+ },
641
+ onError(error) {
642
+ recordRuntimeEvent("telegram", error, { phase: "manual-inactive-thread-cleanup" });
643
+ },
623
644
  });
624
645
  const updateAdmissionRuntimeBinding =
625
646
  Updates.createTelegramUpdateAdmissionRuntimeBinding<Pi.ExtensionContext>({
@@ -1187,7 +1208,8 @@ export default function (pi: Pi.ExtensionAPI) {
1187
1208
  sendInteractiveMessage,
1188
1209
  answerCallbackQuery,
1189
1210
  ...configControls,
1190
- reviewInactiveThreads: inactiveThreadCleanupReviewRuntime.review,
1211
+ reviewInactiveThreads: inactiveThreadCleanupRuntime.review,
1212
+ cleanInactiveThreads: inactiveThreadCleanupRuntime.clean,
1191
1213
  getThreadDisplayMode() {
1192
1214
  return threadStore.getBotState().threadMode === "enabled"
1193
1215
  ? Config.resolveTelegramThreadDisplayMode(configStore.get())
@@ -1342,6 +1364,27 @@ export default function (pi: Pi.ExtensionAPI) {
1342
1364
  },
1343
1365
  getAdmissionScope: getTelegramUpdateAdmissionScope,
1344
1366
  getAdmissionJournalBinding: getTelegramQueueJournalBinding,
1367
+ getCommandReplyTarget(message) {
1368
+ if (
1369
+ message.chat.type !== "private" ||
1370
+ "business_connection_id" in message ||
1371
+ !Updates.getTelegramUpdateExecutionFence(message) ||
1372
+ lockRuntime.owns()
1373
+ ) return undefined;
1374
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1375
+ const binding = resolveTelegramFollowerJournalBinding();
1376
+ const target = telegramBusFollowerRegistrationState.getTarget();
1377
+ if (
1378
+ !telegramBusFollowerRegistrationState.isRegistered() ||
1379
+ !binding ||
1380
+ binding.recoveryKey !==
1381
+ followerAdmissionLifecycleRuntime.getJournalBindingKey() ||
1382
+ !target ||
1383
+ (threadStore.getBotState().threadMode === "enabled" &&
1384
+ target.threadId === undefined)
1385
+ ) throw new Error("Telegram command reply lost its registered recipient.");
1386
+ return { ...target };
1387
+ },
1345
1388
  beginCommandEffectWork: publicationRuntime.beginWork,
1346
1389
  getMessageOwnership: messageOwnershipRuntime.getForwardOwnership,
1347
1390
  recordMessageOwnership: messageOwnershipRuntime.recordRouted,
@@ -288,6 +288,18 @@ export function resolveGenerativeAppDir(agentDir: string, app: string): string {
288
288
  return join(getAppsRoot(agentDir), app);
289
289
  }
290
290
 
291
+ export async function isGenerativeAppInstalled(
292
+ agentDir: string,
293
+ app: string,
294
+ ): Promise<boolean> {
295
+ try {
296
+ await stat(resolveGenerativeAppModulePath(agentDir, app));
297
+ return true;
298
+ } catch {
299
+ return false;
300
+ }
301
+ }
302
+
291
303
  export function resolveGenerativeAppModulePath(
292
304
  agentDir: string,
293
305
  app: string,
@@ -14,6 +14,7 @@ import type {
14
14
  AgentStartEvent,
15
15
  AssistantMessageEvent,
16
16
  BeforeAgentStartEvent,
17
+ BeforeAgentStartEventResult,
17
18
  ExtensionAPI,
18
19
  ExtensionContext,
19
20
  InputEvent,
@@ -61,6 +62,7 @@ type TelegramBeforeAgentStartEvent = Omit<
61
62
 
62
63
  export interface TelegramBeforeAgentStartResult {
63
64
  systemPrompt?: string | string[];
65
+ message?: BeforeAgentStartEventResult["message"];
64
66
  }
65
67
 
66
68
  type TelegramBeforeAgentStartReturn =
@@ -90,7 +90,7 @@ export interface TelegramSettingsMenuCallbackDeps extends TelegramSettingsMutati
90
90
  export interface TelegramSettingsMenuRuntime<TContext> {
91
91
  openSettingsMenu: (
92
92
  chatId: number,
93
- replyToMessageId: number,
93
+ replyToMessageId: number | undefined,
94
94
  ctx: TContext,
95
95
  threadId?: number,
96
96
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
@@ -207,7 +207,7 @@ export function buildAutomaticThreadCleanupSettingsText(
207
207
  "<code>-</code> <code>on</code> (default): delete the bound thread and release Telegram authority on graceful quit.",
208
208
  "<code>-</code> <code>off</code>: preserve the tab as a restart hint; manual <code>/telegram-disconnect</code> still confirms and deletes it.",
209
209
  "",
210
- "Review inactive tabs checks current owner and work evidence. Review never deletes tabs.",
210
+ "On the leader instance, Review inactive tabs checks current owner and work evidence, including tabs without a letter. Review never deletes tabs.",
211
211
  ].join("\n");
212
212
  }
213
213
 
@@ -217,7 +217,8 @@ function buildInactiveThreadReviewText(count: number): string {
217
217
  "",
218
218
  `${count} proven inactive tab${count === 1 ? "" : "s"}.`,
219
219
  "No tabs were deleted.",
220
- "Deletion requires a separate confirmed Clean action.",
220
+ "Clean inactive tabs deletes these Telegram tabs and all their messages. Pi session history and project files are not removed.",
221
+ "Clean rechecks owner and work protection; a changed or protected tab is skipped. Choose Back to leave without deleting anything.",
221
222
  ].join("\n");
222
223
  }
223
224
 
@@ -264,7 +264,7 @@ export interface TelegramMenuActionRuntimeDeps<
264
264
  canOfferInFlightModelSwitch: (ctx: TContext) => boolean;
265
265
  sendTextReply: (
266
266
  chatId: number,
267
- replyToMessageId: number,
267
+ replyToMessageId: number | undefined,
268
268
  text: string,
269
269
  options?: {
270
270
  target?: { chatId: number; threadId?: number };
@@ -294,21 +294,21 @@ export interface TelegramMenuActionRuntime<
294
294
  ) => Promise<void>;
295
295
  sendStatusMessage: (
296
296
  chatId: number,
297
- replyToMessageId: number,
297
+ replyToMessageId: number | undefined,
298
298
  ctx: TContext,
299
299
  threadId?: number,
300
300
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
301
301
  ) => Promise<void>;
302
302
  openModelMenu: (
303
303
  chatId: number,
304
- replyToMessageId: number,
304
+ replyToMessageId: number | undefined,
305
305
  ctx: TContext,
306
306
  threadId?: number,
307
307
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
308
308
  ) => Promise<void>;
309
309
  openThinkingMenu: (
310
310
  chatId: number,
311
- replyToMessageId: number,
311
+ replyToMessageId: number | undefined,
312
312
  ctx: TContext,
313
313
  threadId?: number,
314
314
  options?: Pick<TelegramApiCallOptions, "assertAuthority">,
@@ -10,6 +10,7 @@ import {
10
10
  type AgentSettledEvent,
11
11
  type AgentStartEvent,
12
12
  type BeforeAgentStartEvent,
13
+ type BeforeAgentStartEventResult,
13
14
  type ExtensionAPI,
14
15
  type ExtensionCommandContext,
15
16
  type ExtensionContext,
@@ -33,6 +34,7 @@ export type {
33
34
  AgentStartEvent,
34
35
  AssistantMessageEvent,
35
36
  BeforeAgentStartEvent,
37
+ BeforeAgentStartEventResult,
36
38
  ExtensionAPI,
37
39
  ExtensionCommandContext,
38
40
  ExtensionContext,