@gajae-code/coding-agent 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (599) hide show
  1. package/CHANGELOG.md +184 -2
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +9 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +70 -6
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +1 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/gajae-pet-widget.d.ts +8 -30
  87. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  88. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  89. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  90. package/dist/types/modes/components/pet-capability.d.ts +11 -25
  91. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  92. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  93. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  94. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  95. package/dist/types/modes/interactive-mode.d.ts +1 -0
  96. package/dist/types/modes/types.d.ts +1 -0
  97. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  98. package/dist/types/runtime-mcp/config.d.ts +9 -0
  99. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  100. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  101. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  102. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  103. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  104. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  105. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  106. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  107. package/dist/types/runtime-mcp/types.d.ts +102 -10
  108. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  109. package/dist/types/sdk/broker/broker.d.ts +3 -55
  110. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  111. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  112. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  113. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  114. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  117. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  118. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  119. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  120. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  121. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  122. package/dist/types/sdk/bus/index.d.ts +85 -99
  123. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  124. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  125. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  126. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  127. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  128. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  129. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  130. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  131. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  132. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  133. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  135. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  136. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  137. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  138. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  139. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  140. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  141. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  142. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  143. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  144. package/dist/types/sdk/cli/rows.d.ts +2 -11
  145. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  146. package/dist/types/sdk/client/client.d.ts +97 -1
  147. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  148. package/dist/types/sdk/host/control/index.d.ts +1 -1
  149. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  150. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  151. package/dist/types/sdk/host/host.d.ts +15 -1
  152. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  153. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  154. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  155. package/dist/types/sdk/host/types.d.ts +1 -1
  156. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  157. package/dist/types/sdk/index.d.ts +2 -1
  158. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  159. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  160. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  161. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  162. package/dist/types/sdk/mcp/server.d.ts +9 -6
  163. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  164. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  165. package/dist/types/sdk/prompt-status.d.ts +10 -3
  166. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  167. package/dist/types/sdk/receipt-state.d.ts +17 -0
  168. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  169. package/dist/types/sdk/router/index.d.ts +1 -0
  170. package/dist/types/sdk/router/session-router.d.ts +152 -0
  171. package/dist/types/sdk/session-list.d.ts +22 -0
  172. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  173. package/dist/types/sdk/session.d.ts +34 -1
  174. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  175. package/dist/types/session/account-inventory.d.ts +86 -0
  176. package/dist/types/session/agent-session.d.ts +145 -6
  177. package/dist/types/session/agent-storage.d.ts +10 -0
  178. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  179. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  180. package/dist/types/session/messages.d.ts +1 -1
  181. package/dist/types/session/secure-token-file.d.ts +3 -0
  182. package/dist/types/session/session-manager.d.ts +40 -1
  183. package/dist/types/session/startup-auth-config.d.ts +44 -0
  184. package/dist/types/session/terminal-abort.d.ts +378 -0
  185. package/dist/types/session/yield-queue.d.ts +7 -0
  186. package/dist/types/session-import/claude.d.ts +26 -0
  187. package/dist/types/session-import/codex.d.ts +88 -0
  188. package/dist/types/session-import/command.d.ts +35 -0
  189. package/dist/types/session-import/detect.d.ts +22 -0
  190. package/dist/types/session-import/index.d.ts +7 -0
  191. package/dist/types/session-import/provider-codex.d.ts +22 -0
  192. package/dist/types/session-import/provider-service.d.ts +38 -0
  193. package/dist/types/session-import/redact.d.ts +23 -0
  194. package/dist/types/session-import/service.d.ts +47 -0
  195. package/dist/types/session-import/types.d.ts +120 -0
  196. package/dist/types/setup/paseo/check.d.ts +16 -0
  197. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  198. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  199. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  200. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  201. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  202. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  203. package/dist/types/setup/paseo/remove.d.ts +14 -0
  204. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  205. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  206. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  207. package/dist/types/skill-state/active-state.d.ts +5 -1
  208. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  209. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  210. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  211. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  212. package/dist/types/slash-commands/types.d.ts +7 -1
  213. package/dist/types/task/executor.d.ts +10 -0
  214. package/dist/types/task/index.d.ts +3 -1
  215. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  216. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  217. package/dist/types/tools/bash.d.ts +2 -6
  218. package/dist/types/tools/browser/attach.d.ts +11 -0
  219. package/dist/types/tools/browser/launch.d.ts +20 -0
  220. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  221. package/dist/types/tools/browser/render.d.ts +1 -0
  222. package/dist/types/tools/browser.d.ts +3 -1
  223. package/dist/types/tools/image-gen.d.ts +9 -2
  224. package/dist/types/tools/index.d.ts +8 -0
  225. package/dist/types/tools/monitor.d.ts +1 -1
  226. package/dist/types/tools/skill-discovery.d.ts +8 -1
  227. package/dist/types/tools/subagent.d.ts +2 -0
  228. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  229. package/dist/types/utils/herdr-pane.d.ts +76 -0
  230. package/package.json +19 -9
  231. package/scripts/build-binary.ts +2 -1
  232. package/scripts/build-sdk-package-smoke.ts +2 -13
  233. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  234. package/scripts/compile-args.ts +2 -0
  235. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  236. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  237. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  238. package/scripts/generate-tool-catalog.ts +6 -4
  239. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  240. package/src/async/job-manager.ts +352 -17
  241. package/src/async/support.ts +25 -0
  242. package/src/autoresearch/dashboard.ts +28 -13
  243. package/src/capability/index.ts +3 -1
  244. package/src/capability/mcp.ts +6 -0
  245. package/src/capability/types.ts +6 -0
  246. package/src/cli/accounts-cli.ts +395 -0
  247. package/src/cli/args.ts +23 -0
  248. package/src/cli/auth-broker-cli.ts +136 -66
  249. package/src/cli/auth-gateway-cli.ts +94 -70
  250. package/src/cli/config-cli.ts +27 -2
  251. package/src/cli/crash-cli.ts +78 -0
  252. package/src/cli/customize-doctor.ts +1605 -0
  253. package/src/cli/daemon-cli.ts +12 -3
  254. package/src/cli/fast-help.ts +1 -0
  255. package/src/cli/mcp-cli.ts +128 -60
  256. package/src/cli/notify-cli.ts +8 -14
  257. package/src/cli/root-flags.ts +8 -0
  258. package/src/cli/setup-cli.ts +145 -0
  259. package/src/cli/skills-cli.ts +38 -2
  260. package/src/cli/update-cli.ts +167 -12
  261. package/src/cli.ts +4 -0
  262. package/src/commands/accounts.ts +71 -0
  263. package/src/commands/crash.ts +35 -0
  264. package/src/commands/customize.ts +84 -0
  265. package/src/commands/daemon.ts +2 -2
  266. package/src/commands/deep-interview.ts +2 -0
  267. package/src/commands/launch.ts +1 -0
  268. package/src/commands/mcp.ts +12 -7
  269. package/src/commands/ralplan.ts +2 -0
  270. package/src/commands/sdk.ts +65 -32
  271. package/src/commands/setup.ts +5 -0
  272. package/src/commands/skills.ts +14 -4
  273. package/src/commands/ultragoal.ts +10 -1
  274. package/src/commands/update.ts +26 -15
  275. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  276. package/src/config/atomic-yaml-patch.ts +769 -29
  277. package/src/config/config-schema-version.ts +10 -0
  278. package/src/config/file-lock.ts +70 -1
  279. package/src/config/mcp-schema.json +52 -14
  280. package/src/config/model-profile-activation.ts +20 -1
  281. package/src/config/model-profiles.ts +29 -19
  282. package/src/config/model-registry.ts +132 -21
  283. package/src/config/models-config-schema.ts +1 -1
  284. package/src/config/settings-schema.ts +76 -6
  285. package/src/config/settings.ts +4038 -199
  286. package/src/config/skill-settings-defaults.ts +41 -3
  287. package/src/config/task-delegation.ts +87 -0
  288. package/src/config/update-channel.ts +23 -0
  289. package/src/coordinator-mcp/server.ts +377 -308
  290. package/src/crash/index-store.ts +723 -0
  291. package/src/crash/nudge.ts +89 -0
  292. package/src/crash/record-loader.ts +149 -0
  293. package/src/crash/report.ts +551 -0
  294. package/src/crash/sanitize.ts +129 -0
  295. package/src/cursor.ts +36 -10
  296. package/src/customization/import.ts +946 -0
  297. package/src/customization/index.ts +4 -0
  298. package/src/customization/inventory.ts +335 -0
  299. package/src/customization/mutations.ts +141 -0
  300. package/src/customization/types.ts +223 -0
  301. package/src/daemon/control-types.ts +8 -1
  302. package/src/daemon/operator-contract.ts +6 -6
  303. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  304. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  305. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  306. package/src/discovery/claude-hooks.ts +38 -0
  307. package/src/discovery/claude.ts +142 -209
  308. package/src/discovery/codex-hooks.ts +38 -0
  309. package/src/discovery/codex.ts +132 -198
  310. package/src/discovery/helpers.ts +8 -1
  311. package/src/discovery/index.ts +2 -0
  312. package/src/discovery/mcp-compat.ts +234 -0
  313. package/src/discovery/mcp-json.ts +6 -0
  314. package/src/edit/renderer.ts +40 -0
  315. package/src/edit/streaming.ts +4 -2
  316. package/src/exec/bash-executor.ts +24 -20
  317. package/src/export/html/index.ts +51 -2
  318. package/src/extensibility/extensions/loader.ts +9 -1
  319. package/src/extensibility/extensions/runner.ts +17 -4
  320. package/src/extensibility/extensions/types.ts +31 -6
  321. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  322. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  323. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  324. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  325. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  326. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  327. package/src/extensibility/hooks/loader.ts +154 -10
  328. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  329. package/src/extensibility/skill-management.ts +368 -0
  330. package/src/extensibility/skills.ts +52 -16
  331. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  332. package/src/gjc-runtime/launch-worktree.ts +56 -2
  333. package/src/gjc-runtime/linux-proc.ts +7 -1
  334. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  335. package/src/gjc-runtime/session-state-lock.ts +992 -0
  336. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  337. package/src/gjc-runtime/state-writer.ts +39 -14
  338. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  339. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  340. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  341. package/src/gjc-runtime/workflow-settings.ts +596 -0
  342. package/src/goals/tools/goal-tool.ts +6 -1
  343. package/src/hooks/events.ts +291 -0
  344. package/src/hooks/native-skill-hook.ts +4 -0
  345. package/src/hooks/normalize.ts +369 -0
  346. package/src/hooks/skill-state.ts +165 -46
  347. package/src/internal-urls/docs-index.generated.ts +40 -32
  348. package/src/main.ts +32 -5
  349. package/src/migrate/mcp-mapper.ts +25 -2
  350. package/src/modes/acp/abort-scope.ts +29 -0
  351. package/src/modes/acp/acp-agent.ts +423 -98
  352. package/src/modes/components/assistant-message.ts +6 -1
  353. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  354. package/src/modes/components/customization/import-wizard.ts +372 -0
  355. package/src/modes/components/customization/index.ts +1 -0
  356. package/src/modes/components/gajae-pet-widget.ts +539 -213
  357. package/src/modes/components/irc-sidebar.ts +11 -0
  358. package/src/modes/components/iterm-pet-transport.ts +585 -0
  359. package/src/modes/components/oauth-selector.ts +277 -86
  360. package/src/modes/components/pet-capability.ts +39 -40
  361. package/src/modes/components/settings-selector.ts +9 -0
  362. package/src/modes/components/status-line/gh.ts +45 -1
  363. package/src/modes/components/status-line/segments.ts +16 -1
  364. package/src/modes/components/tool-status-header.ts +13 -3
  365. package/src/modes/controllers/command-controller.ts +20 -8
  366. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  367. package/src/modes/controllers/input-controller.ts +10 -0
  368. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  369. package/src/modes/controllers/selector-controller.ts +177 -72
  370. package/src/modes/controllers/stt-controller.ts +3 -3
  371. package/src/modes/interactive-mode.ts +88 -16
  372. package/src/modes/print-mode.ts +35 -6
  373. package/src/modes/runtime-init.ts +21 -68
  374. package/src/modes/types.ts +1 -0
  375. package/src/modes/utils/injected-user-submission.ts +25 -0
  376. package/src/modes/utils/terminal-bell.ts +15 -1
  377. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  378. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  379. package/src/prompts/agents/critic.md +1 -1
  380. package/src/prompts/system/rlm-research.md +1 -1
  381. package/src/prompts/system/system-prompt.md +0 -2
  382. package/src/prompts/tools/bash.md +7 -1
  383. package/src/prompts/tools/browser.md +1 -1
  384. package/src/prompts/tools/cron.md +6 -6
  385. package/src/prompts/tools/read.md +0 -2
  386. package/src/prompts/tools/skill-discovery.md +4 -2
  387. package/src/runtime-mcp/client.ts +427 -23
  388. package/src/runtime-mcp/config.ts +37 -5
  389. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  390. package/src/runtime-mcp/manager.ts +52 -2
  391. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  392. package/src/runtime-mcp/oauth-flow.ts +46 -1
  393. package/src/runtime-mcp/pool-key.ts +5 -1
  394. package/src/runtime-mcp/pool.test.ts +9 -0
  395. package/src/runtime-mcp/protocol.test.ts +353 -0
  396. package/src/runtime-mcp/protocol.ts +624 -0
  397. package/src/runtime-mcp/scope-config.ts +78 -0
  398. package/src/runtime-mcp/tool-bridge.ts +44 -10
  399. package/src/runtime-mcp/transports/http.ts +84 -13
  400. package/src/runtime-mcp/types.ts +123 -12
  401. package/src/sdk/acp/adapter.ts +328 -97
  402. package/src/sdk/broker/broker.ts +160 -741
  403. package/src/sdk/broker/ensure.ts +9 -1
  404. package/src/sdk/broker/lifecycle.ts +510 -116
  405. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  406. package/src/sdk/broker/process-incarnation.ts +19 -0
  407. package/src/sdk/broker/session-index.ts +516 -117
  408. package/src/sdk/broker/startup-failure.ts +28 -5
  409. package/src/sdk/broker/transport.ts +1 -13
  410. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  411. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  412. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  413. package/src/sdk/bus/config.ts +13 -0
  414. package/src/sdk/bus/conversation-store.ts +59 -171
  415. package/src/sdk/bus/daemon-paths.ts +20 -0
  416. package/src/sdk/bus/discord-conversation.ts +2 -0
  417. package/src/sdk/bus/discord-daemon.ts +524 -126
  418. package/src/sdk/bus/discord-live-provider.ts +206 -79
  419. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  420. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  421. package/src/sdk/bus/index.ts +2480 -456
  422. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  423. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  424. package/src/sdk/bus/managed-daemon.ts +2 -145
  425. package/src/sdk/bus/notification-service.ts +196 -0
  426. package/src/sdk/bus/operator-runtime.ts +1 -1
  427. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  428. package/src/sdk/bus/reconciliation-store.ts +553 -24
  429. package/src/sdk/bus/session-control.ts +49 -3
  430. package/src/sdk/bus/slack-conversation.ts +4 -0
  431. package/src/sdk/bus/slack-daemon.ts +733 -134
  432. package/src/sdk/bus/slack-live-provider.ts +65 -15
  433. package/src/sdk/bus/slack-provider.ts +4 -0
  434. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  435. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  436. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  437. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  438. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  439. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  440. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  441. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  442. package/src/sdk/bus/telegram-reference.ts +1 -226
  443. package/src/sdk/bus/telegram-setup.ts +2 -2
  444. package/src/sdk/bus/threaded-render.ts +1 -0
  445. package/src/sdk/bus/topic-registry.ts +279 -146
  446. package/src/sdk/cli/rows.ts +2 -35
  447. package/src/sdk/cli/session-cli.ts +776 -700
  448. package/src/sdk/client/client.ts +892 -1
  449. package/src/sdk/host/control/dispatch.ts +93 -58
  450. package/src/sdk/host/control/index.ts +1 -1
  451. package/src/sdk/host/control/operations.ts +18 -10
  452. package/src/sdk/host/control/runtime-gate.ts +40 -0
  453. package/src/sdk/host/host.ts +63 -12
  454. package/src/sdk/host/query/handlers.ts +66 -7
  455. package/src/sdk/host/reverse-leases.ts +2 -2
  456. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  457. package/src/sdk/host/session-runtime.test.ts +2787 -34
  458. package/src/sdk/host/session-runtime.ts +1489 -101
  459. package/src/sdk/host/surface-policy.ts +12 -8
  460. package/src/sdk/host/types.ts +1 -1
  461. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  462. package/src/sdk/host/websocket-transport.ts +85 -50
  463. package/src/sdk/index.ts +2 -1
  464. package/src/sdk/lifecycle/broker-client.ts +53 -0
  465. package/src/sdk/lifecycle/client.ts +234 -0
  466. package/src/sdk/lifecycle/index.ts +3 -0
  467. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  468. package/src/sdk/lifecycle/service.ts +720 -0
  469. package/src/sdk/mcp/server.ts +226 -188
  470. package/src/sdk/prompt-deadline-lease.ts +55 -0
  471. package/src/sdk/prompt-deadline-manager.ts +148 -0
  472. package/src/sdk/prompt-status.ts +11 -3
  473. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  474. package/src/sdk/protocol/operation-registry.ts +71 -8
  475. package/src/sdk/receipt-state.ts +43 -0
  476. package/src/sdk/reconciliation-extensions.ts +14 -0
  477. package/src/sdk/router/index.ts +1 -0
  478. package/src/sdk/router/session-router.ts +1658 -0
  479. package/src/sdk/session-list.ts +93 -0
  480. package/src/sdk/session-reconnect.ts +45 -1
  481. package/src/sdk/session.ts +716 -120
  482. package/src/sdk/transport/serve-cli.ts +18 -34
  483. package/src/session/account-inventory.ts +603 -0
  484. package/src/session/agent-session.ts +2857 -582
  485. package/src/session/agent-storage.ts +35 -0
  486. package/src/session/blob-store.ts +32 -1
  487. package/src/session/coordinator-tool-label.ts +40 -0
  488. package/src/session/internal/managed-session-scope.ts +4 -0
  489. package/src/session/internal/managed-session-storage.ts +173 -12
  490. package/src/session/messages.ts +1 -1
  491. package/src/session/secure-token-file.ts +80 -0
  492. package/src/session/session-manager.ts +730 -58
  493. package/src/session/session-storage.ts +25 -16
  494. package/src/session/startup-auth-config.ts +297 -0
  495. package/src/session/terminal-abort.ts +1253 -0
  496. package/src/session/yield-queue.ts +57 -15
  497. package/src/session-import/claude.ts +334 -0
  498. package/src/session-import/codex.ts +1033 -0
  499. package/src/session-import/command.ts +77 -0
  500. package/src/session-import/detect.ts +133 -0
  501. package/src/session-import/index.ts +7 -0
  502. package/src/session-import/provider-codex.ts +327 -0
  503. package/src/session-import/provider-service.ts +457 -0
  504. package/src/session-import/redact.ts +128 -0
  505. package/src/session-import/service.ts +1264 -0
  506. package/src/session-import/types.ts +148 -0
  507. package/src/setup/paseo/check.ts +213 -0
  508. package/src/setup/paseo/install-saga.ts +293 -0
  509. package/src/setup/paseo/json-publisher.ts +263 -0
  510. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  511. package/src/setup/paseo/paseo-ownership.ts +224 -0
  512. package/src/setup/paseo/paseo-setup.ts +253 -0
  513. package/src/setup/paseo/provider-config.ts +139 -0
  514. package/src/setup/paseo/remove.ts +183 -0
  515. package/src/setup/paseo/result-types.ts +79 -0
  516. package/src/setup/paseo/setup-deps.ts +175 -0
  517. package/src/setup/paseo/skills-bridge.ts +260 -0
  518. package/src/skill-state/active-state.ts +21 -11
  519. package/src/slash-commands/acp-builtins.ts +9 -1
  520. package/src/slash-commands/builtin-registry.ts +98 -16
  521. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  522. package/src/slash-commands/helpers/mcp.ts +9 -3
  523. package/src/slash-commands/helpers/usage-report.ts +61 -56
  524. package/src/slash-commands/types.ts +7 -2
  525. package/src/task/executor.ts +30 -11
  526. package/src/task/index.ts +66 -7
  527. package/src/task/render.ts +23 -9
  528. package/src/tools/ask-answer-registry.ts +23 -7
  529. package/src/tools/ask.ts +8 -4
  530. package/src/tools/bash-sleep-advisory.ts +115 -0
  531. package/src/tools/bash.ts +98 -18
  532. package/src/tools/browser/attach.ts +71 -3
  533. package/src/tools/browser/launch.ts +86 -13
  534. package/src/tools/browser/profile-discovery.ts +57 -7
  535. package/src/tools/browser/render.ts +8 -1
  536. package/src/tools/browser.ts +130 -21
  537. package/src/tools/computer.enforcement.test.ts +1 -1
  538. package/src/tools/computer.ts +3 -2
  539. package/src/tools/cron.ts +12 -1
  540. package/src/tools/image-gen.ts +27 -16
  541. package/src/tools/index.ts +8 -0
  542. package/src/tools/job.ts +33 -6
  543. package/src/tools/monitor.ts +41 -2
  544. package/src/tools/skill-discovery.ts +21 -29
  545. package/src/tools/subagent.ts +142 -24
  546. package/src/tools/tool-catalog.generated.ts +6 -6
  547. package/src/tools/ultragoal-ask-guard.ts +10 -3
  548. package/src/utils/herdr-pane.ts +345 -0
  549. package/src/utils/title-generator.ts +2 -0
  550. package/src/web/scrapers/index.ts +6 -4
  551. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  552. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  553. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  554. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  555. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  556. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  557. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  558. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  559. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  560. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  561. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  562. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  563. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  564. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  565. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  566. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  567. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  568. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  569. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  570. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  571. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  572. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  573. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  574. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  575. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  576. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  577. package/dist/types/session/auth-broker-config.d.ts +0 -13
  578. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  579. package/dist/types/skills/skills.test.d.ts +0 -1
  580. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  581. package/dist/types/task/token-log.test.d.ts +0 -1
  582. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  583. package/dist/types/tools/descriptors.test.d.ts +0 -1
  584. package/scripts/g005-daemon-smoke.ts +0 -180
  585. package/scripts/g011-daemon-path-smoke.ts +0 -153
  586. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  587. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  588. package/src/sdk/bus/telegram-cli.ts +0 -212
  589. package/src/sdk/elevation/allowlist.ts +0 -54
  590. package/src/sdk/elevation/audit.ts +0 -208
  591. package/src/sdk/elevation/capability.ts +0 -41
  592. package/src/sdk/elevation/digest.ts +0 -129
  593. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  594. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  595. package/src/sdk/elevation/owner.ts +0 -109
  596. package/src/session/auth-broker-config.ts +0 -109
  597. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  598. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  599. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -15,6 +15,7 @@ import type {
15
15
  TextContent,
16
16
  Usage,
17
17
  } from "@gajae-code/ai/core";
18
+ import { hasAdjacentPrivateThinkingBlocks } from "@gajae-code/ai/core";
18
19
  import type * as native from "@gajae-code/natives";
19
20
 
20
21
  function nativeSessionManager(): typeof import("@gajae-code/natives") {
@@ -39,6 +40,7 @@ import {
39
40
  Snowflake,
40
41
  toError,
41
42
  } from "@gajae-code/utils";
43
+ import { EDIT_SNAPSHOT_EXTERNALIZED_NOTICE, editSnapshotReceipt } from "../edit/renderer";
42
44
  import type { TtsrInjectionRecord } from "../export/ttsr";
43
45
  import { assertSafePathComponent } from "../gjc-runtime/session-layout";
44
46
  import { writeTextAtomic } from "../gjc-runtime/state-writer";
@@ -89,9 +91,11 @@ import {
89
91
  captureManagedFileNoFollow,
90
92
  fsyncManagedArtifactTree,
91
93
  MANAGED_ARTIFACT_MAX_FILE_BYTES,
94
+ type ManagedAppendReceipt,
92
95
  type ManagedBoundedAppendExpectation,
93
96
  ManagedCommittedMutationError,
94
97
  type ManagedDirectoryRoot,
98
+ type ManagedFileIdentity,
95
99
  type ManagedFileSnapshot,
96
100
  ManagedSessionDescendantStore,
97
101
  type ManagedSessionSecurityPolicy,
@@ -1746,7 +1750,7 @@ async function writeOwnerOnlyFileNoReplace(filePath: string, content: Uint8Array
1746
1750
  await fsyncDirectoryPath(path.dirname(filePath));
1747
1751
  }
1748
1752
 
1749
- const MEMORY_GUARD_CHECKPOINT_FILE_MAX_BYTES = 64 * 1024 * 1024;
1753
+ const MEMORY_GUARD_CHECKPOINT_FILE_MAX_BYTES = 128 * 1024 * 1024;
1750
1754
  const MEMORY_GUARD_CHECKPOINT_BLOB_MAX_ENTRIES = 4096;
1751
1755
  const MEMORY_GUARD_CHECKPOINT_BLOB_TOTAL_MAX_BYTES = 64 * 1024 * 1024;
1752
1756
 
@@ -2042,6 +2046,49 @@ export class SessionAppendPersistenceError extends Error {
2042
2046
  }
2043
2047
  }
2044
2048
 
2049
+ /**
2050
+ * Typed near-limit append outcome (#4566).
2051
+ *
2052
+ * A live managed append that would cross the per-file transcript cap is now
2053
+ * preflighted: when the append alone cannot fit even after a full rewrite of
2054
+ * the live entries, this deterministic error replaces the generic
2055
+ * `SessionAppendPersistenceError: content_too_large` abort. It states whether
2056
+ * the in-memory entry was kept (so the just-committed source mutation keeps
2057
+ * its receipt on the next successful persist) and how to continue.
2058
+ */
2059
+ export class SessionNearLimitAppendError extends Error {
2060
+ readonly code = "near_limit_append" as const;
2061
+ /** Serialized size (bytes) of the entry that could not fit. */
2062
+ readonly entryBytes: number;
2063
+ /** Live-entry rewrite size (bytes) the recovery already attempted. */
2064
+ readonly liveBytes: number;
2065
+ /** Managed per-file cap in force when the append was rejected. */
2066
+ readonly capBytes: number;
2067
+ /** True when the entry remains in the resident list awaiting the next persist. */
2068
+ readonly entryRetained: boolean;
2069
+
2070
+ constructor(details: {
2071
+ entryBytes: number;
2072
+ liveBytes: number;
2073
+ capBytes: number;
2074
+ entryRetained: boolean;
2075
+ }) {
2076
+ super(
2077
+ [
2078
+ `near_limit_append: entry (${details.entryBytes} B) plus live transcript (${details.liveBytes} B) exceeds the managed per-file limit (${details.capBytes} B).`,
2079
+ details.entryRetained
2080
+ ? "The appended entry is retained in memory; its effect (including any committed source edit) is recorded and will persist on the next successful write. Compact the session (`/compact`) or export to a fresh session (`gjc export <session-file>`) before continuing."
2081
+ : "The appended entry was rolled back from memory; re-issue it after compacting the session (`/compact`) or exporting to a fresh session (`gjc export <session-file>`).",
2082
+ ].join(" "),
2083
+ );
2084
+ this.name = "SessionNearLimitAppendError";
2085
+ this.entryBytes = details.entryBytes;
2086
+ this.liveBytes = details.liveBytes;
2087
+ this.capBytes = details.capBytes;
2088
+ this.entryRetained = details.entryRetained;
2089
+ }
2090
+ }
2091
+
2045
2092
  export class SessionManagedStorageError extends Error {
2046
2093
  readonly code = "managed_storage_unsupported";
2047
2094
 
@@ -2628,9 +2675,10 @@ function measureJsonLikeBytes(value: unknown, visited = new Set<object>()): numb
2628
2675
 
2629
2676
  /** Measure one materialized {@link SessionContext} and throw when over the budget. */
2630
2677
  function assertSessionContextWithinBudget(context: SessionContext): SessionContext {
2678
+ const budgetBytes = effectiveSessionContextBudgetBytes();
2631
2679
  const measuredBytes = measureJsonLikeBytes(context);
2632
- if (measuredBytes > SESSION_CONTEXT_MATERIALIZATION_BUDGET_BYTES) {
2633
- throw new SessionContextTooLargeError(measuredBytes);
2680
+ if (measuredBytes > budgetBytes) {
2681
+ throw new SessionContextTooLargeError(measuredBytes, budgetBytes);
2634
2682
  }
2635
2683
  return context;
2636
2684
  }
@@ -3163,6 +3211,24 @@ function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: Mana
3163
3211
  left.bytes.equals(right.bytes)
3164
3212
  );
3165
3213
  }
3214
+
3215
+ function managedFileSnapshotMatchesDescriptor(snapshot: ManagedFileSnapshot, descriptor: DescriptorSnapshot): boolean {
3216
+ return (
3217
+ snapshot.identity.dev === descriptor.dev &&
3218
+ snapshot.identity.ino === descriptor.ino &&
3219
+ snapshot.identity.nlink === descriptor.nlink &&
3220
+ snapshot.identity.size === descriptor.size &&
3221
+ snapshot.identity.mtimeNs === descriptor.mtimeNs &&
3222
+ snapshot.identity.ctimeNs === descriptor.ctimeNs
3223
+ );
3224
+ }
3225
+
3226
+ function unrefDelay(ms: number): Promise<void> {
3227
+ const { promise, resolve } = Promise.withResolvers<void>();
3228
+ const timer = setTimeout(resolve, ms);
3229
+ timer.unref();
3230
+ return promise;
3231
+ }
3166
3232
  const trustedSessionDestinations = new WeakSet<SessionDestination>();
3167
3233
  const explicitProfileAgentDirs = new WeakMap<SessionDestination, string>();
3168
3234
  const managedSecurityPolicies = new WeakMap<ManagedSessionSecurityContext, ManagedSessionSecurityPolicy>();
@@ -3426,6 +3492,18 @@ function descriptorSnapshotAsStorageStat(descriptor: DescriptorSnapshot): Sessio
3426
3492
  return { ...descriptor, mtimeMs, mtime: new Date(mtimeMs), isFile: true };
3427
3493
  }
3428
3494
 
3495
+ function managedIdentityFromDescriptor(descriptor: DescriptorSnapshot): ManagedFileIdentity {
3496
+ if (descriptor.nlink === undefined) throw new Error("managed_identity_nlink_unavailable");
3497
+ return {
3498
+ dev: descriptor.dev,
3499
+ ino: descriptor.ino,
3500
+ nlink: descriptor.nlink,
3501
+ size: descriptor.size,
3502
+ mtimeNs: descriptor.mtimeNs,
3503
+ ctimeNs: descriptor.ctimeNs,
3504
+ };
3505
+ }
3506
+
3429
3507
  function retainedManagedInspectionStorage(
3430
3508
  storage: SessionStorage,
3431
3509
  store: ManagedSessionDescendantStore,
@@ -3901,6 +3979,18 @@ function revalidateTranscriptIdentityBounded(
3901
3979
  return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
3902
3980
  }
3903
3981
 
3982
+ function revalidateStrictResumeInspection(
3983
+ filePath: string,
3984
+ storage: SessionStorage,
3985
+ inspection: ResumeInspectionSnapshot,
3986
+ ): boolean {
3987
+ if (!storage.readRangeSync) {
3988
+ const inspected = inspectResumeSessionFile(filePath, storage);
3989
+ return !("kind" in inspected) && sameResumeIdentity(inspection.identity, inspected.identity);
3990
+ }
3991
+ return revalidateTranscriptIdentityBounded(filePath, storage, inspection.identity).kind === "valid";
3992
+ }
3993
+
3904
3994
  function createTranscriptSnapshotHandle(
3905
3995
  inspection: BoundedTranscriptInspection,
3906
3996
  filePath: string,
@@ -3967,8 +4057,9 @@ function createTranscriptSnapshotHandle(
3967
4057
  },
3968
4058
  materialize(): Uint8Array {
3969
4059
  if (closed) throw new Error("transcript_handle_closed");
3970
- if (size > SESSION_CONTEXT_MATERIALIZATION_BUDGET_BYTES - TRANSCRIPT_CAPTURE_CHUNK_BYTES) {
3971
- throw new SessionContextTooLargeError(size, SESSION_CONTEXT_MATERIALIZATION_BUDGET_BYTES);
4060
+ const budgetBytes = effectiveSessionContextBudgetBytes();
4061
+ if (size > budgetBytes - TRANSCRIPT_CAPTURE_CHUNK_BYTES) {
4062
+ throw new SessionContextTooLargeError(size, budgetBytes);
3972
4063
  }
3973
4064
  const output = Buffer.allocUnsafe(size);
3974
4065
  for (let offset = 0; offset < size; offset += TRANSCRIPT_CAPTURE_CHUNK_BYTES) {
@@ -4587,6 +4678,12 @@ async function movePathAcrossDevicesSafe(source: string, destination: string): P
4587
4678
 
4588
4679
  const MAX_PERSIST_CHARS = 500_000;
4589
4680
  const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
4681
+ /**
4682
+ * Inline cap for edit-result snapshot bodies (`EditToolDetails.oldText` /
4683
+ * `.newText` and per-file copies). Bodies up to this size persist verbatim;
4684
+ * larger ones persist as a digest receipt (#4566).
4685
+ */
4686
+ const EDIT_SNAPSHOT_INLINE_MAX_CHARS = 16 * 1024;
4590
4687
  /** Minimum base64 length to externalize to blob store (skip tiny inline images) */
4591
4688
  const BLOB_EXTERNALIZE_THRESHOLD = 1024;
4592
4689
  const TEXT_CONTENT_KEY = "content";
@@ -4714,6 +4811,77 @@ function truncateString(value: string, maxLength: number): string {
4714
4811
  return truncated;
4715
4812
  }
4716
4813
 
4814
+ /**
4815
+ * Bound durable edit-result snapshot bodies (#4566).
4816
+ *
4817
+ * A tiny edit to a large file used to persist the complete pre- and post-edit
4818
+ * file bodies in `EditToolDetails.oldText`/`newText` (and each
4819
+ * `perFileResults[]` copy), so every patch added ~2x file size to the managed
4820
+ * transcript and long sessions hit the 64 MiB per-file append limit mid-turn.
4821
+ *
4822
+ * Persisted edit results keep full bodies only under the inline cap; larger
4823
+ * bodies are replaced by a fixed-size receipt (`oldTextDigest`/`newTextDigest`
4824
+ * = byte length + SHA-256) plus a marker, so rendering, diagnostics, diffs,
4825
+ * paths, and source-change accounting stay intact while the durable cost per
4826
+ * edit is bounded independently of file size. Live in-process results are
4827
+ * untouched — ACP `diff` ToolCallContent still receives full bodies.
4828
+ */
4829
+ function boundEditSnapshotFields(value: unknown, visited: WeakSet<object>): unknown {
4830
+ if (typeof value !== "object" || value === null) return value;
4831
+ if (visited.has(value)) return value;
4832
+ visited.add(value);
4833
+
4834
+ const boundOne = (entry: Record<string, unknown>): Record<string, unknown> => {
4835
+ let changed = false;
4836
+ const next: Record<string, unknown> = { ...entry };
4837
+ const pairs: Array<["oldText", "oldTextDigest"] | ["newText", "newTextDigest"]> = [
4838
+ ["oldText", "oldTextDigest"],
4839
+ ["newText", "newTextDigest"],
4840
+ ];
4841
+ for (const [bodyKey, digestKey] of pairs) {
4842
+ const body = entry[bodyKey];
4843
+ if (typeof body !== "string" || body.length <= EDIT_SNAPSHOT_INLINE_MAX_CHARS) continue;
4844
+ const receipt = editSnapshotReceipt(body);
4845
+ if (receipt === undefined) continue;
4846
+ next[digestKey] = receipt;
4847
+ next[bodyKey] = EDIT_SNAPSHOT_EXTERNALIZED_NOTICE;
4848
+ changed = true;
4849
+ }
4850
+ return changed ? next : entry;
4851
+ };
4852
+
4853
+ if (Array.isArray(value)) {
4854
+ let changed = false;
4855
+ const result: unknown[] = new Array(value.length);
4856
+ for (let i = 0; i < value.length; i++) {
4857
+ const item = value[i];
4858
+ const bounded =
4859
+ typeof item === "object" && item !== null && !Array.isArray(item)
4860
+ ? boundOne(item as Record<string, unknown>)
4861
+ : item;
4862
+ result[i] = bounded === item ? boundEditSnapshotFields(item, visited) : bounded;
4863
+ if (result[i] !== item) changed = true;
4864
+ }
4865
+ return changed ? result : value;
4866
+ }
4867
+
4868
+ for (const [key, child] of Object.entries(value)) {
4869
+ if (child !== null && typeof child === "object" && !Array.isArray(child)) {
4870
+ const boundedChild = boundOne(child as Record<string, unknown>);
4871
+ if (boundedChild !== child) {
4872
+ return { ...value, [key]: boundedChild };
4873
+ }
4874
+ }
4875
+ }
4876
+ for (const [key, child] of Object.entries(value)) {
4877
+ const bounded = boundEditSnapshotFields(child, visited);
4878
+ if (bounded !== child) {
4879
+ return { ...(value as Record<string, unknown>), [key]: bounded };
4880
+ }
4881
+ }
4882
+ return boundOne(value as Record<string, unknown>);
4883
+ }
4884
+
4717
4885
  function isImageBlock(value: unknown): value is { type: "image"; data: string; mimeType?: string } {
4718
4886
  return (
4719
4887
  typeof value === "object" &&
@@ -5547,7 +5715,10 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
5547
5715
  }
5548
5716
 
5549
5717
  async function prepareEntryForPersistence(entry: FileEntry, blobStore: BlobStore): Promise<FileEntry> {
5550
- return truncateForPersistence(entry, blobStore);
5718
+ // Bound edit snapshots before the generic 500k string truncation so the
5719
+ // receipt hashes/lengths identify the exact source bodies, not truncated
5720
+ // prefixes of files larger than MAX_PERSIST_CHARS (#4566).
5721
+ return (await truncateForPersistence(boundEditSnapshotFieldsForEntry(entry), blobStore)) as FileEntry;
5551
5722
  }
5552
5723
 
5553
5724
  /**
@@ -5652,7 +5823,27 @@ function truncateForPersistenceSync(obj: unknown, blobStore: BlobStore, key?: st
5652
5823
  }
5653
5824
 
5654
5825
  function prepareEntryForPersistenceSync(entry: FileEntry, blobStore: BlobStore): FileEntry {
5655
- return truncateForPersistenceSync(entry, blobStore) as FileEntry;
5826
+ // Keep this ordering identical to the async path: snapshot receipts must be
5827
+ // computed from the complete body before generic persistence truncation.
5828
+ return truncateForPersistenceSync(boundEditSnapshotFieldsForEntry(entry), blobStore) as FileEntry;
5829
+ }
5830
+
5831
+ /**
5832
+ * Apply {@link boundEditSnapshotFields} to a persisted entry's edit-result
5833
+ * details. Only `message` entries with `role === "toolResult"` and a `details`
5834
+ * object can carry edit snapshots; everything else returns unchanged, so the
5835
+ * walk never touches unrelated entries (#4566).
5836
+ */
5837
+ function boundEditSnapshotFieldsForEntry(entry: FileEntry): FileEntry {
5838
+ if (entry.type !== "message") return entry;
5839
+ const message = entry.message;
5840
+ if (message === null || typeof message !== "object" || (message as { role?: unknown }).role !== "toolResult")
5841
+ return entry;
5842
+ const details = (message as { details?: unknown }).details;
5843
+ if (details === null || typeof details !== "object" || Array.isArray(details)) return entry;
5844
+ const bounded = boundEditSnapshotFields(details, new WeakSet());
5845
+ if (bounded === details) return entry;
5846
+ return { ...entry, message: { ...(message as object), details: bounded } } as FileEntry;
5656
5847
  }
5657
5848
 
5658
5849
  class NdjsonFileWriter {
@@ -6475,6 +6666,7 @@ interface SessionManagerStateSnapshot {
6475
6666
  sessionName: string | undefined;
6476
6667
  titleSource: "auto" | "user" | undefined;
6477
6668
  sessionFile: string | undefined;
6669
+ managedPersistExpectedIdentity: ManagedFileIdentity | undefined;
6478
6670
  flushed: boolean;
6479
6671
  ensuredOnDisk: boolean;
6480
6672
  needsFullRewriteOnNextPersist: boolean;
@@ -6495,6 +6687,15 @@ export const SessionManagerTestHooks: {
6495
6687
  afterForkTranscriptPublished?: () => void | Promise<void>;
6496
6688
  beforeEphemeralArtifactManagerInstall?: (dir: string) => void | Promise<void>;
6497
6689
  beforePersistPatchFence?: (attempt: number) => void;
6690
+ beforeStrictMissingCheck?: (filePath: string, storage: SessionStorage) => void;
6691
+ beforeManagedResumeAcceptance?: (filePath: string, storage: SessionStorage) => void;
6692
+ beforeManagedResumeReturn?: (filePath: string, storage: SessionStorage) => void;
6693
+ beforeManagedSourceStat?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
6694
+ beforeManagedMissingInit?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
6695
+ beforeManagedMissingPublish?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
6696
+ beforeManagedMissingReturn?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
6697
+ afterManagedMissingAssertion?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
6698
+ beforeManagedSwitchIdentity?: (filePath: string, storage: SessionStorage) => void | Promise<void>;
6498
6699
  /** Internal first-open GC strategy override; omitted means current. */
6499
6700
  firstOpenGcStrategy?: SessionMemoryGcStrategy;
6500
6701
  /** Internal first-open secondary-artifact mode override; omitted means auto. */
@@ -6511,6 +6712,8 @@ export const SessionManagerTestHooks: {
6511
6712
  lastSidecarInitError?: string;
6512
6713
  /** Test-only generated-ID cache capacity override. */
6513
6714
  coldIdHashMaxEntriesOverride?: number;
6715
+ /** Test-only session-context budget override (in-process; does not leak to subprocesses). */
6716
+ sessionContextBudgetBytesOverride?: number;
6514
6717
  } = {};
6515
6718
 
6516
6719
  function materializedCacheMaxBytes(): number {
@@ -6552,6 +6755,13 @@ function coldIdHashMaxEntries(): number {
6552
6755
  throw new RangeError(`coldIdHashMaxEntriesOverride must be between 1 and ${COLD_ID_HASH_CAPACITY}.`);
6553
6756
  return override;
6554
6757
  }
6758
+ function effectiveSessionContextBudgetBytes(): number {
6759
+ const override = SessionManagerTestHooks.sessionContextBudgetBytesOverride;
6760
+ if (override === undefined) return SESSION_CONTEXT_MATERIALIZATION_BUDGET_BYTES;
6761
+ if (!Number.isSafeInteger(override) || override < 1)
6762
+ throw new RangeError("sessionContextBudgetBytesOverride must be a positive safe integer.");
6763
+ return override;
6764
+ }
6555
6765
 
6556
6766
  function emptyFirstOpenTelemetry(
6557
6767
  strategy: SessionMemoryGcStrategy = "pressure",
@@ -6802,6 +7012,9 @@ export class SessionManager {
6802
7012
  #persistChain: Promise<void> = Promise.resolve();
6803
7013
  #persistError: Error | undefined;
6804
7014
  #persistErrorReported = false;
7015
+ /** Defense-in-depth (#4443): one-shot warn for adjacent private thinking blocks in persisted assistant transcripts. */
7016
+ #warnedAdjacentThinkingPersist = false;
7017
+ #closeRetryPending = false;
6805
7018
  /** Depth of the non-yielding same-session persistence fence (reentrancy counter). */
6806
7019
  #persistenceFenceDepth = 0;
6807
7020
  /** Publication fence counter carried by the mutable `.spill.commit` marker. */
@@ -6833,6 +7046,7 @@ export class SessionManager {
6833
7046
  #managedSidecarSecurityContext: ManagedSessionSecurityContext | undefined;
6834
7047
  #managedSidecarCacheSessionFile: string | undefined;
6835
7048
  #managedRangeExpectedDescriptor: DescriptorSnapshot | undefined;
7049
+ #managedPersistExpectedIdentity: ManagedFileIdentity | undefined;
6836
7050
  #boundedReadStorageProxy: SessionStorage | undefined;
6837
7051
  #boundedManagedSource:
6838
7052
  | { path: string; store: ManagedSessionDescendantStore; descriptor: DescriptorSnapshot; owned: boolean }
@@ -6897,6 +7111,7 @@ export class SessionManager {
6897
7111
  private readonly persist: boolean,
6898
7112
  storage: SessionStorage,
6899
7113
  private destination: SessionDestination = explicitDestination(sessionDir),
7114
+ skipEnsureSessionDir = false,
6900
7115
  ) {
6901
7116
  this.#storage = new Proxy(storage, {
6902
7117
  get: (target, property, receiver) => {
@@ -6999,7 +7214,7 @@ export class SessionManager {
6999
7214
  this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentTextBlobStore;
7000
7215
  this.#residentImageBlobStore = this.#blobStore;
7001
7216
  if (persist && sessionDir) {
7002
- this.#storage.ensureDirSync(sessionDir);
7217
+ if (!skipEnsureSessionDir) this.#storage.ensureDirSync(sessionDir);
7003
7218
  // Canonicalize the trusted session directory (single choke point for every
7004
7219
  // creation path: create/open/moveTo/fork/SDK) so benign ancestor symlinks
7005
7220
  // (e.g. macOS `/var -> /private/var`, a symlinked `$HOME`) are resolved to a
@@ -7448,6 +7663,7 @@ export class SessionManager {
7448
7663
  flushed: this.#flushed,
7449
7664
  ensuredOnDisk: this.#ensuredOnDisk,
7450
7665
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
7666
+ managedPersistExpectedIdentity: this.#managedPersistExpectedIdentity,
7451
7667
  fileEntries: [],
7452
7668
  materializedFileEntries: [],
7453
7669
  adoptedArtifactManager: this.#adoptedArtifactManager,
@@ -7482,6 +7698,7 @@ export class SessionManager {
7482
7698
  this.#flushed = snapshot.flushed;
7483
7699
  this.#ensuredOnDisk = snapshot.ensuredOnDisk;
7484
7700
  this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
7701
+ this.#managedPersistExpectedIdentity = snapshot.managedPersistExpectedIdentity;
7485
7702
  this.#adoptedArtifactManager = snapshot.adoptedArtifactManager;
7486
7703
  }
7487
7704
  captureState(): SessionManagerStateSnapshot {
@@ -7498,6 +7715,7 @@ export class SessionManager {
7498
7715
  flushed: this.#flushed,
7499
7716
  ensuredOnDisk: this.#ensuredOnDisk,
7500
7717
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
7718
+ managedPersistExpectedIdentity: this.#managedPersistExpectedIdentity,
7501
7719
  // Snapshot entry objects by reference: switch/reload replaces the active entry array,
7502
7720
  // so rollback does not need structured cloning of extension/custom details.
7503
7721
  fileEntries: [...this.#fileEntries],
@@ -7533,6 +7751,7 @@ export class SessionManager {
7533
7751
  this.#flushed = snapshot.flushed;
7534
7752
  this.#ensuredOnDisk = snapshot.ensuredOnDisk;
7535
7753
  this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
7754
+ this.#managedPersistExpectedIdentity = snapshot.managedPersistExpectedIdentity;
7536
7755
  if (!retainsPersistWriter) {
7537
7756
  this.#persistWriter = undefined;
7538
7757
  this.#persistWriterPath = undefined;
@@ -7581,6 +7800,7 @@ export class SessionManager {
7581
7800
  this.#titleSource = state.header.titleSource;
7582
7801
  this.#sessionFile = state.sessionFile;
7583
7802
  this.#managedRangeExpectedDescriptor = undefined;
7803
+ this.#managedPersistExpectedIdentity = undefined;
7584
7804
  this.#clearBoundedManagedSource();
7585
7805
  this.#flushed = false;
7586
7806
  this.#needsFullRewriteOnNextPersist = false;
@@ -7822,6 +8042,7 @@ export class SessionManager {
7822
8042
  this.#usageStatistics = commit.usageStatistics;
7823
8043
  this.#flushed = true;
7824
8044
  this.#ensuredOnDisk = true;
8045
+ this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(terminalDescriptor);
7825
8046
  this.#lazyReopenSucceeded = true;
7826
8047
  this.#lazyReopenFallbackReason = undefined;
7827
8048
  initialized = true;
@@ -7881,9 +8102,14 @@ export class SessionManager {
7881
8102
  for (let attempt = 0; attempt < 3000; attempt++) {
7882
8103
  const lock = this.#storage.acquireExclusiveLockSync(lockPath);
7883
8104
  if (lock) {
7884
- if (contended && this.#storage.existsSync(commitPath)) {
8105
+ try {
8106
+ if (contended && this.#storage.existsSync(commitPath)) {
8107
+ this.#releaseExclusiveLockWithRetry(lock);
8108
+ return "published";
8109
+ }
8110
+ } catch (error) {
7885
8111
  this.#releaseExclusiveLockWithRetry(lock);
7886
- return "published";
8112
+ throw error;
7887
8113
  }
7888
8114
  return lock;
7889
8115
  }
@@ -8003,6 +8229,7 @@ export class SessionManager {
8003
8229
  this.#lazyReopenFallbackReason = "bounded_first_open_descriptor_changed";
8004
8230
  return false;
8005
8231
  }
8232
+ this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(finalDescriptor);
8006
8233
  this.#sessionId = discovery.header.id;
8007
8234
  this.#sessionName = discovery.header.title;
8008
8235
  this.#titleSource = discovery.header.titleSource;
@@ -8686,9 +8913,24 @@ export class SessionManager {
8686
8913
  }
8687
8914
 
8688
8915
  /** Initialize with a specific session file (used by factory methods). */
8689
- async #initSessionFile(sessionFile: string, initializeMissing = false): Promise<void> {
8916
+ async #initSessionFile(
8917
+ sessionFile: string,
8918
+ initializeMissing = false,
8919
+ strictResume?: { inspection: ResumeInspectionSnapshot; storage: SessionStorage; reuseEntries?: boolean },
8920
+ requireExisting = false,
8921
+ deferPersistenceUntilAccepted = false,
8922
+ ): Promise<void> {
8690
8923
  let strictManagedFallbackEntries: FileEntry[] | undefined;
8924
+ let strictManagedFallbackMigrationApplied = false;
8691
8925
  const resolvedSessionFile = this.#storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
8926
+ const revalidateStrictResume = (): void => {
8927
+ if (
8928
+ strictResume &&
8929
+ !revalidateStrictResumeInspection(resolvedSessionFile, strictResume.storage, strictResume.inspection)
8930
+ )
8931
+ throw new Error("Could not open session: unstable");
8932
+ };
8933
+ revalidateStrictResume();
8692
8934
  const sidecarRoot =
8693
8935
  this.destination.kind === "managed"
8694
8936
  ? this.#managedSidecarRoot(resolvedSessionFile)
@@ -8710,6 +8952,7 @@ export class SessionManager {
8710
8952
  this.#storage.existsSync(`${sidecarRoot}/.session-memory.spill.commit`);
8711
8953
  if (boundedTranscriptAdmitted && (await this.#tryInitSessionFileFromSidecar(resolvedSessionFile))) {
8712
8954
  writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
8955
+ revalidateStrictResume();
8713
8956
  return;
8714
8957
  }
8715
8958
 
@@ -8719,9 +8962,16 @@ export class SessionManager {
8719
8962
  this.#sessionMemoryAutoDisabledReason = "sidecar_reload_failures";
8720
8963
  }
8721
8964
  writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
8965
+ revalidateStrictResume();
8722
8966
  return;
8723
8967
  }
8724
- if (initializeMissing && !this.#storage.existsSync(resolvedSessionFile)) {
8968
+ revalidateStrictResume();
8969
+ SessionManagerTestHooks.beforeStrictMissingCheck?.(resolvedSessionFile, this.#storage);
8970
+ const transcriptMissing =
8971
+ (initializeMissing || strictResume !== undefined || requireExisting) &&
8972
+ !this.#storage.existsSync(resolvedSessionFile);
8973
+ if ((strictResume || requireExisting) && transcriptMissing) throw new Error("Could not open session: unstable");
8974
+ if (initializeMissing && transcriptMissing) {
8725
8975
  const fresh = this.#freshSessionState(undefined, resolvedSessionFile);
8726
8976
  const prepared = this.#prepareFreshSessionTransition(fresh, "memory-fallback");
8727
8977
  this.#applyFreshSessionMetadata(fresh);
@@ -8733,7 +8983,9 @@ export class SessionManager {
8733
8983
  this.#ensuredOnDisk = true;
8734
8984
  return;
8735
8985
  }
8986
+ if (strictResume?.reuseEntries !== false) strictManagedFallbackEntries = strictResume?.inspection.entries;
8736
8987
  if (
8988
+ !strictResume &&
8737
8989
  this.destination.kind === "managed" &&
8738
8990
  this.#sessionMemoryMode === "enabled" &&
8739
8991
  process.platform !== "win32"
@@ -8741,10 +8993,12 @@ export class SessionManager {
8741
8993
  const inspected = inspectResumeSessionFile(resolvedSessionFile, this.#storage);
8742
8994
  if ("kind" in inspected) throw new Error(`Could not open session: ${inspected.reason}`);
8743
8995
  strictManagedFallbackEntries = inspected.entries;
8996
+ strictManagedFallbackMigrationApplied = inspected.migrationApplied;
8744
8997
  }
8745
8998
  const eagerStat = this.#statSync(resolvedSessionFile);
8746
8999
  if (eagerStat.size > EAGER_RESUME_TRANSCRIPT_MAX_BYTES) throw new SessionTranscriptOversizedError(eagerStat.size);
8747
9000
  const entries = strictManagedFallbackEntries ?? (await loadEntriesFromFile(resolvedSessionFile, this.#storage));
9001
+ revalidateStrictResume();
8748
9002
  if (entries.length === 0) {
8749
9003
  const fresh = this.#freshSessionState(undefined, resolvedSessionFile);
8750
9004
  const prepared = this.#prepareFreshSessionTransition(fresh, "memory-fallback");
@@ -8759,7 +9013,12 @@ export class SessionManager {
8759
9013
  }
8760
9014
  const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
8761
9015
  const sessionId = header?.id ?? createSessionId();
8762
- const migrationApplied = migrateToCurrentVersion(entries);
9016
+ const migrationApplied =
9017
+ strictResume && strictResume.reuseEntries !== false
9018
+ ? strictResume.inspection.migrationApplied
9019
+ : strictManagedFallbackEntries
9020
+ ? strictManagedFallbackMigrationApplied
9021
+ : migrateToCurrentVersion(entries);
8763
9022
  await resolveBlobRefsInEntries(entries, this.#blobStore);
8764
9023
  const prepared = this.#prepareResidentTextStoreTransition(
8765
9024
  {
@@ -8782,7 +9041,9 @@ export class SessionManager {
8782
9041
  writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
8783
9042
  this.#flushed = true;
8784
9043
  this.#ensuredOnDisk = true;
8785
- await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
9044
+ this.#adoptManagedPersistIdentity(resolvedSessionFile);
9045
+ if (!strictResume && !deferPersistenceUntilAccepted)
9046
+ await this.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
8786
9047
  if (publishedSidecarWasPresent && this.#lazyReopenAttempted && !this.#lazyReopenSucceeded) {
8787
9048
  this.#sessionMemoryMode = "shadow";
8788
9049
  this.#sessionMemoryAutoDisabledReason = "sidecar_reload_failures";
@@ -8820,6 +9081,7 @@ export class SessionManager {
8820
9081
  this.#commitResidentTextStoreTransition(prepared);
8821
9082
  this.#flushed = true;
8822
9083
  this.#ensuredOnDisk = true;
9084
+ this.#adoptManagedPersistIdentity(resolvedSessionFile);
8823
9085
  }
8824
9086
 
8825
9087
  /** Initialize with a new session (used by factory methods). */
@@ -8933,6 +9195,7 @@ export class SessionManager {
8933
9195
  titleSource: this.#titleSource,
8934
9196
  sessionFile: this.#sessionFile,
8935
9197
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
9198
+ managedPersistExpectedIdentity: this.#managedPersistExpectedIdentity,
8936
9199
  };
8937
9200
  this.#persistError = undefined;
8938
9201
  this.#persistErrorReported = false;
@@ -8944,6 +9207,8 @@ export class SessionManager {
8944
9207
  try {
8945
9208
  managedTransition?.adopt();
8946
9209
  writeTerminalBreadcrumb(this.cwd, resolvedSessionFile);
9210
+ await SessionManagerTestHooks.beforeManagedSwitchIdentity?.(resolvedSessionFile, this.#storage);
9211
+ this.#adoptManagedPersistIdentity(resolvedSessionFile);
8947
9212
  this.#commitResidentTextStoreTransition(prepared);
8948
9213
  } catch (error) {
8949
9214
  managedTransition?.rollback();
@@ -8952,6 +9217,7 @@ export class SessionManager {
8952
9217
  this.#titleSource = previous.titleSource;
8953
9218
  this.#sessionFile = previous.sessionFile;
8954
9219
  this.#needsFullRewriteOnNextPersist = previous.needsFullRewriteOnNextPersist;
9220
+ this.#managedPersistExpectedIdentity = previous.managedPersistExpectedIdentity;
8955
9221
  prepared.dispose();
8956
9222
  throw error;
8957
9223
  }
@@ -8984,6 +9250,7 @@ export class SessionManager {
8984
9250
  flushed: this.#flushed,
8985
9251
  needsFullRewriteOnNextPersist: this.#needsFullRewriteOnNextPersist,
8986
9252
  ensuredOnDisk: this.#ensuredOnDisk,
9253
+ managedPersistExpectedIdentity: this.#managedPersistExpectedIdentity,
8987
9254
  artifactManager: this.#artifactManager,
8988
9255
  artifactManagerSessionFile: this.#artifactManagerSessionFile,
8989
9256
  adoptedArtifactManager: this.#adoptedArtifactManager,
@@ -9007,6 +9274,7 @@ export class SessionManager {
9007
9274
  this.#flushed = previous.flushed;
9008
9275
  this.#needsFullRewriteOnNextPersist = previous.needsFullRewriteOnNextPersist;
9009
9276
  this.#ensuredOnDisk = previous.ensuredOnDisk;
9277
+ this.#managedPersistExpectedIdentity = previous.managedPersistExpectedIdentity;
9010
9278
  this.#artifactManager = previous.artifactManager;
9011
9279
  this.#artifactManagerSessionFile = previous.artifactManagerSessionFile;
9012
9280
  this.#adoptedArtifactManager = previous.adoptedArtifactManager;
@@ -9162,7 +9430,10 @@ export class SessionManager {
9162
9430
  );
9163
9431
  if (this.destination.kind === "managed") {
9164
9432
  const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
9165
- await this.#managedTranscriptStore(stage.sessionFile).replace(path.basename(stage.sessionFile), bytes);
9433
+ await this.#managedTranscriptStore(stage.sessionFile).publishNoReplace(
9434
+ path.basename(stage.sessionFile),
9435
+ bytes,
9436
+ );
9166
9437
  } else {
9167
9438
  const staleCleanupError = await this.#cleanupPreparedNewSessionPersistence(stage);
9168
9439
  if (staleCleanupError) throw staleCleanupError;
@@ -9466,6 +9737,7 @@ export class SessionManager {
9466
9737
  this.#artifactManagerSessionFile = null;
9467
9738
  this.#adoptedArtifactManager = null;
9468
9739
  this.#commitResidentTextStoreTransition(transition);
9740
+ if (stage.flushed) this.#adoptManagedPersistIdentity(stage.sessionFile);
9469
9741
  this.#retireEphemeralArtifacts();
9470
9742
  stage.committed = true;
9471
9743
  this.#preparedNewSessions.delete(stage);
@@ -9492,6 +9764,7 @@ export class SessionManager {
9492
9764
  }
9493
9765
  this.#preparedNewSessions.delete(stage);
9494
9766
  stage.discarded = true;
9767
+ if (this.#preparedNewSessions.size === 0 && this.#fileEntries.length === 0) this.#releaseOwnedManagedAuthority();
9495
9768
  }
9496
9769
 
9497
9770
  async #retryPreparedNewSessionCleanups(): Promise<void> {
@@ -9794,6 +10067,7 @@ export class SessionManager {
9794
10067
  this.#needsFullRewriteOnNextPersist = false;
9795
10068
  this.#ensuredOnDisk = true;
9796
10069
  this.#commitResidentTextStoreTransition(transition);
10070
+ this.#adoptManagedPersistIdentity(newSessionFile);
9797
10071
  return { oldSessionFile, newSessionFile };
9798
10072
  }
9799
10073
 
@@ -9975,7 +10249,7 @@ export class SessionManager {
9975
10249
  parentStore.removeTreeExpected(stagingName, stagingSnapshot);
9976
10250
  } catch (cleanupError) {
9977
10251
  const cleanupMessage = toError(cleanupError).message;
9978
- if (cleanupMessage !== "cleanup_pending") {
10252
+ if (cleanupMessage !== "cleanup_pending" && cleanupMessage !== "not_found") {
9979
10253
  throw new Error(`Failed to clean up managed fork artifacts: ${cleanupMessage}`, {
9980
10254
  cause: toError(error),
9981
10255
  });
@@ -10243,6 +10517,13 @@ export class SessionManager {
10243
10517
  managedMove && managedDestinationStore
10244
10518
  ? { directory: path.resolve(newSessionDir), store: managedDestinationStore }
10245
10519
  : null;
10520
+ // A transcript is only guaranteed to exist at the destination when the old file was
10521
+ // actually present and moved (hadSessionFile). Absent/fresh/deleted-source sessions
10522
+ // have no destination transcript yet, so adopting a strict expected identity here
10523
+ // would invent identity for a nonexistent file; defer adoption until the first real
10524
+ // publication (#writeEntriesAtomicallySync / #appendManagedRecordsSync) instead.
10525
+ if (hadSessionFile) this.#adoptManagedPersistIdentity(this.#sessionFile);
10526
+ else this.#managedPersistExpectedIdentity = undefined;
10246
10527
 
10247
10528
  const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
10248
10529
  try {
@@ -10279,6 +10560,10 @@ export class SessionManager {
10279
10560
  managedMove && managedSourceStore
10280
10561
  ? { directory: path.resolve(previousSessionDir), store: managedSourceStore }
10281
10562
  : null;
10563
+ // Same rationale as the forward path: only adopt strict identity when the source
10564
+ // transcript actually existed (and was thus restorable) before the failed move.
10565
+ if (hadSessionFile) this.#adoptManagedPersistIdentity(previousSessionFile);
10566
+ else this.#managedPersistExpectedIdentity = undefined;
10282
10567
  const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
10283
10568
  if (header) applyHeaderPatch(header, { cwd: previousCwd });
10284
10569
  this.#headerExportRevision++;
@@ -14095,6 +14380,20 @@ export class SessionManager {
14095
14380
  return classification;
14096
14381
  }
14097
14382
 
14383
+ #adoptManagedPersistIdentity(sessionFile = this.#sessionFile): void {
14384
+ if (this.destination.kind !== "managed" || !sessionFile) {
14385
+ this.#managedPersistExpectedIdentity = undefined;
14386
+ return;
14387
+ }
14388
+ const sessionDir = path.resolve(path.dirname(sessionFile));
14389
+ if (sessionDir !== path.resolve(this.destination.directory)) {
14390
+ this.#managedPersistExpectedIdentity = undefined;
14391
+ return;
14392
+ }
14393
+ const descriptor = this.#managedTranscriptStore(sessionFile).descriptorExpected(path.basename(sessionFile));
14394
+ if (!descriptor) throw new Error("managed_persist_identity_unavailable");
14395
+ this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(descriptor);
14396
+ }
14098
14397
  /** Current transcript descriptor, or null when unavailable. */
14099
14398
  #managedDescriptorSnapshotOrNull(): DescriptorSnapshot | null {
14100
14399
  const sessionFile = this.#sessionFile;
@@ -14127,7 +14426,23 @@ export class SessionManager {
14127
14426
  this.#withSessionPersistenceFenceSync(() => {
14128
14427
  if (this.destination.kind === "managed") {
14129
14428
  const bytes = Buffer.from(`${entries.map(entry => JSON.stringify(entry)).join("\n")}\n`, "utf8");
14130
- this.#managedTranscriptStore(sessionFile).replaceSync(path.basename(sessionFile), bytes);
14429
+ const store = this.#managedTranscriptStore(sessionFile);
14430
+ const relativePath = path.basename(sessionFile);
14431
+ if (this.#managedPersistExpectedIdentity) {
14432
+ try {
14433
+ store.replaceExpectedIdentitySync(relativePath, bytes, this.#managedPersistExpectedIdentity);
14434
+ } catch (err) {
14435
+ // A confirmed missing predecessor can be recreated from the complete
14436
+ // resident transcript. Any present-but-different identity still fails
14437
+ // closed so a concurrent successor is never overwritten.
14438
+ if (!isEnoent(err)) throw err;
14439
+ this.#managedPersistExpectedIdentity = undefined;
14440
+ store.replaceSync(relativePath, bytes);
14441
+ }
14442
+ } else store.replaceSync(relativePath, bytes);
14443
+ const descriptor = store.descriptorExpected(relativePath);
14444
+ if (!descriptor) throw new Error("managed_replace_identity_unavailable");
14445
+ this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(descriptor);
14131
14446
  this.#publishCommitMarkerFromCurrentTranscriptSync();
14132
14447
  return;
14133
14448
  }
@@ -14634,6 +14949,74 @@ export class SessionManager {
14634
14949
  this.#clearBoundedManagedSource();
14635
14950
  }
14636
14951
 
14952
+ #releaseRejectedOpenResources(): void {
14953
+ this.#releaseResidentTextStore();
14954
+ if (this.#preparedNewSessions.size === 0) this.#releaseOwnedManagedAuthority();
14955
+ this.#releaseClosedSessionState();
14956
+ }
14957
+
14958
+ async #retryRejectedOpenWriterCleanup(): Promise<void> {
14959
+ for (let attempt = 0; this.#persistWriter?.getCloseState() === "close_failed_retryable"; attempt++) {
14960
+ await unrefDelay(Math.min(100 * 2 ** Math.min(attempt, 6), 5_000));
14961
+ try {
14962
+ await this.#closePersistWriterInternal();
14963
+ } catch (error) {
14964
+ logger.warn("Retained rejected-open writer close retry failed", { error: toError(error).message });
14965
+ }
14966
+ }
14967
+ // The loop only exits once the writer is terminal (`closed` or the quarantined
14968
+ // `close_unknown`) or was already cleared by a successful close, so ownership of
14969
+ // the descriptor and of the rejected session's resident state always ends here.
14970
+ if (this.#persistWriter) {
14971
+ this.#persistWriter = undefined;
14972
+ this.#persistWriterPath = undefined;
14973
+ }
14974
+ this.#releaseRejectedOpenResources();
14975
+ }
14976
+
14977
+ async #discardRejectedOpenState(): Promise<void> {
14978
+ let closeError: Error | undefined;
14979
+ for (let attempt = 0; this.#persistWriter && attempt < 2; attempt++) {
14980
+ try {
14981
+ await this.#closePersistWriterInternal();
14982
+ } catch (error) {
14983
+ closeError = toError(error);
14984
+ if (this.#persistWriter?.getCloseState() !== "close_failed_retryable") break;
14985
+ }
14986
+ }
14987
+ const state = this.#persistWriter?.getCloseState();
14988
+ if (state === "closed") {
14989
+ // The OS close was dispatched and confirmed, so cleanup succeeded even though
14990
+ // `close()` rethrew a queued write/flush drain failure. Reporting that drain
14991
+ // error as a cleanup failure would mask the real resume rejection, so record
14992
+ // it and let the caller surface the resume error alone.
14993
+ this.#persistWriter = undefined;
14994
+ this.#persistWriterPath = undefined;
14995
+ this.#releaseRejectedOpenResources();
14996
+ if (closeError)
14997
+ logger.warn("Rejected-open writer drained with errors before a confirmed close", {
14998
+ error: closeError.message,
14999
+ });
15000
+ return;
15001
+ }
15002
+ if (state === "close_unknown") {
15003
+ this.#persistWriter = undefined;
15004
+ this.#persistWriterPath = undefined;
15005
+ this.#releaseRejectedOpenResources();
15006
+ throw closeError ?? new Error("Rejected open writer close outcome is unknown");
15007
+ }
15008
+ if (this.#persistWriter) {
15009
+ // Certified pre-dispatch failure: the descriptor is still owned, so the writer
15010
+ // and the resident state it guards are retained until a retry reaches a
15011
+ // terminal close. The retry loop keeps this manager reachable on its own.
15012
+ void this.#retryRejectedOpenWriterCleanup().catch(error => {
15013
+ logger.warn("Rejected-open writer cleanup retry loop failed", { error: toError(error).message });
15014
+ });
15015
+ throw closeError ?? new Error("Rejected open writer close remains retryable");
15016
+ }
15017
+ this.#releaseRejectedOpenResources();
15018
+ }
15019
+
14637
15020
  /** Close the persistent writer after flushing all pending data. */
14638
15021
  async close(): Promise<void> {
14639
15022
  // Drain any uncommitted prepared successors before releasing resources so
@@ -14645,19 +15028,34 @@ export class SessionManager {
14645
15028
  error: toError(error).message,
14646
15029
  });
14647
15030
  }
14648
- await this.#queuePersistTask(async () => {
14649
- if (this.#persistWriter) {
14650
- await this.#closePersistWriterInternal();
14651
- this.#flushed = true;
14652
- }
14653
- if (this.#needsFullRewriteOnNextPersist && !this.#readOnlyResume) await this.#rewriteFileContents();
14654
- });
14655
- this.#retireEphemeralArtifacts();
14656
- await this.#drainEphemeralArtifactCleanups();
15031
+ let closeError: unknown;
15032
+ let taskStarted = false;
15033
+ try {
15034
+ await this.#queuePersistTask(
15035
+ async () => {
15036
+ taskStarted = true;
15037
+ if (this.#persistWriter) {
15038
+ await this.#closePersistWriterInternal();
15039
+ this.#flushed = true;
15040
+ }
15041
+ if (this.#needsFullRewriteOnNextPersist && !this.#readOnlyResume) await this.#rewriteFileContents();
15042
+ },
15043
+ { ignoreError: this.#closeRetryPending },
15044
+ );
15045
+ this.#persistError = undefined;
15046
+ this.#persistErrorReported = false;
15047
+ this.#closeRetryPending = false;
15048
+ this.#retireEphemeralArtifacts();
15049
+ await this.#drainEphemeralArtifactCleanups();
15050
+ } catch (error) {
15051
+ closeError = error;
15052
+ if (taskStarted) this.#closeRetryPending = true;
15053
+ }
15054
+ const terminalError = closeError ?? this.#persistError;
15055
+ if (terminalError) throw terminalError;
14657
15056
  this.#releaseResidentTextStore();
14658
- this.#releaseOwnedManagedAuthority();
15057
+ if (this.#preparedNewSessions.size === 0) this.#releaseOwnedManagedAuthority();
14659
15058
  this.#releaseClosedSessionState();
14660
- if (this.#persistError) throw this.#persistError;
14661
15059
  }
14662
15060
  /** Flush while open, then strictly close; retryable close skips the invalid second flush. */
14663
15061
  async flushAndCloseStrict(): Promise<SessionManagerCloseOutcome> {
@@ -14720,7 +15118,7 @@ export class SessionManager {
14720
15118
  } catch (error) {
14721
15119
  outcome = { kind: "close_unknown", error: toError(error) };
14722
15120
  }
14723
- this.#releaseOwnedManagedAuthority();
15121
+ if (this.#preparedNewSessions.size === 0) this.#releaseOwnedManagedAuthority();
14724
15122
  this.#releaseClosedSessionState();
14725
15123
  }
14726
15124
  return outcome;
@@ -15646,9 +16044,25 @@ export class SessionManager {
15646
16044
  const store = this.#managedTranscriptStore(sessionFile);
15647
16045
  const relativePath = path.basename(sessionFile);
15648
16046
  const bytes = Buffer.from(`${records.map(record => JSON.stringify(record)).join("\n")}\n`, "utf8");
15649
- // R2.3: the post-operation receipt descriptor (retained or Darwin
15650
- // replacement) feeds the commit snapshot directly — no pathname stat.
15651
- const receipt = store.appendSync(relativePath, bytes);
16047
+ let receipt: ManagedAppendReceipt;
16048
+ if (this.#managedPersistExpectedIdentity) {
16049
+ try {
16050
+ receipt = store.appendExpectedIdentitySync(relativePath, bytes, this.#managedPersistExpectedIdentity);
16051
+ } catch (err) {
16052
+ const predecessorMissing = store.descriptorExpected(relativePath) === null;
16053
+ if (
16054
+ !isEnoent(err) &&
16055
+ (!(err instanceof Error) || err.message !== "managed_append_identity_mismatch" || !predecessorMissing)
16056
+ )
16057
+ throw err;
16058
+ // Appending only the new records would create a truncated transcript.
16059
+ // Recreate the missing file from the complete resident entry set instead.
16060
+ this.#managedPersistExpectedIdentity = undefined;
16061
+ this.#rewriteFileSync();
16062
+ return;
16063
+ }
16064
+ } else receipt = store.appendSync(relativePath, bytes);
16065
+ this.#managedPersistExpectedIdentity = receipt.identity;
15652
16066
  this.#publishSessionCommitMarkerSync(receipt.descriptor);
15653
16067
  });
15654
16068
  }
@@ -15806,9 +16220,62 @@ export class SessionManager {
15806
16220
  // in-memory entries, shrinking the file below the limit. The entry has
15807
16221
  // already been added to #fileEntries by #appendEntryWithinPersistenceFence.
15808
16222
  if (err instanceof Error && err.message === "content_too_large") {
15809
- this.#rewriteFileSync();
16223
+ // Typed near-limit contract (#4566): recover by rewriting only the
16224
+ // live in-memory entries, then verify the recovered file actually
16225
+ // holds the just-appended entry. When even the rewrite cannot fit
16226
+ // the entry (live content alone is at the cap), surface the typed
16227
+ // near-limit outcome instead of silently succeeding without the
16228
+ // receipt for an effect that already committed (e.g. a source edit).
16229
+ const entryBytes = (() => {
16230
+ try {
16231
+ const materialized = materializeResidentEntryForPersistenceSync(
16232
+ entry,
16233
+ this.#residentBlobStores(),
16234
+ new Map(),
16235
+ );
16236
+ return Buffer.byteLength(
16237
+ `${JSON.stringify(prepareEntryForPersistenceSync(materialized, this.#blobStore))}\n`,
16238
+ "utf8",
16239
+ );
16240
+ } catch {
16241
+ return 0;
16242
+ }
16243
+ })();
16244
+ const liveBytesBefore = this.getTranscriptFileBytes();
16245
+ try {
16246
+ this.#rewriteFileSync();
16247
+ } catch (rewriteError) {
16248
+ // The recovery rewrite itself failed. The appended entry stays in
16249
+ // the resident list (its effect, including any committed source
16250
+ // edit, is not lost), but the receipt is not durable yet: report
16251
+ // the typed near-limit outcome instead of an unclassified abort.
16252
+ if (rewriteError instanceof Error && rewriteError.message === "content_too_large") {
16253
+ this.#needsFullRewriteOnNextPersist = true;
16254
+ throw new SessionNearLimitAppendError({
16255
+ entryBytes,
16256
+ liveBytes: liveBytesBefore,
16257
+ capBytes: MANAGED_ARTIFACT_MAX_FILE_BYTES,
16258
+ entryRetained: this.#byId.has(entry.id),
16259
+ });
16260
+ }
16261
+ throw rewriteError;
16262
+ }
15810
16263
  if (publishResumeBreadcrumb) writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
15811
16264
  this.#readOnlyResume = false;
16265
+ // Post-rewrite verification: a rewrite that still cannot fit the
16266
+ // entry leaves an effect/receipt gap and must be reported, never
16267
+ // silently swallowed as a successful append.
16268
+ const liveBytesAfter = this.getTranscriptFileBytes();
16269
+ const entryRetained = liveBytesAfter <= MANAGED_ARTIFACT_MAX_FILE_BYTES && this.#byId.has(entry.id);
16270
+ if (!entryRetained) {
16271
+ this.#needsFullRewriteOnNextPersist = true;
16272
+ throw new SessionNearLimitAppendError({
16273
+ entryBytes,
16274
+ liveBytes: liveBytesAfter || liveBytesBefore,
16275
+ capBytes: MANAGED_ARTIFACT_MAX_FILE_BYTES,
16276
+ entryRetained: this.#byId.has(entry.id),
16277
+ });
16278
+ }
15812
16279
  return;
15813
16280
  }
15814
16281
  this.#recordPersistError(err);
@@ -15816,6 +16283,43 @@ export class SessionManager {
15816
16283
  }
15817
16284
  }
15818
16285
 
16286
+ /**
16287
+ * Defense-in-depth (#4443): detect directly adjacent thinking/redacted_thinking
16288
+ * blocks in a persisted assistant transcript message. Non-mutating read-only
16289
+ * diagnostic that fires at most once per session manager instance. Bounded to
16290
+ * development/test builds only via an explicit environment gate so normal
16291
+ * production never emits the diagnostic or transcript-path metadata.
16292
+ *
16293
+ * Only Anthropic-origin assistant messages are inspected: the adjacency class
16294
+ * is specific to the Anthropic wire contract (#4416), and a provider-neutral
16295
+ * check would false-positive on OpenAI Responses assemblies that emit one
16296
+ * internal `thinking` block per reasoning output item.
16297
+ *
16298
+ * Names only the envelope shape (content block count), never raw thinking
16299
+ * text, signatures, or redacted payloads.
16300
+ */
16301
+ #warnAdjacentPrivateThinking(message: Parameters<typeof this.appendMessage>[0]): void {
16302
+ if (this.#warnedAdjacentThinkingPersist) return;
16303
+ // Explicit environment gate: production builds must not emit this
16304
+ // diagnostic or transcript-path metadata. `NODE_ENV` is the canonical
16305
+ // build-mode signal; the gate is robust to undefined (default: silent).
16306
+ if (process.env.NODE_ENV !== "development" && process.env.NODE_ENV !== "test") return;
16307
+ // Only assistant messages carry content block arrays with thinking blocks.
16308
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) return;
16309
+ // Restrict to Anthropic-origin: the adjacency rejection is specific to the
16310
+ // Anthropic replay wire contract. Other providers tolerate adjacent
16311
+ // reasoning blocks, so a provider-neutral check would be a false positive.
16312
+ if (message.api !== "anthropic-messages") return;
16313
+ if (!hasAdjacentPrivateThinkingBlocks(message.content)) return;
16314
+ this.#warnedAdjacentThinkingPersist = true;
16315
+ logger.warn("Session transcript: persisted assistant message has adjacent thinking blocks", {
16316
+ role: "assistant",
16317
+ provider: message.provider,
16318
+ contentBlockCount: message.content.length,
16319
+ hasAdjacentPrivateBlocks: true,
16320
+ });
16321
+ }
16322
+
15819
16323
  #appendEntry(entry: SessionEntry): void {
15820
16324
  this.#withSessionPersistenceFenceSync(() => this.#appendEntryWithinPersistenceFence(entry));
15821
16325
  }
@@ -15874,6 +16378,16 @@ export class SessionManager {
15874
16378
  this.#needsFullRewriteOnNextPersist = true;
15875
16379
  throw new SessionAppendPersistenceError("current_append", residentEntry.id, this.#persistError ?? error);
15876
16380
  }
16381
+ // Typed near-limit recovery (#4566) already ran its deterministic
16382
+ // rewrite inside _persist and deliberately kept the entry resident so
16383
+ // the committed effect keeps its receipt. Do not roll it back or wrap
16384
+ // it into a generic SessionAppendPersistenceError: propagate the typed
16385
+ // outcome with its structured fields intact.
16386
+ if (error instanceof SessionNearLimitAppendError) {
16387
+ if (sidecarAppendCharge > 0) activeRuntime?.accountant.release(sidecarAppendCharge);
16388
+ if (sidecarTailCharge > 0) activeRuntime?.tailCache.release(sidecarTailCharge);
16389
+ throw error;
16390
+ }
15877
16391
  const removed = this.#fileEntries.pop();
15878
16392
  if (removed !== residentEntry)
15879
16393
  throw new Error("Session append rollback lost resident ordering.", { cause: error });
@@ -16031,6 +16545,12 @@ export class SessionManager {
16031
16545
  message,
16032
16546
  };
16033
16547
  associateSessionMessageEntryId(message, entry.id);
16548
+ // Defense-in-depth (#4443): detect directly adjacent thinking/redacted_thinking
16549
+ // blocks in a persisted assistant transcript message and warn once per session.
16550
+ // This is a read-only observation — storage is NEVER mutated. The diagnostic is
16551
+ // bounded to development/test to avoid production noise, and names only the
16552
+ // envelope shape (block count), never raw thinking text, signatures, or payloads.
16553
+ this.#warnAdjacentPrivateThinking(message);
16034
16554
  this.#appendEntry(entry);
16035
16555
  const residentEntry = this.#byId.get(entry.id);
16036
16556
  if (residentEntry?.type === "message") transferSessionMessageIdentity([message], [residentEntry.message]);
@@ -17460,6 +17980,7 @@ export class SessionManager {
17460
17980
  this.#sessionFile = newSessionFile;
17461
17981
  this.#flushed = true;
17462
17982
  this.#commitResidentTextStoreTransition(transition);
17983
+ this.#adoptManagedPersistIdentity(newSessionFile);
17463
17984
  return newSessionFile;
17464
17985
  }
17465
17986
 
@@ -18156,6 +18677,7 @@ export class SessionManager {
18156
18677
  throw error;
18157
18678
  }
18158
18679
  }
18680
+ let retainedFallbackSnapshot: ManagedFileSnapshot | undefined;
18159
18681
  let sourceSize: number | undefined;
18160
18682
  try {
18161
18683
  sourceSize = boundedStorage.statSync(managedSourcePath).size;
@@ -18171,12 +18693,32 @@ export class SessionManager {
18171
18693
  )
18172
18694
  return manager;
18173
18695
  }
18696
+ if (manager.#boundedManagedSource && sourceSize <= eagerHydrationMaxBytes()) {
18697
+ const boundedSource = manager.#boundedManagedSource;
18698
+ retainedFallbackSnapshot = boundedSource.store.readExpected(path.basename(managedSourcePath)) ?? undefined;
18699
+ const terminalDescriptor = boundedSource.store.descriptorExpected(path.basename(managedSourcePath));
18700
+ if (
18701
+ !retainedFallbackSnapshot ||
18702
+ !terminalDescriptor ||
18703
+ !sameDescriptor(boundedSource.descriptor, terminalDescriptor) ||
18704
+ !managedFileSnapshotMatchesDescriptor(retainedFallbackSnapshot, boundedSource.descriptor)
18705
+ )
18706
+ throw new Error("source_changed");
18707
+ }
18174
18708
  } finally {
18175
18709
  manager.#clearBoundedManagedSource();
18176
18710
  }
18177
18711
  if (sourceSize === undefined) throw new Error("Managed source size unavailable");
18178
18712
  if (sourceSize > eagerHydrationMaxBytes()) throw new SessionTranscriptOversizedError(sourceSize);
18179
- const forkEntries = await loadEntriesFromFile(managedSourcePath, storage);
18713
+ const forkEntries = retainedFallbackSnapshot
18714
+ ? (() => {
18715
+ const entries = parseSessionEntries(
18716
+ new TextDecoder("utf-8", { fatal: true }).decode(retainedFallbackSnapshot.bytes),
18717
+ );
18718
+ const header = entries[0] as SessionHeader | undefined;
18719
+ return header?.type === "session" && typeof header.id === "string" ? entries : [];
18720
+ })()
18721
+ : await loadEntriesFromFile(managedSourcePath, storage);
18180
18722
  migrateToCurrentVersion(forkEntries);
18181
18723
  await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
18182
18724
  const sourceHeader = forkEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
@@ -18298,17 +18840,46 @@ export class SessionManager {
18298
18840
  destination,
18299
18841
  );
18300
18842
  manager.#sessionMemoryMode = sessionMemoryMode;
18301
- await manager.#initSessionFile(filePath);
18302
- if (strictSmallInspection) {
18303
- const revalidated = revalidateTranscriptIdentityBounded(filePath, storage, strictSmallInspection.identity);
18304
- if (revalidated.kind !== "valid" || manager.#sessionId !== strictSmallInspection.identity.sessionId) {
18305
- manager.setSessionMemoryMode("off");
18306
- await manager.close();
18307
- throw new Error("Could not open session: unstable");
18843
+ try {
18844
+ await manager.#initSessionFile(
18845
+ filePath,
18846
+ false,
18847
+ strictSmallInspection ? { inspection: strictSmallInspection, storage, reuseEntries: false } : undefined,
18848
+ );
18849
+ if (strictSmallInspection) {
18850
+ if (
18851
+ !revalidateStrictResumeInspection(filePath, storage, strictSmallInspection) ||
18852
+ manager.#sessionId !== strictSmallInspection.identity.sessionId
18853
+ )
18854
+ throw new Error("Could not open session: unstable");
18855
+ await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
18308
18856
  }
18857
+ manager.buildSessionContext();
18858
+ return manager;
18859
+ } catch (error) {
18860
+ manager.setSessionMemoryMode("off");
18861
+ for (const sidecarPath of manager.#disposableSidecarPaths()) {
18862
+ if (!sidecarPath) continue;
18863
+ try {
18864
+ manager.#storage.unlinkSync(sidecarPath);
18865
+ } catch (cleanupError) {
18866
+ if (!isEnoent(cleanupError))
18867
+ logger.warn("Rejected explicit resume sidecar cleanup failed", {
18868
+ error: toError(cleanupError).message,
18869
+ });
18870
+ }
18871
+ }
18872
+ manager.#sidecarRuntime = undefined;
18873
+ try {
18874
+ await manager.#discardRejectedOpenState();
18875
+ } catch (cleanupError) {
18876
+ throw new AggregateError(
18877
+ [toError(error), toError(cleanupError)],
18878
+ "Rejected explicit resume cleanup failed",
18879
+ );
18880
+ }
18881
+ throw error;
18309
18882
  }
18310
- manager.buildSessionContext();
18311
- return manager;
18312
18883
  }
18313
18884
  const sameManagedDirectory = path.dirname(path.resolve(filePath)) === path.resolve(destination.directory);
18314
18885
  let managedInspectionStore: ManagedSessionDescendantStore | undefined;
@@ -18319,16 +18890,68 @@ export class SessionManager {
18319
18890
  }
18320
18891
  let managedSourceSize: number | undefined;
18321
18892
  try {
18893
+ await SessionManagerTestHooks.beforeManagedSourceStat?.(filePath, managedInspectionStorage);
18322
18894
  managedSourceSize = managedInspectionStorage.statSync(filePath).size;
18323
- } catch {
18324
- // Strict inspection below reports the stable failure.
18895
+ } catch (error) {
18896
+ if (!isEnoent(error)) {
18897
+ managedInspectionStore?.close();
18898
+ throw error;
18899
+ }
18900
+ if (managedInspectionStore) {
18901
+ managedInspectionStore.assertBound();
18902
+ await SessionManagerTestHooks.beforeManagedMissingInit?.(filePath, managedInspectionStorage);
18903
+ managedInspectionStore.assertBound();
18904
+ const manager = new SessionManager(
18905
+ getProjectDir(),
18906
+ destination.directory,
18907
+ true,
18908
+ storage,
18909
+ destination,
18910
+ true,
18911
+ );
18912
+ manager.#sessionMemoryMode = sessionMemoryMode;
18913
+ try {
18914
+ await SessionManagerTestHooks.beforeManagedMissingPublish?.(filePath, managedInspectionStorage);
18915
+ managedInspectionStore.assertBound();
18916
+ await SessionManagerTestHooks.afterManagedMissingAssertion?.(filePath, managedInspectionStorage);
18917
+ const fresh = manager.#freshSessionState(undefined, filePath);
18918
+ const prepared = manager.#prepareFreshSessionTransition(fresh, "memory-fallback");
18919
+ manager.#applyFreshSessionMetadata(fresh);
18920
+ manager.#commitResidentTextStoreTransition(prepared, false);
18921
+ manager.#retireEphemeralArtifacts();
18922
+ const content = `${JSON.stringify(prepareEntryForPersistenceSync(fresh.header, manager.#blobStore))}\n`;
18923
+ managedInspectionStore.publishNoReplaceSync(path.basename(filePath), Buffer.from(content, "utf8"));
18924
+ const publishedTranscript = managedInspectionStore.readExpected(path.basename(filePath));
18925
+ if (publishedTranscript?.bytes?.equals(Buffer.from(content, "utf8")) !== true)
18926
+ throw new Error("Could not open session: unstable");
18927
+ if (manager.#effectiveSessionMemoryMode() !== "off") {
18928
+ manager.#buildDisposableSidecars(manager.#fileEntries);
18929
+ if (manager.#coldSidecarActive()) manager.#retireColdEntries();
18930
+ }
18931
+ manager.#flushed = true;
18932
+ manager.#ensuredOnDisk = true;
18933
+ await SessionManagerTestHooks.beforeManagedMissingReturn?.(filePath, managedInspectionStorage);
18934
+ const returnTranscript = managedInspectionStore.readExpected(path.basename(filePath));
18935
+ if (!returnTranscript || !managedFileSnapshotEquals(returnTranscript, publishedTranscript))
18936
+ throw new Error("Could not open session: unstable");
18937
+ manager.#managedPersistExpectedIdentity = returnTranscript.identity;
18938
+ writeTerminalBreadcrumb(manager.cwd, filePath);
18939
+ return manager;
18940
+ } catch (createError) {
18941
+ await manager.#discardRejectedOpenState();
18942
+ throw createError;
18943
+ } finally {
18944
+ managedInspectionStore.close();
18945
+ }
18946
+ }
18947
+ // Cross-directory missing candidates continue through the migration path below.
18325
18948
  }
18326
18949
  const managedResumeBounded =
18327
- sessionMemoryMode === "enabled" ||
18328
- (sessionMemoryMode === "auto" &&
18329
- process.platform !== "win32" &&
18330
- managedSourceSize !== undefined &&
18331
- managedSourceSize >= autoModeMinTranscriptBytes());
18950
+ managedSourceSize !== undefined &&
18951
+ (sessionMemoryMode === "enabled" ||
18952
+ (sessionMemoryMode === "auto" &&
18953
+ process.platform !== "win32" &&
18954
+ managedSourceSize >= autoModeMinTranscriptBytes()));
18332
18955
  let strictManagedSmallInspection: ResumeInspectionSnapshot | undefined;
18333
18956
  if (
18334
18957
  managedResumeBounded &&
@@ -18345,32 +18968,78 @@ export class SessionManager {
18345
18968
  throw error;
18346
18969
  }
18347
18970
  }
18971
+ const managedBoundedDescriptor =
18972
+ managedResumeBounded && sameManagedDirectory
18973
+ ? managedInspectionStore?.descriptorExpected(path.basename(filePath))
18974
+ : undefined;
18975
+ if (managedResumeBounded && sameManagedDirectory && !managedBoundedDescriptor) {
18976
+ managedInspectionStore?.close();
18977
+ throw new Error("Could not open session: unstable");
18978
+ }
18348
18979
 
18349
18980
  if (managedResumeBounded && sameManagedDirectory) {
18350
18981
  const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
18351
18982
  manager.#sessionMemoryMode = sessionMemoryMode;
18352
18983
  try {
18353
- await manager.#initSessionFile(filePath, true);
18984
+ await manager.#initSessionFile(
18985
+ filePath,
18986
+ true,
18987
+ strictManagedSmallInspection
18988
+ ? { inspection: strictManagedSmallInspection, storage: managedInspectionStorage }
18989
+ : undefined,
18990
+ true,
18991
+ true,
18992
+ );
18993
+ SessionManagerTestHooks.beforeManagedResumeAcceptance?.(filePath, managedInspectionStorage);
18994
+ const terminalManagedDescriptor = managedInspectionStore?.descriptorExpected(path.basename(filePath));
18995
+ if (
18996
+ !managedBoundedDescriptor ||
18997
+ !terminalManagedDescriptor ||
18998
+ !sameDescriptor(managedBoundedDescriptor, terminalManagedDescriptor)
18999
+ )
19000
+ throw new Error("Could not open session: unstable");
18354
19001
  if (strictManagedSmallInspection) {
18355
- const revalidated = revalidateTranscriptIdentityBounded(
18356
- filePath,
18357
- managedInspectionStorage,
18358
- strictManagedSmallInspection.identity,
18359
- );
18360
19002
  if (
18361
- revalidated.kind !== "valid" ||
19003
+ !revalidateStrictResumeInspection(filePath, managedInspectionStorage, strictManagedSmallInspection) ||
18362
19004
  manager.#sessionId !== strictManagedSmallInspection.identity.sessionId
18363
19005
  ) {
18364
19006
  manager.setSessionMemoryMode("off");
18365
19007
  throw new Error("Could not open session: unstable");
18366
19008
  }
18367
19009
  }
19010
+ if (manager.#sanitizeLoadedOpenAIResponsesReplayMetadata().length > 0)
19011
+ manager.#needsFullRewriteOnNextPersist = true;
19012
+ SessionManagerTestHooks.beforeManagedResumeReturn?.(filePath, managedInspectionStorage);
19013
+ const returnManagedDescriptor = managedInspectionStore?.descriptorExpected(path.basename(filePath));
19014
+ if (!returnManagedDescriptor || !sameDescriptor(managedBoundedDescriptor, returnManagedDescriptor))
19015
+ throw new Error("Could not open session: unstable");
19016
+ manager.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(returnManagedDescriptor);
18368
19017
  const header = manager.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
18369
19018
  if (header?.cwd) manager.cwd = header.cwd;
18370
19019
  manager.buildSessionContext();
18371
19020
  return manager;
18372
19021
  } catch (error) {
18373
- await manager.close().catch(() => {});
19022
+ manager.setSessionMemoryMode("off");
19023
+ for (const sidecarPath of manager.#disposableSidecarPaths()) {
19024
+ if (!sidecarPath) continue;
19025
+ try {
19026
+ manager.#storage.unlinkSync(sidecarPath);
19027
+ } catch (cleanupError) {
19028
+ if (!isEnoent(cleanupError))
19029
+ logger.warn("Rejected managed resume sidecar cleanup failed", {
19030
+ error: toError(cleanupError).message,
19031
+ });
19032
+ }
19033
+ }
19034
+ manager.#sidecarRuntime = undefined;
19035
+ try {
19036
+ await manager.#discardRejectedOpenState();
19037
+ } catch (cleanupError) {
19038
+ throw new AggregateError(
19039
+ [toError(error), toError(cleanupError)],
19040
+ "Rejected managed resume cleanup failed",
19041
+ );
19042
+ }
18374
19043
  throw error;
18375
19044
  } finally {
18376
19045
  managedInspectionStore?.close();
@@ -18462,6 +19131,7 @@ export class SessionManager {
18462
19131
  const finalDescriptor = store.descriptorExpected(path.basename(resolved));
18463
19132
  if (!finalDescriptor || !sameDescriptor(capturedDescriptor, finalDescriptor))
18464
19133
  throw new Error("source_changed");
19134
+ boundedManager.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(finalDescriptor);
18465
19135
  boundedManager.buildSessionContext();
18466
19136
  return boundedManager;
18467
19137
  }
@@ -18901,6 +19571,7 @@ export class SessionManager {
18901
19571
  manager.destination = destination;
18902
19572
  manager.#sessionFile = finalSessionFile;
18903
19573
  manager.#commitResidentTextStoreTransition(finalTransition);
19574
+ manager.#adoptManagedPersistIdentity(finalSessionFile);
18904
19575
  } catch (error) {
18905
19576
  finalTransition.dispose();
18906
19577
  throw error;
@@ -19251,6 +19922,7 @@ export class SessionManager {
19251
19922
  this.#sessionFile = promotionPath;
19252
19923
  this.#recoveryPromotionTranscriptPath = undefined;
19253
19924
  this.#commitResidentTextStoreTransition(transition);
19925
+ this.#adoptManagedPersistIdentity(promotionPath);
19254
19926
  writeTerminalBreadcrumb(this.cwd, promotionPath);
19255
19927
  this.#recoveryHydrationContext = undefined;
19256
19928
  }