@oh-my-pi/pi-coding-agent 17.2.4 → 17.2.6

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 (348) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/{CHANGELOG-bpmhv26t.md → CHANGELOG-gs76k6wc.md} +69 -0
  3. package/dist/cli.js +4438 -3962
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/cli/args.d.ts +4 -1
  6. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  7. package/dist/types/cli/command-help.d.ts +99 -0
  8. package/dist/types/cli/gc-cli.d.ts +1 -0
  9. package/dist/types/cli/help-extra.d.ts +2 -0
  10. package/dist/types/cli/thinking-levels.d.ts +5 -0
  11. package/dist/types/cli/update-cli.d.ts +13 -0
  12. package/dist/types/commands/browser-relay.d.ts +38 -0
  13. package/dist/types/commands/commit.d.ts +3 -0
  14. package/dist/types/commands/complete.d.ts +1 -1
  15. package/dist/types/commands/completions.d.ts +6 -0
  16. package/dist/types/commands/grep.d.ts +3 -0
  17. package/dist/types/commands/join.d.ts +4 -0
  18. package/dist/types/commands/launch-help.d.ts +171 -0
  19. package/dist/types/commands/launch.d.ts +5 -1
  20. package/dist/types/commands/models.d.ts +3 -0
  21. package/dist/types/commands/say.d.ts +10 -0
  22. package/dist/types/commands/worktree.d.ts +3 -0
  23. package/dist/types/config/keybindings.d.ts +5 -0
  24. package/dist/types/config/model-discovery.d.ts +2 -2
  25. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  26. package/dist/types/config/models-config-schema.d.ts +2 -0
  27. package/dist/types/config/models-config.d.ts +1 -0
  28. package/dist/types/config/service-tier.d.ts +2 -0
  29. package/dist/types/config/settings-schema.d.ts +69 -21
  30. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  31. package/dist/types/cursor.d.ts +7 -4
  32. package/dist/types/edit/index.d.ts +3 -3
  33. package/dist/types/edit/modes/replace.d.ts +22 -15
  34. package/dist/types/edit/renderer.d.ts +1 -1
  35. package/dist/types/eval/py/kernel.d.ts +3 -1
  36. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  37. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  38. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  39. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  40. package/dist/types/launch/broker.d.ts +1 -1
  41. package/dist/types/launch/client.d.ts +15 -4
  42. package/dist/types/launch/ensure.d.ts +8 -0
  43. package/dist/types/launch/paths.d.ts +1 -1
  44. package/dist/types/launch/protocol.d.ts +19 -2
  45. package/dist/types/lsp/client.d.ts +2 -0
  46. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  47. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  48. package/dist/types/lsp/mux/server.d.ts +18 -0
  49. package/dist/types/lsp/types.d.ts +22 -2
  50. package/dist/types/main.d.ts +2 -0
  51. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  52. package/dist/types/mnemopi/state.d.ts +0 -23
  53. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  54. package/dist/types/modes/components/btw-panel.d.ts +3 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  56. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  57. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  58. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  59. package/dist/types/modes/controllers/btw-controller.d.ts +2 -0
  60. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  61. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  62. package/dist/types/modes/interactive-mode.d.ts +5 -1
  63. package/dist/types/modes/types.d.ts +4 -1
  64. package/dist/types/modes/workflow.d.ts +2 -1
  65. package/dist/types/sdk.d.ts +3 -1
  66. package/dist/types/secrets/index.d.ts +1 -1
  67. package/dist/types/security/contracts/schemas.d.ts +405 -403
  68. package/dist/types/session/agent-session-types.d.ts +8 -1
  69. package/dist/types/session/agent-session.d.ts +26 -2
  70. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  71. package/dist/types/session/launch-completion.d.ts +10 -0
  72. package/dist/types/session/session-advisors.d.ts +1 -1
  73. package/dist/types/session/session-context.d.ts +2 -1
  74. package/dist/types/session/session-entries.d.ts +15 -1
  75. package/dist/types/session/session-maintenance.d.ts +1 -0
  76. package/dist/types/session/session-manager.d.ts +23 -11
  77. package/dist/types/session/session-stats.d.ts +10 -0
  78. package/dist/types/session/session-storage.d.ts +15 -3
  79. package/dist/types/session/yield-queue.d.ts +6 -1
  80. package/dist/types/system-prompt.d.ts +4 -1
  81. package/dist/types/task/index.d.ts +2 -1
  82. package/dist/types/task/spawn-policy.d.ts +5 -0
  83. package/dist/types/thinking.d.ts +1 -7
  84. package/dist/types/tools/ast-grep.d.ts +1 -1
  85. package/dist/types/tools/browser/registry.d.ts +2 -1
  86. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  87. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  88. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  89. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  90. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  91. package/dist/types/tools/browser/render.d.ts +1 -0
  92. package/dist/types/tools/browser.d.ts +3 -0
  93. package/dist/types/tools/computer/exposure.d.ts +9 -7
  94. package/dist/types/tools/computer/protocol.d.ts +70 -17
  95. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  96. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  97. package/dist/types/tools/computer/worker.d.ts +42 -7
  98. package/dist/types/tools/computer-renderer.d.ts +7 -5
  99. package/dist/types/tools/computer.d.ts +25 -44
  100. package/dist/types/tools/context.d.ts +3 -0
  101. package/dist/types/tools/glob.d.ts +1 -1
  102. package/dist/types/tools/grep.d.ts +1 -1
  103. package/dist/types/tools/index.d.ts +9 -0
  104. package/dist/types/tools/render-utils.d.ts +2 -0
  105. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  106. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  107. package/dist/types/tools/xdev.d.ts +8 -0
  108. package/dist/types/utils/clipboard.d.ts +2 -2
  109. package/dist/types/web/kagi.d.ts +1 -0
  110. package/dist/types/web/search/index.d.ts +1 -1
  111. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  112. package/dist/types/web/search/providers/base.d.ts +2 -0
  113. package/dist/types/web/search/providers/brave.d.ts +1 -0
  114. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  115. package/dist/types/web/search/providers/codex.d.ts +1 -0
  116. package/dist/types/web/search/providers/exa.d.ts +1 -0
  117. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  118. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  119. package/dist/types/web/search/providers/jina.d.ts +1 -0
  120. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  121. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  122. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  123. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  124. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  125. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  126. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  127. package/dist/types/web/search/providers/utils.d.ts +6 -9
  128. package/dist/types/web/search/providers/zai.d.ts +1 -0
  129. package/dist/types/web/search/types.d.ts +4 -0
  130. package/package.json +12 -12
  131. package/src/cli/args.ts +7 -87
  132. package/src/cli/browser-relay-cli.ts +119 -0
  133. package/src/cli/command-help.ts +103 -0
  134. package/src/cli/completion-gen.ts +5 -5
  135. package/src/cli/flag-tables.ts +9 -0
  136. package/src/cli/gc-cli.ts +641 -21
  137. package/src/cli/help-extra.ts +89 -0
  138. package/src/cli/setup-cli.ts +13 -21
  139. package/src/cli/thinking-levels.ts +7 -0
  140. package/src/cli/update-cli.ts +26 -7
  141. package/src/cli-commands.ts +170 -33
  142. package/src/cli.ts +17 -5
  143. package/src/commands/acp.ts +3 -1
  144. package/src/commands/agents.ts +3 -2
  145. package/src/commands/auth-broker.ts +3 -2
  146. package/src/commands/auth-gateway.ts +3 -2
  147. package/src/commands/bench.ts +2 -3
  148. package/src/commands/browser-relay.ts +53 -0
  149. package/src/commands/cleanse.ts +2 -2
  150. package/src/commands/commit.ts +3 -2
  151. package/src/commands/complete.ts +2 -1
  152. package/src/commands/completions.ts +3 -2
  153. package/src/commands/config.ts +3 -2
  154. package/src/commands/dry-balance.ts +2 -2
  155. package/src/commands/gallery.ts +3 -2
  156. package/src/commands/gc.ts +3 -2
  157. package/src/commands/grep.ts +3 -2
  158. package/src/commands/grievances.ts +3 -2
  159. package/src/commands/install.ts +2 -2
  160. package/src/commands/join.ts +3 -2
  161. package/src/commands/launch-help.ts +116 -0
  162. package/src/commands/launch.ts +7 -191
  163. package/src/commands/models.ts +3 -2
  164. package/src/commands/plugin.ts +3 -2
  165. package/src/commands/read.ts +3 -2
  166. package/src/commands/say.ts +3 -2
  167. package/src/commands/setup.ts +3 -2
  168. package/src/commands/shell.ts +3 -2
  169. package/src/commands/ssh.ts +3 -2
  170. package/src/commands/stats.ts +3 -2
  171. package/src/commands/tiny-models.ts +2 -2
  172. package/src/commands/token.ts +2 -2
  173. package/src/commands/ttsr.ts +2 -2
  174. package/src/commands/update.ts +3 -2
  175. package/src/commands/usage.ts +3 -2
  176. package/src/commands/web-search.ts +3 -2
  177. package/src/commands/worktree.ts +3 -2
  178. package/src/config/keybindings.ts +5 -0
  179. package/src/config/model-discovery.ts +44 -27
  180. package/src/config/model-registry.ts +19 -13
  181. package/src/config/models-config-schema-bundle.ts +9 -0
  182. package/src/config/resolve-config-value.ts +2 -1
  183. package/src/config/service-tier.ts +5 -0
  184. package/src/config/settings-schema.ts +72 -20
  185. package/src/config/settings.ts +1 -1
  186. package/src/cursor-bridge-tools.ts +1 -1
  187. package/src/cursor.ts +10 -14
  188. package/src/edit/diff.ts +17 -8
  189. package/src/edit/index.ts +20 -7
  190. package/src/edit/modes/replace.ts +93 -40
  191. package/src/edit/renderer.ts +1 -1
  192. package/src/edit/streaming.ts +14 -17
  193. package/src/eval/py/kernel.ts +2 -1
  194. package/src/eval/py/spawn-options.ts +24 -29
  195. package/src/export/html/index.ts +10 -3
  196. package/src/export/html/template.js +8 -26
  197. package/src/export/share.ts +4 -0
  198. package/src/extensibility/extensions/wrapper.ts +1 -0
  199. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  200. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  201. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  202. package/src/extensibility/skills.ts +11 -0
  203. package/src/extensibility/utils.ts +10 -2
  204. package/src/launch/broker.ts +223 -7
  205. package/src/launch/client.ts +192 -21
  206. package/src/launch/ensure.ts +73 -0
  207. package/src/launch/paths.ts +1 -1
  208. package/src/launch/protocol.ts +54 -2
  209. package/src/lsp/client.ts +23 -2
  210. package/src/lsp/index.ts +8 -0
  211. package/src/lsp/mux/daemon.ts +348 -0
  212. package/src/lsp/mux/protocol.ts +96 -0
  213. package/src/lsp/mux/server.ts +797 -0
  214. package/src/lsp/types.ts +28 -2
  215. package/src/main.ts +20 -8
  216. package/src/mcp/config-writer.ts +1 -1
  217. package/src/mcp/oauth-flow.ts +11 -1
  218. package/src/mnemopi/embed-client.ts +49 -2
  219. package/src/mnemopi/state.ts +57 -23
  220. package/src/modes/components/assistant-message.ts +11 -0
  221. package/src/modes/components/btw-panel.ts +41 -4
  222. package/src/modes/components/chat-transcript-builder.ts +4 -0
  223. package/src/modes/components/custom-editor.ts +9 -0
  224. package/src/modes/components/read-tool-group.ts +10 -0
  225. package/src/modes/components/status-line/component.ts +1 -1
  226. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  227. package/src/modes/components/tool-execution.ts +8 -0
  228. package/src/modes/components/tree-selector.ts +46 -48
  229. package/src/modes/controllers/btw-controller.ts +55 -7
  230. package/src/modes/controllers/command-controller.ts +31 -0
  231. package/src/modes/controllers/event-controller.ts +17 -0
  232. package/src/modes/controllers/input-controller.ts +63 -8
  233. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  234. package/src/modes/controllers/selector-controller.ts +20 -0
  235. package/src/modes/interactive-mode.ts +18 -2
  236. package/src/modes/types.ts +9 -1
  237. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  238. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  239. package/src/modes/utils/ui-helpers.ts +6 -10
  240. package/src/modes/workflow.ts +8 -2
  241. package/src/prompts/agents/init.md +1 -1
  242. package/src/prompts/session/launch-completion.md +1 -0
  243. package/src/prompts/system/plan-mode-active.md +2 -2
  244. package/src/prompts/system/system-prompt.md +26 -52
  245. package/src/prompts/system/workflow-notice.md +3 -3
  246. package/src/prompts/tools/ast-grep.md +1 -1
  247. package/src/prompts/tools/bash.md +1 -1
  248. package/src/prompts/tools/browser.md +1 -0
  249. package/src/prompts/tools/computer.md +21 -21
  250. package/src/prompts/tools/glob.md +1 -1
  251. package/src/prompts/tools/grep.md +1 -1
  252. package/src/prompts/tools/replace.md +5 -4
  253. package/src/prompts/tools/task.md +4 -4
  254. package/src/sdk.ts +49 -6
  255. package/src/secrets/index.ts +1 -1
  256. package/src/security/contracts/schemas.ts +205 -183
  257. package/src/security/contracts/validation.ts +5 -1
  258. package/src/security/store.ts +2 -2
  259. package/src/session/agent-session-types.ts +9 -1
  260. package/src/session/agent-session.ts +204 -15
  261. package/src/session/indexed-session-storage.ts +9 -0
  262. package/src/session/launch-completion.ts +37 -0
  263. package/src/session/prewalk.ts +24 -2
  264. package/src/session/session-advisors.ts +2 -2
  265. package/src/session/session-context.ts +42 -12
  266. package/src/session/session-dump-format.ts +1 -1
  267. package/src/session/session-entries.ts +17 -1
  268. package/src/session/session-maintenance.ts +38 -4
  269. package/src/session/session-manager.ts +123 -67
  270. package/src/session/session-paths.ts +118 -55
  271. package/src/session/session-stats.ts +48 -3
  272. package/src/session/session-storage.ts +30 -22
  273. package/src/session/stream-guards.ts +1 -1
  274. package/src/session/yield-queue.ts +121 -16
  275. package/src/slash-commands/builtin-registry.ts +37 -24
  276. package/src/system-prompt.ts +8 -3
  277. package/src/task/index.ts +23 -8
  278. package/src/task/spawn-policy.ts +14 -0
  279. package/src/thinking.ts +2 -8
  280. package/src/tools/ast-grep.ts +10 -4
  281. package/src/tools/browser/cmux/cmux-tab.ts +97 -14
  282. package/src/tools/browser/cmux/rpc.ts +2 -8
  283. package/src/tools/browser/registry.ts +53 -2
  284. package/src/tools/browser/relay/bridge.ts +945 -0
  285. package/src/tools/browser/relay/daemon.ts +117 -0
  286. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  287. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  288. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  289. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  290. package/src/tools/browser/relay/kind.ts +41 -0
  291. package/src/tools/browser/relay/protocol.ts +54 -0
  292. package/src/tools/browser/relay/server.ts +141 -0
  293. package/src/tools/browser/render.ts +4 -1
  294. package/src/tools/browser/shared-daemon.ts +7 -58
  295. package/src/tools/browser/tab-supervisor.ts +5 -4
  296. package/src/tools/browser/tab-worker.ts +33 -12
  297. package/src/tools/browser.ts +26 -1
  298. package/src/tools/computer/exposure.ts +11 -35
  299. package/src/tools/computer/protocol.ts +51 -10
  300. package/src/tools/computer/supervisor.ts +227 -124
  301. package/src/tools/computer/worker-entry.ts +11 -6
  302. package/src/tools/computer/worker.ts +695 -81
  303. package/src/tools/computer-renderer.ts +104 -65
  304. package/src/tools/computer.ts +134 -428
  305. package/src/tools/context.ts +3 -0
  306. package/src/tools/glob.ts +9 -2
  307. package/src/tools/grep.ts +12 -6
  308. package/src/tools/hub/launch.ts +122 -6
  309. package/src/tools/index.ts +9 -0
  310. package/src/tools/inspect-image.ts +32 -6
  311. package/src/tools/render-utils.ts +2 -0
  312. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  313. package/src/tools/tool-timeouts.ts +1 -0
  314. package/src/tools/xdev.ts +29 -2
  315. package/src/utils/clipboard.ts +40 -14
  316. package/src/utils/command-args.ts +26 -28
  317. package/src/utils/tool-choice.ts +0 -13
  318. package/src/web/kagi.ts +2 -1
  319. package/src/web/search/index.ts +18 -2
  320. package/src/web/search/providers/anthropic.ts +4 -1
  321. package/src/web/search/providers/base.ts +2 -0
  322. package/src/web/search/providers/brave.ts +3 -1
  323. package/src/web/search/providers/browser-page.ts +6 -4
  324. package/src/web/search/providers/codex.ts +4 -1
  325. package/src/web/search/providers/duckduckgo.ts +2 -1
  326. package/src/web/search/providers/ecosia.ts +2 -1
  327. package/src/web/search/providers/exa.ts +4 -2
  328. package/src/web/search/providers/firecrawl.ts +3 -1
  329. package/src/web/search/providers/gemini.ts +8 -2
  330. package/src/web/search/providers/google.ts +2 -1
  331. package/src/web/search/providers/jina.ts +12 -2
  332. package/src/web/search/providers/kagi.ts +3 -0
  333. package/src/web/search/providers/kimi.ts +5 -1
  334. package/src/web/search/providers/mojeek.ts +2 -1
  335. package/src/web/search/providers/parallel.ts +5 -1
  336. package/src/web/search/providers/perplexity.ts +6 -3
  337. package/src/web/search/providers/public.ts +1 -1
  338. package/src/web/search/providers/searxng.ts +11 -5
  339. package/src/web/search/providers/startpage.ts +10 -4
  340. package/src/web/search/providers/synthetic.ts +11 -5
  341. package/src/web/search/providers/tavily.ts +3 -1
  342. package/src/web/search/providers/tinyfish.ts +3 -1
  343. package/src/web/search/providers/utils.ts +12 -10
  344. package/src/web/search/providers/xai.ts +2 -2
  345. package/src/web/search/providers/zai.ts +18 -3
  346. package/src/web/search/types.ts +6 -0
  347. package/dist/types/config/file-lock.d.ts +0 -29
  348. package/src/config/file-lock.ts +0 -164
@@ -2,10 +2,17 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { getTerminalId } from "@oh-my-pi/pi-tui";
5
- import { getSessionsDir, getTerminalSessionsDir, isEnoent, logger, resolveEquivalentPath } from "@oh-my-pi/pi-utils";
5
+ import {
6
+ getSessionsDir,
7
+ getTerminalSessionsDir,
8
+ isEnoent,
9
+ isRecord,
10
+ logger,
11
+ resolveEquivalentPath,
12
+ } from "@oh-my-pi/pi-utils";
6
13
  import type { SessionStorage } from "./session-storage";
7
14
 
8
- const migratedSessionRoots = new Set<string>();
15
+ const SESSION_HEADER_PREFIX_BYTES = 4096;
9
16
 
10
17
  /**
11
18
  * Merge or rename a legacy session directory into its canonical target.
@@ -35,12 +42,16 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
35
42
  return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
36
43
  }
37
44
 
38
- function encodeRelativeSessionDirName(prefix: string, relative: string): string {
45
+ function encodeLegacyRelativeSessionDirName(prefix: string, relative: string): string {
39
46
  const encoded = relative.replace(/[/\\:]/g, "-");
40
47
  return encoded ? (prefix.endsWith("-") ? `${prefix}${encoded}` : `${prefix}-${encoded}`) : prefix;
41
48
  }
42
49
 
43
- function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
50
+ function getDefaultSessionDirName(cwd: string): {
51
+ encodedDirName: string;
52
+ legacyRelativeDirName: string | undefined;
53
+ resolvedCwd: string;
54
+ } {
44
55
  const resolvedCwd = path.resolve(cwd);
45
56
  const canonicalCwd = resolveEquivalentPath(resolvedCwd);
46
57
  const home = os.homedir();
@@ -49,72 +60,111 @@ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolv
49
60
  const canonicalTempRoot = resolveEquivalentPath(tempRoot);
50
61
  const homeRelative = path.relative(canonicalHome, canonicalCwd);
51
62
  const tempRelative = path.relative(canonicalTempRoot, canonicalCwd);
52
- const encodedDirName =
53
- homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))
54
- ? encodeRelativeSessionDirName("-", homeRelative)
55
- : tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))
56
- ? encodeRelativeSessionDirName("-tmp", tempRelative)
57
- : encodeLegacyAbsoluteSessionDirName(canonicalCwd);
58
- return { encodedDirName, resolvedCwd };
59
- }
60
63
 
61
- /**
62
- * Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
63
- * Runs once per sessions root on first access, best-effort.
64
- */
65
- function migrateHomeSessionDirs(sessionsRoot: string): void {
66
- if (migratedSessionRoots.has(sessionsRoot)) return;
67
- migratedSessionRoots.add(sessionsRoot);
64
+ let scope: "home" | "tmp" | "abs";
65
+ let legacyRelativeDirName: string | undefined;
66
+ if (homeRelative === "" || (!homeRelative.startsWith("..") && !path.isAbsolute(homeRelative))) {
67
+ scope = "home";
68
+ legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-", homeRelative);
69
+ } else if (tempRelative === "" || (!tempRelative.startsWith("..") && !path.isAbsolute(tempRelative))) {
70
+ scope = "tmp";
71
+ legacyRelativeDirName = encodeLegacyRelativeSessionDirName("-tmp", tempRelative);
72
+ } else {
73
+ scope = "abs";
74
+ }
68
75
 
69
- const home = os.homedir();
70
- const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
71
- const oldPrefix = `--${homeEncoded}-`;
72
- const oldExact = `--${homeEncoded}--`;
76
+ const normalized = canonicalCwd.replaceAll("\\", "/");
77
+ const readable = path
78
+ .basename(canonicalCwd)
79
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
80
+ .replace(/^-+|-+$/g, "")
81
+ .slice(-80);
82
+ const digest = Bun.SHA256.hash(normalized, "hex");
83
+ const encodedDirName = `${scope}-${readable || "project"}-${digest}`;
84
+ return { encodedDirName, legacyRelativeDirName, resolvedCwd };
85
+ }
73
86
 
74
- let entries: string[];
87
+ function readSessionCwd(sessionFile: string, buffer: Buffer): string | undefined {
88
+ let descriptor: number | undefined;
75
89
  try {
76
- entries = fs.readdirSync(sessionsRoot);
90
+ descriptor = fs.openSync(sessionFile, "r");
91
+ const bytesRead = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
92
+ const prefix = buffer.toString("utf8", 0, bytesRead);
93
+ for (const line of prefix.split(/\r?\n/, 3)) {
94
+ try {
95
+ const record: unknown = JSON.parse(line);
96
+ if (isRecord(record) && record.type === "session" && typeof record.cwd === "string") {
97
+ return record.cwd;
98
+ }
99
+ } catch {
100
+ // Ignore title slots or truncated/corrupt headers.
101
+ }
102
+ }
77
103
  } catch {
78
- return;
104
+ // Best-effort migration leaves unreadable sessions in the current bucket.
105
+ } finally {
106
+ if (descriptor !== undefined) fs.closeSync(descriptor);
79
107
  }
108
+ return undefined;
109
+ }
80
110
 
111
+ function moveSessionBundle(sourceDir: string, targetDir: string, sessionName: string, entries: string[]): void {
112
+ fs.mkdirSync(targetDir, { recursive: true });
113
+ const artifactsName = path.basename(sessionName, ".jsonl");
81
114
  for (const entry of entries) {
82
- let remainder: string;
83
- if (entry === oldExact) {
84
- remainder = "";
85
- } else if (entry.startsWith(oldPrefix) && entry.endsWith("--")) {
86
- remainder = entry.slice(oldPrefix.length, -2);
115
+ if (entry !== sessionName && entry !== artifactsName && !entry.startsWith(`${sessionName}.`)) continue;
116
+ const source = path.join(sourceDir, entry);
117
+ if (!fs.existsSync(source)) continue;
118
+ const target = path.join(targetDir, entry);
119
+ const existing = fs.statSync(target, { throwIfNoEntry: false });
120
+ if (!existing) {
121
+ fs.renameSync(source, target);
122
+ } else if (existing.isDirectory() && fs.statSync(source).isDirectory()) {
123
+ migrateSessionDirPath(source, target);
87
124
  } else {
88
- continue;
89
- }
90
-
91
- const newName = remainder ? `-${remainder}` : "-";
92
- const oldPath = path.join(sessionsRoot, entry);
93
- const newPath = path.join(sessionsRoot, newName);
94
-
95
- try {
96
- migrateSessionDirPath(oldPath, newPath);
97
- } catch {
98
- // Best effort
125
+ fs.rmSync(source, { recursive: true, force: true });
99
126
  }
100
127
  }
101
128
  }
102
129
 
103
- function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
104
- const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
105
- if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
106
-
107
- try {
108
- migrateSessionDirPath(legacyDir, sessionDir);
109
- } catch {
110
- // Best effort
130
+ function rerouteCollidingSessions(
131
+ cwd: string,
132
+ legacyDir: string,
133
+ sessionsRoot: string,
134
+ kind: "relative" | "absolute",
135
+ ): void {
136
+ const entries = fs.readdirSync(legacyDir);
137
+ const currentCwd = resolveEquivalentPath(path.resolve(cwd));
138
+ const buffer = Buffer.allocUnsafe(SESSION_HEADER_PREFIX_BYTES);
139
+ for (const entry of entries) {
140
+ if (!entry.endsWith(".jsonl")) continue;
141
+ const recordedCwd = readSessionCwd(path.join(legacyDir, entry), buffer);
142
+ if (!recordedCwd) continue;
143
+ const recordedCanonical = resolveEquivalentPath(path.resolve(recordedCwd));
144
+ if (
145
+ recordedCanonical === currentCwd ||
146
+ !fs.statSync(recordedCanonical, { throwIfNoEntry: false })?.isDirectory()
147
+ ) {
148
+ continue;
149
+ }
150
+ const recordedNames = getDefaultSessionDirName(recordedCanonical);
151
+ const recordedLegacyName =
152
+ kind === "relative"
153
+ ? recordedNames.legacyRelativeDirName
154
+ : encodeLegacyAbsoluteSessionDirName(recordedCanonical);
155
+ if (recordedLegacyName !== path.basename(legacyDir)) continue;
156
+ moveSessionBundle(legacyDir, path.join(sessionsRoot, recordedNames.encodedDirName), entry, entries);
111
157
  }
112
158
  }
113
159
 
114
160
  export function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
115
161
  const currentDirName = path.basename(sessionDir);
116
- const { encodedDirName } = getDefaultSessionDirName(cwd);
117
- if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
162
+ const { encodedDirName, legacyRelativeDirName } = getDefaultSessionDirName(cwd);
163
+ if (
164
+ currentDirName !== encodedDirName &&
165
+ currentDirName !== legacyRelativeDirName &&
166
+ currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)
167
+ ) {
118
168
  return undefined;
119
169
  }
120
170
  return path.dirname(sessionDir);
@@ -130,10 +180,23 @@ export function computeDefaultSessionDir(
130
180
  storage: SessionStorage,
131
181
  sessionsRoot: string = getSessionsDir(),
132
182
  ): string {
133
- const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
134
- migrateHomeSessionDirs(sessionsRoot);
183
+ const { encodedDirName, legacyRelativeDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
135
184
  const sessionDir = path.join(sessionsRoot, encodedDirName);
136
- migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
185
+ const legacyDirs: Array<{ kind: "relative" | "absolute"; name: string | undefined }> = [
186
+ { kind: "relative", name: legacyRelativeDirName },
187
+ { kind: "absolute", name: encodeLegacyAbsoluteSessionDirName(resolvedCwd) },
188
+ ];
189
+ for (const legacy of legacyDirs) {
190
+ if (!legacy.name) continue;
191
+ const legacyDir = path.join(sessionsRoot, legacy.name);
192
+ if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) continue;
193
+ try {
194
+ rerouteCollidingSessions(resolvedCwd, legacyDir, sessionsRoot, legacy.kind);
195
+ migrateSessionDirPath(legacyDir, sessionDir);
196
+ } catch {
197
+ // Best effort
198
+ }
199
+ }
137
200
  storage.ensureDirSync(sessionDir);
138
201
  return sessionDir;
139
202
  }
@@ -34,6 +34,11 @@ export interface SessionStatsTrackerHost {
34
34
  sessionId(): string;
35
35
  }
36
36
 
37
+ function correctedPromptTokens(assistant: AssistantMessage): number {
38
+ const providerPromptTokens = assistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(assistant.usage);
39
+ return Math.max(0, providerPromptTokens - (assistant.contextSnapshot?.historyRewriteTokensRemoved ?? 0));
40
+ }
41
+
37
42
  /** Computes session totals and tracks the in-flight context estimate. */
38
43
  export class SessionStatsTracker {
39
44
  readonly #host: SessionStatsTrackerHost;
@@ -160,8 +165,7 @@ export class SessionStatsTracker {
160
165
  const useAnchor =
161
166
  anchorAssistant !== undefined && anchorIndex !== -1 && (!pending || anchorIndex >= pending.cutoffCount);
162
167
  if (useAnchor && anchorAssistant) {
163
- const promptTokens =
164
- anchorAssistant.contextSnapshot?.promptTokens ?? calculatePromptTokens(anchorAssistant.usage);
168
+ const promptTokens = correctedPromptTokens(anchorAssistant);
165
169
  const nonMessageTokens =
166
170
  anchorAssistant.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
167
171
  anchored = true;
@@ -199,7 +203,7 @@ export class SessionStatsTracker {
199
203
  ) {
200
204
  continue;
201
205
  }
202
- const promptTokens = message.contextSnapshot?.promptTokens ?? calculatePromptTokens(message.usage);
206
+ const promptTokens = correctedPromptTokens(message);
203
207
  const nonMessageTokens =
204
208
  message.contextSnapshot?.nonMessageTokens ?? computeNonMessageTokens(this.#host.session);
205
209
  let tailTokens = 0;
@@ -256,6 +260,47 @@ export class SessionStatsTracker {
256
260
  return this.#pendingContextSnapshot?.nonMessageTokens;
257
261
  }
258
262
 
263
+ /**
264
+ * Apply an estimated prompt-prefix reduction to the current provider anchor.
265
+ *
266
+ * History after the anchor is estimated live by {@link getContextBreakdown};
267
+ * callers must pass only savings from entries already included in the
268
+ * anchor's provider-reported prompt. Persisting the correction on the
269
+ * assistant snapshot keeps reloads accurate, and the next successful
270
+ * assistant response naturally replaces it with a fresh provider anchor.
271
+ */
272
+ recordAnchoredHistoryRewrite(tokensRemoved: number): void {
273
+ if (!Number.isFinite(tokensRemoved) || tokensRemoved <= 0) return;
274
+
275
+ const branchEntries = this.#host.sessionManager.getBranch();
276
+ const latestCompaction = getLatestCompactionEntry(branchEntries);
277
+ const compactionIndex = latestCompaction ? branchEntries.lastIndexOf(latestCompaction) : -1;
278
+ for (let index = branchEntries.length - 1; index > compactionIndex; index--) {
279
+ const entry = branchEntries[index];
280
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
281
+ const assistant = entry.message;
282
+ if (
283
+ assistant.stopReason === "aborted" ||
284
+ assistant.stopReason === "error" ||
285
+ !assistant.usage ||
286
+ !hasContextTokenUsage(assistant.usage)
287
+ ) {
288
+ continue;
289
+ }
290
+
291
+ if (!assistant.contextSnapshot) {
292
+ assistant.contextSnapshot = {
293
+ promptTokens: calculatePromptTokens(assistant.usage),
294
+ nonMessageTokens: computeNonMessageTokens(this.#host.session),
295
+ };
296
+ }
297
+ const snapshot = assistant.contextSnapshot;
298
+ snapshot.historyRewriteTokensRemoved = (snapshot.historyRewriteTokensRemoved ?? 0) + Math.floor(tokensRemoved);
299
+ this.#contextUsageRevision++;
300
+ return;
301
+ }
302
+ }
303
+
259
304
  /** Sets or clears the in-flight context snapshot. */
260
305
  setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
261
306
  this.#pendingContextSnapshot = snapshot;
@@ -14,13 +14,25 @@ export interface SessionStorageStat {
14
14
 
15
15
  export interface SessionStorageWriter {
16
16
  /**
17
- * Append one newline-terminated line. File storage batches same-turn appends
18
- * until its microtask boundary; memory storage updates in-body; indexed
19
- * backends queue in call order.
17
+ * Append one newline-terminated line.
18
+ *
19
+ * File and memory storage apply the line synchronously before the returned
20
+ * promise settles, so a software crash after `append` returns (or after a
21
+ * fire-and-forget call begins) still sees the entry on disk / in body. No
22
+ * `fsync` — power loss may still drop the last page. Indexed backends update
23
+ * the local index immediately and queue the remote publish in call order.
20
24
  *
21
25
  * `line` MUST include the trailing newline.
22
26
  */
23
27
  append(line: string): Promise<void>;
28
+ /**
29
+ * Synchronous append when the backend can apply the line before return.
30
+ * File and memory implement this so {@link SessionManager} can latch the
31
+ * first write failure before the appending call returns (surfaced by a later
32
+ * flushSync/close/next append — the turn loop does not throw from append).
33
+ * Indexed backends update the local index immediately and queue remote I/O.
34
+ */
35
+ appendSync?(line: string): void;
24
36
  /** Resolve once all queued appends complete. No fsync. */
25
37
  flush(): Promise<void>;
26
38
  /** Drain synchronously flushable queued work when the backend supports it. No fsync. */
@@ -93,8 +105,6 @@ class FileSessionStorageWriter implements SessionStorageWriter {
93
105
  #closed = false;
94
106
  #error: Error | undefined;
95
107
  #onError: ((err: Error) => void) | undefined;
96
- #pending = "";
97
- #flushScheduled = false;
98
108
 
99
109
  constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
100
110
  this.#onError = options?.onError;
@@ -129,34 +139,29 @@ class FileSessionStorageWriter implements SessionStorageWriter {
129
139
  }
130
140
  }
131
141
 
132
- #flushPendingNow(): void {
133
- this.#flushScheduled = false;
134
- if (this.#pending.length === 0) return;
135
- const pending = this.#pending;
136
- this.#pending = "";
142
+ appendSync(line: string): void {
143
+ if (this.#closed) throw new Error("Writer closed");
144
+ if (this.#error) throw this.#error;
145
+ // Write in-body so software crash after the call still sees the entry.
146
+ // Microtask batching used to leave completed transcript lines only in
147
+ // memory until the next event-loop turn; process crash then lost every
148
+ // post-checkpoint event. flush/flushSync remain no-op drains (no fsync).
137
149
  try {
138
- this.#writeNow(pending);
150
+ this.#writeNow(line);
139
151
  } catch (err) {
140
- this.#recordError(err);
152
+ throw this.#recordError(err);
141
153
  }
142
154
  }
143
155
 
144
156
  async append(line: string): Promise<void> {
145
- if (this.#closed) throw new Error("Writer closed");
146
- if (this.#error) throw this.#error;
147
- this.#pending += line;
148
- if (this.#flushScheduled) return;
149
- this.#flushScheduled = true;
150
- queueMicrotask(() => this.#flushPendingNow());
157
+ this.appendSync(line);
151
158
  }
152
159
 
153
160
  async flush(): Promise<void> {
154
- this.#flushPendingNow();
155
161
  if (this.#error) throw this.#error;
156
162
  }
157
163
 
158
164
  flushSync(): void {
159
- this.#flushPendingNow();
160
165
  if (this.#error) throw this.#error;
161
166
  }
162
167
 
@@ -166,7 +171,6 @@ class FileSessionStorageWriter implements SessionStorageWriter {
166
171
 
167
172
  async close(): Promise<void> {
168
173
  if (this.#closed) return;
169
- this.#flushPendingNow();
170
174
  this.#closed = true;
171
175
  // Unregister from finalization - we're closing properly
172
176
  writerRegistry.unregister(this);
@@ -487,7 +491,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
487
491
  return error;
488
492
  }
489
493
 
490
- async append(line: string): Promise<void> {
494
+ appendSync(line: string): void {
491
495
  if (this.#closed) throw new Error("Writer closed");
492
496
  if (this.#error) throw this.#error;
493
497
  try {
@@ -498,6 +502,10 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
498
502
  }
499
503
  }
500
504
 
505
+ async append(line: string): Promise<void> {
506
+ this.appendSync(line);
507
+ }
508
+
501
509
  async flush(): Promise<void> {
502
510
  if (this.#error) throw this.#error;
503
511
  }
@@ -164,7 +164,7 @@ export class StreamingEditGuard {
164
164
  const toolCall = messageContent[contentIndex] as ToolCall;
165
165
  if (toolCall.name !== "edit") return undefined;
166
166
  const args = toolCall.arguments;
167
- if (!args || typeof args !== "object" || Array.isArray(args) || "old_text" in args || "new_text" in args) {
167
+ if (!args || typeof args !== "object" || Array.isArray(args) || "old_string" in args || "new_string" in args) {
168
168
  return undefined;
169
169
  }
170
170
  const filePath = typeof args.path === "string" ? args.path : undefined;
@@ -1,4 +1,4 @@
1
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
1
+ import { type AgentMessage, ASIDE_MESSAGE_COMMIT, ASIDE_MESSAGE_DISCARD } from "@oh-my-pi/pi-agent-core";
2
2
  import { logger } from "@oh-my-pi/pi-utils";
3
3
 
4
4
  export interface YieldDispatcher<P> {
@@ -25,6 +25,17 @@ interface StoredDispatcher {
25
25
  skipIdleFlush?: boolean;
26
26
  }
27
27
 
28
+ interface StoredEntry {
29
+ value: unknown;
30
+ resolve?: () => void;
31
+ reject?: (error: Error) => void;
32
+ }
33
+
34
+ interface BuiltMessage {
35
+ message: AgentMessage;
36
+ entries: StoredEntry[];
37
+ }
38
+
28
39
  function formatError(error: unknown): string {
29
40
  return error instanceof Error ? error.message : String(error);
30
41
  }
@@ -32,7 +43,7 @@ function formatError(error: unknown): string {
32
43
  export class YieldQueue {
33
44
  readonly #options: YieldQueueOptions;
34
45
  readonly #dispatchers = new Map<string, StoredDispatcher>();
35
- readonly #entries = new Map<string, unknown[]>();
46
+ readonly #entries = new Map<string, StoredEntry[]>();
36
47
  #idleFlushPending = false;
37
48
 
38
49
  constructor(options: YieldQueueOptions) {
@@ -49,14 +60,27 @@ export class YieldQueue {
49
60
  return () => {
50
61
  if (this.#dispatchers.get(kind) !== stored) return;
51
62
  this.#dispatchers.delete(kind);
63
+ this.#rejectEntries(this.#entries.get(kind) ?? [], new Error(`Yield queue dispatcher removed: ${kind}`));
52
64
  this.#entries.delete(kind);
53
65
  };
54
66
  }
55
67
 
56
68
  enqueue<P>(kind: string, entry: P): void {
69
+ this.#enqueue(kind, { value: entry });
70
+ }
71
+
72
+ enqueueWithReceipt<P>(kind: string, entry: P): Promise<void> {
73
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
74
+ if (!this.#enqueue(kind, { value: entry, resolve, reject })) {
75
+ reject(new Error(`Yield queue entry ignored for unregistered kind: ${kind}`));
76
+ }
77
+ return promise;
78
+ }
79
+
80
+ #enqueue(kind: string, entry: StoredEntry): boolean {
57
81
  if (!this.#dispatchers.has(kind)) {
58
82
  logger.warn("Yield queue entry ignored for unregistered kind", { kind });
59
- return;
83
+ return false;
60
84
  }
61
85
  let entries = this.#entries.get(kind);
62
86
  if (!entries) {
@@ -67,6 +91,7 @@ export class YieldQueue {
67
91
  if (!this.#options.isStreaming() && !this.#dispatchers.get(kind)!.skipIdleFlush) {
68
92
  this.#scheduleIdleFlush();
69
93
  }
94
+ return true;
70
95
  }
71
96
 
72
97
  has(kind?: string): boolean {
@@ -77,30 +102,55 @@ export class YieldQueue {
77
102
  return false;
78
103
  }
79
104
 
105
+ /** Arrange an idle flush for entries queued near the end of a streaming run. */
106
+ requestIdleFlush(): void {
107
+ for (const [kind, dispatcher] of this.#dispatchers) {
108
+ if (!dispatcher.skipIdleFlush && this.has(kind)) {
109
+ this.#scheduleIdleFlush();
110
+ return;
111
+ }
112
+ }
113
+ }
114
+
80
115
  async flush(mode: YieldFlushMode): Promise<void> {
81
116
  if (mode === "idle") {
82
117
  this.#idleFlushPending = false;
83
118
  }
84
- const idleMessages: AgentMessage[] = [];
119
+ const idleMessages: BuiltMessage[] = [];
85
120
  for (const [kind, dispatcher] of this.#dispatchers) {
121
+ if (mode === "idle" && dispatcher.skipIdleFlush) continue;
86
122
  const entries = this.#drain(kind);
87
123
  if (entries.length === 0) continue;
88
- const message = this.#build(kind, dispatcher, entries);
89
- if (!message) continue;
124
+ const built = this.#build(kind, dispatcher, entries);
125
+ if (!built) continue;
90
126
  if (mode === "streaming") {
91
127
  try {
92
- this.#options.injectStreaming?.(message);
128
+ if (!this.#options.injectStreaming) throw new Error("Streaming injection is unavailable");
129
+ this.#options.injectStreaming(built.message);
130
+ this.#resolveEntries(built.entries);
93
131
  } catch (error) {
132
+ const dispatchError = error instanceof Error ? error : new Error(String(error));
133
+ this.#rejectEntries(built.entries, dispatchError);
94
134
  logger.warn("Yield queue streaming dispatch failed", { kind, error: formatError(error) });
95
135
  }
96
136
  } else {
97
- idleMessages.push(message);
137
+ idleMessages.push(built);
98
138
  }
99
139
  }
100
140
  if (mode === "idle" && idleMessages.length > 0) {
141
+ for (const item of idleMessages) this.#attachEntrySettlement(item);
101
142
  try {
102
- await this.#options.injectIdle(idleMessages);
143
+ await this.#options.injectIdle(idleMessages.map(item => item.message));
144
+ for (const item of idleMessages) {
145
+ (item.message as AgentMessage & { [ASIDE_MESSAGE_COMMIT]?: () => void })[ASIDE_MESSAGE_COMMIT]?.();
146
+ }
103
147
  } catch (error) {
148
+ const dispatchError = error instanceof Error ? error : new Error(String(error));
149
+ for (const item of idleMessages) {
150
+ (item.message as AgentMessage & { [ASIDE_MESSAGE_DISCARD]?: (error: Error) => void })[
151
+ ASIDE_MESSAGE_DISCARD
152
+ ]?.(dispatchError);
153
+ }
104
154
  logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
105
155
  }
106
156
  }
@@ -119,7 +169,12 @@ export class YieldQueue {
119
169
  for (const [kind, dispatcher] of this.#dispatchers) {
120
170
  const entries = this.#drain(kind);
121
171
  if (entries.length === 0) continue;
122
- thunks.push(() => this.#build(kind, dispatcher, entries));
172
+ thunks.push(() => {
173
+ const built = this.#build(kind, dispatcher, entries);
174
+ if (!built) return null;
175
+ this.#attachEntrySettlement(built);
176
+ return built.message;
177
+ });
123
178
  }
124
179
  return thunks;
125
180
  }
@@ -127,14 +182,22 @@ export class YieldQueue {
127
182
  /** Drop queued entries. With `kind`, drop only that kind's entries (leaving
128
183
  * any pending idle-flush for other kinds intact); otherwise drop everything. */
129
184
  clear(kind?: string): void {
185
+ const error = new Error("Yield queue entry cleared before dispatch");
130
186
  if (kind !== undefined) {
187
+ this.#rejectEntries(this.#entries.get(kind) ?? [], error);
131
188
  this.#entries.delete(kind);
132
189
  return;
133
190
  }
191
+ for (const entries of this.#entries.values()) this.#rejectEntries(entries, error);
134
192
  this.#entries.clear();
135
193
  this.#idleFlushPending = false;
136
194
  }
137
195
 
196
+ /** Clear a scheduled-flush latch when its host task is cancelled before running. */
197
+ cancelIdleFlushScheduling(): void {
198
+ this.#idleFlushPending = false;
199
+ }
200
+
138
201
  #scheduleIdleFlush(): void {
139
202
  if (this.#idleFlushPending) return;
140
203
  this.#idleFlushPending = true;
@@ -150,34 +213,76 @@ export class YieldQueue {
150
213
  }
151
214
  }
152
215
 
153
- #drain(kind: string): unknown[] {
216
+ #drain(kind: string): StoredEntry[] {
154
217
  const entries = this.#entries.get(kind);
155
218
  if (!entries || entries.length === 0) return [];
156
219
  this.#entries.delete(kind);
157
220
  return entries;
158
221
  }
159
222
 
160
- #build(kind: string, dispatcher: StoredDispatcher, entries: unknown[]): AgentMessage | null {
161
- const survivors: unknown[] = [];
223
+ #build(kind: string, dispatcher: StoredDispatcher, entries: StoredEntry[]): BuiltMessage | null {
224
+ const survivors: StoredEntry[] = [];
162
225
  for (const entry of entries) {
163
226
  if (dispatcher.isStale) {
164
227
  let stale: boolean;
165
228
  try {
166
- stale = dispatcher.isStale(entry);
229
+ stale = dispatcher.isStale(entry.value);
167
230
  } catch (error) {
231
+ const staleError = error instanceof Error ? error : new Error(String(error));
232
+ entry.reject?.(staleError);
168
233
  logger.warn("Yield queue stale check failed", { kind, error: formatError(error) });
169
234
  continue;
170
235
  }
171
- if (stale) continue;
236
+ if (stale) {
237
+ entry.reject?.(new Error(`Yield queue entry became stale: ${kind}`));
238
+ continue;
239
+ }
172
240
  }
173
241
  survivors.push(entry);
174
242
  }
175
243
  if (survivors.length === 0) return null;
176
244
  try {
177
- return dispatcher.build(survivors);
245
+ const message = dispatcher.build(survivors.map(entry => entry.value));
246
+ if (!message) {
247
+ this.#rejectEntries(survivors, new Error(`Yield queue dispatcher skipped entry: ${kind}`));
248
+ return null;
249
+ }
250
+ return { message, entries: survivors };
178
251
  } catch (error) {
252
+ const buildError = error instanceof Error ? error : new Error(String(error));
253
+ this.#rejectEntries(survivors, buildError);
179
254
  logger.warn("Yield queue build failed", { kind, error: formatError(error) });
180
255
  return null;
181
256
  }
182
257
  }
258
+
259
+ #attachEntrySettlement(built: BuiltMessage): void {
260
+ let settled = false;
261
+ Object.defineProperties(built.message, {
262
+ [ASIDE_MESSAGE_COMMIT]: {
263
+ configurable: true,
264
+ value: () => {
265
+ if (settled) return;
266
+ settled = true;
267
+ this.#resolveEntries(built.entries);
268
+ },
269
+ },
270
+ [ASIDE_MESSAGE_DISCARD]: {
271
+ configurable: true,
272
+ value: (error: Error) => {
273
+ if (settled) return;
274
+ settled = true;
275
+ this.#rejectEntries(built.entries, error);
276
+ },
277
+ },
278
+ });
279
+ }
280
+
281
+ #resolveEntries(entries: StoredEntry[]): void {
282
+ for (const entry of entries) entry.resolve?.();
283
+ }
284
+
285
+ #rejectEntries(entries: StoredEntry[], error: Error): void {
286
+ for (const entry of entries) entry.reject?.(error);
287
+ }
183
288
  }