@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -5,10 +5,12 @@
5
5
  */
6
6
 
7
7
  import { execFile } from "node:child_process";
8
- import { parentPort, workerData } from "node:worker_threads";
9
8
  import { pathToFileURL } from "node:url";
9
+ import { parentPort, workerData } from "node:worker_threads";
10
10
 
11
- if (!parentPort) throw new Error("Generative App worker requires a parent port.");
11
+ if (!parentPort) {
12
+ throw new Error("Generative App worker requires a parent port.");
13
+ }
12
14
 
13
15
  const controller = new AbortController();
14
16
  const runningChildren = new Set();
@@ -25,11 +27,17 @@ function runBoundedProcess(input) {
25
27
  }
26
28
  const command = input.command?.trim();
27
29
  if (!command) throw new Error("Generative App run.command is required.");
28
- if (!Array.isArray(input.args ?? []) || (input.args?.length ?? 0) > workerData.runMaxArgs) {
29
- throw new Error(`Generative App run.args accepts at most ${workerData.runMaxArgs} strings.`);
30
+ if (
31
+ !Array.isArray(input.args ?? []) ||
32
+ (input.args?.length ?? 0) > workerData.runMaxArgs
33
+ ) {
34
+ throw new Error(
35
+ `Generative App run.args accepts at most ${workerData.runMaxArgs} strings.`,
36
+ );
30
37
  }
31
38
  const args = (input.args ?? []).map((argument) => {
32
- if (typeof argument !== "string") throw new Error("Generative App run.args must contain strings.");
39
+ if (typeof argument !== "string")
40
+ throw new Error("Generative App run.args must contain strings.");
33
41
  return argument;
34
42
  });
35
43
  if (!input.cwd || typeof input.cwd !== "string") {
@@ -82,11 +90,15 @@ function runBoundedProcess(input) {
82
90
  try {
83
91
  const module = await import(pathToFileURL(workerData.modulePath).href);
84
92
  if (typeof module.init !== "function") {
85
- throw new Error(`Generative App ${workerData.app} must export named function init.`);
93
+ throw new Error(
94
+ `Generative App ${workerData.app} must export named function init.`,
95
+ );
86
96
  }
87
97
  const method = module[workerData.method];
88
98
  if (typeof method !== "function") {
89
- throw new Error(`Generative App ${workerData.app} does not export method ${workerData.method}.`);
99
+ throw new Error(
100
+ `Generative App ${workerData.app} does not export method ${workerData.method}.`,
101
+ );
90
102
  }
91
103
  const result = await method({
92
104
  ...(workerData.argumentPresent ? { argument: workerData.argument } : {}),
@@ -174,11 +174,6 @@ export declare function installGenerativeApp(options: GenerativeAppRuntimeOption
174
174
  script: string;
175
175
  }): Promise<GenerativeAppInvocationResult>;
176
176
  export declare function parseGenerativeAppBoundAction(prompt: string): GenerativeAppBoundAction | undefined;
177
- export declare function invokeGenerativeAppBoundAction(options: GenerativeAppRuntimeOptions & {
178
- expectedGeneration?: string;
179
- expectedRevision?: number;
180
- prompt: string;
181
- }): Promise<GenerativeAppInvocationResult | undefined>;
182
177
  export declare function bindGenerativeApp(options: GenerativeAppRuntimeOptions & {
183
178
  argument?: unknown;
184
179
  method?: string;
@@ -8,6 +8,8 @@ import { appendFile, copyFile, lstat, mkdir, readFile, rename, rm, stat, writeFi
8
8
  import { basename, dirname, extname, join, resolve } from "node:path";
9
9
  import { Worker } from "node:worker_threads";
10
10
  import { Type } from "@sinclair/typebox";
11
+ import { renameTelegramPathWithRetry } from "./locks.js";
12
+ import { isProcessAlive } from "./process-identity.js";
11
13
  const GENERATIVE_APP_NAME = /^[a-z][a-z0-9-]{0,31}$/u;
12
14
  const GENERATIVE_APP_METHOD = /^[a-z][a-z0-9_]{0,31}$/u;
13
15
  const GENERATIVE_APP_MAX_MODULE_BYTES = 1024 * 1024;
@@ -29,17 +31,6 @@ function byteLength(value) {
29
31
  function wait(ms) {
30
32
  return new Promise((resolveWait) => setTimeout(resolveWait, ms));
31
33
  }
32
- function isProcessAlive(pid) {
33
- if (!Number.isInteger(pid) || pid <= 0)
34
- return false;
35
- try {
36
- process.kill(pid, 0);
37
- return true;
38
- }
39
- catch (error) {
40
- return error.code === "EPERM";
41
- }
42
- }
43
34
  function assertAppName(app) {
44
35
  if (!GENERATIVE_APP_NAME.test(app)) {
45
36
  throw new Error("Generative App name must match /^[a-z][a-z0-9-]{0,31}$/.");
@@ -102,7 +93,9 @@ async function writeFileAtomic(path, content) {
102
93
  await mkdir(dirname(path), { recursive: true });
103
94
  const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
104
95
  await writeFile(temporary, content, { encoding: "utf8", mode: 0o600 });
105
- await rename(temporary, path);
96
+ // Windows may briefly deny replacing a file another reader or scanner holds; share the bounded retry.
97
+ if (!renameTelegramPathWithRetry(temporary, path))
98
+ throw new Error("Generative App staging file disappeared before publication.");
106
99
  }
107
100
  async function acquireGenerativeAppTransitionLock(appDir, waitMs = GENERATIVE_APP_LOCK_WAIT_MS) {
108
101
  const lockDir = `${appDir}.transition.lock`;
@@ -175,7 +168,8 @@ async function acquireGenerativeAppTransitionLock(appDir, waitMs = GENERATIVE_AP
175
168
  .then((metadata) => Date.now() - metadata.mtimeMs > 1_000)
176
169
  .catch(() => false)
177
170
  : false;
178
- if ((currentOwnerPid !== undefined && !isProcessAlive(currentOwnerPid)) ||
171
+ if ((currentOwnerPid !== undefined &&
172
+ !isProcessAlive(currentOwnerPid)) ||
179
173
  currentOwnerlessStale) {
180
174
  await rm(lockDir, { recursive: true, force: true });
181
175
  }
@@ -278,7 +272,9 @@ async function readStateTimeline(appDir) {
278
272
  envelopes.push(parsed);
279
273
  }
280
274
  catch {
281
- const hasLaterContent = lines.slice(index + 1).some((entry) => entry.trim());
275
+ const hasLaterContent = lines
276
+ .slice(index + 1)
277
+ .some((entry) => entry.trim());
282
278
  if (!hasLaterContent) {
283
279
  await writeFileAtomic(path, envelopes.map((envelope) => JSON.stringify(envelope)).join("\n") +
284
280
  (envelopes.length > 0 ? "\n" : ""));
@@ -615,24 +611,6 @@ export function parseGenerativeAppBoundAction(prompt) {
615
611
  app,
616
612
  };
617
613
  }
618
- export async function invokeGenerativeAppBoundAction(options) {
619
- const action = parseGenerativeAppBoundAction(options.prompt);
620
- if (!action)
621
- return undefined;
622
- return await invokeGenerativeApp({
623
- agentDir: options.agentDir,
624
- ...(action.argument !== undefined ? { argument: action.argument } : {}),
625
- ...(options.expectedGeneration !== undefined
626
- ? { expectedGeneration: options.expectedGeneration }
627
- : {}),
628
- ...(options.expectedRevision !== undefined
629
- ? { expectedRevision: options.expectedRevision }
630
- : {}),
631
- method: action.method,
632
- methodTimeoutMs: options.methodTimeoutMs,
633
- app: action.app,
634
- });
635
- }
636
614
  export async function bindGenerativeApp(options) {
637
615
  const hasScript = typeof options.script === "string";
638
616
  const hasMethod = typeof options.method === "string";
@@ -645,7 +623,9 @@ export async function bindGenerativeApp(options) {
645
623
  return hasScript
646
624
  ? await installGenerativeApp({
647
625
  agentDir: options.agentDir,
648
- ...(options.argument !== undefined ? { argument: options.argument } : {}),
626
+ ...(options.argument !== undefined
627
+ ? { argument: options.argument }
628
+ : {}),
649
629
  methodTimeoutMs: options.methodTimeoutMs,
650
630
  app: options.app,
651
631
  replace: options.replace,
@@ -653,7 +633,9 @@ export async function bindGenerativeApp(options) {
653
633
  })
654
634
  : await invokeGenerativeApp({
655
635
  agentDir: options.agentDir,
656
- ...(options.argument !== undefined ? { argument: options.argument } : {}),
636
+ ...(options.argument !== undefined
637
+ ? { argument: options.argument }
638
+ : {}),
657
639
  method: options.method,
658
640
  methodTimeoutMs: options.methodTimeoutMs,
659
641
  app: options.app,
@@ -739,7 +721,9 @@ export function createGenerativeAppLiveSurfaceRuntime(deps) {
739
721
  schedule(record, result.refreshAfterMs);
740
722
  }
741
723
  catch (error) {
742
- const classification = deps.classifyEditError?.(error) ?? { kind: "unknown" };
724
+ const classification = deps.classifyEditError?.(error) ?? {
725
+ kind: "unknown",
726
+ };
743
727
  if (!ownsRecord(key, record))
744
728
  return;
745
729
  deps.recordRuntimeEvent?.("generative-app", error, {
@@ -785,7 +769,8 @@ export function createGenerativeAppLiveSurfaceRuntime(deps) {
785
769
  const handle = frame.digest === surface.initialDigest
786
770
  ? surface.handle
787
771
  : await deps.edit(frame);
788
- if (result.refreshAfterMs === undefined || !deps.isCurrent({ ...surface, handle }))
772
+ if (result.refreshAfterMs === undefined ||
773
+ !deps.isCurrent({ ...surface, handle }))
789
774
  return;
790
775
  open({
791
776
  ...surface,
@@ -842,7 +827,9 @@ export function registerTelegramBindTool(pi, deps) {
842
827
  const view = {
843
828
  text: planned.markdown,
844
829
  parseMode: "markdown",
845
- ...(planned.replyMarkup !== undefined ? { replyMarkup: planned.replyMarkup } : {}),
830
+ ...(planned.replyMarkup !== undefined
831
+ ? { replyMarkup: planned.replyMarkup }
832
+ : {}),
846
833
  };
847
834
  return {
848
835
  digest: createHash("sha256").update(JSON.stringify(view)).digest("hex"),
@@ -862,7 +849,9 @@ export function registerTelegramBindTool(pi, deps) {
862
849
  if (!edited.ok)
863
850
  throw Object.assign(new Error(edited.message), {
864
851
  deliveryFailureReason: edited.reason,
865
- ...(edited.retryAfterMs === undefined ? {} : { retryAfterMs: edited.retryAfterMs }),
852
+ ...(edited.retryAfterMs === undefined
853
+ ? {}
854
+ : { retryAfterMs: edited.retryAfterMs }),
866
855
  });
867
856
  return { delivery: edited.value, view };
868
857
  },
@@ -878,12 +867,18 @@ export function registerTelegramBindTool(pi, deps) {
878
867
  return { kind: "unavailable" };
879
868
  }
880
869
  return {
881
- kind: failure.deliveryFailureReason === "commit-unknown" ? "unknown" : "terminal",
870
+ kind: failure.deliveryFailureReason === "commit-unknown"
871
+ ? "unknown"
872
+ : "terminal",
882
873
  };
883
874
  },
884
875
  recordRuntimeEvent: deps.recordRuntimeEvent,
885
- ...(deps.liveSurfaceSetTimer ? { setTimer: deps.liveSurfaceSetTimer } : {}),
886
- ...(deps.liveSurfaceClearTimer ? { clearTimer: deps.liveSurfaceClearTimer } : {}),
876
+ ...(deps.liveSurfaceSetTimer
877
+ ? { setTimer: deps.liveSurfaceSetTimer }
878
+ : {}),
879
+ ...(deps.liveSurfaceClearTimer
880
+ ? { clearTimer: deps.liveSurfaceClearTimer }
881
+ : {}),
887
882
  })
888
883
  : undefined;
889
884
  deps.setLiveSurfaceRuntime?.(liveSurfaces);
@@ -904,7 +899,9 @@ export function registerTelegramBindTool(pi, deps) {
904
899
  try {
905
900
  const result = await bindGenerativeApp({
906
901
  agentDir: deps.agentDir,
907
- ...(params.argument !== undefined ? { argument: params.argument } : {}),
902
+ ...(params.argument !== undefined
903
+ ? { argument: params.argument }
904
+ : {}),
908
905
  ...("method" in params && typeof params.method === "string"
909
906
  ? { method: params.method }
910
907
  : {}),
@@ -917,10 +914,10 @@ export function registerTelegramBindTool(pi, deps) {
917
914
  : {}),
918
915
  methodTimeoutMs: deps.methodTimeoutMs,
919
916
  });
920
- const activeTurn = params.display === false
921
- ? undefined
922
- : deps.getActiveTurn?.();
923
- if (activeTurn && deps.planOutput && (deps.sendView || deps.sendMarkdownReply)) {
917
+ const activeTurn = params.display === false ? undefined : deps.getActiveTurn?.();
918
+ if (activeTurn &&
919
+ deps.planOutput &&
920
+ (deps.sendView || deps.sendMarkdownReply)) {
924
921
  try {
925
922
  const planned = deps.planOutput(result.output, {
926
923
  binding: {
@@ -973,7 +970,12 @@ export function registerTelegramBindTool(pi, deps) {
973
970
  });
974
971
  }
975
972
  return {
976
- content: [{ type: "text", text: formatDisplayedGenerativeAppToolOutput() }],
973
+ content: [
974
+ {
975
+ type: "text",
976
+ text: formatDisplayedGenerativeAppToolOutput(),
977
+ },
978
+ ],
977
979
  details: { ...result, displayed: true, messageId },
978
980
  };
979
981
  }
@@ -984,13 +986,23 @@ export function registerTelegramBindTool(pi, deps) {
984
986
  method: result.method,
985
987
  });
986
988
  return {
987
- content: [{ type: "text", text: formatGenerativeAppToolOutput(result.output) }],
989
+ content: [
990
+ {
991
+ type: "text",
992
+ text: formatGenerativeAppToolOutput(result.output),
993
+ },
994
+ ],
988
995
  details: { ...result, displayed: false, displayFailed: true },
989
996
  };
990
997
  }
991
998
  }
992
999
  return {
993
- content: [{ type: "text", text: formatGenerativeAppToolOutput(result.output) }],
1000
+ content: [
1001
+ {
1002
+ type: "text",
1003
+ text: formatGenerativeAppToolOutput(result.output),
1004
+ },
1005
+ ],
994
1006
  details: { ...result, displayed: false },
995
1007
  };
996
1008
  }
@@ -10,9 +10,13 @@ export interface TelegramInboundHandlerConfig {
10
10
  mime?: string | string[];
11
11
  type?: string | string[];
12
12
  template?: string | TelegramInboundCommandTemplateConfig[];
13
+ /** Falsy skips this handler so selection continues with the next match. */
14
+ when?: CommandTemplateObjectConfig["when"];
13
15
  args?: string[];
14
16
  defaults?: Record<string, unknown>;
15
17
  timeout?: number | string;
18
+ retry?: CommandTemplateObjectConfig["retry"];
19
+ recover?: CommandTemplateObjectConfig["recover"];
16
20
  }
17
21
  export interface TelegramInboundHandlerFile {
18
22
  path: string;
@@ -38,6 +42,7 @@ export interface TelegramInboundHandlerExecOptions {
38
42
  signal?: AbortSignal;
39
43
  stdin?: string;
40
44
  retry?: number;
45
+ recover?: () => Promise<void>;
41
46
  }
42
47
  export interface TelegramInboundHandlerExecResult {
43
48
  stdout: string;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { readFile } from "node:fs/promises";
7
7
  import { basename } from "node:path";
8
- import { buildCommandTemplateInvocation, expandCommandTemplateConfigs, normalizeCommandTemplateConfig, substituteCommandTemplateToken, } from "./command-templates.js";
8
+ import { buildCommandTemplateInvocation, createCommandTemplateRecovery, getCommandTemplateCompositionSteps, getCommandTemplateConfiguredTimeout, getCommandTemplateStepTimeout, normalizeCommandTemplateConfig, resolveCommandTemplateNumericField, shouldRunCommandTemplateConfig, } from "./command-templates.js";
9
9
  import { getTelegramVoiceTranscriptionProviders } from "./voice.js";
10
10
  const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
11
11
  const INBOUND_HANDLER_REGISTRY_KEY = "__piTelegramInboundHandlers__";
@@ -155,42 +155,10 @@ export function buildTelegramInboundHandlerInvocation(handler, file, cwd, append
155
155
  throw new Error("Inbound handler template is required");
156
156
  return buildTelegramInboundTemplateInvocation(handler, file, cwd, appendFileIfMissing);
157
157
  }
158
- function resolveTelegramInboundNumericControlField(value, values, label) {
159
- if (value === undefined)
160
- return undefined;
161
- const resolved = typeof value === "string"
162
- ? substituteCommandTemplateToken(value, values, label)
163
- : value;
164
- if (resolved === "")
165
- return undefined;
166
- const numeric = Number(resolved);
167
- if (!Number.isFinite(numeric) || numeric < 0)
168
- throw new Error(`Command template ${label} must be a non-negative number.`);
169
- return numeric;
170
- }
171
- function getTelegramInboundHandlerConfiguredTimeout(handler) {
172
- const timeout = typeof handler === "string" ? undefined : handler.timeout;
173
- return resolveTelegramInboundNumericControlField(timeout, {}, "timeout");
174
- }
175
158
  function getTelegramInboundHandlerTimeout(handler) {
176
- return (getTelegramInboundHandlerConfiguredTimeout(handler) ??
159
+ return (getCommandTemplateConfiguredTimeout(handler) ??
177
160
  DEFAULT_INBOUND_HANDLER_TIMEOUT_MS);
178
161
  }
179
- function getRemainingTelegramInboundTimeout(timeout, startedAt) {
180
- return Math.max(1, timeout - (Date.now() - startedAt));
181
- }
182
- function getTelegramInboundInitialCompositionStepTimeout(handler, step) {
183
- const timeout = getTelegramInboundHandlerTimeout(handler);
184
- const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
185
- return stepTimeout === undefined ? timeout : Math.min(stepTimeout, timeout);
186
- }
187
- function getTelegramInboundCompositionStepTimeout(handler, step, startedAt) {
188
- const remaining = getRemainingTelegramInboundTimeout(getTelegramInboundHandlerTimeout(handler), startedAt);
189
- const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
190
- return stepTimeout === undefined
191
- ? remaining
192
- : Math.min(stepTimeout, remaining);
193
- }
194
162
  function getTelegramInboundHandlerKind(handler) {
195
163
  if (Array.isArray(handler.template))
196
164
  return "composition";
@@ -208,28 +176,31 @@ function formatTelegramInboundHandlerFailure(result) {
208
176
  parts.push(`stdout:\n${truncateTelegramInboundFailureStream(result.stdout)}`);
209
177
  return parts.join("\n\n");
210
178
  }
179
+ function getTelegramInboundHandlerRetryOptions(handler, values, cwd, timeout, deps) {
180
+ if (typeof handler !== "object" || handler.retry === undefined)
181
+ return {};
182
+ const recover = createCommandTemplateRecovery(handler, values, {
183
+ cwd,
184
+ timeout,
185
+ execCommand: deps.execCommand,
186
+ });
187
+ return {
188
+ retry: resolveCommandTemplateNumericField(handler.retry, {}, "retry"),
189
+ ...(recover ? { recover } : {}),
190
+ };
191
+ }
211
192
  async function executeTelegramInboundHandlerInvocation(handler, file, cwd, deps, appendFileIfMissing = true, timeout = getTelegramInboundHandlerTimeout(handler), stdin) {
212
193
  const invocation = buildTelegramInboundHandlerInvocation(handler, file, cwd, appendFileIfMissing);
213
194
  const result = await deps.execCommand(invocation.command, invocation.args, {
214
195
  cwd,
215
196
  timeout,
216
- ...(typeof handler === "object" && handler.retry !== undefined
217
- ? {
218
- retry: resolveTelegramInboundNumericControlField(handler.retry, {}, "retry"),
219
- }
220
- : {}),
197
+ ...getTelegramInboundHandlerRetryOptions(handler, getTelegramInboundHandlerTemplateValues(file), cwd, timeout, deps),
221
198
  ...(stdin !== undefined ? { stdin } : {}),
222
199
  });
223
200
  if (result.code !== 0)
224
201
  throw new Error(formatTelegramInboundHandlerFailure(result));
225
202
  return truncateTelegramInboundOutput(result.stdout);
226
203
  }
227
- function getTelegramInboundHandlerCompositionSteps(handler) {
228
- if (Array.isArray(handler.template)) {
229
- return expandCommandTemplateConfigs(handler);
230
- }
231
- return [];
232
- }
233
204
  function getTelegramTextHandlerFile() {
234
205
  return {
235
206
  path: "",
@@ -264,28 +235,24 @@ async function executeTelegramTextHandlerInvocation(handler, text, cwd, deps, ti
264
235
  cwd,
265
236
  timeout,
266
237
  stdin: text,
267
- ...(typeof handler === "object" && handler.retry !== undefined
268
- ? {
269
- retry: resolveTelegramInboundNumericControlField(handler.retry, {}, "retry"),
270
- }
271
- : {}),
238
+ ...getTelegramInboundHandlerRetryOptions(handler, getTelegramInboundHandlerTemplateValues(getTelegramTextHandlerFile(), text), cwd, timeout, deps),
272
239
  });
273
240
  if (result.code !== 0)
274
241
  throw new Error(formatTelegramInboundHandlerFailure(result));
275
242
  return truncateTelegramInboundOutput(result.stdout);
276
243
  }
277
244
  async function executeTelegramTextHandler(handler, text, cwd, deps) {
278
- const steps = getTelegramInboundHandlerCompositionSteps(handler);
245
+ const steps = getCommandTemplateCompositionSteps(handler);
279
246
  if (steps.length === 0) {
280
247
  return (await executeTelegramTextHandlerInvocation(handler, text, cwd, deps)).trim();
281
248
  }
282
249
  const startedAt = Date.now();
283
250
  let output = text;
284
251
  for (const [index, step] of steps.entries()) {
252
+ if (!shouldRunCommandTemplateConfig(step, getTelegramInboundHandlerTemplateValues(getTelegramTextHandlerFile(), output)))
253
+ continue;
285
254
  try {
286
- output = await executeTelegramTextHandlerInvocation(step, output, cwd, deps, index === 0
287
- ? getTelegramInboundInitialCompositionStepTimeout(handler, step)
288
- : getTelegramInboundCompositionStepTimeout(handler, step, startedAt));
255
+ output = await executeTelegramTextHandlerInvocation(step, output, cwd, deps, getCommandTemplateStepTimeout(getTelegramInboundHandlerTimeout(handler), step, index === 0 ? undefined : Date.now() - startedAt));
289
256
  }
290
257
  catch (error) {
291
258
  if (typeof step === "object" && step.failure === "root")
@@ -303,6 +270,9 @@ async function processTelegramTextHandlers(options) {
303
270
  let text = options.rawText;
304
271
  for (const handler of findTelegramTextHandlers(options.handlers)) {
305
272
  try {
273
+ // An unresolvable guard fails only this handler, like its command would.
274
+ if (!shouldRunCommandTemplateConfig(handler, getTelegramInboundHandlerTemplateValues(getTelegramTextHandlerFile(), text)))
275
+ continue;
306
276
  const output = await executeTelegramTextHandler(handler, text, options.cwd, options);
307
277
  if (output)
308
278
  text = output;
@@ -384,18 +354,22 @@ async function readBuiltInTelegramTextAttachment(file) {
384
354
  return truncateTelegramInboundOutput(`[${name}]\n${normalized}`);
385
355
  }
386
356
  async function executeTelegramInboundHandler(handler, file, cwd, deps) {
387
- const steps = getTelegramInboundHandlerCompositionSteps(handler);
357
+ const steps = getCommandTemplateCompositionSteps(handler);
388
358
  if (steps.length === 0) {
389
359
  const output = await executeTelegramInboundHandlerInvocation(handler, file, cwd, deps);
390
360
  return output.trim();
391
361
  }
392
362
  const startedAt = Date.now();
363
+ const values = getTelegramInboundHandlerTemplateValues(file);
393
364
  let output = "";
394
- for (const [index, step] of steps.entries()) {
365
+ let started = false;
366
+ for (const step of steps) {
367
+ if (!shouldRunCommandTemplateConfig(step, values))
368
+ continue;
369
+ const first = !started;
370
+ started = true;
395
371
  try {
396
- output = await executeTelegramInboundHandlerInvocation(step, file, cwd, deps, false, index === 0
397
- ? getTelegramInboundInitialCompositionStepTimeout(handler, step)
398
- : getTelegramInboundCompositionStepTimeout(handler, step, startedAt), index === 0 ? undefined : output);
372
+ output = await executeTelegramInboundHandlerInvocation(step, file, cwd, deps, false, getCommandTemplateStepTimeout(getTelegramInboundHandlerTimeout(handler), step, first ? undefined : Date.now() - startedAt), first ? undefined : output);
399
373
  }
400
374
  catch (error) {
401
375
  if (typeof step === "object" && step.failure === "root")
@@ -420,6 +394,8 @@ export async function processTelegramInboundHandlers(options) {
420
394
  const handlers = findTelegramInboundHandlers(options.handlers, file);
421
395
  for (const handler of handlers) {
422
396
  try {
397
+ if (!shouldRunCommandTemplateConfig(handler, getTelegramInboundHandlerTemplateValues(file)))
398
+ continue;
423
399
  const output = await executeTelegramInboundHandler(handler, file, options.cwd, options);
424
400
  if (output) {
425
401
  outputs.push({ file, output, handler });
@@ -5,7 +5,7 @@
5
5
  * bounded atomic publication, durable queue-receipt/failure state, and compaction.
6
6
  * It does not own polling, update execution, queue admission, or follower routing.
7
7
  */
8
- import { type TelegramProcessLiveness } from "./bus.ts";
8
+ import { type TelegramProcessLiveness } from "./process-identity.ts";
9
9
  import { type TelegramWorkspaceAdmissionLedger, type TelegramWorkspaceAdmissionScope } from "./workspace-admission.ts";
10
10
  export declare const TELEGRAM_UPDATE_JOURNAL_VERSION: 1;
11
11
  export declare const TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION: 2;
@@ -150,11 +150,18 @@ export interface TelegramUpdateJournalLegacyCustodyDisposition {
150
150
  export type TelegramUpdateJournalOperatorDisposition = TelegramUpdateJournalTerminalOperatorDisposition | TelegramUpdateJournalLegacyCustodyDisposition;
151
151
  export declare const TELEGRAM_ROUTING_INPUT_TTL_MS: number;
152
152
  /** A source-only choice deadline, never a queue lease or a Thread deletion grant. */
153
+ /** Where a source's chooser was published, so a restarted owner can revive that exact message. */
154
+ export interface TelegramUpdateJournalRoutingChooser {
155
+ chatId: number;
156
+ threadId?: number;
157
+ messageId: number;
158
+ }
153
159
  export interface TelegramUpdateJournalRoutingInput {
154
160
  operatorUserId: number;
155
161
  publishedAtMs: number;
156
162
  expiresAtMs: number;
157
163
  phase: "waiting" | "selected";
164
+ chooser?: TelegramUpdateJournalRoutingChooser;
158
165
  }
159
166
  export interface TelegramUpdateJournalEntry {
160
167
  updateId: number;
@@ -381,6 +388,7 @@ export type TelegramRoutingInputExpiryEvidence = Omit<TelegramUpdateJournalAband
381
388
  export interface TelegramRoutingInputJournal {
382
389
  arm(input: TelegramRoutingInputAuthority & {
383
390
  publishedAtMs: number;
391
+ chooser?: TelegramUpdateJournalRoutingChooser;
384
392
  }): TelegramUpdateJournalEntry[];
385
393
  select(input: TelegramRoutingInputAuthority): {
386
394
  issued: boolean;
@@ -415,7 +423,7 @@ export interface TelegramUpdateJournalStore {
415
423
  recoverDeadQueueOwner(input: TelegramUpdateJournalDeadQueueOwnerRecoveryInput): TelegramUpdateJournalDeadQueueOwnerRecoveryResult;
416
424
  removeCompleted(updateIds: readonly number[]): TelegramUpdateJournalRemoveResult;
417
425
  /** Guarded completion; every supplied source must still match, and absence is a conflict, not an ACK. */
418
- removeCompletedExact(updateIds: readonly number[], expectedSources: readonly TelegramUpdateJournalEntryDigest[], completions?: readonly TelegramUpdateJournalSourceCompletion[]): TelegramUpdateJournalRemoveResult;
426
+ removeCompletedExact(updateIds: readonly number[], expectedSources: readonly TelegramUpdateJournalEntryDigest[], completions?: readonly TelegramUpdateJournalSourceCompletion[], isCurrent?: () => boolean): TelegramUpdateJournalRemoveResult;
419
427
  /** Strict read-only observation; neither source absence nor a different scope is completion evidence. */
420
428
  inspectSourceCompletion(expected: TelegramUpdateJournalSourceCompletion): TelegramUpdateJournalSourceCompletion | undefined;
421
429
  /** Complete strict observation of a retained, unoffered v1 queue receipt; never acquires readiness. */
@@ -716,10 +724,32 @@ export interface TelegramJournalNamespaceInspection {
716
724
  export declare function inspectTelegramProfileJournalNamespace(input: TelegramJournalNamespaceInspectionInput): TelegramJournalNamespaceInspection;
717
725
  /** Read-only evidence over polling, retained flat recipients and role-neutral session journals. */
718
726
  export declare function inspectTelegramSessionJournalNamespace(input: TelegramJournalNamespaceInspectionInput): TelegramJournalNamespaceInspection;
727
+ /** Whether an update addresses this private-chat Thread anywhere in its payload (message, callback, reaction, …). */
728
+ export declare function doesTelegramJournalUpdateNameThread(update: unknown, target: {
729
+ chatId: number;
730
+ threadId: number;
731
+ }): boolean;
732
+ /** A button tap alone carries no input, so it never keeps a tab whose inputs are resolved (the in-flight Cancel itself). */
733
+ export declare function isTelegramJournalLoneCallbackUpdate(update: unknown): boolean;
719
734
  /** Complete-empty protection only; caller holds source serialization through consumption. Never deletion authority. */
720
735
  export declare function isTelegramThreadCleanupJournalNamespaceClear(input: TelegramJournalNamespaceInspectionInput & {
721
736
  requiredJournalBindingKeys: readonly string[];
722
737
  withSourceReference: NonNullable<TelegramJournalNamespaceInspectionInput["withSourceReference"]>;
738
+ /**
739
+ * The tab being cleaned and its own recorded inputs. When given, a plain pending input with no custody that
740
+ * neither names this tab nor belongs to it cannot be delivered here and does not protect it, nor does a plain
741
+ * button tap even in this tab; without it, every non-empty journal protects, as before.
742
+ */
743
+ cleanup?: {
744
+ target: {
745
+ chatId: number;
746
+ threadId: number;
747
+ };
748
+ ownInputs: readonly {
749
+ journalBindingKey: string;
750
+ updateIds: readonly number[];
751
+ }[];
752
+ };
723
753
  }): boolean;
724
754
  export declare function publishTelegramUpdateJournalSegment(path: string, segment: TelegramUpdateJournalSegment): TelegramUpdateJournalSegmentPublicationResult;
725
755
  export declare function createTelegramUpdateQueueHandoffToken(): string;
@@ -761,7 +791,7 @@ export interface TelegramUpdateJournalRuntimeBindingResolverDeps {
761
791
  withSourceSerialization?: TelegramUpdateJournalStoreOptions["withSourceSerialization"];
762
792
  getWorkspaceAdmission?: () => Pick<TelegramWorkspaceAdmissionLedger, "acquireAdmission" | "releaseAdmission"> | undefined;
763
793
  onRecovery?: (event: TelegramUpdateJournalRecoveryEvent) => void;
764
- /** Strict no-follow source handles; defaults to platform support (absent on Windows). */
794
+ /** Strict private source handles; on by default on every platform (see TELEGRAM_STRICT_READ_FLAGS). */
765
795
  strictSourceAccess?: boolean;
766
796
  }
767
797
  export declare function createTelegramUpdateJournalRuntimeBindingResolver(deps: TelegramUpdateJournalRuntimeBindingResolverDeps): () => TelegramUpdateJournalRuntimeBinding | undefined;