@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1

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 (349) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4685 -4614
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +374 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  25. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  26. package/dist/types/live/attestation.d.ts +2 -0
  27. package/dist/types/live/controller.d.ts +48 -0
  28. package/dist/types/live/protocol.d.ts +85 -0
  29. package/dist/types/live/protocol.test.d.ts +1 -0
  30. package/dist/types/live/transport.d.ts +35 -0
  31. package/dist/types/live/visualizer.d.ts +30 -0
  32. package/dist/types/lsp/client.d.ts +16 -1
  33. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  34. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  35. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  36. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  37. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  38. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  39. package/dist/types/modes/components/custom-message.d.ts +1 -1
  40. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  41. package/dist/types/modes/components/message-frame.d.ts +8 -4
  42. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  43. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  44. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  45. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  46. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  48. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  49. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +4 -1
  54. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  55. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  56. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  57. package/dist/types/registry/agent-registry.d.ts +11 -4
  58. package/dist/types/sdk.d.ts +15 -1
  59. package/dist/types/secrets/index.d.ts +15 -2
  60. package/dist/types/secrets/obfuscator.d.ts +113 -15
  61. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  62. package/dist/types/session/agent-session-events.d.ts +84 -0
  63. package/dist/types/session/agent-session-types.d.ts +321 -0
  64. package/dist/types/session/agent-session.d.ts +135 -753
  65. package/dist/types/session/async-job-delivery.d.ts +28 -0
  66. package/dist/types/session/auth-storage.d.ts +1 -1
  67. package/dist/types/session/bash-runner.d.ts +75 -0
  68. package/dist/types/session/blob-store.d.ts +11 -1
  69. package/dist/types/session/blob-store.test.d.ts +1 -0
  70. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  71. package/dist/types/session/eval-runner.d.ts +51 -0
  72. package/dist/types/session/irc-bridge.d.ts +46 -0
  73. package/dist/types/session/messages.d.ts +24 -0
  74. package/dist/types/session/model-controls.d.ts +180 -0
  75. package/dist/types/session/prewalk.d.ts +46 -0
  76. package/dist/types/session/queued-messages.d.ts +22 -0
  77. package/dist/types/session/redis-session-storage.d.ts +1 -0
  78. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  79. package/dist/types/session/role-models.d.ts +15 -0
  80. package/dist/types/session/session-advisors.d.ts +228 -0
  81. package/dist/types/session/session-entries.d.ts +8 -0
  82. package/dist/types/session/session-handoff.d.ts +70 -0
  83. package/dist/types/session/session-history-format.d.ts +6 -1
  84. package/dist/types/session/session-maintenance.d.ts +246 -0
  85. package/dist/types/session/session-manager.d.ts +41 -0
  86. package/dist/types/session/session-memory.d.ts +56 -0
  87. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  88. package/dist/types/session/session-stats.d.ts +48 -0
  89. package/dist/types/session/session-tools.d.ts +158 -0
  90. package/dist/types/session/session-workspace.d.ts +32 -0
  91. package/dist/types/session/stream-guards.d.ts +44 -0
  92. package/dist/types/session/streaming-output.d.ts +8 -0
  93. package/dist/types/session/todo-tracker.d.ts +56 -0
  94. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  95. package/dist/types/session/turn-recovery.d.ts +225 -0
  96. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  97. package/dist/types/stt/index.d.ts +0 -2
  98. package/dist/types/stt/stt-controller.d.ts +7 -0
  99. package/dist/types/system-prompt.d.ts +2 -0
  100. package/dist/types/task/executor.d.ts +4 -1
  101. package/dist/types/task/output-manager.d.ts +3 -1
  102. package/dist/types/tiny/title-client.d.ts +10 -0
  103. package/dist/types/tools/approval.d.ts +1 -0
  104. package/dist/types/tools/browser/launch.d.ts +5 -0
  105. package/dist/types/tools/builtin-names.d.ts +1 -1
  106. package/dist/types/tools/computer/protocol.d.ts +43 -0
  107. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  108. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  109. package/dist/types/tools/computer/worker.d.ts +15 -0
  110. package/dist/types/tools/computer-renderer.d.ts +22 -0
  111. package/dist/types/tools/computer.d.ts +71 -0
  112. package/dist/types/tools/context.d.ts +2 -0
  113. package/dist/types/tools/default-renderer.d.ts +21 -0
  114. package/dist/types/tools/image-gen.d.ts +4 -4
  115. package/dist/types/tools/image-providers.d.ts +38 -0
  116. package/dist/types/tools/index.d.ts +7 -0
  117. package/dist/types/tools/path-utils.d.ts +8 -0
  118. package/dist/types/tools/todo.d.ts +10 -4
  119. package/dist/types/tools/xdev.d.ts +5 -1
  120. package/dist/types/tts/streaming-player.d.ts +10 -29
  121. package/dist/types/tts/vocalizer.d.ts +5 -0
  122. package/dist/types/utils/jj.d.ts +29 -0
  123. package/dist/types/utils/lang-from-path.d.ts +1 -0
  124. package/dist/types/utils/title-generator.d.ts +30 -0
  125. package/dist/types/utils/tools-manager.d.ts +1 -2
  126. package/dist/types/vibe/runtime.d.ts +36 -8
  127. package/dist/types/web/search/index.d.ts +1 -1
  128. package/dist/types/web/search/provider.d.ts +15 -5
  129. package/dist/types/web/search/types.d.ts +94 -0
  130. package/package.json +12 -12
  131. package/scripts/generate-share-viewer.ts +4 -6
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +3 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/setup-cli.ts +2 -14
  141. package/src/cli/usage-cli.ts +10 -1
  142. package/src/cli.ts +8 -0
  143. package/src/collab/host.ts +3 -2
  144. package/src/commands/bench.ts +18 -4
  145. package/src/commands/launch.ts +4 -0
  146. package/src/config/model-registry.ts +16 -2
  147. package/src/config/models-config-schema.ts +15 -4
  148. package/src/config/provider-globals.ts +9 -9
  149. package/src/config/service-tier.ts +26 -1
  150. package/src/config/settings-schema.ts +270 -51
  151. package/src/config/settings.ts +81 -2
  152. package/src/dap/client.ts +37 -4
  153. package/src/edit/index.ts +49 -12
  154. package/src/eval/executor-base.ts +1 -0
  155. package/src/eval/js/executor.ts +2 -0
  156. package/src/eval/py/executor.ts +103 -33
  157. package/src/exec/bash-executor.ts +90 -6
  158. package/src/exec/direnv.ts +145 -0
  159. package/src/export/html/args.ts +20 -0
  160. package/src/export/html/index.ts +45 -47
  161. package/src/export/html/template.css +19 -1
  162. package/src/export/html/template.html +6 -0
  163. package/src/export/html/template.js +21 -0
  164. package/src/export/html/tool-views.generated.js +31 -31
  165. package/src/export/html/web-palette.ts +116 -132
  166. package/src/export/share.ts +249 -49
  167. package/src/extensibility/extensions/loader.ts +29 -1
  168. package/src/extensibility/extensions/runner.ts +6 -0
  169. package/src/extensibility/extensions/types.ts +33 -2
  170. package/src/extensibility/extensions/wrapper.ts +68 -12
  171. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  172. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  173. package/src/hindsight/state.ts +64 -6
  174. package/src/internal-urls/local-protocol.ts +2 -1
  175. package/src/internal-urls/memory-protocol.ts +2 -2
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/attestation.ts +91 -0
  180. package/src/live/controller.ts +517 -0
  181. package/src/live/prompts/agent-final-message.md +3 -0
  182. package/src/live/prompts/live-instructions.md +23 -0
  183. package/src/live/protocol.test.ts +140 -0
  184. package/src/live/protocol.ts +233 -0
  185. package/src/live/transport.ts +422 -0
  186. package/src/live/visualizer.ts +214 -0
  187. package/src/lsp/client.ts +29 -9
  188. package/src/lsp/index.ts +19 -4
  189. package/src/main.ts +8 -1
  190. package/src/mcp/config-writer.test.ts +43 -0
  191. package/src/mcp/config-writer.ts +109 -82
  192. package/src/mcp/oauth-discovery.ts +10 -2
  193. package/src/mcp/smithery-auth.test.ts +29 -0
  194. package/src/mcp/smithery-auth.ts +3 -2
  195. package/src/memories/index.ts +40 -20
  196. package/src/memory-backend/tool-names.ts +2 -0
  197. package/src/mnemopi/backend.ts +24 -7
  198. package/src/modes/acp/acp-agent.ts +56 -10
  199. package/src/modes/acp/acp-event-mapper.ts +8 -1
  200. package/src/modes/components/agent-hub.ts +1 -1
  201. package/src/modes/components/assistant-message.ts +7 -2
  202. package/src/modes/components/custom-message.ts +4 -1
  203. package/src/modes/components/login-dialog.ts +14 -4
  204. package/src/modes/components/message-frame.ts +14 -8
  205. package/src/modes/components/plan-review-overlay.ts +350 -35
  206. package/src/modes/components/session-account-selector.ts +62 -0
  207. package/src/modes/components/settings-defs.ts +23 -1
  208. package/src/modes/components/settings-selector.ts +169 -1
  209. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  210. package/src/modes/components/status-line/component.ts +123 -2
  211. package/src/modes/components/tool-execution.ts +28 -112
  212. package/src/modes/controllers/command-controller.ts +56 -15
  213. package/src/modes/controllers/event-controller.ts +111 -16
  214. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  215. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  216. package/src/modes/controllers/input-controller.ts +47 -39
  217. package/src/modes/controllers/live-command-controller.ts +255 -0
  218. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  219. package/src/modes/controllers/omfg-controller.ts +44 -40
  220. package/src/modes/controllers/selector-controller.ts +80 -10
  221. package/src/modes/controllers/session-focus-controller.ts +6 -1
  222. package/src/modes/interactive-mode.ts +176 -38
  223. package/src/modes/runtime-init.ts +2 -0
  224. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  225. package/src/modes/theme/theme.ts +1 -1
  226. package/src/modes/types.ts +4 -1
  227. package/src/modes/utils/context-usage.ts +23 -7
  228. package/src/modes/utils/ui-helpers.ts +10 -5
  229. package/src/plan-mode/plan-files.ts +40 -0
  230. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  231. package/src/prompts/bench/cache-prefix.md +3 -0
  232. package/src/prompts/bench/cache-suffix.md +1 -0
  233. package/src/prompts/system/computer-safety.md +14 -0
  234. package/src/prompts/system/custom-system-prompt.md +1 -1
  235. package/src/prompts/system/project-prompt.md +8 -1
  236. package/src/prompts/system/subagent-async-pending.md +6 -0
  237. package/src/prompts/system/system-prompt.md +1 -1
  238. package/src/prompts/system/workflow-notice.md +48 -40
  239. package/src/prompts/system/xdev-mount-notice.md +4 -0
  240. package/src/prompts/tools/computer.md +26 -0
  241. package/src/prompts/tools/todo.md +3 -1
  242. package/src/registry/agent-lifecycle.ts +116 -46
  243. package/src/registry/agent-registry.ts +35 -8
  244. package/src/registry/persisted-agents.ts +26 -2
  245. package/src/sdk.ts +215 -136
  246. package/src/secrets/index.ts +127 -4
  247. package/src/secrets/obfuscator.ts +2328 -105
  248. package/src/session/acp-permission-gate.ts +165 -0
  249. package/src/session/agent-session-error-log.test.ts +1 -1
  250. package/src/session/agent-session-events.ts +66 -0
  251. package/src/session/agent-session-types.ts +343 -0
  252. package/src/session/agent-session.ts +1940 -12218
  253. package/src/session/artifacts.ts +6 -5
  254. package/src/session/async-job-delivery.ts +74 -0
  255. package/src/session/auth-storage.ts +1 -0
  256. package/src/session/bash-runner.ts +326 -0
  257. package/src/session/blob-store.test.ts +56 -0
  258. package/src/session/blob-store.ts +18 -2
  259. package/src/session/checkpoint-entries.ts +81 -0
  260. package/src/session/eval-runner.ts +217 -0
  261. package/src/session/indexed-session-storage.ts +9 -2
  262. package/src/session/irc-bridge.ts +203 -0
  263. package/src/session/messages.ts +266 -1
  264. package/src/session/model-controls.ts +714 -0
  265. package/src/session/prewalk.ts +252 -0
  266. package/src/session/queued-messages.ts +93 -0
  267. package/src/session/redis-session-storage.ts +51 -11
  268. package/src/session/retry-fallback-chains.ts +455 -0
  269. package/src/session/role-models.ts +85 -0
  270. package/src/session/session-advisors.ts +1679 -0
  271. package/src/session/session-context.ts +6 -3
  272. package/src/session/session-entries.ts +8 -0
  273. package/src/session/session-handoff.ts +308 -0
  274. package/src/session/session-history-format.ts +20 -9
  275. package/src/session/session-loader.ts +0 -46
  276. package/src/session/session-maintenance.ts +2983 -0
  277. package/src/session/session-manager.ts +390 -30
  278. package/src/session/session-memory.ts +222 -0
  279. package/src/session/session-provider-boundary.ts +307 -0
  280. package/src/session/session-stats.ts +293 -0
  281. package/src/session/session-tools.ts +980 -0
  282. package/src/session/session-workspace.ts +53 -0
  283. package/src/session/stream-guards.ts +417 -0
  284. package/src/session/streaming-output.ts +52 -5
  285. package/src/session/todo-tracker.ts +380 -0
  286. package/src/session/ttsr-coordinator.ts +496 -0
  287. package/src/session/turn-recovery.ts +1629 -0
  288. package/src/slash-commands/builtin-registry.ts +257 -20
  289. package/src/slash-commands/helpers/session-pin.ts +44 -0
  290. package/src/slash-commands/helpers/usage-report.ts +22 -3
  291. package/src/stt/downloader.ts +0 -2
  292. package/src/stt/index.ts +0 -2
  293. package/src/stt/stt-controller.ts +57 -146
  294. package/src/system-prompt.ts +45 -17
  295. package/src/task/executor.ts +285 -70
  296. package/src/task/index.ts +98 -105
  297. package/src/task/output-manager.ts +25 -3
  298. package/src/task/persisted-revive.ts +4 -3
  299. package/src/task/structured-subagent.ts +1 -0
  300. package/src/tiny/title-client.ts +22 -0
  301. package/src/tools/approval.ts +55 -10
  302. package/src/tools/bash-interactive.ts +116 -86
  303. package/src/tools/bash-skill-urls.ts +28 -2
  304. package/src/tools/bash.ts +339 -123
  305. package/src/tools/browser/launch.ts +9 -1
  306. package/src/tools/browser/tab-supervisor.ts +8 -5
  307. package/src/tools/browser.ts +94 -47
  308. package/src/tools/builtin-names.ts +1 -0
  309. package/src/tools/computer/protocol.ts +28 -0
  310. package/src/tools/computer/supervisor.ts +258 -0
  311. package/src/tools/computer/worker-entry.ts +25 -0
  312. package/src/tools/computer/worker.ts +135 -0
  313. package/src/tools/computer-renderer.ts +108 -0
  314. package/src/tools/computer.ts +433 -0
  315. package/src/tools/context.ts +2 -0
  316. package/src/tools/default-renderer.ts +139 -0
  317. package/src/tools/essential-tools.ts +1 -0
  318. package/src/tools/image-gen.ts +20 -28
  319. package/src/tools/image-providers.ts +50 -0
  320. package/src/tools/index.ts +14 -0
  321. package/src/tools/path-utils.ts +49 -2
  322. package/src/tools/read.ts +156 -88
  323. package/src/tools/renderers.ts +9 -1
  324. package/src/tools/todo.ts +101 -11
  325. package/src/tools/vibe.ts +1 -2
  326. package/src/tools/write.ts +51 -1
  327. package/src/tools/xdev.ts +121 -29
  328. package/src/tts/streaming-player.ts +82 -283
  329. package/src/tts/vocalizer.ts +18 -1
  330. package/src/utils/clipboard.ts +1 -30
  331. package/src/utils/jj.ts +125 -4
  332. package/src/utils/lang-from-path.ts +7 -0
  333. package/src/utils/mac-file-urls.applescript +37 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/utils/tool-choice.ts +14 -0
  336. package/src/utils/tools-manager.ts +1 -19
  337. package/src/vibe/runtime.ts +852 -72
  338. package/src/web/search/index.ts +3 -6
  339. package/src/web/search/provider.ts +31 -16
  340. package/src/web/search/providers/xai.ts +7 -1
  341. package/src/web/search/types.ts +3 -0
  342. package/dist/types/stt/recorder.d.ts +0 -30
  343. package/dist/types/stt/transcriber.d.ts +0 -14
  344. package/dist/types/stt/wav.d.ts +0 -29
  345. package/dist/types/tts/player.d.ts +0 -32
  346. package/src/stt/recorder.ts +0 -551
  347. package/src/stt/transcriber.ts +0 -60
  348. package/src/stt/wav.ts +0 -173
  349. package/src/tts/player.ts +0 -137
@@ -0,0 +1,53 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+
4
+ /**
5
+ * Filesystem workspace of a session: one current/default directory plus a
6
+ * non-empty ordered list of workspace directories.
7
+ *
8
+ * `cwd` remains the default directory for relative-path resolution and
9
+ * backward compatibility. `directories` always contains `cwd` first, followed
10
+ * by any additional directories in their supplied order (deduplicated).
11
+ * Directory order is stable but carries no semantic hierarchy.
12
+ *
13
+ * Workspace directories come from the platform (ACP/editor), CLI, or config —
14
+ * never from filesystem walk-up discovery.
15
+ */
16
+ export interface SessionWorkspace {
17
+ /** Current/default directory for compatibility and relative path resolution. */
18
+ cwd: string;
19
+ /** Non-empty ordered list of absolute normalized directories; `cwd` is always first. */
20
+ directories: string[];
21
+ }
22
+
23
+ /** Expand a leading `~`/`~/` and resolve to an absolute path (relative input resolves against `base`). */
24
+ export function normalizeWorkspaceDirectory(directory: string, base?: string): string {
25
+ let expanded = directory;
26
+ if (expanded === "~") {
27
+ expanded = os.homedir();
28
+ } else if (expanded.startsWith("~/") || expanded.startsWith(`~${path.sep}`)) {
29
+ expanded = path.join(os.homedir(), expanded.slice(2));
30
+ }
31
+ return base ? path.resolve(base, expanded) : path.resolve(expanded);
32
+ }
33
+
34
+ /**
35
+ * Build a normalized {@link SessionWorkspace} from a cwd and optional
36
+ * additional directories. Additional entries are normalized (relative entries
37
+ * resolve against the normalized cwd), deduplicated, and appended after `cwd`
38
+ * preserving their supplied order.
39
+ */
40
+ export function normalizeSessionWorkspace(args: { cwd: string; directories?: string[] }): SessionWorkspace {
41
+ const cwd = normalizeWorkspaceDirectory(args.cwd);
42
+ const directories = [cwd];
43
+ for (const directory of args.directories ?? []) {
44
+ const normalized = normalizeWorkspaceDirectory(directory, cwd);
45
+ if (!directories.includes(normalized)) directories.push(normalized);
46
+ }
47
+ return { cwd, directories };
48
+ }
49
+
50
+ /** The workspace directories beyond `cwd`, in order (ACP `additionalDirectories` shape). */
51
+ export function additionalWorkspaceDirectories(workspace: SessionWorkspace): string[] {
52
+ return workspace.directories.filter(directory => directory !== workspace.cwd);
53
+ }
@@ -0,0 +1,417 @@
1
+ import * as fs from "node:fs";
2
+ import type { Agent, AgentEvent, AgentMessage, AgentTurnEndContext } from "@oh-my-pi/pi-agent-core";
3
+ import type { AssistantMessage, AssistantMessageEvent, Model, ToolCall } from "@oh-my-pi/pi-ai";
4
+ import { GeminiHeaderRunDetector, isGeminiThinkingModel } from "@oh-my-pi/pi-ai/utils/thinking-loop";
5
+ import { type RepeatedToolCallDetection, ToolCallLoopGuard } from "@oh-my-pi/pi-ai/utils/tool-call-loop-guard";
6
+ import { isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
7
+ import type { Settings } from "../config/settings";
8
+ import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
9
+ import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
10
+ import geminiToolReminderTemplate from "../prompts/system/gemini-tool-call-reminder.md" with { type: "text" };
11
+ import toolCallLoopRedirectTemplate from "../prompts/system/tool-call-loop-redirect.md" with { type: "text" };
12
+ import type { SecretObfuscator } from "../secrets/obfuscator";
13
+ import { assertEditableFile } from "../tools/auto-generated-guard";
14
+ import { isInternalUrlPath, normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
15
+ import { ToolError } from "../tools/tool-errors";
16
+ import type { CustomMessage } from "./messages";
17
+ import type { SessionManager } from "./session-manager";
18
+
19
+ const GEMINI_HEADER_INTERRUPT_REASON = "Interrupted: emit a tool call instead of more planning";
20
+ const GEMINI_TOOL_REMINDER_TYPE = "gemini-tool-call-reminder";
21
+ const TOOL_CALL_LOOP_REDIRECT_TYPE = "tool-call-loop-redirect";
22
+
23
+ /** Capabilities borrowed by the session's streaming and loop guards. */
24
+ export interface StreamGuardsHost {
25
+ agent: Agent;
26
+ settings: Settings;
27
+ sessionManager: SessionManager;
28
+ obfuscator: SecretObfuscator | undefined;
29
+ model(): Model | undefined;
30
+ isDisposed(): boolean;
31
+ promptGeneration(): number;
32
+ localProtocolOptions(): LocalProtocolOptions;
33
+ emitNotice(level: "info" | "warning" | "error", message: string, source?: string): void;
34
+ schedulePostPromptTask(task: (signal: AbortSignal) => Promise<void>): void;
35
+ discardAssistantTurn(message: AssistantMessage): void;
36
+ }
37
+
38
+ /** Guards streamed edit calls against generated files and invalid patch previews. */
39
+ export class StreamingEditGuard {
40
+ readonly #host: StreamGuardsHost;
41
+ #abortTriggered = false;
42
+ #checkedLineCounts = new Map<string, number>();
43
+ #precheckedToolCallIds = new Set<string>();
44
+ #fileCache = new Map<string, string>();
45
+ #lastToolCallId: string | undefined;
46
+
47
+ constructor(host: StreamGuardsHost) {
48
+ this.#host = host;
49
+ }
50
+
51
+ /** Whether the current turn was aborted by streaming edit validation. */
52
+ get abortTriggered(): boolean {
53
+ return this.#abortTriggered;
54
+ }
55
+
56
+ /** Clears all turn-scoped streaming edit state. */
57
+ reset(): void {
58
+ this.#abortTriggered = false;
59
+ this.#checkedLineCounts.clear();
60
+ this.#precheckedToolCallIds.clear();
61
+ this.#fileCache.clear();
62
+ }
63
+
64
+ /** Pre-caches and validates a streamed edit as its arguments arrive. */
65
+ preCache(event: AgentEvent): void {
66
+ if (this.#abortTriggered || event.type !== "message_update") return;
67
+ const assistantEvent = event.assistantMessageEvent;
68
+ if (
69
+ assistantEvent.type !== "toolcall_start" &&
70
+ assistantEvent.type !== "toolcall_delta" &&
71
+ assistantEvent.type !== "toolcall_end"
72
+ ) {
73
+ return;
74
+ }
75
+ const streamingEdit = this.#getToolCall(event);
76
+ if (!streamingEdit) return;
77
+
78
+ // The auto-generated guard runs unconditionally: editing a generated file
79
+ // is never the user's intent, and the cost of a false-positive abort is one
80
+ // wasted turn vs. silently corrupting a regenerated source.
81
+ const shouldCheckAutoGenerated =
82
+ !streamingEdit.toolCall.id || !this.#precheckedToolCallIds.has(streamingEdit.toolCall.id);
83
+ if (shouldCheckAutoGenerated) {
84
+ if (streamingEdit.toolCall.id) this.#precheckedToolCallIds.add(streamingEdit.toolCall.id);
85
+ this.#abortForAutoGeneratedPath(streamingEdit.toolCall, streamingEdit.path, streamingEdit.resolvedPath);
86
+ }
87
+
88
+ // File-cache priming feeds maybeAbort's removed-lines check, which is the
89
+ // optional patch-preview verification gated by edit.streamingAbort.
90
+ if (this.#host.settings.get("edit.streamingAbort")) this.#ensureFileCache(streamingEdit.resolvedPath);
91
+ }
92
+
93
+ /** Invalidates cached source text after an edit tool result lands. */
94
+ invalidate(filePath: string): void {
95
+ const resolvedPath = this.#resolveSessionFsPath(filePath);
96
+ if (resolvedPath !== undefined) this.#fileCache.delete(resolvedPath);
97
+ }
98
+
99
+ /** Aborts a streamed edit whose completed patch preview cannot apply. */
100
+ maybeAbort(event: AgentEvent): void {
101
+ if (!this.#host.settings.get("edit.streamingAbort") || this.#abortTriggered || event.type !== "message_update") {
102
+ return;
103
+ }
104
+ const assistantEvent = event.assistantMessageEvent;
105
+ if (assistantEvent.type !== "toolcall_end" && assistantEvent.type !== "toolcall_delta") return;
106
+ const streamingEdit = this.#getToolCall(event);
107
+ if (!streamingEdit?.toolCall.id) return;
108
+
109
+ const { toolCall, path, resolvedPath, diff, op, rename } = streamingEdit;
110
+ if (!diff || (op && op !== "update") || !diff.includes("\n")) return;
111
+ const lastNewlineIndex = diff.lastIndexOf("\n");
112
+ if (lastNewlineIndex < 0) return;
113
+ const diffForCheck = diff.endsWith("\n") ? diff : diff.slice(0, lastNewlineIndex + 1);
114
+ if (diffForCheck.trim().length === 0) return;
115
+
116
+ let normalizedDiff = normalizeDiff(diffForCheck.replace(/\r/g, ""));
117
+ if (!normalizedDiff) return;
118
+ if (this.#host.obfuscator) normalizedDiff = this.#host.obfuscator.deobfuscate(normalizedDiff);
119
+ if (!normalizedDiff) return;
120
+ const lines = normalizedDiff.split("\n");
121
+ if (!lines.some(line => line.startsWith("+") || line.startsWith("-"))) return;
122
+
123
+ const lineCount = lines.length;
124
+ const lastChecked = this.#checkedLineCounts.get(toolCall.id);
125
+ if (lastChecked !== undefined && lineCount <= lastChecked) return;
126
+ this.#checkedLineCounts.set(toolCall.id, lineCount);
127
+
128
+ const removedLines = lines
129
+ .filter(line => line.startsWith("-") && !line.startsWith("--- "))
130
+ .map(line => line.slice(1));
131
+ if (removedLines.length > 0) {
132
+ let cachedContent = this.#fileCache.get(resolvedPath);
133
+ if (cachedContent === undefined) {
134
+ this.#ensureFileCache(resolvedPath);
135
+ cachedContent = this.#fileCache.get(resolvedPath);
136
+ }
137
+ if (cachedContent !== undefined) {
138
+ const missing = removedLines.find(line => !cachedContent.includes(normalizeToLF(line)));
139
+ if (missing) this.#abortPatch(toolCall.id, path, `Failed to find expected lines in ${path}:\n${missing}`);
140
+ return;
141
+ }
142
+ if (assistantEvent.type === "toolcall_delta") return;
143
+ void this.#checkRemovedLines(toolCall.id, path, resolvedPath, removedLines);
144
+ return;
145
+ }
146
+ if (assistantEvent.type === "toolcall_delta") return;
147
+ void this.#checkPreviewPatch(toolCall.id, path, rename, normalizedDiff);
148
+ }
149
+
150
+ #getToolCall(event: AgentEvent):
151
+ | {
152
+ toolCall: ToolCall;
153
+ path: string;
154
+ resolvedPath: string;
155
+ diff?: string;
156
+ op?: string;
157
+ rename?: string;
158
+ }
159
+ | undefined {
160
+ if (event.type !== "message_update" || event.message.role !== "assistant") return undefined;
161
+ const contentIndex = event.assistantMessageEvent.contentIndex ?? 0;
162
+ const messageContent = event.message.content;
163
+ if (!Array.isArray(messageContent) || contentIndex < 0 || contentIndex >= messageContent.length) return undefined;
164
+ const toolCall = messageContent[contentIndex] as ToolCall;
165
+ if (toolCall.name !== "edit") return undefined;
166
+ const args = toolCall.arguments;
167
+ if (!args || typeof args !== "object" || Array.isArray(args) || "old_text" in args || "new_text" in args) {
168
+ return undefined;
169
+ }
170
+ const filePath = typeof args.path === "string" ? args.path : undefined;
171
+ if (!filePath) return undefined;
172
+ // local:// URLs resolve to artifacts; other internal URLs have no local path.
173
+ const resolvedPath = this.#resolveSessionFsPath(filePath);
174
+ if (resolvedPath === undefined) return undefined;
175
+ return {
176
+ toolCall,
177
+ path: filePath,
178
+ resolvedPath,
179
+ diff: typeof args.diff === "string" ? args.diff : undefined,
180
+ op: typeof args.op === "string" ? args.op : undefined,
181
+ rename: typeof args.rename === "string" ? args.rename : undefined,
182
+ };
183
+ }
184
+
185
+ #abortForAutoGeneratedPath(toolCall: ToolCall, filePath: string, resolvedPath: string): void {
186
+ if (this.#lastToolCallId === toolCall.id) return;
187
+ this.#lastToolCallId = toolCall.id;
188
+ void assertEditableFile(resolvedPath, filePath).catch(error => {
189
+ if (!(error instanceof ToolError) || this.#lastToolCallId !== toolCall.id) return;
190
+ if (!this.#abortTriggered) {
191
+ this.#abortTriggered = true;
192
+ logger.warn("Streaming edit aborted due to auto-generated file guard", {
193
+ toolCallId: toolCall.id,
194
+ path: filePath,
195
+ });
196
+ this.#host.agent.abort();
197
+ }
198
+ });
199
+ }
200
+
201
+ #ensureFileCache(resolvedPath: string): void {
202
+ if (this.#fileCache.has(resolvedPath)) return;
203
+ try {
204
+ const rawText = fs.readFileSync(resolvedPath, "utf-8");
205
+ const { text } = stripBom(rawText);
206
+ this.#fileCache.set(resolvedPath, normalizeToLF(text));
207
+ } catch {
208
+ // Read errors are handled by the edit tool itself.
209
+ }
210
+ }
211
+
212
+ #resolveSessionFsPath(filePath: string): string | undefined {
213
+ const normalized = normalizeLocalScheme(filePath);
214
+ if (normalized.startsWith("local:")) {
215
+ return resolveLocalUrlToPath(normalized, this.#host.localProtocolOptions());
216
+ }
217
+ if (isInternalUrlPath(normalized)) return undefined;
218
+ return resolveToCwd(normalized, this.#host.sessionManager.getCwd());
219
+ }
220
+
221
+ async #checkRemovedLines(
222
+ toolCallId: string,
223
+ filePath: string,
224
+ resolvedPath: string,
225
+ removedLines: string[],
226
+ ): Promise<void> {
227
+ if (this.#abortTriggered) return;
228
+ try {
229
+ const { text } = stripBom(await Bun.file(resolvedPath).text());
230
+ const normalizedContent = normalizeToLF(text);
231
+ const missing = removedLines.find(line => !normalizedContent.includes(normalizeToLF(line)));
232
+ if (missing)
233
+ this.#abortPatch(toolCallId, filePath, `Failed to find expected lines in ${filePath}:\n${missing}`);
234
+ } catch (error) {
235
+ if (!isEnoent(error)) {
236
+ // Unexpected fallback read errors remain non-fatal.
237
+ }
238
+ }
239
+ }
240
+
241
+ async #checkPreviewPatch(
242
+ toolCallId: string,
243
+ filePath: string,
244
+ rename: string | undefined,
245
+ normalizedDiff: string,
246
+ ): Promise<void> {
247
+ if (this.#abortTriggered) return;
248
+ try {
249
+ await previewPatch(
250
+ { path: filePath, op: "update", rename, diff: normalizedDiff },
251
+ {
252
+ cwd: this.#host.sessionManager.getCwd(),
253
+ allowFuzzy: this.#host.settings.get("edit.fuzzyMatch"),
254
+ fuzzyThreshold: this.#host.settings.get("edit.fuzzyThreshold"),
255
+ },
256
+ );
257
+ } catch (error) {
258
+ if (error instanceof ParseError) return;
259
+ this.#abortPatch(toolCallId, filePath, error instanceof Error ? error.message : String(error));
260
+ }
261
+ }
262
+
263
+ #abortPatch(toolCallId: string, filePath: string, error: string): void {
264
+ this.#abortTriggered = true;
265
+ logger.warn("Streaming edit aborted due to patch preview failure", { toolCallId, path: filePath, error });
266
+ this.#host.agent.abort();
267
+ }
268
+ }
269
+
270
+ /** Detects cross-turn tool loops and Gemini reasoning-header runaways. */
271
+ export class LoopGuards {
272
+ readonly #host: StreamGuardsHost;
273
+ #geminiHeaderDetector: GeminiHeaderRunDetector | undefined;
274
+ #toolCallLoopGuard: ToolCallLoopGuard | undefined;
275
+ #toolCallLoopGuardSettingsKey: string | undefined;
276
+
277
+ constructor(host: StreamGuardsHost) {
278
+ this.#host = host;
279
+ }
280
+
281
+ /** Records a completed turn and injects a redirect when calls repeat. */
282
+ recordTurn(messages: AgentMessage[], context: AgentTurnEndContext | undefined): void {
283
+ if (context?.message.role !== "assistant") return;
284
+ const detection = this.#activeToolCallLoopGuard()?.recordTurn({
285
+ message: context.message,
286
+ toolResults: context.toolResults,
287
+ });
288
+ if (detection) this.#injectToolCallLoopRedirect(messages, detection);
289
+ }
290
+
291
+ /** Feeds a streamed assistant event to the Gemini header-runaway detector. */
292
+ onAssistantEvent(message: AssistantMessage, event: AssistantMessageEvent): void {
293
+ if (event.type === "thinking_start") {
294
+ this.#geminiHeaderDetector = this.#geminiHeaderGuardActive() ? new GeminiHeaderRunDetector() : undefined;
295
+ return;
296
+ }
297
+ const detector = this.#geminiHeaderDetector;
298
+ if (!detector) return;
299
+ if (event.type === "thinking_delta") {
300
+ if (detector.push(event.delta)) this.#interruptGeminiHeaderRunaway(detector.count, message.timestamp);
301
+ return;
302
+ }
303
+ if (event.type === "text_start" || event.type === "toolcall_start") detector.reset();
304
+ }
305
+
306
+ #activeToolCallLoopGuard(): ToolCallLoopGuard | undefined {
307
+ if (this.#host.settings.get("model.toolCallLoopGuard.enabled") !== true) {
308
+ this.#toolCallLoopGuard = undefined;
309
+ this.#toolCallLoopGuardSettingsKey = undefined;
310
+ return undefined;
311
+ }
312
+ const threshold = this.#host.settings.get("model.toolCallLoopGuard.threshold");
313
+ const exemptTools = this.#host.settings
314
+ .get("model.toolCallLoopGuard.exemptTools")
315
+ .filter((tool): tool is string => typeof tool === "string" && tool.length > 0);
316
+ const settingsKey = `${threshold}:${JSON.stringify(exemptTools)}`;
317
+ if (!this.#toolCallLoopGuard || this.#toolCallLoopGuardSettingsKey !== settingsKey) {
318
+ this.#toolCallLoopGuard = new ToolCallLoopGuard({ threshold, exemptTools });
319
+ this.#toolCallLoopGuardSettingsKey = settingsKey;
320
+ }
321
+ return this.#toolCallLoopGuard;
322
+ }
323
+
324
+ #injectToolCallLoopRedirect(messages: AgentMessage[], detection: RepeatedToolCallDetection): void {
325
+ const content = prompt.render(toolCallLoopRedirectTemplate, {
326
+ tool_name: detection.toolName,
327
+ count: detection.count,
328
+ arguments_summary: detection.argumentsSummary,
329
+ result_summary: detection.resultSummary || "(no text result)",
330
+ });
331
+ const details = {
332
+ toolName: detection.toolName,
333
+ count: detection.count,
334
+ argumentsSummary: detection.argumentsSummary,
335
+ resultSummary: detection.resultSummary,
336
+ };
337
+ logger.warn("cross-turn tool-call loop detected", { toolName: detection.toolName, count: detection.count });
338
+ const redirectMessage: CustomMessage = {
339
+ role: "custom",
340
+ customType: TOOL_CALL_LOOP_REDIRECT_TYPE,
341
+ content,
342
+ display: false,
343
+ details,
344
+ attribution: "agent",
345
+ timestamp: Date.now(),
346
+ };
347
+ messages.push(redirectMessage);
348
+ if (this.#host.agent.state.messages !== messages) this.#host.agent.appendMessage(redirectMessage);
349
+ this.#host.sessionManager.appendCustomMessageEntry(
350
+ TOOL_CALL_LOOP_REDIRECT_TYPE,
351
+ content,
352
+ false,
353
+ details,
354
+ "agent",
355
+ );
356
+ }
357
+
358
+ #geminiHeaderGuardActive(): boolean {
359
+ const model = this.#host.model();
360
+ return (
361
+ process.env.PI_NO_THINKING_LOOP_GUARD !== "1" &&
362
+ this.#host.settings.get("model.loopGuard.enabled") === true &&
363
+ this.#host.settings.get("model.loopGuard.toolCallReminder") === true &&
364
+ model !== undefined &&
365
+ isGeminiThinkingModel(model)
366
+ );
367
+ }
368
+
369
+ #interruptGeminiHeaderRunaway(headerCount: number, targetTimestamp: number): void {
370
+ const model = this.#host.model();
371
+ logger.warn("Gemini reasoning-header runaway; interrupting to require a tool call", {
372
+ model: model?.id,
373
+ provider: model?.provider,
374
+ headers: headerCount,
375
+ });
376
+ this.#host.emitNotice(
377
+ "warning",
378
+ `Interrupted ${headerCount} planning headers with no tool call; reminded the model to issue one.`,
379
+ "loop-guard",
380
+ );
381
+ this.#host.agent.abort(GEMINI_HEADER_INTERRUPT_REASON);
382
+ const generation = this.#host.promptGeneration();
383
+ this.#host.schedulePostPromptTask(async signal => {
384
+ if (signal.aborted || this.#host.isDisposed() || this.#host.promptGeneration() !== generation) return;
385
+ await this.#host.agent.waitForIdle();
386
+ if (signal.aborted || this.#host.isDisposed() || this.#host.promptGeneration() !== generation) return;
387
+ const aborted = this.#host.agent.state.messages.findLast(
388
+ (message): message is AssistantMessage =>
389
+ message.role === "assistant" && message.timestamp === targetTimestamp,
390
+ );
391
+ if (aborted) this.#host.discardAssistantTurn(aborted);
392
+ const content = prompt.render(geminiToolReminderTemplate, { count: headerCount });
393
+ const details = { headers: headerCount };
394
+ this.#host.agent.appendMessage({
395
+ role: "custom",
396
+ customType: GEMINI_TOOL_REMINDER_TYPE,
397
+ content,
398
+ display: false,
399
+ details,
400
+ attribution: "agent",
401
+ timestamp: Date.now(),
402
+ });
403
+ this.#host.sessionManager.appendCustomMessageEntry(
404
+ GEMINI_TOOL_REMINDER_TYPE,
405
+ content,
406
+ false,
407
+ details,
408
+ "agent",
409
+ );
410
+ try {
411
+ await this.#host.agent.continue();
412
+ } catch (error) {
413
+ logger.warn("gemini tool-call reminder continue failed", { error: String(error) });
414
+ }
415
+ });
416
+ }
417
+ }
@@ -756,6 +756,10 @@ export class OutputSink {
756
756
  // Queue of chunks waiting for the file sink to be created.
757
757
  #pendingFileWrites?: string[];
758
758
  #fileReady = false;
759
+ /** In-flight sink creation, awaited by finalize/dispose so a fd opened by a late chunk is still released. */
760
+ #fileCreation?: Promise<void>;
761
+ /** Set once the spill file has been closed; guards double-close and post-finalize resurrection. */
762
+ #finalized = false;
759
763
 
760
764
  readonly #artifactPath?: string;
761
765
  readonly #artifactId?: string;
@@ -842,6 +846,7 @@ export class OutputSink {
842
846
  * synchronously. File sink writes are deferred and serialized internally.
843
847
  */
844
848
  push(chunk: string): void {
849
+ if (this.#finalized) return;
845
850
  chunk = sanitizeWithOptionalSixelPassthrough(chunk, text => sanitizeText(this.#normalizeCarriageReturns(text)));
846
851
 
847
852
  // Throttled onChunk: coalesce chunks arriving inside the throttle window.
@@ -1018,7 +1023,7 @@ export class OutputSink {
1018
1023
  // resolves (typically <2). The cap is enforced on drain.
1019
1024
  if (!this.#pendingFileWrites) {
1020
1025
  this.#pendingFileWrites = [chunk];
1021
- void this.#createFileSink();
1026
+ this.#fileCreation = this.#createFileSink();
1022
1027
  } else {
1023
1028
  this.#pendingFileWrites.push(chunk);
1024
1029
  }
@@ -1255,10 +1260,7 @@ export class OutputSink {
1255
1260
  this.#flushPendingChunk();
1256
1261
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1257
1262
 
1258
- if (this.#file) {
1259
- this.#flushArtifactTailIfCapped();
1260
- await this.#file.sink.end();
1261
- }
1263
+ await this.#finalizeFile();
1262
1264
 
1263
1265
  // Compose the visible output. With head retention, splice head + marker
1264
1266
  // + tail when content was elided. Otherwise return the rolling buffer.
@@ -1321,6 +1323,51 @@ export class OutputSink {
1321
1323
  artifactId: this.#file?.artifactId,
1322
1324
  };
1323
1325
  }
1326
+
1327
+ /**
1328
+ * Flush any capped artifact tail and close the spill file descriptor,
1329
+ * awaiting an in-flight sink creation so a descriptor opened by a late
1330
+ * chunk is still released. Idempotent via {@link #finalized}: the artifact
1331
+ * is finalized exactly once whether the caller reached {@link dump} or
1332
+ * bailed through {@link dispose}. `#file` is left set so {@link dump} can
1333
+ * still read `artifactId` for its summary.
1334
+ */
1335
+ async #finalizeFile(): Promise<void> {
1336
+ if (this.#finalized) return;
1337
+ this.#finalized = true;
1338
+ if (this.#fileCreation) {
1339
+ await this.#fileCreation.catch(() => undefined);
1340
+ }
1341
+ const file = this.#file;
1342
+ if (!file) return;
1343
+ // The tail/notice replay writes to the sink and can throw (e.g. a disk
1344
+ // write error). Closing the descriptor MUST still happen — otherwise the
1345
+ // fd leaks and the replay error masks the original tool error that put us
1346
+ // on this path. Both failures are swallowed so dispose() never throws.
1347
+ try {
1348
+ this.#flushArtifactTailIfCapped();
1349
+ } catch {
1350
+ /* ignore */
1351
+ } finally {
1352
+ try {
1353
+ await file.sink.end();
1354
+ } catch {
1355
+ /* ignore */
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ /**
1361
+ * Release the artifact spill descriptor on an exit path that skips
1362
+ * {@link dump} — a thrown error or abort. Idempotent and safe in a
1363
+ * `finally`: if {@link dump} already ran this is a no-op, otherwise it
1364
+ * flushes the capped tail and closes the sink so the descriptor is not
1365
+ * leaked until a later unrelated read hits `EMFILE` (issue #6463).
1366
+ */
1367
+ async dispose(): Promise<void> {
1368
+ this.#clearPendingChunkTimer();
1369
+ await this.#finalizeFile();
1370
+ }
1324
1371
  }
1325
1372
 
1326
1373
  // =============================================================================