@sayknow-cli/coding-agent 0.3.7 → 0.3.9

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 (272) hide show
  1. package/dist/types/append-only-mode.d.ts +31 -0
  2. package/dist/types/capability/rule.d.ts +8 -0
  3. package/dist/types/cli/completion-cli.d.ts +41 -0
  4. package/dist/types/cli/config-cli.d.ts +1 -0
  5. package/dist/types/cli/fixture-report.d.ts +20 -0
  6. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  7. package/dist/types/cli/nofile-limit.d.ts +11 -0
  8. package/dist/types/cli/notify-cli.d.ts +7 -0
  9. package/dist/types/cli/update-cli.d.ts +22 -0
  10. package/dist/types/cli.d.ts +124 -1
  11. package/dist/types/commands/completion.d.ts +29 -0
  12. package/dist/types/commands/config.d.ts +3 -0
  13. package/dist/types/commands/deep-interview.d.ts +3 -0
  14. package/dist/types/config/file-lock.d.ts +7 -6
  15. package/dist/types/config/keybindings.d.ts +2 -2
  16. package/dist/types/config/model-profile-activation.d.ts +6 -0
  17. package/dist/types/config/model-profiles.d.ts +1 -0
  18. package/dist/types/config/settings-schema.d.ts +119 -1
  19. package/dist/types/config/settings.d.ts +8 -0
  20. package/dist/types/discovery/index.d.ts +1 -0
  21. package/dist/types/export/html/template.generated.d.ts +1 -1
  22. package/dist/types/export/ttsr.d.ts +14 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  24. package/dist/types/extensibility/extensions/types.d.ts +8 -0
  25. package/dist/types/goals/runtime.d.ts +1 -0
  26. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  27. package/dist/types/main.d.ts +4 -1
  28. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  29. package/dist/types/modes/components/model-selector.d.ts +4 -0
  30. package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
  31. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  32. package/dist/types/modes/components/status-line.d.ts +9 -0
  33. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  34. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  35. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  36. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  38. package/dist/types/modes/interactive-mode.d.ts +4 -0
  39. package/dist/types/modes/print-mode.d.ts +13 -1
  40. package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
  41. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  42. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  44. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  45. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  46. package/dist/types/modes/types.d.ts +3 -0
  47. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  48. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  49. package/dist/types/notifications/config-commands.d.ts +9 -0
  50. package/dist/types/notifications/config.d.ts +30 -1
  51. package/dist/types/notifications/daemon-paths.d.ts +1 -0
  52. package/dist/types/notifications/index.d.ts +8 -3
  53. package/dist/types/notifications/lifecycle-commands.d.ts +12 -3
  54. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  55. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  56. package/dist/types/notifications/rich-draft.d.ts +68 -0
  57. package/dist/types/notifications/rich-render.d.ts +90 -0
  58. package/dist/types/notifications/telegram-daemon.d.ts +45 -3
  59. package/dist/types/notifications/telegram-reference.d.ts +7 -5
  60. package/dist/types/notifications/threaded-render.d.ts +14 -0
  61. package/dist/types/runtime-api-key.d.ts +7 -0
  62. package/dist/types/sdk.d.ts +3 -0
  63. package/dist/types/session/agent-session.d.ts +61 -6
  64. package/dist/types/session/agent-storage.d.ts +1 -0
  65. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  66. package/dist/types/session/messages.d.ts +4 -0
  67. package/dist/types/session/session-manager.d.ts +17 -1
  68. package/dist/types/skc-runtime/deep-interview-ambiguity.d.ts +39 -0
  69. package/dist/types/skc-runtime/deep-interview-recorder.d.ts +10 -1
  70. package/dist/types/skc-runtime/deep-interview-state.d.ts +17 -2
  71. package/dist/types/skc-runtime/session-state-sidecar.d.ts +7 -0
  72. package/dist/types/skc-runtime/state-writer.d.ts +3 -0
  73. package/dist/types/skc-runtime/tmux-common.d.ts +1 -0
  74. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  75. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +120 -13
  76. package/dist/types/skill-state/active-state.d.ts +18 -1
  77. package/dist/types/system-prompt.d.ts +18 -2
  78. package/dist/types/task/executor.d.ts +3 -0
  79. package/dist/types/task/fork-context-budget.d.ts +7 -0
  80. package/dist/types/task/token-log.d.ts +30 -0
  81. package/dist/types/task/token-log.test.d.ts +1 -0
  82. package/dist/types/tools/bisect.d.ts +81 -0
  83. package/dist/types/tools/computer.d.ts +2 -2
  84. package/dist/types/tools/index.d.ts +1 -0
  85. package/dist/types/tools/search.d.ts +2 -2
  86. package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
  87. package/dist/types/utils/file-mentions.d.ts +14 -0
  88. package/dist/types/utils/git.d.ts +32 -0
  89. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  90. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  91. package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
  92. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
  93. package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
  94. package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
  95. package/package.json +8 -8
  96. package/src/append-only-mode.ts +58 -0
  97. package/src/autoresearch/prompt.md +1 -1
  98. package/src/autoresearch/tools/log-experiment.ts +1 -1
  99. package/src/capability/rule.ts +9 -0
  100. package/src/cli/completion-cli.ts +234 -0
  101. package/src/cli/config-cli.ts +61 -9
  102. package/src/cli/fast-help.ts +1 -0
  103. package/src/cli/fixture-report.test.ts +230 -0
  104. package/src/cli/fixture-report.ts +171 -0
  105. package/src/cli/nofile-limit.ts +48 -0
  106. package/src/cli/notify-cli.ts +103 -17
  107. package/src/cli/telegram-cli.ts +1 -1
  108. package/src/cli/update-cli.ts +148 -33
  109. package/src/cli.ts +29 -1
  110. package/src/commands/completion.ts +84 -0
  111. package/src/commands/config.ts +2 -0
  112. package/src/commands/contribution-prep.ts +1 -1
  113. package/src/commands/deep-interview.ts +2 -1
  114. package/src/config/file-lock.ts +73 -34
  115. package/src/config/keybindings.ts +3 -3
  116. package/src/config/model-profile-activation.ts +65 -0
  117. package/src/config/model-profiles.ts +31 -2
  118. package/src/config/settings-schema.ts +76 -1
  119. package/src/config/settings.ts +22 -0
  120. package/src/defaults/skc/skills/team/SKILL.md +17 -14
  121. package/src/defaults/skc/skills/ultragoal/SKILL.md +59 -4
  122. package/src/defaults/skc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  123. package/src/defaults/skc-defaults.ts +9 -0
  124. package/src/discovery/helpers.ts +10 -0
  125. package/src/discovery/index.ts +1 -0
  126. package/src/export/html/template.generated.ts +1 -1
  127. package/src/export/html/template.js +60 -2
  128. package/src/export/ttsr.ts +96 -18
  129. package/src/extensibility/extensions/runner.ts +183 -219
  130. package/src/extensibility/extensions/types.ts +9 -0
  131. package/src/extensibility/skc-plugins/runtime-adapters.ts +156 -44
  132. package/src/goals/runtime.ts +10 -2
  133. package/src/hindsight/backend.ts +1 -3
  134. package/src/hooks/native-skill-hook.ts +124 -4
  135. package/src/internal-urls/docs-index.generated.ts +17 -8
  136. package/src/lsp/client.ts +1 -0
  137. package/src/main.ts +95 -43
  138. package/src/memories/index.ts +12 -1
  139. package/src/modes/components/assistant-message.ts +241 -82
  140. package/src/modes/components/custom-editor.ts +1 -0
  141. package/src/modes/components/footer.ts +7 -2
  142. package/src/modes/components/model-selector.ts +54 -11
  143. package/src/modes/components/queued-message-selector.ts +92 -0
  144. package/src/modes/components/settings-selector.ts +4 -0
  145. package/src/modes/components/status-line.ts +167 -54
  146. package/src/modes/components/thinking-selector.ts +10 -8
  147. package/src/modes/components/tool-execution.ts +44 -19
  148. package/src/modes/components/welcome.ts +181 -44
  149. package/src/modes/controllers/command-controller.ts +40 -4
  150. package/src/modes/controllers/event-controller.ts +77 -49
  151. package/src/modes/controllers/extension-ui-controller.ts +12 -11
  152. package/src/modes/controllers/input-controller.ts +192 -35
  153. package/src/modes/controllers/selector-controller.ts +168 -24
  154. package/src/modes/interactive-mode.ts +56 -16
  155. package/src/modes/print-mode.ts +43 -4
  156. package/src/modes/prompt-action-autocomplete.ts +21 -20
  157. package/src/modes/rpc/rpc-mode.ts +32 -12
  158. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  159. package/src/modes/rpc/rpc-types.ts +9 -0
  160. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  161. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  162. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  163. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  164. package/src/modes/types.ts +3 -0
  165. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  166. package/src/modes/utils/injected-user-submission.ts +94 -0
  167. package/src/modes/utils/ui-helpers.ts +20 -7
  168. package/src/notifications/config-commands.ts +20 -0
  169. package/src/notifications/config.ts +52 -5
  170. package/src/notifications/daemon-paths.ts +2 -0
  171. package/src/notifications/index.ts +144 -18
  172. package/src/notifications/lifecycle-commands.ts +74 -23
  173. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  174. package/src/notifications/lifecycle-orchestrator.ts +19 -6
  175. package/src/notifications/reply-sent-store.ts +134 -0
  176. package/src/notifications/rich-draft.ts +107 -0
  177. package/src/notifications/rich-render.ts +142 -0
  178. package/src/notifications/telegram-daemon-control.ts +5 -3
  179. package/src/notifications/telegram-daemon.ts +606 -98
  180. package/src/notifications/telegram-reference.ts +17 -16
  181. package/src/notifications/threaded-render.ts +45 -2
  182. package/src/prompts/agents/architect.md +19 -10
  183. package/src/prompts/agents/critic.md +16 -18
  184. package/src/prompts/agents/executor.md +14 -11
  185. package/src/prompts/agents/planner.md +18 -13
  186. package/src/prompts/ci-green-request.md +3 -8
  187. package/src/prompts/goals/goal-continuation.md +0 -5
  188. package/src/prompts/goals/goal-mode-active.md +0 -5
  189. package/src/prompts/system/agent-creation-architect.md +2 -2
  190. package/src/prompts/system/plan-mode-active.md +1 -1
  191. package/src/prompts/system/plan-mode-subagent.md +3 -3
  192. package/src/prompts/system/project-prompt.md +0 -13
  193. package/src/prompts/system/subagent-system-prompt.md +3 -2
  194. package/src/prompts/system/system-prompt.md +29 -17
  195. package/src/prompts/system/volatile-project-context.md +10 -0
  196. package/src/prompts/system/web-search.md +4 -7
  197. package/src/prompts/tools/apply-patch.md +6 -0
  198. package/src/prompts/tools/ast-edit.md +1 -0
  199. package/src/prompts/tools/bash.md +1 -1
  200. package/src/prompts/tools/bisect.md +22 -0
  201. package/src/prompts/tools/browser.md +1 -1
  202. package/src/prompts/tools/computer.md +2 -2
  203. package/src/prompts/tools/cron.md +0 -2
  204. package/src/prompts/tools/eval.md +2 -2
  205. package/src/prompts/tools/find.md +1 -1
  206. package/src/prompts/tools/github.md +6 -6
  207. package/src/prompts/tools/goal.md +1 -3
  208. package/src/prompts/tools/image-gen.md +12 -5
  209. package/src/prompts/tools/irc.md +5 -4
  210. package/src/prompts/tools/job.md +2 -2
  211. package/src/prompts/tools/lsp.md +1 -1
  212. package/src/prompts/tools/monitor.md +2 -2
  213. package/src/prompts/tools/patch.md +2 -2
  214. package/src/prompts/tools/replace.md +2 -21
  215. package/src/prompts/tools/rewind.md +1 -1
  216. package/src/prompts/tools/search-tool-bm25.md +6 -11
  217. package/src/prompts/tools/search.md +1 -1
  218. package/src/prompts/tools/skill.md +3 -2
  219. package/src/prompts/tools/ssh.md +2 -1
  220. package/src/prompts/tools/subagent.md +2 -2
  221. package/src/prompts/tools/task.md +4 -5
  222. package/src/prompts/tools/todo-write.md +1 -1
  223. package/src/prompts/tools/vim.md +1 -0
  224. package/src/prompts/tools/web-search.md +2 -0
  225. package/src/prompts/tools/write.md +8 -2
  226. package/src/runtime-api-key.ts +16 -0
  227. package/src/sdk.ts +88 -36
  228. package/src/session/agent-session.ts +1013 -141
  229. package/src/session/agent-storage.ts +4 -0
  230. package/src/session/file-mention-pruning.ts +64 -0
  231. package/src/session/messages.ts +4 -0
  232. package/src/session/session-manager.ts +103 -4
  233. package/src/skc-runtime/deep-interview-ambiguity.ts +206 -0
  234. package/src/skc-runtime/deep-interview-recorder.ts +62 -8
  235. package/src/skc-runtime/deep-interview-state.ts +44 -2
  236. package/src/skc-runtime/session-state-sidecar.ts +263 -20
  237. package/src/skc-runtime/state-runtime.ts +12 -2
  238. package/src/skc-runtime/state-writer.ts +15 -0
  239. package/src/skc-runtime/tmux-common.ts +1 -0
  240. package/src/skc-runtime/ultragoal-guard.ts +55 -66
  241. package/src/skc-runtime/ultragoal-receipt-freshness.ts +315 -0
  242. package/src/skc-runtime/ultragoal-runtime.ts +1672 -159
  243. package/src/skc-runtime/workflow-manifest.generated.json +113 -2
  244. package/src/skc-runtime/workflow-manifest.ts +74 -1
  245. package/src/skill-state/active-state.ts +141 -5
  246. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  247. package/src/slash-commands/builtin-registry.ts +296 -72
  248. package/src/system-prompt.ts +57 -16
  249. package/src/task/executor.ts +132 -24
  250. package/src/task/fork-context-advisory.ts +2 -7
  251. package/src/task/fork-context-budget.ts +9 -0
  252. package/src/task/index.ts +13 -6
  253. package/src/task/output-manager.ts +20 -8
  254. package/src/task/receipt.ts +26 -7
  255. package/src/task/token-log.test.ts +188 -0
  256. package/src/task/token-log.ts +179 -0
  257. package/src/tools/bisect.ts +367 -0
  258. package/src/tools/computer.ts +246 -43
  259. package/src/tools/index.ts +3 -0
  260. package/src/tools/output-meta.ts +116 -4
  261. package/src/tools/search.ts +8 -2
  262. package/src/tools/skill.ts +15 -0
  263. package/src/tools/telegram-send.ts +6 -0
  264. package/src/tools/ultragoal-ask-guard.ts +39 -5
  265. package/src/utils/file-mentions.ts +39 -7
  266. package/src/utils/git.ts +73 -0
  267. package/src/utils/pasted-image-path.ts +170 -0
  268. package/src/web/scrapers/youtube.ts +24 -12
  269. package/src/workflow/workflow-intent-diff.ts +29 -6
  270. package/src/workflow/workflow-intent-report-consumer.ts +221 -0
  271. package/src/workflow/workflow-intent-report-schema.ts +129 -0
  272. package/src/workflow/workflow-intent-report.ts +135 -0
@@ -98,7 +98,7 @@ export interface ComputerScreenshotDetails {
98
98
  scaleY?: number;
99
99
  originX?: number;
100
100
  originY?: number;
101
- displayEpoch?: string;
101
+ displayEpoch?: number;
102
102
  captureId?: string;
103
103
  pngBytes?: number;
104
104
  path?: string;
@@ -144,7 +144,7 @@ type NativeScreenshot = {
144
144
  scaleY?: number;
145
145
  originX?: number;
146
146
  originY?: number;
147
- displayEpoch?: string;
147
+ displayEpoch?: number;
148
148
  captureId?: string;
149
149
  };
150
150
 
@@ -175,6 +175,7 @@ let controllerFactory: ComputerControllerFactory = createNativeComputerControlle
175
175
  let platformOverrideForTests: NodeJS.Platform | undefined;
176
176
  let archOverrideForTests: NodeJS.Architecture | undefined;
177
177
  const screenshotFallbackDirs = new WeakMap<ToolSession, Promise<string>>();
178
+ const latestScreenshotContexts = new WeakMap<ToolSession, ScreenshotContext>();
178
179
 
179
180
  const COMPUTER_INLINE_SCREENSHOT_MAX_WIDTH = 1568;
180
181
  const COMPUTER_INLINE_SCREENSHOT_MAX_HEIGHT = 1568;
@@ -275,12 +276,24 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
275
276
  const timeoutSeconds = clampTimeout("computer", params.timeout);
276
277
  const timeoutMs = timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined;
277
278
  const controller = controllerFactory();
278
- // Native ComputerController methods are synchronous and accept no AbortSignal,
279
- // so cancellation is honored before dispatch and wait() is bounded by timeoutMs.
279
+ const deadline = createComputerDeadline(timeoutMs);
280
280
  if (params.action === "batch") {
281
- const batchResult = await dispatchBatchComputerActions(controller, params.actions, timeoutMs, hotkey);
281
+ const batchResult = await dispatchBatchComputerActions(
282
+ controller,
283
+ params.actions,
284
+ timeoutMs,
285
+ hotkey,
286
+ latestScreenshotContexts.get(this.session),
287
+ shouldCapturePostActionScreenshot(params, this.session),
288
+ Boolean(this.session.settings.get("computer.autoScreenshot")),
289
+ signal,
290
+ deadline,
291
+ );
282
292
  details.steps = batchResult.steps;
283
- if (batchResult.screenshot) details.screenshot = batchResult.screenshot;
293
+ if (batchResult.screenshot) {
294
+ details.screenshot = batchResult.screenshot;
295
+ rememberLatestScreenshot(this.session, batchResult.screenshot);
296
+ }
284
297
  details.status = batchResult.failedStep ? "error" : "success";
285
298
  if (batchResult.failedStep) {
286
299
  details.code = batchResult.failedStep.code;
@@ -307,9 +320,21 @@ export class ComputerTool implements AgentTool<typeof computerSchema, ComputerTo
307
320
  .done()
308
321
  : toolResult(details).text(details.message).done();
309
322
  }
310
- const result = await dispatchComputerAction(controller, params, timeoutMs);
323
+ let result = await dispatchComputerAction(
324
+ controller,
325
+ params,
326
+ deadline,
327
+ latestScreenshotContexts.get(this.session),
328
+ signal,
329
+ );
330
+ if (shouldCapturePostActionScreenshot(params, this.session)) {
331
+ result = await captureScreenshot(controller, deadline, signal);
332
+ }
311
333
  const screenshot = normalizeScreenshot(result);
312
- if (screenshot) details.screenshot = screenshot;
334
+ if (screenshot) {
335
+ details.screenshot = screenshot;
336
+ rememberLatestScreenshot(this.session, screenshot);
337
+ }
313
338
  details.status = "success";
314
339
  details.message = describeComputerSuccess(details);
315
340
  if (screenshot) {
@@ -342,6 +367,10 @@ interface CoordinateBounds {
342
367
  originY?: number;
343
368
  }
344
369
 
370
+ interface ScreenshotContext extends CoordinateBounds {
371
+ displayEpoch?: number;
372
+ }
373
+
345
374
  function validatePointerCoordinates(action: string, x: number, y: number, bounds: CoordinateBounds | undefined): void {
346
375
  if (!bounds) return;
347
376
  const minX = bounds.originX ?? 0;
@@ -355,42 +384,189 @@ function validatePointerCoordinates(action: string, x: number, y: number, bounds
355
384
  }
356
385
  }
357
386
 
358
- function dispatchComputerAction(
387
+ function expectedEpochFromContext(context: ScreenshotContext | undefined): number | undefined {
388
+ return typeof context?.displayEpoch === "number" &&
389
+ Number.isFinite(context.displayEpoch) &&
390
+ context.displayEpoch >= 0
391
+ ? context.displayEpoch
392
+ : undefined;
393
+ }
394
+
395
+ function rememberLatestScreenshot(session: ToolSession, screenshot: ComputerScreenshotDetails): void {
396
+ latestScreenshotContexts.set(session, {
397
+ widthPx: screenshot.widthPx,
398
+ heightPx: screenshot.heightPx,
399
+ originX: screenshot.originX,
400
+ originY: screenshot.originY,
401
+ displayEpoch: screenshot.displayEpoch,
402
+ });
403
+ }
404
+
405
+ function captureScreenshot(
359
406
  controller: NativeController,
360
- params: SingleComputerParams,
407
+ deadline: ComputerDeadline | undefined,
408
+ signal?: AbortSignal,
409
+ ): Promise<unknown> {
410
+ return runComputerOperation(
411
+ () => {
412
+ if (!controller.screenshot) missingNativeMethod("screenshot", "screenshot");
413
+ return controller.screenshot();
414
+ },
415
+ deadline,
416
+ signal,
417
+ );
418
+ }
419
+
420
+ function missingNativeMethod(action: string, method: string): never {
421
+ throw new ToolError(`COMPUTER_UNAVAILABLE: Native ComputerController.${method} is unavailable for ${action}.`, {
422
+ code: "COMPUTER_UNAVAILABLE",
423
+ });
424
+ }
425
+
426
+ function shouldCapturePostActionScreenshot(
427
+ params: Pick<ComputerParams, "action" | "include_screenshot">,
428
+ session: Pick<ToolSession, "settings">,
429
+ ): boolean {
430
+ return (
431
+ params.action !== "screenshot" &&
432
+ (params.include_screenshot === true || Boolean(session.settings.get("computer.autoScreenshot")))
433
+ );
434
+ }
435
+
436
+ interface ComputerDeadline {
437
+ expiresAtMs: number;
438
+ }
439
+
440
+ class ComputerTimeoutError extends Error {
441
+ constructor() {
442
+ super("Computer action timed out.");
443
+ this.name = "TimeoutError";
444
+ }
445
+ }
446
+
447
+ function createComputerDeadline(
361
448
  timeoutMs: number | undefined,
362
- bounds?: CoordinateBounds,
363
- ): Promise<unknown> | unknown {
364
- // expectedEpoch is undefined until lossless epoch transport lands (follow-up):
365
- // the native gate skips the stale-display check when the epoch is absent.
366
- switch (params.action) {
367
- case "screenshot":
368
- return controller.screenshot?.();
369
- case "click":
370
- validatePointerCoordinates("click", params.x, params.y, bounds);
371
- return controller.click?.(undefined, params.x, params.y, params.button ?? "left");
372
- case "double_click":
373
- validatePointerCoordinates("double_click", params.x, params.y, bounds);
374
- return controller.doubleClick?.(undefined, params.x, params.y, params.button ?? "left");
375
- case "move":
376
- validatePointerCoordinates("move", params.x, params.y, bounds);
377
- return controller.move?.(undefined, params.x, params.y);
378
- case "drag":
379
- validatePointerCoordinates("drag start", params.x, params.y, bounds);
380
- validatePointerCoordinates("drag end", params.to_x, params.to_y, bounds);
381
- return controller.drag?.(undefined, params.x, params.y, params.to_x, params.to_y, params.button ?? "left");
382
- case "scroll":
383
- validatePointerCoordinates("scroll", params.x, params.y, bounds);
384
- return controller.scroll?.(undefined, params.x, params.y, params.scroll_x, params.scroll_y);
385
- case "type":
386
- return controller.type?.(undefined, params.text);
387
- case "keypress":
388
- return controller.keypress?.(undefined, params.keys);
389
- case "wait":
390
- return controller.wait?.(undefined, capWaitMs(params.ms, timeoutMs));
449
+ parent?: ComputerDeadline,
450
+ ): ComputerDeadline | undefined {
451
+ const localExpiresAt = timeoutMs && timeoutMs > 0 ? performance.now() + timeoutMs : undefined;
452
+ const parentExpiresAt = parent?.expiresAtMs;
453
+ const expiresAtMs =
454
+ localExpiresAt === undefined
455
+ ? parentExpiresAt
456
+ : parentExpiresAt === undefined
457
+ ? localExpiresAt
458
+ : Math.min(localExpiresAt, parentExpiresAt);
459
+ return expiresAtMs === undefined ? undefined : { expiresAtMs };
460
+ }
461
+
462
+ function remainingComputerTimeoutMs(deadline: ComputerDeadline | undefined): number | undefined {
463
+ if (!deadline) return undefined;
464
+ const remaining = Math.ceil(deadline.expiresAtMs - performance.now());
465
+ if (remaining <= 0) throw new ComputerTimeoutError();
466
+ return remaining;
467
+ }
468
+
469
+ function assertComputerDeadline(deadline: ComputerDeadline | undefined): void {
470
+ remainingComputerTimeoutMs(deadline);
471
+ }
472
+
473
+ async function runComputerOperation<T>(
474
+ operation: () => Promise<T> | T,
475
+ deadline: ComputerDeadline | undefined,
476
+ signal?: AbortSignal,
477
+ ): Promise<T> {
478
+ throwIfAborted(signal);
479
+ const timeoutMs = remainingComputerTimeoutMs(deadline);
480
+ let timeout: ReturnType<typeof setTimeout> | undefined;
481
+ let removeAbortListener: (() => void) | undefined;
482
+ const operationPromise = Promise.resolve().then(operation);
483
+ operationPromise.catch(() => undefined);
484
+ const guards: Array<Promise<never>> = [];
485
+ if (timeoutMs !== undefined) {
486
+ guards.push(
487
+ new Promise((_, reject) => {
488
+ timeout = setTimeout(() => reject(new ComputerTimeoutError()), timeoutMs);
489
+ }),
490
+ );
491
+ }
492
+ if (signal) {
493
+ guards.push(
494
+ new Promise((_, reject) => {
495
+ const onAbort = () => reject(new ToolAbortError());
496
+ signal.addEventListener("abort", onAbort, { once: true });
497
+ removeAbortListener = () => signal.removeEventListener("abort", onAbort);
498
+ }),
499
+ );
500
+ }
501
+ try {
502
+ const result = await (guards.length > 0 ? Promise.race([operationPromise, ...guards]) : operationPromise);
503
+ throwIfAborted(signal);
504
+ assertComputerDeadline(deadline);
505
+ return result;
506
+ } finally {
507
+ if (timeout) clearTimeout(timeout);
508
+ removeAbortListener?.();
391
509
  }
392
510
  }
393
511
 
512
+ function dispatchComputerAction(
513
+ controller: NativeController,
514
+ params: SingleComputerParams,
515
+ deadline: ComputerDeadline | undefined,
516
+ context?: ScreenshotContext,
517
+ signal?: AbortSignal,
518
+ ): Promise<unknown> {
519
+ const expectedEpoch = expectedEpochFromContext(context);
520
+ return runComputerOperation(
521
+ () => {
522
+ switch (params.action) {
523
+ case "screenshot":
524
+ if (!controller.screenshot) missingNativeMethod("screenshot", "screenshot");
525
+ return controller.screenshot();
526
+ case "click":
527
+ validatePointerCoordinates("click", params.x, params.y, context);
528
+ if (!controller.click) missingNativeMethod("click", "click");
529
+ return controller.click(expectedEpoch, params.x, params.y, params.button ?? "left");
530
+ case "double_click":
531
+ validatePointerCoordinates("double_click", params.x, params.y, context);
532
+ if (!controller.doubleClick) missingNativeMethod("double_click", "doubleClick");
533
+ return controller.doubleClick(expectedEpoch, params.x, params.y, params.button ?? "left");
534
+ case "move":
535
+ validatePointerCoordinates("move", params.x, params.y, context);
536
+ if (!controller.move) missingNativeMethod("move", "move");
537
+ return controller.move(expectedEpoch, params.x, params.y);
538
+ case "drag":
539
+ validatePointerCoordinates("drag start", params.x, params.y, context);
540
+ validatePointerCoordinates("drag end", params.to_x, params.to_y, context);
541
+ if (!controller.drag) missingNativeMethod("drag", "drag");
542
+ return controller.drag(
543
+ expectedEpoch,
544
+ params.x,
545
+ params.y,
546
+ params.to_x,
547
+ params.to_y,
548
+ params.button ?? "left",
549
+ );
550
+ case "scroll":
551
+ validatePointerCoordinates("scroll", params.x, params.y, context);
552
+ if (!controller.scroll) missingNativeMethod("scroll", "scroll");
553
+ return controller.scroll(expectedEpoch, params.x, params.y, params.scroll_x, params.scroll_y);
554
+ case "type":
555
+ if (!controller.type) missingNativeMethod("type", "type");
556
+ return controller.type(undefined, params.text);
557
+ case "keypress":
558
+ if (!controller.keypress) missingNativeMethod("keypress", "keypress");
559
+ return controller.keypress(undefined, params.keys);
560
+ case "wait":
561
+ if (!controller.wait) missingNativeMethod("wait", "wait");
562
+ return controller.wait(undefined, capWaitMs(params.ms, remainingComputerTimeoutMs(deadline)));
563
+ }
564
+ },
565
+ deadline,
566
+ signal,
567
+ );
568
+ }
569
+
394
570
  interface BatchDispatchResult {
395
571
  steps: ComputerToolDetails[];
396
572
  screenshot?: ComputerScreenshotDetails;
@@ -403,21 +579,34 @@ async function dispatchBatchComputerActions(
403
579
  actions: readonly SingleComputerParams[],
404
580
  timeoutMs: number | undefined,
405
581
  hotkey?: string,
582
+ initialContext?: ScreenshotContext,
583
+ includeBatchScreenshot = false,
584
+ autoScreenshot = false,
585
+ signal?: AbortSignal,
586
+ deadline?: ComputerDeadline,
406
587
  ): Promise<BatchDispatchResult> {
407
588
  const steps: ComputerToolDetails[] = [];
408
589
  let lastScreenshot: ComputerScreenshotDetails | undefined;
409
590
  let lastScreenshotSource: unknown;
410
- let bounds: CoordinateBounds | undefined;
591
+ let context = initialContext;
411
592
  for (const single of actions) {
412
593
  const stepDetails = detailsFromParams(single);
413
594
  try {
414
- const result = await dispatchComputerAction(controller, single, timeoutMs, bounds);
595
+ throwIfAborted(signal);
596
+ assertComputerDeadline(deadline);
597
+ const stepTimeoutMs = stepTimeoutFromParams(single, timeoutMs);
598
+ const stepDeadline = createComputerDeadline(stepTimeoutMs, deadline);
599
+ let result = await dispatchComputerAction(controller, single, stepDeadline, context, signal);
600
+ if (single.action !== "screenshot" && (single.include_screenshot === true || autoScreenshot)) {
601
+ result = await captureScreenshot(controller, stepDeadline, signal);
602
+ }
603
+
415
604
  const screenshot = normalizeScreenshot(result);
416
605
  if (screenshot) {
417
606
  stepDetails.screenshot = screenshot;
418
607
  lastScreenshot = screenshot;
419
608
  lastScreenshotSource = result;
420
- bounds = screenshot;
609
+ context = screenshot;
421
610
  }
422
611
  stepDetails.status = "success";
423
612
  stepDetails.message = describeComputerSuccess(stepDetails);
@@ -437,9 +626,19 @@ async function dispatchBatchComputerActions(
437
626
  }
438
627
  steps.push(stepDetails);
439
628
  }
629
+ if (includeBatchScreenshot) {
630
+ lastScreenshotSource = await captureScreenshot(controller, deadline, signal);
631
+ lastScreenshot = normalizeScreenshot(lastScreenshotSource) ?? lastScreenshot;
632
+ }
440
633
  return { steps, screenshot: lastScreenshot, screenshotSource: lastScreenshotSource };
441
634
  }
442
635
 
636
+ function stepTimeoutFromParams(params: SingleComputerParams, batchTimeoutMs: number | undefined): number | undefined {
637
+ if (params.timeout === undefined) return batchTimeoutMs;
638
+ const timeoutSeconds = clampTimeout("computer", params.timeout);
639
+ return timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined;
640
+ }
641
+
443
642
  function detailsFromParams(params: ComputerParams): ComputerToolDetails {
444
643
  const details: ComputerToolDetails = { action: params.action, status: "success" };
445
644
  if ("x" in params) details.x = params.x;
@@ -476,12 +675,16 @@ function normalizeScreenshot(value: unknown): ComputerScreenshotDetails | undefi
476
675
  scaleY: shot.scaleY,
477
676
  originX: shot.originX,
478
677
  originY: shot.originY,
479
- displayEpoch: shot.displayEpoch,
678
+ displayEpoch: normalizeDisplayEpoch(shot.displayEpoch),
480
679
  captureId: shot.captureId,
481
680
  pngBytes: getPngByteLength(shot.png),
482
681
  };
483
682
  }
484
683
 
684
+ function normalizeDisplayEpoch(value: unknown): number | undefined {
685
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
686
+ }
687
+
485
688
  function fullResolutionImageContentFromNativeResult(value: unknown): ImageContent | undefined {
486
689
  const candidate =
487
690
  value && typeof value === "object" && "screenshot" in value
@@ -34,6 +34,7 @@ import { AstEditTool } from "./ast-edit";
34
34
  import { AstGrepTool } from "./ast-grep";
35
35
  import { BashTool } from "./bash";
36
36
  import type { BashRestrictionProfile } from "./bash-allowed-prefixes";
37
+ import { BisectTool } from "./bisect";
37
38
  import { BrowserTool } from "./browser";
38
39
  import { CalculatorTool } from "./calculator";
39
40
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
@@ -72,6 +73,7 @@ export * from "./ask";
72
73
  export * from "./ast-edit";
73
74
  export * from "./ast-grep";
74
75
  export * from "./bash";
76
+ export * from "./bisect";
75
77
  export * from "./browser";
76
78
  export * from "./calculator";
77
79
  export * from "./checkpoint";
@@ -387,6 +389,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
387
389
  render_mermaid: s => new RenderMermaidTool(s),
388
390
  ask: AskTool.createIf,
389
391
  debug: DebugTool.createIf,
392
+ bisect: s => new BisectTool(s),
390
393
  eval: s => new EvalTool(s),
391
394
  calc: s => new CalculatorTool(s),
392
395
  ssh: loadSshTool,
@@ -554,13 +554,17 @@ function getSpillConfig(s: Settings | undefined) {
554
554
  | "tools.artifactSpillThreshold"
555
555
  | "tools.artifactTailBytes"
556
556
  | "tools.artifactTailLines"
557
- | "tools.artifactHeadBytes";
557
+ | "tools.artifactHeadBytes"
558
+ | "tools.maxInlineResultBytes"
559
+ | "tools.readArtifactSpillThreshold";
558
560
  const get = <P extends Path>(path: P) => s?.get(path) ?? getDefault(path);
559
561
  return {
560
562
  threshold: get("tools.artifactSpillThreshold") * 1024,
563
+ readThreshold: get("tools.readArtifactSpillThreshold") * 1024,
561
564
  tailBytes: get("tools.artifactTailBytes") * 1024,
562
565
  tailLines: get("tools.artifactTailLines"),
563
566
  headBytes: get("tools.artifactHeadBytes") * 1024,
567
+ maxInlineBytes: get("tools.maxInlineResultBytes") * 1024,
564
568
  };
565
569
  }
566
570
 
@@ -596,8 +600,12 @@ async function spillLargeResultToArtifact(
596
600
  ): Promise<AgentToolResult> {
597
601
  const sessionManager = context?.sessionManager;
598
602
  if (!sessionManager) return result;
599
- if (toolName === "read") return result;
600
- const { threshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
603
+ const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
604
+ // `read` manages its own per-range truncation, but the combined multi-range
605
+ // output has no cap — enforce a read-specific (higher) combined threshold
606
+ // instead of exempting read entirely. 0 disables read spill (backstop only).
607
+ const effectiveThreshold = toolName === "read" ? readThreshold : threshold;
608
+ if (effectiveThreshold <= 0) return result;
601
609
 
602
610
  // Skip if tool already saved an artifact
603
611
  const existingMeta: OutputMeta | undefined = result.details?.meta;
@@ -614,7 +622,7 @@ async function spillLargeResultToArtifact(
614
622
 
615
623
  const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
616
624
  const totalBytes = Buffer.byteLength(fullText, "utf-8");
617
- if (totalBytes <= threshold) return result;
625
+ if (totalBytes <= effectiveThreshold) return result;
618
626
 
619
627
  // Save full output as artifact
620
628
  const artifactId = await sessionManager.saveArtifact(fullText, toolName);
@@ -691,6 +699,106 @@ async function spillLargeResultToArtifact(
691
699
  return { ...result, content: newContent, details: newDetails };
692
700
  }
693
701
 
702
+ /**
703
+ * Absolute inline-size backstop enforced after {@link spillLargeResultToArtifact}.
704
+ *
705
+ * The threshold-based spill above has escape hatches: it early-returns for the
706
+ * `read` tool and for results that already carry an `artifactId` (a tool may set
707
+ * partial truncation meta yet still emit oversized inline text). This backstop
708
+ * closes those gaps: when `tools.maxInlineResultBytes` is configured (> 0), any
709
+ * final result whose inline text exceeds the cap is force-saved to an artifact
710
+ * (reusing an existing artifactId to avoid double-artifacting) and truncated to a
711
+ * head+tail view that fits the cap. Disabled by default (opt-in pending
712
+ * measurement); a 0 cap returns the result untouched.
713
+ */
714
+ async function enforceInlineResultBackstop(
715
+ result: AgentToolResult,
716
+ toolName: string,
717
+ context: AgentToolContext | undefined,
718
+ ): Promise<AgentToolResult> {
719
+ const { maxInlineBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
720
+ if (maxInlineBytes <= 0) return result;
721
+
722
+ const textParts: string[] = [];
723
+ for (const block of result.content) {
724
+ if (block.type === "text" && block.text) {
725
+ textParts.push(block.text);
726
+ }
727
+ }
728
+ if (textParts.length === 0) return result;
729
+
730
+ const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
731
+ const totalBytes = Buffer.byteLength(fullText, "utf-8");
732
+ if (totalBytes <= maxInlineBytes) return result;
733
+
734
+ // Reuse an existing artifact (avoid double-artifacting); otherwise save the
735
+ // full output so the truncated view keeps a reference to the complete text.
736
+ const existingMeta: OutputMeta | undefined = result.details?.meta;
737
+ let artifactId = existingMeta?.truncation?.artifactId;
738
+ if (!artifactId) {
739
+ artifactId = (await context?.sessionManager?.saveArtifact(fullText, toolName)) ?? undefined;
740
+ }
741
+
742
+ // Budget head+tail below the cap, reserving room for the elision marker so the
743
+ // composed `<head>\n<marker>\n<tail>` view never exceeds the configured cap.
744
+ const MARKER_RESERVE = 256;
745
+ const budget = maxInlineBytes - MARKER_RESERVE;
746
+ const useMiddle = headBytes > 0 && budget > 0;
747
+ let truncated = useMiddle
748
+ ? truncateMiddle(fullText, {
749
+ maxBytes: budget,
750
+ maxLines: tailLines * 2,
751
+ maxHeadBytes: Math.min(headBytes, Math.floor(budget / 2)),
752
+ maxHeadLines: tailLines,
753
+ })
754
+ : truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
755
+
756
+ // Defensive clamp: guarantee the contract even for pathological marker sizes.
757
+ if (Buffer.byteLength(truncated.content, "utf-8") > maxInlineBytes) {
758
+ truncated = truncateTail(fullText, { maxBytes: maxInlineBytes, maxLines: tailLines });
759
+ }
760
+
761
+ const newContent: (TextContent | ImageContent)[] = [];
762
+ for (const block of result.content) {
763
+ if (block.type !== "text") {
764
+ newContent.push(block);
765
+ }
766
+ }
767
+ newContent.push({ type: "text", text: truncated.content });
768
+
769
+ const outputLines = truncated.outputLines ?? truncated.totalLines;
770
+ const outputBytes = truncated.outputBytes ?? truncated.totalBytes;
771
+ const truncationMeta: TruncationMeta =
772
+ truncated.truncatedBy === "middle"
773
+ ? {
774
+ direction: "middle",
775
+ truncatedBy: "middle",
776
+ totalLines: truncated.totalLines,
777
+ totalBytes: truncated.totalBytes,
778
+ outputLines,
779
+ outputBytes,
780
+ maxBytes: maxInlineBytes,
781
+ elidedLines: truncated.elidedLines ?? Math.max(0, truncated.totalLines - outputLines),
782
+ elidedBytes: truncated.elidedBytes ?? Math.max(0, truncated.totalBytes - outputBytes),
783
+ artifactId,
784
+ }
785
+ : {
786
+ direction: "tail",
787
+ truncatedBy: truncated.truncatedBy ?? "bytes",
788
+ totalLines: truncated.totalLines,
789
+ totalBytes: truncated.totalBytes,
790
+ outputLines,
791
+ outputBytes,
792
+ maxBytes: maxInlineBytes,
793
+ shownRange: { start: truncated.totalLines - outputLines + 1, end: truncated.totalLines },
794
+ artifactId,
795
+ };
796
+
797
+ const newMeta: OutputMeta = { ...(existingMeta ?? {}), truncation: truncationMeta };
798
+ const newDetails = { ...(result.details ?? {}), meta: newMeta };
799
+ return { ...result, content: newContent, details: newDetails };
800
+ }
801
+
694
802
  // =============================================================================
695
803
  // Tool wrapper
696
804
  // =============================================================================
@@ -711,6 +819,10 @@ async function wrappedExecute(
711
819
  // Spill large results to artifact, truncate to tail
712
820
  result = await spillLargeResultToArtifact(result, this.name, context);
713
821
 
822
+ // Absolute inline-size backstop: catches oversized text the threshold spill
823
+ // skipped (read exemption, tools with pre-existing partial artifact meta).
824
+ result = await enforceInlineResultBackstop(result, this.name, context);
825
+
714
826
  // Append notices from meta
715
827
  const meta: OutputMeta | undefined = result.details?.meta;
716
828
  if (meta) {
@@ -44,7 +44,10 @@ const searchSchema = z
44
44
  paths: z
45
45
  .array(z.string().describe("file, directory, glob, or internal URL to search"))
46
46
  .min(1)
47
- .describe("files, directories, globs, or internal URLs to search"),
47
+ .optional()
48
+ .describe(
49
+ "files, directories, globs, or internal URLs to search (defaults to the working directory when omitted)",
50
+ ),
48
51
  i: z.boolean().optional().describe("case-insensitive search"),
49
52
  gitignore: z.boolean().optional().describe("respect gitignore"),
50
53
  skip: z
@@ -236,7 +239,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
236
239
  _onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
237
240
  _toolContext?: AgentToolContext,
238
241
  ): Promise<AgentToolResult<SearchToolDetails>> {
239
- const { pattern, paths, i, gitignore, skip } = params;
242
+ const { pattern, i, gitignore, skip } = params;
243
+ // Recurring real-world failure mode: models omit `paths` for repo-wide
244
+ // searches. Default to the working directory instead of failing the call.
245
+ const paths = params.paths ?? ["."];
240
246
 
241
247
  return untilAborted(signal, async () => {
242
248
  const normalizedPattern = pattern.trim();
@@ -37,6 +37,8 @@ function normalizeSkillName(name: string | undefined): string {
37
37
  return (name ?? "").trim();
38
38
  }
39
39
 
40
+ const SKILL_NAME_GLOB_PATTERN = /[*?[\]{}]/;
41
+
40
42
  type SkillToolInput = z.infer<typeof skillSchema>;
41
43
 
42
44
  export interface SkillToolDetails {
@@ -87,6 +89,19 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
87
89
  if (!requestedName) {
88
90
  throw new ToolError("skill tool: `name` is required");
89
91
  }
92
+ // Fail fast on glob/wildcard names (e.g. `*`). A model that sees the
93
+ // `--skills '*'` launch filter may echo the glob into the skill tool.
94
+ // Without this guard `*` slips through to the generic unknown-skill
95
+ // path only because no skill is literally named `*`, so a future
96
+ // odd skill name could get dispatched — and the actionable signal
97
+ // that names are concrete (not globs) is lost. Reject before any
98
+ // dispatch or handoff state mutation so the call ends immediately
99
+ // instead of spawning skill work that burns the whole turn budget.
100
+ if (SKILL_NAME_GLOB_PATTERN.test(requestedName)) {
101
+ throw new ToolError(
102
+ `skill tool: "${requestedName}" is not a valid skill name. The name must be a single concrete skill (e.g. "ralplan"), not a glob or wildcard pattern. Pass one exact skill name as shown in /skill:<name>.`,
103
+ );
104
+ }
90
105
  const activeState = this.#session.getActiveSkillState?.();
91
106
  const activeSkill = normalizeSkillName(activeState?.skill);
92
107
  if (activeSkill && requestedName === activeSkill) {
@@ -6,6 +6,9 @@ import { getTelegramFileSink } from "../notifications/attachment-registry";
6
6
  import { getNotificationConfig, isGloballyConfigured } from "../notifications/config";
7
7
  import type { ToolSession } from "./index";
8
8
 
9
+ const TELEGRAM_SEND_MAX_FILE_BYTES = 50 * 1024 * 1024;
10
+ const TELEGRAM_SEND_MAX_FILE_MIB = TELEGRAM_SEND_MAX_FILE_BYTES / (1024 * 1024);
11
+
9
12
  const telegramSendSchema = z.object({
10
13
  path: z
11
14
  .string()
@@ -75,6 +78,9 @@ export class TelegramSendTool implements AgentTool<typeof telegramSendSchema, Te
75
78
  if (!stat.isFile()) {
76
79
  return { ok: false, error: "not a regular file" };
77
80
  }
81
+ if (stat.size > TELEGRAM_SEND_MAX_FILE_BYTES) {
82
+ return { ok: false, error: `file exceeds Telegram document limit (${TELEGRAM_SEND_MAX_FILE_MIB} MiB)` };
83
+ }
78
84
  return { ok: true, path: real };
79
85
  }
80
86