@kortix/sdk 0.13.4 → 0.13.7

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 (380) hide show
  1. package/README.md +63 -6
  2. package/dist/browser/cache/idb-sync-cache.d.ts +19 -1
  3. package/dist/browser/cache/idb-sync-cache.d.ts.map +1 -1
  4. package/dist/browser/cache/idb-sync-cache.js +39 -4
  5. package/dist/browser/cache/idb-sync-cache.js.map +1 -1
  6. package/dist/browser/session-sync/server-transcript-mirror.d.ts +82 -0
  7. package/dist/browser/session-sync/server-transcript-mirror.d.ts.map +1 -0
  8. package/dist/browser/session-sync/server-transcript-mirror.js +143 -0
  9. package/dist/browser/session-sync/server-transcript-mirror.js.map +1 -0
  10. package/dist/browser/session-sync/session-sync-registry.d.ts +11 -1
  11. package/dist/browser/session-sync/session-sync-registry.d.ts.map +1 -1
  12. package/dist/browser/session-sync/session-sync-registry.js +117 -9
  13. package/dist/browser/session-sync/session-sync-registry.js.map +1 -1
  14. package/dist/browser/session-sync/visibility.d.ts +19 -0
  15. package/dist/browser/session-sync/visibility.d.ts.map +1 -0
  16. package/dist/browser/session-sync/visibility.js +16 -0
  17. package/dist/browser/session-sync/visibility.js.map +1 -0
  18. package/dist/browser/stores/sandbox-connection-store.d.ts +13 -1
  19. package/dist/browser/stores/sandbox-connection-store.d.ts.map +1 -1
  20. package/dist/browser/stores/sandbox-connection-store.js +6 -3
  21. package/dist/browser/stores/sandbox-connection-store.js.map +1 -1
  22. package/dist/browser/stores/session-working-store.d.ts +10 -5
  23. package/dist/browser/stores/session-working-store.d.ts.map +1 -1
  24. package/dist/browser/stores/session-working-store.js +32 -8
  25. package/dist/browser/stores/session-working-store.js.map +1 -1
  26. package/dist/browser/stores/sync-store.d.ts +56 -2
  27. package/dist/browser/stores/sync-store.d.ts.map +1 -1
  28. package/dist/browser/stores/sync-store.js +682 -402
  29. package/dist/browser/stores/sync-store.js.map +1 -1
  30. package/dist/core/auth/app-viewer.d.ts +57 -0
  31. package/dist/core/auth/app-viewer.d.ts.map +1 -0
  32. package/dist/core/auth/app-viewer.js +82 -0
  33. package/dist/core/auth/app-viewer.js.map +1 -0
  34. package/dist/core/auth/session.d.ts +45 -0
  35. package/dist/core/auth/session.d.ts.map +1 -0
  36. package/dist/core/auth/session.js +113 -0
  37. package/dist/core/auth/session.js.map +1 -0
  38. package/dist/core/client/kortix.d.ts +37 -29
  39. package/dist/core/client/kortix.d.ts.map +1 -1
  40. package/dist/core/client/kortix.js +61 -8
  41. package/dist/core/client/kortix.js.map +1 -1
  42. package/dist/core/http/config.d.ts +4 -2
  43. package/dist/core/http/config.d.ts.map +1 -1
  44. package/dist/core/http/config.js.map +1 -1
  45. package/dist/core/http/opencode-errors.d.ts +17 -0
  46. package/dist/core/http/opencode-errors.d.ts.map +1 -1
  47. package/dist/core/http/opencode-errors.js +25 -0
  48. package/dist/core/http/opencode-errors.js.map +1 -1
  49. package/dist/core/rest/platform-client/auth.d.ts +120 -0
  50. package/dist/core/rest/platform-client/auth.d.ts.map +1 -0
  51. package/dist/core/rest/platform-client/auth.js +98 -0
  52. package/dist/core/rest/platform-client/auth.js.map +1 -0
  53. package/dist/core/rest/platform-client/host-boundary.d.ts +5 -0
  54. package/dist/core/rest/platform-client/host-boundary.d.ts.map +1 -1
  55. package/dist/core/rest/platform-client/host-boundary.js.map +1 -1
  56. package/dist/core/rest/projects-client/accounts.d.ts +57 -0
  57. package/dist/core/rest/projects-client/accounts.d.ts.map +1 -1
  58. package/dist/core/rest/projects-client/accounts.js +31 -0
  59. package/dist/core/rest/projects-client/accounts.js.map +1 -1
  60. package/dist/core/rest/projects-client/apps.d.ts +9 -0
  61. package/dist/core/rest/projects-client/apps.d.ts.map +1 -1
  62. package/dist/core/rest/projects-client/apps.js.map +1 -1
  63. package/dist/core/rest/projects-client/billing.d.ts +2 -0
  64. package/dist/core/rest/projects-client/billing.d.ts.map +1 -1
  65. package/dist/core/rest/projects-client/billing.js.map +1 -1
  66. package/dist/core/rest/projects-client/channels.d.ts +0 -3
  67. package/dist/core/rest/projects-client/channels.d.ts.map +1 -1
  68. package/dist/core/rest/projects-client/channels.js +0 -6
  69. package/dist/core/rest/projects-client/channels.js.map +1 -1
  70. package/dist/core/rest/projects-client/connectors.d.ts +104 -19
  71. package/dist/core/rest/projects-client/connectors.d.ts.map +1 -1
  72. package/dist/core/rest/projects-client/connectors.js +56 -2
  73. package/dist/core/rest/projects-client/connectors.js.map +1 -1
  74. package/dist/core/rest/projects-client/index.d.ts +1 -2
  75. package/dist/core/rest/projects-client/index.d.ts.map +1 -1
  76. package/dist/core/rest/projects-client/index.js +1 -2
  77. package/dist/core/rest/projects-client/index.js.map +1 -1
  78. package/dist/core/rest/projects-client/oauth-clients.d.ts +47 -0
  79. package/dist/core/rest/projects-client/oauth-clients.d.ts.map +1 -0
  80. package/dist/core/rest/projects-client/oauth-clients.js +33 -0
  81. package/dist/core/rest/projects-client/oauth-clients.js.map +1 -0
  82. package/dist/core/rest/projects-client/projects.d.ts +23 -6
  83. package/dist/core/rest/projects-client/projects.d.ts.map +1 -1
  84. package/dist/core/rest/projects-client/projects.js +6 -3
  85. package/dist/core/rest/projects-client/projects.js.map +1 -1
  86. package/dist/core/rest/projects-client/session-sandbox.d.ts +52 -0
  87. package/dist/core/rest/projects-client/session-sandbox.d.ts.map +1 -1
  88. package/dist/core/rest/projects-client/session-sandbox.js.map +1 -1
  89. package/dist/core/rest/projects-client/sessions.d.ts +142 -34
  90. package/dist/core/rest/projects-client/sessions.d.ts.map +1 -1
  91. package/dist/core/rest/projects-client/sessions.js +48 -19
  92. package/dist/core/rest/projects-client/sessions.js.map +1 -1
  93. package/dist/core/runtime/attachment-part.d.ts +26 -0
  94. package/dist/core/runtime/attachment-part.d.ts.map +1 -0
  95. package/dist/core/runtime/attachment-part.js +41 -0
  96. package/dist/core/runtime/attachment-part.js.map +1 -0
  97. package/dist/core/runtime/client.d.ts +4 -2
  98. package/dist/core/runtime/client.d.ts.map +1 -1
  99. package/dist/core/runtime/client.js +3 -2
  100. package/dist/core/runtime/client.js.map +1 -1
  101. package/dist/core/runtime/opencode-v2-event-aliases.d.ts +184 -0
  102. package/dist/core/runtime/opencode-v2-event-aliases.d.ts.map +1 -0
  103. package/dist/core/runtime/opencode-v2-event-aliases.js +2 -0
  104. package/dist/core/runtime/opencode-v2-event-aliases.js.map +1 -0
  105. package/dist/core/runtime/pty.d.ts +11 -1
  106. package/dist/core/runtime/pty.d.ts.map +1 -1
  107. package/dist/core/runtime/pty.js +19 -6
  108. package/dist/core/runtime/pty.js.map +1 -1
  109. package/dist/core/session/connection.d.ts +64 -0
  110. package/dist/core/session/connection.d.ts.map +1 -0
  111. package/dist/core/session/connection.js +36 -0
  112. package/dist/core/session/connection.js.map +1 -0
  113. package/dist/core/session/index.d.ts +2 -0
  114. package/dist/core/session/index.d.ts.map +1 -1
  115. package/dist/core/session/index.js +2 -0
  116. package/dist/core/session/index.js.map +1 -1
  117. package/dist/core/session/open-bundle.d.ts +110 -0
  118. package/dist/core/session/open-bundle.d.ts.map +1 -0
  119. package/dist/core/session/open-bundle.js +183 -0
  120. package/dist/core/session/open-bundle.js.map +1 -0
  121. package/dist/core/session/poll-owner.d.ts +35 -0
  122. package/dist/core/session/poll-owner.d.ts.map +1 -0
  123. package/dist/core/session/poll-owner.js +90 -0
  124. package/dist/core/session/poll-owner.js.map +1 -0
  125. package/dist/core/session/preview-auth.d.ts +25 -0
  126. package/dist/core/session/preview-auth.d.ts.map +1 -1
  127. package/dist/core/session/preview-auth.js +31 -1
  128. package/dist/core/session/preview-auth.js.map +1 -1
  129. package/dist/core/session/preview-config.d.ts +26 -0
  130. package/dist/core/session/preview-config.d.ts.map +1 -0
  131. package/dist/core/session/preview-config.js +95 -0
  132. package/dist/core/session/preview-config.js.map +1 -0
  133. package/dist/core/session/preview-options.d.ts +25 -0
  134. package/dist/core/session/preview-options.d.ts.map +1 -0
  135. package/dist/core/session/preview-options.js +45 -0
  136. package/dist/core/session/preview-options.js.map +1 -0
  137. package/dist/core/session/preview.d.ts +8 -3
  138. package/dist/core/session/preview.d.ts.map +1 -1
  139. package/dist/core/session/preview.js +11 -4
  140. package/dist/core/session/preview.js.map +1 -1
  141. package/dist/core/session/rewind.d.ts +67 -24
  142. package/dist/core/session/rewind.d.ts.map +1 -1
  143. package/dist/core/session/rewind.js +74 -26
  144. package/dist/core/session/rewind.js.map +1 -1
  145. package/dist/core/session/server-store/active.d.ts +10 -12
  146. package/dist/core/session/server-store/active.d.ts.map +1 -1
  147. package/dist/core/session/server-store/active.js +11 -9
  148. package/dist/core/session/server-store/active.js.map +1 -1
  149. package/dist/core/session/server-store/url-helpers.d.ts +11 -2
  150. package/dist/core/session/server-store/url-helpers.d.ts.map +1 -1
  151. package/dist/core/session/server-store/url-helpers.js +11 -2
  152. package/dist/core/session/server-store/url-helpers.js.map +1 -1
  153. package/dist/core/session/static-file-preview.d.ts +50 -0
  154. package/dist/core/session/static-file-preview.d.ts.map +1 -0
  155. package/dist/core/session/static-file-preview.js +91 -0
  156. package/dist/core/session/static-file-preview.js.map +1 -0
  157. package/dist/core/session/url.d.ts +25 -2
  158. package/dist/core/session/url.d.ts.map +1 -1
  159. package/dist/core/session/url.js +87 -11
  160. package/dist/core/session/url.js.map +1 -1
  161. package/dist/core/session/wake-escalation.d.ts +188 -0
  162. package/dist/core/session/wake-escalation.d.ts.map +1 -0
  163. package/dist/core/session/wake-escalation.js +241 -0
  164. package/dist/core/session/wake-escalation.js.map +1 -0
  165. package/dist/core/session/working.d.ts +107 -2
  166. package/dist/core/session/working.d.ts.map +1 -1
  167. package/dist/core/session/working.js +206 -7
  168. package/dist/core/session/working.js.map +1 -1
  169. package/dist/core/session-sync/durable-log.d.ts +96 -0
  170. package/dist/core/session-sync/durable-log.d.ts.map +1 -0
  171. package/dist/core/session-sync/durable-log.js +94 -0
  172. package/dist/core/session-sync/durable-log.js.map +1 -0
  173. package/dist/core/session-sync/fragment.d.ts +27 -0
  174. package/dist/core/session-sync/fragment.d.ts.map +1 -0
  175. package/dist/core/session-sync/fragment.js +24 -0
  176. package/dist/core/session-sync/fragment.js.map +1 -0
  177. package/dist/core/session-sync/session-sync-controller.d.ts +117 -11
  178. package/dist/core/session-sync/session-sync-controller.d.ts.map +1 -1
  179. package/dist/core/session-sync/session-sync-controller.js +278 -26
  180. package/dist/core/session-sync/session-sync-controller.js.map +1 -1
  181. package/dist/core/stream/keepalive.d.ts +17 -0
  182. package/dist/core/stream/keepalive.d.ts.map +1 -0
  183. package/dist/core/stream/keepalive.js +19 -0
  184. package/dist/core/stream/keepalive.js.map +1 -0
  185. package/dist/core/turns/grouping.d.ts.map +1 -1
  186. package/dist/core/turns/grouping.js +73 -23
  187. package/dist/core/turns/grouping.js.map +1 -1
  188. package/dist/core/turns/types.d.ts +5 -0
  189. package/dist/core/turns/types.d.ts.map +1 -1
  190. package/dist/index.d.ts +16 -1
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.js +19 -0
  193. package/dist/index.js.map +1 -1
  194. package/dist/kortix.esm.min.js +25 -25
  195. package/dist/kortix.global.js +25 -25
  196. package/dist/node/app-viewer.d.ts +41 -0
  197. package/dist/node/app-viewer.d.ts.map +1 -0
  198. package/dist/node/app-viewer.js +150 -0
  199. package/dist/node/app-viewer.js.map +1 -0
  200. package/dist/node/auth.d.ts +84 -0
  201. package/dist/node/auth.d.ts.map +1 -0
  202. package/dist/node/auth.js +414 -0
  203. package/dist/node/auth.js.map +1 -0
  204. package/dist/node/server.d.ts +19 -0
  205. package/dist/node/server.d.ts.map +1 -1
  206. package/dist/node/server.js +13 -1
  207. package/dist/node/server.js.map +1 -1
  208. package/dist/platform/auth-core.d.ts +13 -1
  209. package/dist/platform/auth-core.d.ts.map +1 -1
  210. package/dist/platform/auth-core.js +13 -1
  211. package/dist/platform/auth-core.js.map +1 -1
  212. package/dist/react/bedrock-invokable.d.ts +58 -0
  213. package/dist/react/bedrock-invokable.d.ts.map +1 -0
  214. package/dist/react/bedrock-invokable.js +83 -0
  215. package/dist/react/bedrock-invokable.js.map +1 -0
  216. package/dist/react/index.d.ts +3 -0
  217. package/dist/react/index.d.ts.map +1 -1
  218. package/dist/react/index.js +7 -0
  219. package/dist/react/index.js.map +1 -1
  220. package/dist/react/model-flatten.d.ts +14 -1
  221. package/dist/react/model-flatten.d.ts.map +1 -1
  222. package/dist/react/model-flatten.js +3 -4
  223. package/dist/react/model-flatten.js.map +1 -1
  224. package/dist/react/opencode.d.ts +3 -1
  225. package/dist/react/opencode.d.ts.map +1 -1
  226. package/dist/react/opencode.js +5 -1
  227. package/dist/react/opencode.js.map +1 -1
  228. package/dist/react/provider-selection.d.ts +58 -0
  229. package/dist/react/provider-selection.d.ts.map +1 -1
  230. package/dist/react/provider-selection.js +253 -2
  231. package/dist/react/provider-selection.js.map +1 -1
  232. package/dist/react/session-start-giveup.d.ts +12 -0
  233. package/dist/react/session-start-giveup.d.ts.map +1 -0
  234. package/dist/react/session-start-giveup.js +13 -0
  235. package/dist/react/session-start-giveup.js.map +1 -0
  236. package/dist/react/session-title-sync.d.ts.map +1 -1
  237. package/dist/react/session-title-sync.js +15 -5
  238. package/dist/react/session-title-sync.js.map +1 -1
  239. package/dist/react/sign-in-with-kortix.d.ts +346 -0
  240. package/dist/react/sign-in-with-kortix.d.ts.map +1 -0
  241. package/dist/react/sign-in-with-kortix.js +65 -0
  242. package/dist/react/sign-in-with-kortix.js.map +1 -0
  243. package/dist/react/use-admin-accounts.d.ts +36 -1
  244. package/dist/react/use-admin-accounts.d.ts.map +1 -1
  245. package/dist/react/use-admin-accounts.js +19 -0
  246. package/dist/react/use-admin-accounts.js.map +1 -1
  247. package/dist/react/use-gateway-routing-policy.d.ts +1 -0
  248. package/dist/react/use-gateway-routing-policy.d.ts.map +1 -1
  249. package/dist/react/use-gateway-routing-policy.js +12 -1
  250. package/dist/react/use-gateway-routing-policy.js.map +1 -1
  251. package/dist/react/use-kortix-app-viewer.d.ts +13 -0
  252. package/dist/react/use-kortix-app-viewer.d.ts.map +1 -0
  253. package/dist/react/use-kortix-app-viewer.js +29 -0
  254. package/dist/react/use-kortix-app-viewer.js.map +1 -0
  255. package/dist/react/use-model-defaults.d.ts +7 -0
  256. package/dist/react/use-model-defaults.d.ts.map +1 -1
  257. package/dist/react/use-model-defaults.js +7 -1
  258. package/dist/react/use-model-defaults.js.map +1 -1
  259. package/dist/react/use-model-enablement.d.ts.map +1 -1
  260. package/dist/react/use-model-enablement.js +5 -1
  261. package/dist/react/use-model-enablement.js.map +1 -1
  262. package/dist/react/use-model-store.d.ts +31 -0
  263. package/dist/react/use-model-store.d.ts.map +1 -1
  264. package/dist/react/use-model-store.js +31 -2
  265. package/dist/react/use-model-store.js.map +1 -1
  266. package/dist/react/use-opencode-events/handle-event.d.ts.map +1 -1
  267. package/dist/react/use-opencode-events/handle-event.js +26 -0
  268. package/dist/react/use-opencode-events/handle-event.js.map +1 -1
  269. package/dist/react/use-opencode-events/helpers.d.ts +49 -5
  270. package/dist/react/use-opencode-events/helpers.d.ts.map +1 -1
  271. package/dist/react/use-opencode-events/helpers.js +54 -6
  272. package/dist/react/use-opencode-events/helpers.js.map +1 -1
  273. package/dist/react/use-opencode-events/index.d.ts.map +1 -1
  274. package/dist/react/use-opencode-events/index.js +80 -8
  275. package/dist/react/use-opencode-events/index.js.map +1 -1
  276. package/dist/react/use-opencode-events/rehydrate-targets.d.ts +18 -0
  277. package/dist/react/use-opencode-events/rehydrate-targets.d.ts.map +1 -0
  278. package/dist/react/use-opencode-events/rehydrate-targets.js +20 -0
  279. package/dist/react/use-opencode-events/rehydrate-targets.js.map +1 -0
  280. package/dist/react/use-opencode-events/stream-revival.d.ts +37 -0
  281. package/dist/react/use-opencode-events/stream-revival.d.ts.map +1 -0
  282. package/dist/react/use-opencode-events/stream-revival.js +53 -0
  283. package/dist/react/use-opencode-events/stream-revival.js.map +1 -0
  284. package/dist/react/use-opencode-events/use-event-stream-refs.d.ts.map +1 -1
  285. package/dist/react/use-opencode-events/use-event-stream-refs.js +14 -2
  286. package/dist/react/use-opencode-events/use-event-stream-refs.js.map +1 -1
  287. package/dist/react/use-opencode-local.d.ts.map +1 -1
  288. package/dist/react/use-opencode-local.js +27 -8
  289. package/dist/react/use-opencode-local.js.map +1 -1
  290. package/dist/react/use-opencode-pty.d.ts +7 -1
  291. package/dist/react/use-opencode-pty.d.ts.map +1 -1
  292. package/dist/react/use-opencode-pty.js +6 -2
  293. package/dist/react/use-opencode-pty.js.map +1 -1
  294. package/dist/react/use-opencode-sessions/agents.d.ts.map +1 -1
  295. package/dist/react/use-opencode-sessions/agents.js +13 -2
  296. package/dist/react/use-opencode-sessions/agents.js.map +1 -1
  297. package/dist/react/use-opencode-sessions/commands.d.ts +12 -0
  298. package/dist/react/use-opencode-sessions/commands.d.ts.map +1 -1
  299. package/dist/react/use-opencode-sessions/commands.js +15 -3
  300. package/dist/react/use-opencode-sessions/commands.js.map +1 -1
  301. package/dist/react/use-opencode-sessions/index.d.ts +1 -0
  302. package/dist/react/use-opencode-sessions/index.d.ts.map +1 -1
  303. package/dist/react/use-opencode-sessions/index.js +1 -0
  304. package/dist/react/use-opencode-sessions/index.js.map +1 -1
  305. package/dist/react/use-opencode-sessions/keys.d.ts +8 -0
  306. package/dist/react/use-opencode-sessions/keys.d.ts.map +1 -1
  307. package/dist/react/use-opencode-sessions/keys.js +8 -0
  308. package/dist/react/use-opencode-sessions/keys.js.map +1 -1
  309. package/dist/react/use-opencode-sessions/providers.d.ts.map +1 -1
  310. package/dist/react/use-opencode-sessions/providers.js +56 -5
  311. package/dist/react/use-opencode-sessions/providers.js.map +1 -1
  312. package/dist/react/use-opencode-sessions/shared.d.ts +23 -0
  313. package/dist/react/use-opencode-sessions/shared.d.ts.map +1 -1
  314. package/dist/react/use-opencode-sessions/shared.js +31 -0
  315. package/dist/react/use-opencode-sessions/shared.js.map +1 -1
  316. package/dist/react/use-opencode-sessions/vcs.d.ts +28 -0
  317. package/dist/react/use-opencode-sessions/vcs.d.ts.map +1 -0
  318. package/dist/react/use-opencode-sessions/vcs.js +42 -0
  319. package/dist/react/use-opencode-sessions/vcs.js.map +1 -0
  320. package/dist/react/use-poll-owner.d.ts +14 -0
  321. package/dist/react/use-poll-owner.d.ts.map +1 -0
  322. package/dist/react/use-poll-owner.js +32 -0
  323. package/dist/react/use-poll-owner.js.map +1 -0
  324. package/dist/react/use-project-llm-gateway.d.ts +27 -0
  325. package/dist/react/use-project-llm-gateway.d.ts.map +1 -0
  326. package/dist/react/use-project-llm-gateway.js +41 -0
  327. package/dist/react/use-project-llm-gateway.js.map +1 -0
  328. package/dist/react/use-project-models.d.ts +9 -0
  329. package/dist/react/use-project-models.d.ts.map +1 -1
  330. package/dist/react/use-project-models.js +24 -1
  331. package/dist/react/use-project-models.js.map +1 -1
  332. package/dist/react/use-runtime-reconnect.d.ts +11 -0
  333. package/dist/react/use-runtime-reconnect.d.ts.map +1 -1
  334. package/dist/react/use-runtime-reconnect.js +34 -3
  335. package/dist/react/use-runtime-reconnect.js.map +1 -1
  336. package/dist/react/use-session-prompts.d.ts +53 -3
  337. package/dist/react/use-session-prompts.d.ts.map +1 -1
  338. package/dist/react/use-session-prompts.js +133 -21
  339. package/dist/react/use-session-prompts.js.map +1 -1
  340. package/dist/react/use-session-send.d.ts +17 -0
  341. package/dist/react/use-session-send.d.ts.map +1 -1
  342. package/dist/react/use-session-send.js +54 -5
  343. package/dist/react/use-session-send.js.map +1 -1
  344. package/dist/react/use-session-start-give-up.d.ts +20 -0
  345. package/dist/react/use-session-start-give-up.d.ts.map +1 -0
  346. package/dist/react/use-session-start-give-up.js +60 -0
  347. package/dist/react/use-session-start-give-up.js.map +1 -0
  348. package/dist/react/use-session-sync.d.ts +35 -1
  349. package/dist/react/use-session-sync.d.ts.map +1 -1
  350. package/dist/react/use-session-sync.js +135 -53
  351. package/dist/react/use-session-sync.js.map +1 -1
  352. package/dist/react/use-session-working.d.ts +55 -0
  353. package/dist/react/use-session-working.d.ts.map +1 -1
  354. package/dist/react/use-session-working.js +114 -13
  355. package/dist/react/use-session-working.js.map +1 -1
  356. package/dist/react/use-session.d.ts +13 -13
  357. package/dist/react/use-session.d.ts.map +1 -1
  358. package/dist/react/use-session.js +74 -53
  359. package/dist/react/use-session.js.map +1 -1
  360. package/dist/react/use-static-file-preview.d.ts +12 -0
  361. package/dist/react/use-static-file-preview.d.ts.map +1 -0
  362. package/dist/react/use-static-file-preview.js +119 -0
  363. package/dist/react/use-static-file-preview.js.map +1 -0
  364. package/dist/react/use-wake-escalation.d.ts +51 -0
  365. package/dist/react/use-wake-escalation.d.ts.map +1 -0
  366. package/dist/react/use-wake-escalation.js +62 -0
  367. package/dist/react/use-wake-escalation.js.map +1 -0
  368. package/package.json +6 -4
  369. package/dist/browser/session-sync/session-transcript-cache.d.ts +0 -79
  370. package/dist/browser/session-sync/session-transcript-cache.d.ts.map +0 -1
  371. package/dist/browser/session-sync/session-transcript-cache.js +0 -91
  372. package/dist/browser/session-sync/session-transcript-cache.js.map +0 -1
  373. package/dist/core/rest/projects-client/public-voice-join.d.ts +0 -59
  374. package/dist/core/rest/projects-client/public-voice-join.d.ts.map +0 -1
  375. package/dist/core/rest/projects-client/public-voice-join.js +0 -64
  376. package/dist/core/rest/projects-client/public-voice-join.js.map +0 -1
  377. package/dist/core/rest/projects-client/transcription.d.ts +0 -5
  378. package/dist/core/rest/projects-client/transcription.d.ts.map +0 -1
  379. package/dist/core/rest/projects-client/transcription.js +0 -14
  380. package/dist/core/rest/projects-client/transcription.js.map +0 -1
@@ -1,15 +1,50 @@
1
1
  import type { Message, Part, SessionStatus } from '@opencode-ai/sdk/v2/client';
2
+ export declare const SESSION_SYNC_PAGE_SIZE = 100;
2
3
  /**
3
- * Messages per bounded read — the newest-first window a session opens with,
4
- * and the step size when the transcript pulls older history.
4
+ * The FIRST page — the one the user waits on.
5
5
  *
6
- * OpenCode carries tool output as *parts* of the assistant message, so a page
7
- * is roughly half as many turns as it is messages: 50 covers ~25 turns, which
8
- * is a whole ordinary session. That is the point most sessions now open
9
- * fully loaded and never pull at all, while a long one still opens bounded
10
- * instead of dragging its entire history over the sandbox proxy.
6
+ * Time to first paint is bytes, not messages. Measured on a heavy session
7
+ * (essentia, 2026-08-24, a run with hundreds of image reads whose parts carried
8
+ * base64 BEFORE the attachment bytes were stripped from the list):
9
+ *
10
+ * message?limit=50 -> 8,228 kB 30.39 s
11
+ * message?limit=50 -> 24,460 kB 48.76 s
12
+ * message?limit=50 -> 20,284 kB 35.74 s
13
+ * message?limit=50 -> 25,125 kB 29.23 s
14
+ *
15
+ * That was 165-500 kB PER MESSAGE, and it drove the first page down to 20.
16
+ * With the bytes gone (`stripInlineAttachmentBytes`, same session, same day)
17
+ * a message is ~2-7 kB: `limit=20` measured 132 kB, an older page of 50
18
+ * measured 345-420 kB. At that weight a small first page is a false economy —
19
+ * on a normal viewport 20 messages do not fill the screen, so the top sentinel
20
+ * pulled two more pages before the reader touched anything, and three round
21
+ * trips (each paying a CORS preflight, one measured at 3.34 s) replaced one.
22
+ *
23
+ * Fifty fills the view. Older pages go to a hundred: by then the reader is
24
+ * scrolling deliberately, a spinner is honest, and fewer round trips wins.
25
+ *
26
+ * NOTE this is not what fixed the blank transcript. That was structural:
27
+ * `hydrate` ran only after a multi-page backward walk, so NOTHING rendered at
28
+ * any page size — a smaller limit would have meant MORE sequential round trips
29
+ * before that single paint, and a longer blank. The walk is gone (see
30
+ * `loadTail`); this only makes the first paint lighter.
31
+ */
32
+ export declare const SESSION_SYNC_TAIL_PAGE_SIZE = 50;
33
+ /**
34
+ * How far back the tail read will walk to complete a turn before it stops and
35
+ * leaves the rest to "load older".
36
+ *
37
+ * The walk exists so an assistant message is not rendered above its own prompt.
38
+ * It had no ceiling, and a session whose last turn is thousands of messages —
39
+ * an agent run with hundreds of tool calls — therefore paged the WHOLE session
40
+ * 50 at a time, serially, through the sandbox proxy, before painting anything.
41
+ * The read returns 200 the entire time, which is why this looked like a bug in
42
+ * rendering rather than in loading.
43
+ *
44
+ * 10 pages = 500 messages, far past any honest turn, and the cursor survives so
45
+ * nothing becomes unreachable.
11
46
  */
12
- export declare const SESSION_SYNC_PAGE_SIZE = 50;
47
+ export declare const MAX_TURN_BACKFILL_PAGES = 10;
13
48
  export type SessionSyncFreshness = 'idle' | 'loading' | 'fresh' | 'stale' | 'error';
14
49
  export interface SessionSyncMessage {
15
50
  info: Message;
@@ -43,7 +78,16 @@ export interface SessionSyncScheduler {
43
78
  setTimeout?: (handler: () => void, timeoutMs: number) => unknown;
44
79
  clearTimeout?: (handle: unknown) => void;
45
80
  }
46
- export type SessionSyncReason = 'initial' | 'poll' | 'sse-gap' | 'compaction' | 'session-error' | 'send-recovery' | 'manual';
81
+ export type SessionSyncReason = 'initial' | 'poll' | 'sse-gap' | 'compaction' | 'session-error' | 'send-recovery'
82
+ /** The turn ended. The last read of a busy session, and the one that catches
83
+ * a final answer whose closing frames the stream never delivered. */
84
+ | 'turn-end'
85
+ /** The tab became visible again. A backgrounded tab is throttled, not
86
+ * notified, so return is a moment to re-read rather than to assume. */
87
+ | 'visible'
88
+ /** The transcript was evicted while detached and the live stream refilled it
89
+ * from the middle — see `transcriptIsFragment`. */
90
+ | 'eviction' | 'manual';
47
91
  export interface SessionSyncTelemetryEvent {
48
92
  operation: 'tail' | 'older';
49
93
  reason: SessionSyncReason;
@@ -53,10 +97,17 @@ export interface SessionSyncTelemetryEvent {
53
97
  }
54
98
  export interface SessionSyncControllerOptions {
55
99
  sessionId: string;
100
+ /**
101
+ * Read one bounded page. `signal` is the controller's own — aborted on
102
+ * `destroy()` (a scope reset destroys the controller), so a page loader that
103
+ * forwards it into `fetch` cancels a read the tab has navigated away from,
104
+ * and a superseded read can never hydrate the store. Passing it is optional;
105
+ * a loader that ignores it still works, it just cannot be cancelled.
106
+ */
56
107
  loadPage: (request: {
57
108
  limit: number;
58
109
  before?: string;
59
- }) => Promise<SessionSyncPage>;
110
+ }, signal?: AbortSignal) => Promise<SessionSyncPage>;
60
111
  /**
61
112
  * @deprecated Never called. The liveness poll no longer reads status: `GET
62
113
  * .../turn` is the status authority, and this controller's own `setBusy` is
@@ -78,8 +129,23 @@ export interface SessionSyncControllerOptions {
78
129
  onTelemetry?: (event: SessionSyncTelemetryEvent) => void;
79
130
  scheduler?: SessionSyncScheduler;
80
131
  livenessIntervalMs?: number;
132
+ /**
133
+ * Max age of the last tail read while the session is busy, whatever the
134
+ * stream delivers. Default 30s.
135
+ *
136
+ * The quiet-based poll trusts `noteActivity`: any transcript frame renews it.
137
+ * A DEGRADED stream — events lost at the source or the edge, connection
138
+ * alive, a trickle still arriving — therefore postponed the tail read
139
+ * indefinitely while the transcript diverged arbitrarily far from the
140
+ * runtime (prod, 2026-08-26: content minutes behind mid-run, repaired only
141
+ * by reload). Frames arriving proves the wire is up; it proves nothing about
142
+ * the frames that never arrived. This is the bound on how long that
143
+ * difference can go unchecked: one tail page per interval, and against a
144
+ * healthy stream the hydrate is a no-op.
145
+ */
146
+ verifyIntervalMs?: number;
81
147
  }
82
- export interface HttpSessionSyncControllerOptions extends Pick<SessionSyncControllerOptions, 'sessionId' | 'hydrate' | 'markLoaded' | 'setStatus' | 'onTelemetry' | 'scheduler' | 'livenessIntervalMs'> {
148
+ export interface HttpSessionSyncControllerOptions extends Pick<SessionSyncControllerOptions, 'sessionId' | 'hydrate' | 'markLoaded' | 'setStatus' | 'onTelemetry' | 'scheduler' | 'livenessIntervalMs' | 'verifyIntervalMs'> {
83
149
  baseUrl: string;
84
150
  getToken?: () => string | null | Promise<string | null>;
85
151
  fetch?: SessionSyncFetch;
@@ -89,6 +155,21 @@ type HttpSessionSyncSourceOptions = Pick<HttpSessionSyncControllerOptions, 'base
89
155
  export declare function createHttpSessionSyncPageLoader(options: HttpSessionSyncSourceOptions): SessionSyncControllerOptions['loadPage'];
90
156
  export declare function loadHttpSessionHistory(options: HttpSessionSyncSourceOptions): Promise<SessionSyncMessage[]>;
91
157
  export declare function createHttpSessionSyncController(options: HttpSessionSyncControllerOptions): SessionSyncController;
158
+ /**
159
+ * Every message in the session, oldest first.
160
+ *
161
+ * The order is the SERVER's: `MessageV2.page()` orders by `time_created`, so
162
+ * each page arrives already ordered and is concatenated untouched. Paging
163
+ * walks BACKWARDS (page 1 is the newest tail, `before` steps into history), so
164
+ * the transcript is the pages in reverse fetch order.
165
+ *
166
+ * This used to re-sort the union by `info.id.localeCompare(...)`. That was
167
+ * wrong twice over: message ids no longer ascend with time at all (OpenCode
168
+ * 1.18.15 retired that invariant — turn exit reads
169
+ * `lastAssistant.parentID === lastUser.id`, and `MessageV2.latest()` orders by
170
+ * `time.created`), and `localeCompare` is not byte order, so it disagreed with
171
+ * every other id comparison in the product on the same data.
172
+ */
92
173
  export declare function loadCompleteSessionHistory(loadPage: SessionSyncControllerOptions['loadPage']): Promise<SessionSyncMessage[]>;
93
174
  /**
94
175
  * Owns bounded session history synchronization without depending on React,
@@ -98,6 +179,12 @@ export declare class SessionSyncController {
98
179
  private readonly options;
99
180
  private readonly scheduler;
100
181
  private readonly livenessIntervalMs;
182
+ private readonly verifyIntervalMs;
183
+ /** When the last tail read was ISSUED — see `verifyIntervalMs`. */
184
+ private lastTailReadAt;
185
+ /** Consecutive failed tail reads, for the retry backoff. Reset by success. */
186
+ private retryAttempt;
187
+ private tailRetryTimer;
101
188
  private snapshot;
102
189
  private nextCursor;
103
190
  private knownUserMessageIds;
@@ -108,6 +195,13 @@ export declare class SessionSyncController {
108
195
  private lastActivityAt;
109
196
  private listeners;
110
197
  private destroyed;
198
+ /**
199
+ * One controller-lifetime signal, threaded into every read. A scope reset
200
+ * (registry `resetSessionSyncControllersForSession`) or unmount destroys this
201
+ * controller, `destroy()` aborts this, and the in-flight read cancels — so a
202
+ * navigated-away or superseded read frees its socket and can never hydrate.
203
+ */
204
+ private readonly abortController;
111
205
  constructor(options: SessionSyncControllerOptions);
112
206
  getSnapshot: () => SessionSyncSnapshot;
113
207
  subscribe: (listener: () => void) => (() => void);
@@ -126,6 +220,18 @@ export declare class SessionSyncController {
126
220
  setBusy(isBusy: boolean): void;
127
221
  destroy(): void;
128
222
  private loadTail;
223
+ /**
224
+ * Come back for a read that lost.
225
+ *
226
+ * Without this a session got exactly ONE chance to load, and whether it took
227
+ * it depended on a race with the sandbox's boot. Losing that race left the
228
+ * page on "Waking the agent" with no exit but the health probe — the least
229
+ * reliable signal in the system — or a manual reload.
230
+ *
231
+ * Backoff so a genuinely dead box costs little, capped so a box that comes
232
+ * back is picked up promptly.
233
+ */
234
+ private scheduleTailRetry;
129
235
  private loadPage;
130
236
  private loadCompleteOlderTurn;
131
237
  private loadCompleteTurn;
@@ -1 +1 @@
1
- {"version":3,"file":"session-sync-controller.d.ts","sourceRoot":"","sources":["../../../src/core/session-sync/session-sync-controller.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-sync-controller.d.ts","sourceRoot":"","sources":["../../../src/core/session-sync/session-sync-controller.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { SandboxNotReadyError, isSandboxNotReadyError } from '../http/opencode-errors.js';
2
+ import { isAbortError } from '../http/abort-error.js';
1
3
  /**
2
4
  * Messages per bounded read — the newest-first window a session opens with,
3
5
  * and the step size when the transcript pulls older history.
@@ -8,17 +10,75 @@
8
10
  * fully loaded and never pull at all, while a long one still opens bounded
9
11
  * instead of dragging its entire history over the sandbox proxy.
10
12
  */
11
- export const SESSION_SYNC_PAGE_SIZE = 50;
13
+ /** First retry delay after a failed tail read. */
14
+ const TAIL_RETRY_BASE_MS = 1_000;
15
+ /** Ceiling for the retry backoff — a box that comes back is picked up within it. */
16
+ const TAIL_RETRY_MAX_MS = 15_000;
17
+ export const SESSION_SYNC_PAGE_SIZE = 100;
18
+ /**
19
+ * The FIRST page — the one the user waits on.
20
+ *
21
+ * Time to first paint is bytes, not messages. Measured on a heavy session
22
+ * (essentia, 2026-08-24, a run with hundreds of image reads whose parts carried
23
+ * base64 — BEFORE the attachment bytes were stripped from the list):
24
+ *
25
+ * message?limit=50 -> 8,228 kB 30.39 s
26
+ * message?limit=50 -> 24,460 kB 48.76 s
27
+ * message?limit=50 -> 20,284 kB 35.74 s
28
+ * message?limit=50 -> 25,125 kB 29.23 s
29
+ *
30
+ * That was 165-500 kB PER MESSAGE, and it drove the first page down to 20.
31
+ * With the bytes gone (`stripInlineAttachmentBytes`, same session, same day)
32
+ * a message is ~2-7 kB: `limit=20` measured 132 kB, an older page of 50
33
+ * measured 345-420 kB. At that weight a small first page is a false economy —
34
+ * on a normal viewport 20 messages do not fill the screen, so the top sentinel
35
+ * pulled two more pages before the reader touched anything, and three round
36
+ * trips (each paying a CORS preflight, one measured at 3.34 s) replaced one.
37
+ *
38
+ * Fifty fills the view. Older pages go to a hundred: by then the reader is
39
+ * scrolling deliberately, a spinner is honest, and fewer round trips wins.
40
+ *
41
+ * NOTE this is not what fixed the blank transcript. That was structural:
42
+ * `hydrate` ran only after a multi-page backward walk, so NOTHING rendered at
43
+ * any page size — a smaller limit would have meant MORE sequential round trips
44
+ * before that single paint, and a longer blank. The walk is gone (see
45
+ * `loadTail`); this only makes the first paint lighter.
46
+ */
47
+ export const SESSION_SYNC_TAIL_PAGE_SIZE = 50;
48
+ /**
49
+ * How far back the tail read will walk to complete a turn before it stops and
50
+ * leaves the rest to "load older".
51
+ *
52
+ * The walk exists so an assistant message is not rendered above its own prompt.
53
+ * It had no ceiling, and a session whose last turn is thousands of messages —
54
+ * an agent run with hundreds of tool calls — therefore paged the WHOLE session
55
+ * 50 at a time, serially, through the sandbox proxy, before painting anything.
56
+ * The read returns 200 the entire time, which is why this looked like a bug in
57
+ * rendering rather than in loading.
58
+ *
59
+ * 10 pages = 500 messages, far past any honest turn, and the cursor survives so
60
+ * nothing becomes unreachable.
61
+ */
62
+ export const MAX_TURN_BACKFILL_PAGES = 10;
12
63
  export function createHttpSessionSyncPageLoader(options) {
13
64
  const fetchImpl = options.fetch ?? globalThis.fetch;
14
65
  const baseUrl = options.baseUrl.replace(/\/$/, '');
15
- return async ({ limit, before }) => {
66
+ return async ({ limit, before }, signal) => {
16
67
  const query = new URLSearchParams({ limit: String(limit) });
17
68
  if (before)
18
69
  query.set('before', before);
19
70
  const token = await options.getToken?.();
20
- const response = await fetchImpl(`${baseUrl}/session/${encodeURIComponent(options.sessionId)}/message?${query}`, { headers: token ? { Authorization: `Bearer ${token}` } : undefined });
71
+ const response = await fetchImpl(`${baseUrl}/session/${encodeURIComponent(options.sessionId)}/message?${query}`, {
72
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
73
+ ...(signal ? { signal } : {}),
74
+ });
21
75
  if (!response.ok) {
76
+ // A 503 from the sandbox proxy is the box waking, not a failure — throw
77
+ // the retryable, "loading" error so the controller keeps polling instead
78
+ // of painting an error state over a session that is about to load.
79
+ if (response.status === 503) {
80
+ throw new SandboxNotReadyError(`session ${response.status}`);
81
+ }
22
82
  throw new Error(`Session synchronization failed: ${response.status}`);
23
83
  }
24
84
  return {
@@ -39,8 +99,23 @@ export function createHttpSessionSyncController(options) {
39
99
  loadPage: createHttpSessionSyncPageLoader(options),
40
100
  });
41
101
  }
102
+ /**
103
+ * Every message in the session, oldest first.
104
+ *
105
+ * The order is the SERVER's: `MessageV2.page()` orders by `time_created`, so
106
+ * each page arrives already ordered and is concatenated untouched. Paging
107
+ * walks BACKWARDS (page 1 is the newest tail, `before` steps into history), so
108
+ * the transcript is the pages in reverse fetch order.
109
+ *
110
+ * This used to re-sort the union by `info.id.localeCompare(...)`. That was
111
+ * wrong twice over: message ids no longer ascend with time at all (OpenCode
112
+ * 1.18.15 retired that invariant — turn exit reads
113
+ * `lastAssistant.parentID === lastUser.id`, and `MessageV2.latest()` orders by
114
+ * `time.created`), and `localeCompare` is not byte order, so it disagreed with
115
+ * every other id comparison in the product on the same data.
116
+ */
42
117
  export async function loadCompleteSessionHistory(loadPage) {
43
- const messages = new Map();
118
+ const pages = [];
44
119
  const cursors = new Set();
45
120
  let before;
46
121
  do {
@@ -48,9 +123,7 @@ export async function loadCompleteSessionHistory(loadPage) {
48
123
  limit: SESSION_SYNC_PAGE_SIZE,
49
124
  ...(before ? { before } : {}),
50
125
  });
51
- for (const message of page.messages) {
52
- messages.set(message.info.id, message);
53
- }
126
+ pages.push(page.messages);
54
127
  before = page.nextCursor;
55
128
  if (before && cursors.has(before)) {
56
129
  throw new Error(`Session history cursor repeated: ${before}`);
@@ -58,7 +131,20 @@ export async function loadCompleteSessionHistory(loadPage) {
58
131
  if (before)
59
132
  cursors.add(before);
60
133
  } while (before);
61
- return [...messages.values()].sort((a, b) => a.info.id.localeCompare(b.info.id));
134
+ // Oldest page last out of the loop, so walk the pages backwards. A message
135
+ // repeated across two overlapping pages keeps the OLDER page's copy and its
136
+ // older position — the same copy the id-keyed Map used to end up holding.
137
+ const ordered = [];
138
+ const seen = new Set();
139
+ for (let index = pages.length - 1; index >= 0; index--) {
140
+ for (const message of pages[index]) {
141
+ if (seen.has(message.info.id))
142
+ continue;
143
+ seen.add(message.info.id);
144
+ ordered.push(message);
145
+ }
146
+ }
147
+ return ordered;
62
148
  }
63
149
  const defaultScheduler = {
64
150
  now: Date.now,
@@ -75,6 +161,12 @@ export class SessionSyncController {
75
161
  options;
76
162
  scheduler;
77
163
  livenessIntervalMs;
164
+ verifyIntervalMs;
165
+ /** When the last tail read was ISSUED — see `verifyIntervalMs`. */
166
+ lastTailReadAt;
167
+ /** Consecutive failed tail reads, for the retry backoff. Reset by success. */
168
+ retryAttempt = 0;
169
+ tailRetryTimer;
78
170
  snapshot = {
79
171
  freshness: 'idle',
80
172
  hasOlder: false,
@@ -89,11 +181,20 @@ export class SessionSyncController {
89
181
  lastActivityAt;
90
182
  listeners = new Set();
91
183
  destroyed = false;
184
+ /**
185
+ * One controller-lifetime signal, threaded into every read. A scope reset
186
+ * (registry `resetSessionSyncControllersForSession`) or unmount destroys this
187
+ * controller, `destroy()` aborts this, and the in-flight read cancels — so a
188
+ * navigated-away or superseded read frees its socket and can never hydrate.
189
+ */
190
+ abortController = new AbortController();
92
191
  constructor(options) {
93
192
  this.options = options;
94
193
  this.scheduler = options.scheduler ?? defaultScheduler;
95
194
  this.livenessIntervalMs = options.livenessIntervalMs ?? 10_000;
195
+ this.verifyIntervalMs = options.verifyIntervalMs ?? 30_000;
96
196
  this.lastActivityAt = this.scheduler.now();
197
+ this.lastTailReadAt = this.scheduler.now();
97
198
  }
98
199
  getSnapshot = () => this.snapshot;
99
200
  subscribe = (listener) => {
@@ -110,8 +211,13 @@ export class SessionSyncController {
110
211
  return Promise.resolve();
111
212
  if (this.tailRequest)
112
213
  return this.tailRequest;
214
+ // `loading` covers the first read AND a not-ready retry, so a waking box
215
+ // does not flip to `stale` between attempts; a session that already has a
216
+ // fresh transcript revalidates as `stale`.
113
217
  this.update({
114
- freshness: this.snapshot.freshness === 'idle' ? 'loading' : 'stale',
218
+ freshness: this.snapshot.freshness === 'idle' || this.snapshot.freshness === 'loading'
219
+ ? 'loading'
220
+ : 'stale',
115
221
  });
116
222
  this.tailRequest = this.loadTail(reason).finally(() => {
117
223
  this.tailRequest = undefined;
@@ -156,7 +262,21 @@ export class SessionSyncController {
156
262
  */
157
263
  setBusy(isBusy) {
158
264
  if (!isBusy) {
265
+ // The turn is over — and that is exactly when the transcript is most
266
+ // likely to be short. A stream that dropped its last frames leaves the
267
+ // browser holding a truncated answer while the runtime holds the whole
268
+ // one, and stopping the poll here used to make that state PERMANENT:
269
+ // nothing read the tail again until the session was reopened. Reported
270
+ // from a live self-host (2026-08-24): an 8m13s turn finished in the
271
+ // runtime's own terminal while the tab still showed a spinner under a
272
+ // half-written answer.
273
+ //
274
+ // One bounded read, only for a session that was actually busy, so an
275
+ // idle session churns nothing.
276
+ const wasBusy = this.livenessTimer !== undefined;
159
277
  this.stopLivenessTimer();
278
+ if (wasBusy && !this.destroyed)
279
+ void this.reconcile('turn-end');
160
280
  return;
161
281
  }
162
282
  if (this.livenessTimer !== undefined)
@@ -165,14 +285,50 @@ export class SessionSyncController {
165
285
  this.livenessTimer = this.scheduler.setInterval(() => void this.checkLiveness(), this.livenessIntervalMs);
166
286
  }
167
287
  destroy() {
288
+ // `destroyed` FIRST: `stopLivenessTimer` is also reached through
289
+ // `setBusy(false)`, which now fires a turn-end read, and a controller being
290
+ // torn down must not start a request it can never hydrate.
168
291
  this.destroyed = true;
169
292
  this.stopLivenessTimer();
293
+ if (this.tailRetryTimer !== undefined) {
294
+ this.cancelTimer(this.tailRetryTimer);
295
+ this.tailRetryTimer = undefined;
296
+ }
297
+ // Cancel any in-flight read so it frees its socket and cannot hydrate a
298
+ // controller that is being torn down.
299
+ this.abortController.abort();
170
300
  this.listeners.clear();
171
301
  }
172
302
  async loadTail(reason) {
303
+ // Stamped at ISSUE: any tail read — initial, poll, gap, visible — is a
304
+ // verification, so the busy-verification cadence counts from the last
305
+ // attempt rather than piling on top of reads other reasons already ran.
306
+ this.lastTailReadAt = this.scheduler.now();
173
307
  try {
174
- const firstPage = await this.loadPage('tail', reason);
175
- const page = await this.loadCompleteTurn(firstPage, 'tail', reason);
308
+ // ONE PAGE. Render it. This is what OpenCode's own client does, and the
309
+ // reason we now do it too is measured:
310
+ //
311
+ // message?limit=50 200 8,228 kB 30.39 s
312
+ // message?limit=50 200 24,460 kB 48.76 s
313
+ // message?limit=50&before=.. 200 20,284 kB 35.74 s
314
+ // message?limit=50&before=.. 200 25,125 kB 29.23 s
315
+ // -> 78,097 kB transferred, finish 3.8 min, NOTHING on screen
316
+ //
317
+ // (essentia, 2026-08-24, a run with hundreds of image reads: fifty
318
+ // messages weigh 8-25 MB because the parts carry the image bytes.)
319
+ //
320
+ // There used to be a backward WALK here that kept fetching pages until
321
+ // every assistant message had its parent user message in hand, so an
322
+ // assistant reply could never render above its own prompt — and `hydrate`
323
+ // ran only after the walk finished. On a long turn that walk is the whole
324
+ // session, serially, through the sandbox proxy, with an empty thread the
325
+ // entire time. A cosmetic guarantee at the top edge of the window is not
326
+ // worth minutes of blank screen.
327
+ //
328
+ // The window may therefore start on an assistant message whose prompt is
329
+ // one page up. That is exactly what OpenCode shows, and `loadOlder` —
330
+ // which the user drives — still completes the turn when they scroll.
331
+ const page = await this.loadPage('tail', reason);
176
332
  if (this.destroyed)
177
333
  return;
178
334
  this.rememberUserMessages(page.messages);
@@ -181,24 +337,68 @@ export class SessionSyncController {
181
337
  this.setCursor(page.nextCursor);
182
338
  }
183
339
  this.update({ freshness: 'fresh' });
340
+ // SUCCESS ONLY. `markLoaded` used to sit in a `finally`, so a read that
341
+ // FAILED still told the store this session was loaded — and the store's
342
+ // implementation of that plants an empty message list. A first read that
343
+ // lost to a waking box, a 503 from the proxy, or a flapping probe was
344
+ // therefore RECORDED as "this session has no messages", and the UI
345
+ // painted an empty conversation over a session that had plenty. Nothing
346
+ // came back for it either: the mount had already run, and the liveness
347
+ // poll only turns on while a session is working.
348
+ //
349
+ // A successful read of zero messages still marks loaded — that is a fact
350
+ // about the session. A failed read is not a fact about anything.
351
+ this.options.markLoaded();
352
+ this.retryAttempt = 0;
184
353
  }
185
- catch {
186
- if (!this.destroyed) {
187
- this.update({ freshness: 'error' });
188
- }
189
- }
190
- finally {
191
- if (!this.destroyed)
192
- this.options.markLoaded();
354
+ catch (error) {
355
+ if (this.destroyed)
356
+ return;
357
+ // A superseded/cancelled read is not a failure and never hydrates — it
358
+ // must not paint an error over a live transcript, and it must not retry.
359
+ if (isAbortError(error) || this.abortController.signal.aborted)
360
+ return;
361
+ // A sandbox that is still waking is a RETRYABLE, "loading" state, not a
362
+ // fault: the box may come up any second, so keep polling and keep the UI
363
+ // on its loader. Only a genuine failure earns `error`. NEVER an
364
+ // empty-`fresh` — `markLoaded` above ran only on success, so a failed
365
+ // read never records the session as an empty transcript.
366
+ this.update({ freshness: isSandboxNotReadyError(error) ? 'loading' : 'error' });
367
+ this.scheduleTailRetry(reason);
193
368
  }
194
369
  }
370
+ /**
371
+ * Come back for a read that lost.
372
+ *
373
+ * Without this a session got exactly ONE chance to load, and whether it took
374
+ * it depended on a race with the sandbox's boot. Losing that race left the
375
+ * page on "Waking the agent" with no exit but the health probe — the least
376
+ * reliable signal in the system — or a manual reload.
377
+ *
378
+ * Backoff so a genuinely dead box costs little, capped so a box that comes
379
+ * back is picked up promptly.
380
+ */
381
+ scheduleTailRetry(reason) {
382
+ if (this.destroyed || this.tailRetryTimer !== undefined)
383
+ return;
384
+ const delay = Math.min(TAIL_RETRY_BASE_MS * 2 ** this.retryAttempt, TAIL_RETRY_MAX_MS);
385
+ this.retryAttempt += 1;
386
+ this.tailRetryTimer = this.startTimer(() => {
387
+ this.tailRetryTimer = undefined;
388
+ if (this.destroyed)
389
+ return;
390
+ void this.reconcile(reason);
391
+ }, delay);
392
+ }
195
393
  async loadPage(operation, reason, before) {
196
394
  const startedAt = this.scheduler.now();
197
395
  try {
198
396
  const page = await this.options.loadPage({
199
- limit: SESSION_SYNC_PAGE_SIZE,
397
+ // The tail is what someone is waiting for; an older page is what they
398
+ // asked for. Different budgets — see SESSION_SYNC_TAIL_PAGE_SIZE.
399
+ limit: operation === 'tail' ? SESSION_SYNC_TAIL_PAGE_SIZE : SESSION_SYNC_PAGE_SIZE,
200
400
  ...(before ? { before } : {}),
201
- });
401
+ }, this.abortController.signal);
202
402
  this.options.onTelemetry?.({
203
403
  operation,
204
404
  reason,
@@ -221,19 +421,46 @@ export class SessionSyncController {
221
421
  }
222
422
  async loadCompleteOlderTurn(before) {
223
423
  const firstPage = await this.loadPage('older', 'manual', before);
224
- return this.loadCompleteTurn(firstPage, 'older', 'manual', before);
424
+ // Commit each page as it lands, including the page that fails. The walk
425
+ // is up to 11 sequential reads (MAX_TURN_BACKFILL_PAGES + 1) and used to
426
+ // commit all of them in one `.then`, so a rejection on ANY page —
427
+ // including the loop's very first, right after `firstPage` — discarded
428
+ // every successful read and left `nextCursor` unmoved: nothing to resume
429
+ // from, so the retry replayed the identical walk. `onPage` existed for
430
+ // exactly this and was never passed.
431
+ return this.loadCompleteTurn(firstPage, 'older', 'manual', before, (partialPage) => {
432
+ // `rememberUserMessages` mutates the instance-level `knownUserMessageIds`
433
+ // Set (below), and `onPage` fires on a rejection too (the catch in
434
+ // `loadCompleteTurn`, right before it rethrows). So a walk that fails
435
+ // partway permanently records the user-message ids from every page read
436
+ // so far. On the retry, `loadCompleteTurn` seeds its local
437
+ // `knownUserMessageIds` from this now-larger instance Set, which can
438
+ // already satisfy the termination condition ("some assistant has a
439
+ // `parentID` not in `knownUserMessageIds`") from `firstPage` alone — the
440
+ // retry's `while` loop never runs, and `nextCursor` advances by one page
441
+ // instead of walking the rest of the turn. Accepted: the pages already
442
+ // landed in the store on the failed attempt via this same callback,
443
+ // which is the fix this method exists for.
444
+ this.rememberUserMessages(partialPage.messages);
445
+ this.options.hydrate(partialPage.messages);
446
+ this.setCursor(partialPage.nextCursor);
447
+ });
225
448
  }
226
- async loadCompleteTurn(firstPage, operation, reason, initialCursor) {
449
+ async loadCompleteTurn(firstPage, operation, reason, initialCursor, onPage) {
227
450
  const messages = [...firstPage.messages];
228
451
  const knownUserMessageIds = new Set(this.knownUserMessageIds);
229
452
  const seenCursors = new Set(initialCursor ? [initialCursor] : []);
230
453
  let cursor = firstPage.nextCursor;
454
+ let pagesRead = 0;
231
455
  for (const message of firstPage.messages) {
232
456
  if (message.info.role === 'user') {
233
457
  knownUserMessageIds.add(message.info.id);
234
458
  }
235
459
  }
236
460
  while (cursor &&
461
+ // BOUNDED. Without a ceiling a turn of a few thousand messages paged the
462
+ // whole session before anything rendered — see MAX_TURN_BACKFILL_PAGES.
463
+ pagesRead < MAX_TURN_BACKFILL_PAGES &&
237
464
  messages.some((message) => message.info.role === 'assistant' &&
238
465
  Boolean(message.info.parentID) &&
239
466
  !knownUserMessageIds.has(message.info.parentID))) {
@@ -241,7 +468,21 @@ export class SessionSyncController {
241
468
  throw new Error(`Session history cursor repeated: ${cursor}`);
242
469
  }
243
470
  seenCursors.add(cursor);
244
- const page = await this.loadPage(operation, reason, cursor);
471
+ let page;
472
+ try {
473
+ page = await this.loadPage(operation, reason, cursor);
474
+ }
475
+ catch (error) {
476
+ // `messages` already holds every page read so far — firstPage
477
+ // (seeded before the loop) plus every completed iteration. Commit it
478
+ // before rethrowing so a rejection on THIS read (including the
479
+ // loop's very first) cannot drop what was already read.
480
+ onPage?.({ messages: [...messages], nextCursor: cursor });
481
+ throw error;
482
+ }
483
+ if (this.destroyed)
484
+ return { messages, nextCursor: cursor };
485
+ pagesRead += 1;
245
486
  messages.unshift(...page.messages);
246
487
  for (const message of page.messages) {
247
488
  if (message.info.role === 'user') {
@@ -249,6 +490,9 @@ export class SessionSyncController {
249
490
  }
250
491
  }
251
492
  cursor = page.nextCursor;
493
+ // Repaint as each page lands, so a long turn fills in front of the user
494
+ // instead of withholding everything until the walk ends.
495
+ onPage?.({ messages: [...messages], nextCursor: cursor });
252
496
  }
253
497
  return { messages, nextCursor: cursor };
254
498
  }
@@ -260,9 +504,17 @@ export class SessionSyncController {
260
504
  }
261
505
  }
262
506
  async checkLiveness() {
263
- if (this.destroyed || this.scheduler.now() - this.lastActivityAt <= this.livenessIntervalMs) {
507
+ if (this.destroyed)
508
+ return;
509
+ const nowMs = this.scheduler.now();
510
+ const quiet = nowMs - this.lastActivityAt > this.livenessIntervalMs;
511
+ // `noteActivity` proves frames are ARRIVING, not that none were lost. A
512
+ // degraded stream that still delivers a trickle renewed the quiet timer
513
+ // forever while the transcript diverged — so a busy session re-reads the
514
+ // tail at `verifyIntervalMs` no matter how live the stream looks.
515
+ const verifyDue = nowMs - this.lastTailReadAt >= this.verifyIntervalMs;
516
+ if (!quiet && !verifyDue)
264
517
  return;
265
- }
266
518
  // Reconcile the TAIL, and nothing else. This is the repair for a dropped
267
519
  // SSE stream: the transcript catches up on messages the stream never
268
520
  // delivered.