@beeuni/sdk 0.1.0 → 0.3.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 (291) hide show
  1. package/README.md +53 -92
  2. package/dist/beeuni.esm.min.js +36 -34
  3. package/dist/beeuni.global.js +36 -34
  4. package/dist/browser/cache/idb-sync-cache.js +15 -5
  5. package/dist/browser/session-sync/server-transcript-mirror.d.ts +7 -1
  6. package/dist/browser/session-sync/server-transcript-mirror.js +30 -24
  7. package/dist/browser/session-sync/session-sync-registry.d.ts +3 -1
  8. package/dist/browser/session-sync/session-sync-registry.js +48 -3
  9. package/dist/browser/stores/sync-store/keep-unchanged.d.ts +4 -0
  10. package/dist/browser/stores/sync-store/keep-unchanged.js +34 -0
  11. package/dist/browser/stores/sync-store.d.ts +7 -0
  12. package/dist/browser/stores/sync-store.js +122 -35
  13. package/dist/core/attachments/prompt-attachments.d.ts +4 -0
  14. package/dist/core/attachments/prompt-attachments.js +34 -0
  15. package/dist/core/capture/rules.d.ts +17 -0
  16. package/dist/core/capture/rules.js +27 -0
  17. package/dist/core/client/beeuni.d.ts +99 -34
  18. package/dist/core/client/beeuni.js +5 -11
  19. package/dist/core/client/capture.d.ts +105 -0
  20. package/dist/core/client/capture.js +48 -0
  21. package/dist/core/client/facade-catalog.d.ts +0 -4
  22. package/dist/core/client/facade-catalog.js +1 -5
  23. package/dist/core/client/facade-identity.d.ts +1 -0
  24. package/dist/core/client/facade-identity.js +1 -0
  25. package/dist/core/client/project-access-resources.d.ts +3 -1
  26. package/dist/core/client/project-access-resources.js +1 -1
  27. package/dist/core/client/project-connections.d.ts +2 -2
  28. package/dist/core/client/project-connections.js +2 -2
  29. package/dist/core/client/project-operations-admin.d.ts +4 -4
  30. package/dist/core/client/project-operations-review.d.ts +2 -3
  31. package/dist/core/client/project-operations-review.js +1 -0
  32. package/dist/core/client/project-operations-workspace.d.ts +4 -4
  33. package/dist/core/client/project-operations-workspace.js +1 -1
  34. package/dist/core/client/project-operations.d.ts +9 -19
  35. package/dist/core/client/project-operations.js +2 -6
  36. package/dist/core/client/project-platform-security.d.ts +1 -1
  37. package/dist/core/client/project-platform-settings.d.ts +1 -1
  38. package/dist/core/client/session-context.d.ts +2 -0
  39. package/dist/core/client/session-handle.d.ts +13 -0
  40. package/dist/core/client/session-lifecycle-resources.d.ts +11 -0
  41. package/dist/core/client/session-lifecycle-resources.js +4 -0
  42. package/dist/core/client/session-lifecycle-security.d.ts +2 -0
  43. package/dist/core/client/session-lifecycle-security.js +1 -0
  44. package/dist/core/client/session-readiness.d.ts +2 -0
  45. package/dist/core/client/session-readiness.js +20 -1
  46. package/dist/core/http/api-client.d.ts +1 -7
  47. package/dist/core/http/api-client.js +2 -37
  48. package/dist/core/http/auth.d.ts +2 -4
  49. package/dist/core/http/auth.js +2 -8
  50. package/dist/core/http/config.d.ts +2 -0
  51. package/dist/core/http/response-error.js +26 -11
  52. package/dist/core/http/transport.js +6 -1
  53. package/dist/core/models/composer-model.d.ts +14 -0
  54. package/dist/core/models/composer-model.js +21 -1
  55. package/dist/core/rest/platform-client/capture.d.ts +363 -0
  56. package/dist/core/rest/platform-client/capture.js +116 -0
  57. package/dist/core/rest/platform-client/client-policy.d.ts +8 -0
  58. package/dist/core/rest/platform-client/client-policy.js +30 -0
  59. package/dist/core/rest/platform-client/index.d.ts +4 -4
  60. package/dist/core/rest/platform-client/index.js +4 -4
  61. package/dist/core/rest/platform-client/lifecycle.js +18 -12
  62. package/dist/core/rest/platform-client/shared.d.ts +5 -0
  63. package/dist/core/rest/platform-client/shared.js +18 -0
  64. package/dist/core/rest/platform-client/types.d.ts +1 -1
  65. package/dist/core/rest/projects-client/account-secret-resources.d.ts +2 -1
  66. package/dist/core/rest/projects-client/accounts.d.ts +34 -0
  67. package/dist/core/rest/projects-client/accounts.js +10 -0
  68. package/dist/core/rest/projects-client/admin-providers.d.ts +0 -61
  69. package/dist/core/rest/projects-client/admin-providers.js +0 -6
  70. package/dist/core/rest/projects-client/agent-config.d.ts +3 -0
  71. package/dist/core/rest/projects-client/apps-fleet.d.ts +5 -1
  72. package/dist/core/rest/projects-client/apps-fleet.js +2 -2
  73. package/dist/core/rest/projects-client/apps.d.ts +11 -1
  74. package/dist/core/rest/projects-client/change-requests.d.ts +2 -0
  75. package/dist/core/rest/projects-client/change-requests.js +2 -0
  76. package/dist/core/rest/projects-client/connectors.d.ts +13 -55
  77. package/dist/core/rest/projects-client/connectors.js +5 -29
  78. package/dist/core/rest/projects-client/files.d.ts +3 -0
  79. package/dist/core/rest/projects-client/files.js +4 -0
  80. package/dist/core/rest/projects-client/gateway.d.ts +5 -0
  81. package/dist/core/rest/projects-client/git-backend.d.ts +1 -10
  82. package/dist/core/rest/projects-client/git-backend.js +0 -10
  83. package/dist/core/rest/projects-client/iam-actions.d.ts +76 -0
  84. package/dist/core/rest/projects-client/iam-actions.js +101 -0
  85. package/dist/core/rest/projects-client/iam-sso.d.ts +0 -10
  86. package/dist/core/rest/projects-client/iam-sso.js +0 -3
  87. package/dist/core/rest/projects-client/iam.d.ts +1 -0
  88. package/dist/core/rest/projects-client/iam.js +1 -0
  89. package/dist/core/rest/projects-client/index.d.ts +1 -4
  90. package/dist/core/rest/projects-client/index.js +1 -4
  91. package/dist/core/rest/projects-client/notifications.d.ts +1 -1
  92. package/dist/core/rest/projects-client/notifications.js +4 -0
  93. package/dist/core/rest/projects-client/projects.d.ts +4 -2
  94. package/dist/core/rest/projects-client/projects.js +4 -2
  95. package/dist/core/rest/projects-client/review-items.d.ts +2 -0
  96. package/dist/core/rest/projects-client/review-items.js +4 -0
  97. package/dist/core/rest/projects-client/review.d.ts +17 -3
  98. package/dist/core/rest/projects-client/review.js +11 -0
  99. package/dist/core/rest/projects-client/sandbox.d.ts +8 -9
  100. package/dist/core/rest/projects-client/secrets.d.ts +3 -0
  101. package/dist/core/rest/projects-client/session-attachments.js +14 -1
  102. package/dist/core/rest/projects-client/session-sandbox.d.ts +1 -1
  103. package/dist/core/rest/projects-client/sessions.d.ts +31 -4
  104. package/dist/core/rest/projects-client/sessions.js +34 -4
  105. package/dist/core/runtime/runtime-rest-client.js +1 -1
  106. package/dist/core/runtime/runtime-types.d.ts +1 -0
  107. package/dist/core/session/control-stream.d.ts +1 -0
  108. package/dist/core/session/control-stream.js +18 -1
  109. package/dist/core/session/open-bundle.d.ts +4 -4
  110. package/dist/core/session/open-bundle.js +29 -46
  111. package/dist/core/session/turn-end-watch.d.ts +47 -0
  112. package/dist/core/session/turn-end-watch.js +141 -0
  113. package/dist/core/session-log/guard.d.ts +6 -0
  114. package/dist/core/session-log/guard.js +109 -0
  115. package/dist/core/session-log/index.d.ts +7 -0
  116. package/dist/core/session-log/index.js +5 -0
  117. package/dist/core/session-log/saved-log.d.ts +10 -0
  118. package/dist/core/session-log/saved-log.js +25 -0
  119. package/dist/core/session-log/source.d.ts +25 -0
  120. package/dist/core/session-log/source.js +30 -0
  121. package/dist/core/session-log/store.d.ts +21 -0
  122. package/dist/core/session-log/store.js +126 -0
  123. package/dist/core/session-log/tool-call.d.ts +11 -0
  124. package/dist/core/session-log/tool-call.js +65 -0
  125. package/dist/core/session-log/turns/blocks.d.ts +60 -0
  126. package/dist/core/session-log/turns/blocks.js +124 -0
  127. package/dist/core/session-log/turns/history.d.ts +32 -0
  128. package/dist/core/session-log/turns/history.js +99 -0
  129. package/dist/core/session-log/turns/index.d.ts +4 -0
  130. package/dist/core/session-log/turns/index.js +4 -0
  131. package/dist/core/session-log/turns/metrics.d.ts +21 -0
  132. package/dist/core/session-log/turns/metrics.js +29 -0
  133. package/dist/core/session-log/turns/turn.d.ts +55 -0
  134. package/dist/core/session-log/turns/turn.js +279 -0
  135. package/dist/core/session-log/turns/v1-parts.d.ts +8 -0
  136. package/dist/core/session-log/turns/v1-parts.js +43 -0
  137. package/dist/core/session-log/types.d.ts +264 -0
  138. package/dist/core/session-log/types.js +6 -0
  139. package/dist/core/session-log/v1-view.d.ts +13 -0
  140. package/dist/core/session-log/v1-view.js +215 -0
  141. package/dist/core/session-log/v1.d.ts +31 -0
  142. package/dist/core/session-log/v1.js +473 -0
  143. package/dist/core/session-sync/copy-store-types.d.ts +19 -0
  144. package/dist/core/session-sync/copy-store-types.js +1 -0
  145. package/dist/core/session-sync/saved-copy-store.js +25 -4
  146. package/dist/core/session-sync/saved-transcript.d.ts +15 -1
  147. package/dist/core/session-sync/saved-transcript.js +55 -0
  148. package/dist/core/session-sync/session-sync-controller.js +3 -0
  149. package/dist/core/stream/event-stream.js +2 -2
  150. package/dist/core/turns/classify.d.ts +2 -1
  151. package/dist/core/turns/classify.js +7 -3
  152. package/dist/core/turns/edit-blocks.d.ts +1 -0
  153. package/dist/core/turns/edit-blocks.js +15 -0
  154. package/dist/core/turns/parts.d.ts +3 -2
  155. package/dist/core/turns/parts.js +4 -2
  156. package/dist/core/turns/segments/burst-summary.d.ts +1 -0
  157. package/dist/core/turns/segments/burst-summary.js +24 -0
  158. package/dist/core/turns/segments/group-steps.d.ts +4 -3
  159. package/dist/core/turns/segments/group-steps.js +9 -6
  160. package/dist/core/turns/segments/index.d.ts +1 -0
  161. package/dist/core/turns/segments/index.js +1 -0
  162. package/dist/core/turns/segments/narration.d.ts +5 -4
  163. package/dist/core/turns/segments/narration.js +12 -3
  164. package/dist/core/turns/segments/session-activity-groups.d.ts +2 -1
  165. package/dist/core/turns/segments/session-activity-groups.js +3 -1
  166. package/dist/core/turns/segments/step-label.d.ts +2 -1
  167. package/dist/core/turns/segments/step-label.js +3 -1
  168. package/dist/core/turns/segments/work-split.d.ts +10 -0
  169. package/dist/core/turns/segments/work-split.js +40 -0
  170. package/dist/core/turns/segments/working-turn.d.ts +4 -2
  171. package/dist/core/turns/segments/working-turn.js +9 -8
  172. package/dist/core/turns/state.d.ts +2 -7
  173. package/dist/core/turns/state.js +28 -134
  174. package/dist/core/turns/tool-kind.d.ts +1 -1
  175. package/dist/core/turns/tool-kind.js +3 -2
  176. package/dist/core/turns/tool-registry.js +1 -0
  177. package/dist/core/turns/tools/tool-meta.d.ts +3 -2
  178. package/dist/core/turns/tools/tool-meta.js +5 -2
  179. package/dist/core/turns/tools/tool-outcome.d.ts +2 -1
  180. package/dist/core/turns/tools/tool-outcome.js +3 -1
  181. package/dist/core/turns/types.d.ts +1 -1
  182. package/dist/core/turns/view-model.js +1 -13
  183. package/dist/core/warm-session-pool.d.ts +29 -0
  184. package/dist/core/warm-session-pool.js +127 -0
  185. package/dist/genui/catalog.d.ts +28 -0
  186. package/dist/genui/catalog.js +396 -0
  187. package/dist/genui/expansion.d.ts +3 -0
  188. package/dist/genui/expansion.js +295 -0
  189. package/dist/genui/fence-entry.d.ts +2 -0
  190. package/dist/genui/fence-entry.js +1 -0
  191. package/dist/genui/fence.d.ts +21 -0
  192. package/dist/genui/fence.js +138 -0
  193. package/dist/genui/index.d.ts +6 -0
  194. package/dist/genui/index.js +6 -0
  195. package/dist/genui/markdown.d.ts +12 -0
  196. package/dist/genui/markdown.js +104 -0
  197. package/dist/genui/parse.d.ts +6 -0
  198. package/dist/genui/parse.js +75 -0
  199. package/dist/genui/prompt.d.ts +2 -0
  200. package/dist/genui/prompt.js +206 -0
  201. package/dist/genui/react/default-components.d.ts +2 -0
  202. package/dist/genui/react/default-components.js +72 -0
  203. package/dist/genui/react/default-components.test.d.ts +1 -0
  204. package/dist/genui/react/default-components.test.js +94 -0
  205. package/dist/genui/react/genui-block-disabled.test.d.ts +1 -0
  206. package/dist/genui/react/genui-block-disabled.test.js +50 -0
  207. package/dist/genui/react/genui-block.d.ts +28 -0
  208. package/dist/genui/react/genui-block.js +91 -0
  209. package/dist/genui/react/genui-block.test.d.ts +1 -0
  210. package/dist/genui/react/genui-block.test.js +205 -0
  211. package/dist/genui/react/index.d.ts +2 -0
  212. package/dist/genui/react/index.js +2 -0
  213. package/dist/genui/share.d.ts +5 -0
  214. package/dist/genui/share.js +30 -0
  215. package/dist/genui/test-fixtures.d.ts +2 -0
  216. package/dist/genui/test-fixtures.js +16 -0
  217. package/dist/genui/types.d.ts +29 -0
  218. package/dist/genui/types.js +1 -0
  219. package/dist/genui/urls.d.ts +1 -0
  220. package/dist/genui/urls.js +18 -0
  221. package/dist/genui/validate.d.ts +9 -0
  222. package/dist/genui/validate.js +116 -0
  223. package/dist/index.d.ts +4 -4
  224. package/dist/index.js +4 -4
  225. package/dist/internal/connector-catalog.d.ts +48 -0
  226. package/dist/internal/connector-catalog.js +27 -0
  227. package/dist/internal/host-configuration.d.ts +22 -0
  228. package/dist/internal/host-configuration.js +28 -0
  229. package/dist/internal/platform-admin-react.d.ts +12 -0
  230. package/dist/internal/platform-admin-react.js +13 -0
  231. package/dist/internal/platform-admin.d.ts +9 -0
  232. package/dist/internal/platform-admin.js +9 -0
  233. package/dist/platform/ui.d.ts +8 -2
  234. package/dist/platform/ui.js +19 -4
  235. package/dist/react/chat/use-chat-turns.d.ts +1 -1
  236. package/dist/react/human-presence.d.ts +9 -0
  237. package/dist/react/human-presence.js +12 -0
  238. package/dist/react/index.d.ts +4 -13
  239. package/dist/react/index.js +4 -13
  240. package/dist/react/opencode.d.ts +1 -0
  241. package/dist/react/opencode.js +1 -0
  242. package/dist/react/query-contracts.d.ts +6 -0
  243. package/dist/react/query-contracts.js +6 -0
  244. package/dist/react/query-keys.d.ts +17 -2
  245. package/dist/react/query-keys.js +24 -2
  246. package/dist/react/reset-identity-state.js +2 -0
  247. package/dist/react/session-cache-write.d.ts +1 -0
  248. package/dist/react/session-cache-write.js +20 -0
  249. package/dist/react/session-transcript-subscription.js +3 -1
  250. package/dist/react/sign-in-with-beeuni.d.ts +6 -6
  251. package/dist/react/turn-end-handoff.d.ts +13 -0
  252. package/dist/react/turn-end-handoff.js +101 -0
  253. package/dist/react/use-admin-providers.d.ts +3 -6
  254. package/dist/react/use-admin-providers.js +1 -11
  255. package/dist/react/use-capture.d.ts +47 -0
  256. package/dist/react/use-capture.js +173 -0
  257. package/dist/react/use-feature-flag.js +1 -0
  258. package/dist/react/use-opencode-events/handle-event.d.ts +1 -0
  259. package/dist/react/use-opencode-events/handle-event.js +5 -0
  260. package/dist/react/use-opencode-events/handle-interaction-event.js +8 -3
  261. package/dist/react/use-opencode-events/handle-session-event.js +13 -8
  262. package/dist/react/use-opencode-events/handler-context.d.ts +1 -0
  263. package/dist/react/use-opencode-events/helpers.d.ts +1 -0
  264. package/dist/react/use-opencode-events/helpers.js +11 -1
  265. package/dist/react/use-opencode-events/index.js +1 -0
  266. package/dist/react/use-opencode-local.d.ts +5 -1
  267. package/dist/react/use-opencode-local.js +15 -2
  268. package/dist/react/use-opencode-sessions/commands.d.ts +11 -1
  269. package/dist/react/use-opencode-sessions/commands.js +21 -1
  270. package/dist/react/use-opencode-sessions/permissions.js +28 -1
  271. package/dist/react/use-project-sessions.d.ts +1 -1
  272. package/dist/react/use-project-sessions.js +6 -5
  273. package/dist/react/use-prompt-attachments.js +1 -0
  274. package/dist/react/use-review-summary.d.ts +13 -0
  275. package/dist/react/use-review-summary.js +32 -0
  276. package/dist/react/use-runtime-reconnect.js +1 -1
  277. package/dist/react/use-session-list-state.d.ts +6 -0
  278. package/dist/react/use-session-list-state.js +33 -0
  279. package/dist/react/use-session-log.d.ts +16 -0
  280. package/dist/react/use-session-log.js +143 -0
  281. package/dist/react/use-session-messages.d.ts +2 -1
  282. package/dist/react/use-session-messages.js +15 -14
  283. package/dist/react/use-session-stream.js +12 -1
  284. package/dist/react/use-session-sync.d.ts +2 -2
  285. package/dist/react/use-session-sync.js +10 -8
  286. package/dist/react/use-session-transcript-history.js +2 -12
  287. package/dist/react/use-session-working.d.ts +2 -0
  288. package/dist/react/use-session-working.js +10 -0
  289. package/dist/react/use-session.d.ts +6 -0
  290. package/dist/react/use-session.js +88 -6
  291. package/package.json +62 -8
@@ -23,11 +23,21 @@ function openDB() {
23
23
  const pending = new Promise((resolve, reject) => {
24
24
  const request = idb.open(DB_NAME, DB_VERSION);
25
25
  request.onupgradeneeded = () => {
26
- const db = request.result;
27
- if (db.objectStoreNames.contains(RETIRED_STORE_NAME))
28
- db.deleteObjectStore(RETIRED_STORE_NAME);
29
- if (!db.objectStoreNames.contains(STORE_NAME))
30
- db.createObjectStore(STORE_NAME);
26
+ try {
27
+ const db = request.result;
28
+ if (db.objectStoreNames.contains(RETIRED_STORE_NAME))
29
+ db.deleteObjectStore(RETIRED_STORE_NAME);
30
+ if (!db.objectStoreNames.contains(STORE_NAME))
31
+ db.createObjectStore(STORE_NAME);
32
+ }
33
+ catch (error) {
34
+ reject(error);
35
+ try {
36
+ request.transaction?.abort();
37
+ }
38
+ catch {
39
+ }
40
+ }
31
41
  };
32
42
  request.onsuccess = () => {
33
43
  const db = request.result;
@@ -18,7 +18,6 @@ export declare function parseBeeuniSessionScope(scope: string | undefined): {
18
18
  } | null;
19
19
  export declare function loadSessionTranscriptMirror(input: {
20
20
  beeuniSessionScope: string | undefined;
21
- limit?: number;
22
21
  signal?: AbortSignal;
23
22
  child?: string;
24
23
  }): Promise<SessionTranscriptSyncEnvelope | null>;
@@ -29,5 +28,12 @@ export declare function loadOlderSessionTranscriptMirror(input: {
29
28
  signal?: AbortSignal;
30
29
  child?: string;
31
30
  }): Promise<SessionTranscriptSyncEnvelope | null>;
31
+ export declare function loadSavedSessionTranscriptMessages(scope: {
32
+ projectId: string;
33
+ sessionId: string;
34
+ }, signal?: AbortSignal): Promise<Array<{
35
+ info: Message;
36
+ parts: Part[];
37
+ }>>;
32
38
  export declare const SAVED_COPY_REFRESH_DELAY_MS = 3000;
33
39
  export declare function refreshSavedCopy(store: SavedCopyStore, projectId: string, sessionId: string): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { getSessionTranscriptSync, } from "../../core/rest/projects-client/sessions.js";
2
- import { claimOpenBundle, takeOpenBundleTranscript, takeOpenBundleTranscriptAbsence, } from "../../core/session/open-bundle.js";
2
+ import { loadSavedTranscript } from "../../core/session-sync/saved-transcript.js";
3
3
  export const MIRROR_HYDRATE_LIMIT = 40;
4
4
  export function shouldHydrateFromMirror(input) {
5
5
  const { envelope, runtimeSessionId, hasMessages } = input;
@@ -52,31 +52,14 @@ export async function loadSessionTranscriptMirror(input) {
52
52
  const scope = parseBeeuniSessionScope(input.beeuniSessionScope);
53
53
  if (!scope)
54
54
  return null;
55
- if (input.child) {
56
- try {
57
- return await getSessionTranscriptSync(scope.projectId, scope.sessionId, {
58
- limit: input.limit ?? MIRROR_HYDRATE_LIMIT,
55
+ try {
56
+ return input.child
57
+ ? await getSessionTranscriptSync(scope.projectId, scope.sessionId, {
58
+ limit: MIRROR_HYDRATE_LIMIT,
59
59
  signal: input.signal,
60
60
  child: input.child,
61
- });
62
- }
63
- catch {
64
- return null;
65
- }
66
- }
67
- const claimed = claimOpenBundle(scope.projectId, scope.sessionId);
68
- if (claimed)
69
- await claimed;
70
- const stashed = takeOpenBundleTranscript(scope.projectId, scope.sessionId);
71
- if (stashed)
72
- return stashed;
73
- if (takeOpenBundleTranscriptAbsence(scope.projectId, scope.sessionId))
74
- return null;
75
- try {
76
- return await getSessionTranscriptSync(scope.projectId, scope.sessionId, {
77
- limit: input.limit ?? MIRROR_HYDRATE_LIMIT,
78
- signal: input.signal,
79
- });
61
+ })
62
+ : await loadSavedTranscript(scope.projectId, scope.sessionId, { signal: input.signal });
80
63
  }
81
64
  catch {
82
65
  return null;
@@ -98,6 +81,29 @@ export async function loadOlderSessionTranscriptMirror(input) {
98
81
  return null;
99
82
  }
100
83
  }
84
+ const SAVED_EXPORT_WINDOW = 500;
85
+ export async function loadSavedSessionTranscriptMessages(scope, signal) {
86
+ const windows = [];
87
+ const seen = new Set();
88
+ let before = null;
89
+ do {
90
+ const envelope = await getSessionTranscriptSync(scope.projectId, scope.sessionId, {
91
+ limit: SAVED_EXPORT_WINDOW,
92
+ history: true,
93
+ before,
94
+ signal,
95
+ });
96
+ if (!envelope.available)
97
+ break;
98
+ windows.unshift(mirrorMessagesForHydrate(envelope));
99
+ before = envelope.next_cursor ?? null;
100
+ if (before !== null && seen.has(before))
101
+ break;
102
+ if (before !== null)
103
+ seen.add(before);
104
+ } while (before !== null);
105
+ return windows.flat();
106
+ }
101
107
  export const SAVED_COPY_REFRESH_DELAY_MS = 3_000;
102
108
  export async function refreshSavedCopy(store, projectId, sessionId) {
103
109
  try {
@@ -1,5 +1,5 @@
1
1
  import type { Message, Part, SessionStatus } from '../../core/runtime/runtime-types.js';
2
- import { SessionSyncController, type SessionSyncPage, type SessionSyncReason } from '../../core/session-sync/session-sync-controller.js';
2
+ import { SessionSyncController, type SessionSyncPage, type SessionSyncReason, type SessionSyncTelemetryEvent } from '../../core/session-sync/session-sync-controller.js';
3
3
  interface MessagesResponse {
4
4
  data?: Array<{
5
5
  info: Message;
@@ -28,6 +28,7 @@ export declare function readSessionMessagePage(client: SessionMessageClient, ses
28
28
  limit: number;
29
29
  before?: string;
30
30
  }, signal?: AbortSignal): Promise<SessionSyncPage>;
31
+ export declare function reportTelemetry(sessionId: string, event: SessionSyncTelemetryEvent): void;
31
32
  export declare function getSessionSyncController(sessionId: string, client?: SessionMessageClient, runtimeScope?: string): SessionSyncController;
32
33
  export declare function prefetchSessionSyncOnce(sessionId: string, source: SessionPrefetchSource, client?: SessionMessageClient): Promise<boolean>;
33
34
  export declare function clearActiveSessionPrefetches(): void;
@@ -35,6 +36,7 @@ export declare function retainSessionSyncController(sessionId: string, runtimeSc
35
36
  export declare function reconcileSessionTail(sessionId: string, reason: SessionSyncReason, runtimeScope?: string): Promise<void>;
36
37
  export declare function loadSessionRuntimeStatus(sessionId: string, client?: SessionMessageClient): Promise<SessionStatus | null>;
37
38
  export declare function loadSessionTranscriptMessages(sessionId: string): Promise<SessionSyncPage['messages']>;
39
+ export declare function loadSessionExportMessages(sessionId: string, beeuniSessionScope?: string): Promise<SessionSyncPage['messages']>;
38
40
  export declare function noteSessionSyncEvent(event: {
39
41
  type?: string;
40
42
  properties?: unknown;
@@ -4,8 +4,10 @@ import { ApiError } from '../../core/http/api/errors.js';
4
4
  import { SessionSyncController, loadCompleteSessionHistory, } from '../../core/session-sync/session-sync-controller.js';
5
5
  import { getCurrentRuntimeSandboxId, getCurrentRuntimeUrl } from '../../core/session/current-runtime.js';
6
6
  import { useSyncStore } from '../stores/sync-store.js';
7
+ import { loadSavedSessionTranscriptMessages, parseBeeuniSessionScope } from './server-transcript-mirror.js';
7
8
  export const ACTIVE_SESSION_PREFETCH_SOURCE = Symbol('active-session-prefetch');
8
9
  const MAX_CONTROLLERS = 20;
10
+ const IDLE_BEFORE_EVICT_MS = 30_000;
9
11
  const controllers = new Map();
10
12
  function runtimeScopeKey(runtimeScope) {
11
13
  return runtimeScope ?? getCurrentRuntimeSandboxId() ?? 'none';
@@ -83,7 +85,14 @@ export async function readSessionMessagePage(client, sessionId, request, signal)
83
85
  nextCursor: result.response?.headers.get('x-next-cursor') || undefined,
84
86
  };
85
87
  }
86
- function reportTelemetry(sessionId, event) {
88
+ export function reportTelemetry(sessionId, event) {
89
+ try {
90
+ if (!globalThis.localStorage?.getItem('beeuni:debug'))
91
+ return;
92
+ }
93
+ catch {
94
+ return;
95
+ }
87
96
  console.debug('[session-sync]', { sessionId, ...event });
88
97
  }
89
98
  function resolveClient(key) {
@@ -118,8 +127,11 @@ function createController(sessionId, key) {
118
127
  function evictInactiveControllers(protectedKey) {
119
128
  if (controllers.size <= MAX_CONTROLLERS)
120
129
  return;
130
+ const now = Date.now();
121
131
  const inactive = [...controllers.entries()]
122
- .filter(([key, entry]) => entry.consumers === 0 && key !== protectedKey)
132
+ .filter(([key, entry]) => entry.consumers === 0 &&
133
+ key !== protectedKey &&
134
+ now - entry.lastUsedAt > IDLE_BEFORE_EVICT_MS)
123
135
  .sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt);
124
136
  for (const [key, entry] of inactive) {
125
137
  entry.controller.destroy();
@@ -128,6 +140,21 @@ function evictInactiveControllers(protectedKey) {
128
140
  return;
129
141
  }
130
142
  }
143
+ let evictionTimer;
144
+ function scheduleEviction(delayMs = IDLE_BEFORE_EVICT_MS + 1) {
145
+ if (evictionTimer !== undefined)
146
+ return;
147
+ evictionTimer = setTimeout(() => {
148
+ evictionTimer = undefined;
149
+ evictInactiveControllers();
150
+ if (controllers.size <= MAX_CONTROLLERS)
151
+ return;
152
+ const idleSince = Math.min(...[...controllers.values()].filter((entry) => entry.consumers === 0).map((entry) => entry.lastUsedAt));
153
+ if (Number.isFinite(idleSince))
154
+ scheduleEviction(Math.max(1, idleSince + IDLE_BEFORE_EVICT_MS + 1 - Date.now()));
155
+ }, delayMs);
156
+ evictionTimer.unref?.();
157
+ }
131
158
  function getOrCreateRegistryEntry(sessionId, client, initialConsumers = 0, runtimeScope) {
132
159
  const key = controllerKey(sessionId, runtimeScope);
133
160
  const existing = controllers.get(key);
@@ -197,8 +224,10 @@ export function retainSessionSyncController(sessionId, runtimeScope) {
197
224
  return;
198
225
  current.consumers = Math.max(0, current.consumers - 1);
199
226
  current.lastUsedAt = Date.now();
200
- if (current.consumers === 0)
227
+ if (current.consumers === 0) {
201
228
  controller.setBusy(false);
229
+ scheduleEviction();
230
+ }
202
231
  evictInactiveControllers();
203
232
  };
204
233
  }
@@ -217,6 +246,20 @@ export async function loadSessionRuntimeStatus(sessionId, client = getClient())
217
246
  export function loadSessionTranscriptMessages(sessionId) {
218
247
  return loadCompleteSessionHistory((request) => readSessionMessagePage(getClient(), sessionId, request));
219
248
  }
249
+ export async function loadSessionExportMessages(sessionId, beeuniSessionScope) {
250
+ const saved = parseBeeuniSessionScope(beeuniSessionScope);
251
+ let live = [];
252
+ try {
253
+ live = await loadSessionTranscriptMessages(sessionId);
254
+ }
255
+ catch (error) {
256
+ if (!saved)
257
+ throw error;
258
+ }
259
+ if (live.length > 0 || !saved)
260
+ return live;
261
+ return loadSavedSessionTranscriptMessages(saved);
262
+ }
220
263
  const TRANSCRIPT_EVENT_TYPES = new Set([
221
264
  'message.updated',
222
265
  'message.part.updated',
@@ -242,6 +285,8 @@ export function resetSessionSyncControllers() {
242
285
  for (const entry of controllers.values())
243
286
  entry.controller.destroy();
244
287
  controllers.clear();
288
+ clearTimeout(evictionTimer);
289
+ evictionTimer = undefined;
245
290
  }
246
291
  export function resetSessionSyncControllersForSession(sessionId, keepRuntimeScope) {
247
292
  for (const [key, entry] of controllers) {
@@ -0,0 +1,4 @@
1
+ export declare function sameValue(a: unknown, b: unknown): boolean;
2
+ export declare function keepUnchanged<M extends {
3
+ id: string;
4
+ }, P>(previous: readonly M[] | undefined, next: M[], previousParts: Record<string, P[] | undefined>, nextParts: Record<string, P[] | undefined>): M[];
@@ -0,0 +1,34 @@
1
+ export function sameValue(a, b) {
2
+ if (a === b)
3
+ return true;
4
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
5
+ return false;
6
+ if (Array.isArray(a) || Array.isArray(b)) {
7
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
8
+ return false;
9
+ return a.every((value, index) => sameValue(value, b[index]));
10
+ }
11
+ const left = a;
12
+ const right = b;
13
+ const keys = Object.keys(left);
14
+ if (keys.length !== Object.keys(right).length)
15
+ return false;
16
+ return keys.every((key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]));
17
+ }
18
+ export function keepUnchanged(previous, next, previousParts, nextParts) {
19
+ const byId = new Map((previous ?? []).map((message) => [message.id, message]));
20
+ const shared = next.map((message) => {
21
+ const old = byId.get(message.id);
22
+ return old && sameValue(old, message) ? old : message;
23
+ });
24
+ for (const message of shared) {
25
+ const before = previousParts[message.id];
26
+ const after = nextParts[message.id];
27
+ if (before && after && before !== after && sameValue(before, after))
28
+ nextParts[message.id] = before;
29
+ }
30
+ const unchanged = previous !== undefined &&
31
+ previous.length === shared.length &&
32
+ shared.every((message, index) => message === previous[index]);
33
+ return unchanged ? previous : shared;
34
+ }
@@ -1,4 +1,6 @@
1
1
  import type { Message, Event as RuntimeEvent, Part, SessionStatus, Todo } from "../../core/runtime/runtime-types.js";
2
+ import type { SessionLogPage } from "../../core/session-log/source.js";
3
+ import { type SessionLogEventInput, type SessionLogState } from "../../core/session-log/store.js";
2
4
  import { ascendingId } from "./sync-store/ascending-id.js";
3
5
  import { Binary } from "./sync-store/binary.js";
4
6
  import type { FileDiff, MessageError, MessageWithParts, SessionRewindState } from "./sync-store/types.js";
@@ -16,7 +18,11 @@ interface SyncState {
16
18
  todos: Record<string, Todo[]>;
17
19
  sessionRevert: Record<string, SessionRewindState | null>;
18
20
  sessionRevertNeedsTailReconcile: Record<string, boolean>;
21
+ sessionLogs: Record<string, SessionLogState>;
19
22
  applyEvent: (event: RuntimeEvent) => void;
23
+ applySessionLogPage: (sessionId: string, page: SessionLogPage) => void;
24
+ applySessionLogEvent: (sessionId: string, event: SessionLogEventInput) => void;
25
+ dropSessionLog: (sessionId: string) => void;
20
26
  upsertMessage: (sessionID: string, message: Message) => void;
21
27
  removeMessage: (sessionID: string, messageID: string) => void;
22
28
  upsertPart: (messageID: string, part: Part, sessionID?: string) => void;
@@ -62,4 +68,5 @@ interface SyncState {
62
68
  statuses: Record<string, SessionStatus>;
63
69
  }
64
70
  export declare function hasOnlyCacheSourcedMessages(sessionID: string): boolean;
71
+ export declare function hasRuntimeConfirmedMessage(sessionID: string, messageID: string): boolean;
65
72
  export declare const useSyncStore: import("zustand").UseBoundStore<import("zustand").StoreApi<SyncState>>;
@@ -1,11 +1,13 @@
1
1
  "use client";
2
2
  import { create } from "zustand";
3
3
  import { commitSessionRewind, isWithinRewindWindow, stageSessionRewind, } from "../../core/session/rewind.js";
4
+ import { EMPTY_SESSION_LOG_STATE, applySessionLogEvent, applySessionLogPage, } from "../../core/session-log/store.js";
4
5
  import { isRetryableTurnError } from "../../core/turns/open-turn.js";
5
6
  import { ascendingId } from "./sync-store/ascending-id.js";
6
7
  import { Binary } from "./sync-store/binary.js";
7
8
  import { DELTA_EVENT_TAIL_LIMIT } from "./sync-store/delta-event-window.js";
8
9
  import { reconcileHydratedParts } from "./sync-store/reconcile-parts.js";
10
+ import { keepUnchanged } from "./sync-store/keep-unchanged.js";
9
11
  export { ascendingId, Binary };
10
12
  export function sameSessionStatus(a, b) {
11
13
  if (a === b)
@@ -81,6 +83,16 @@ export function hasOnlyCacheSourcedMessages(sessionID) {
81
83
  return false;
82
84
  return messages.every((message) => cached.has(message.id));
83
85
  }
86
+ export function hasRuntimeConfirmedMessage(sessionID, messageID) {
87
+ const messages = useSyncStore.getState().messages[sessionID];
88
+ if (!messages)
89
+ return false;
90
+ let ids = messageIdSets.get(messages);
91
+ if (!ids)
92
+ messageIdSets.set(messages, (ids = new Set(messages.map((message) => message.id))));
93
+ return ids.has(messageID) && !cacheSourcedIds.get(sessionID)?.has(messageID);
94
+ }
95
+ const messageIdSets = new WeakMap();
84
96
  function recordOptimisticEcho(sessionID, optimisticID, echoID) {
85
97
  if (optimisticID === echoID)
86
98
  return;
@@ -138,6 +150,7 @@ function forgetSessionIds(sessionID) {
138
150
  deltaActiveParts.delete(sessionID);
139
151
  bridgedPartIds.delete(sessionID);
140
152
  deltaEventTails.delete(sessionID);
153
+ deltaOffsets.delete(sessionID);
141
154
  stubAssistantIds.delete(sessionID);
142
155
  }
143
156
  const isOptimistic = (sessionID, messageID) => hasTrackedId(optimisticIds, sessionID, messageID);
@@ -160,6 +173,15 @@ const awaitsUserEcho = (sessionID, messageID) => {
160
173
  const bridgedPartIds = new Map();
161
174
  const deltaActiveParts = new Map();
162
175
  const deltaEventTails = new Map();
176
+ const deltaOffsets = new Map();
177
+ function setDeltaOffset(sessionID, key, offset) {
178
+ let bucket = deltaOffsets.get(sessionID);
179
+ if (!bucket) {
180
+ bucket = new Map();
181
+ deltaOffsets.set(sessionID, bucket);
182
+ }
183
+ bucket.set(key, offset);
184
+ }
163
185
  const stubAssistantIds = new Map();
164
186
  function trackStub(sessionID, stubId, parentId) {
165
187
  const bucket = stubAssistantIds.get(sessionID);
@@ -223,6 +245,7 @@ const DETACHED_SESSION_LIMIT = 8;
223
245
  const MESSAGE_ROWS_LIMIT = 20;
224
246
  const sessionMessageRows = new Map();
225
247
  const EMPTY_MESSAGE_ROWS = [];
248
+ const rowsByMessages = new WeakMap();
226
249
  function touchSessionMessageRows(sessionID, entry) {
227
250
  sessionMessageRows.delete(sessionID);
228
251
  sessionMessageRows.set(sessionID, entry);
@@ -280,6 +303,41 @@ function pruneDetachedSessions(messages) {
280
303
  }
281
304
  return evicted;
282
305
  }
306
+ function takeDeltaEventId(sessionID, tailKey, id) {
307
+ let sessionTails = deltaEventTails.get(sessionID);
308
+ if (!sessionTails) {
309
+ sessionTails = new Map();
310
+ deltaEventTails.set(sessionID, sessionTails);
311
+ }
312
+ let appliedIds = sessionTails.get(tailKey);
313
+ if (appliedIds?.has(id))
314
+ return false;
315
+ if (!appliedIds) {
316
+ appliedIds = new Set();
317
+ sessionTails.set(tailKey, appliedIds);
318
+ }
319
+ appliedIds.add(id);
320
+ if (appliedIds.size > DELTA_EVENT_TAIL_LIMIT) {
321
+ const oldest = appliedIds.values().next().value;
322
+ if (oldest !== undefined)
323
+ appliedIds.delete(oldest);
324
+ }
325
+ return true;
326
+ }
327
+ function applySessionLogFrame(sessionId, event) {
328
+ const state = useSyncStore.getState();
329
+ const current = state.sessionLogs[sessionId] ?? EMPTY_SESSION_LOG_STATE;
330
+ const next = applySessionLogEvent(current, event);
331
+ if (next === current)
332
+ return;
333
+ if (event.type === "log.block.delta") {
334
+ const p = event.properties;
335
+ if (event.id && !takeDeltaEventId(p.thread_id, `${p.message_id}:${p.block_id}:log`, event.id))
336
+ return;
337
+ state.noteSessionActivity(p.thread_id);
338
+ }
339
+ useSyncStore.setState((s) => ({ sessionLogs: { ...s.sessionLogs, [sessionId]: next } }));
340
+ }
283
341
  function appendPartDeltas(sessionID, messageID, partID, field, pieces) {
284
342
  trackId(deltaActiveParts, sessionID, partID);
285
343
  let applied = false;
@@ -290,38 +348,38 @@ function appendPartDeltas(sessionID, messageID, partID, field, pieces) {
290
348
  const result = Binary.search(list, partID, (p) => p.id);
291
349
  if (!result.found)
292
350
  return s;
293
- let delta = "";
351
+ const messages = s.messages[sessionID] ?? [];
352
+ let message;
353
+ for (let i = messages.length - 1; i >= 0 && !message; i--)
354
+ if (messages[i].id === messageID)
355
+ message = messages[i];
356
+ if (message?.role === "assistant" && message.time?.completed)
357
+ return s;
358
+ const existing = list[result.index][field] ?? "";
359
+ const offsetKey = `${messageID}:${partID}:${field}`;
360
+ let offset = deltaOffsets.get(sessionID)?.get(offsetKey) ?? existing.length;
361
+ let text = existing;
294
362
  for (const piece of pieces) {
295
- if (piece.id) {
296
- const tailKey = `${messageID}:${partID}:${field}`;
297
- let sessionTails = deltaEventTails.get(sessionID);
298
- if (!sessionTails) {
299
- sessionTails = new Map();
300
- deltaEventTails.set(sessionID, sessionTails);
301
- }
302
- let appliedIds = sessionTails.get(tailKey);
303
- if (appliedIds?.has(piece.id))
304
- continue;
305
- if (!appliedIds) {
306
- appliedIds = new Set();
307
- sessionTails.set(tailKey, appliedIds);
308
- }
309
- appliedIds.add(piece.id);
310
- if (appliedIds.size > DELTA_EVENT_TAIL_LIMIT) {
311
- const oldest = appliedIds.values().next().value;
312
- if (oldest !== undefined)
313
- appliedIds.delete(oldest);
314
- }
363
+ if (piece.id && !takeDeltaEventId(sessionID, offsetKey, piece.id)) {
364
+ if (offset < text.length && text.startsWith(piece.delta, offset))
365
+ offset += piece.delta.length;
366
+ continue;
315
367
  }
316
- delta += piece.delta;
368
+ if (offset < text.length && text.startsWith(piece.delta, offset)) {
369
+ offset += piece.delta.length;
370
+ continue;
371
+ }
372
+ const held = offset < text.length && piece.delta.startsWith(text.slice(offset)) ? text.length - offset : 0;
373
+ text += piece.delta.slice(held);
374
+ offset = text.length;
317
375
  applied = true;
318
376
  }
377
+ setDeltaOffset(sessionID, offsetKey, offset);
319
378
  if (!applied)
320
379
  return s;
321
380
  const next = [...list];
322
381
  const part = { ...next[result.index] };
323
- const existing = part[field];
324
- part[field] = (existing ?? "") + delta;
382
+ part[field] = text;
325
383
  next[result.index] = part;
326
384
  return { parts: { ...s.parts, [messageID]: next } };
327
385
  });
@@ -339,6 +397,7 @@ export const useSyncStore = create()((set, get) => ({
339
397
  todos: {},
340
398
  sessionRevert: {},
341
399
  sessionRevertNeedsTailReconcile: {},
400
+ sessionLogs: {},
342
401
  get statuses() {
343
402
  return get().sessionStatus;
344
403
  },
@@ -346,15 +405,15 @@ export const useSyncStore = create()((set, get) => ({
346
405
  const list = s.messages[sessionID] ?? [];
347
406
  const result = Binary.search(list, message.id, (m) => m.id);
348
407
  const bsValid = result.found && list[result.index]?.id === message.id;
349
- if (bsValid) {
350
- const next = [...list];
351
- next[result.index] = message;
352
- return { messages: { ...s.messages, [sessionID]: next } };
353
- }
354
- const linearIdx = list.findIndex((m) => m.id === message.id);
355
- if (linearIdx !== -1) {
408
+ const index = bsValid ? result.index : list.findIndex((m) => m.id === message.id);
409
+ if (index !== -1) {
410
+ const previous = list[index];
411
+ const completed = previous.role === "assistant" ? previous.time?.completed : undefined;
412
+ const kept = completed && message.role === "assistant" && message.time && !message.time.completed
413
+ ? { ...message, time: { ...message.time, completed } }
414
+ : message;
356
415
  const next = [...list];
357
- next[linearIdx] = message;
416
+ next[index] = kept;
358
417
  return { messages: { ...s.messages, [sessionID]: next } };
359
418
  }
360
419
  const next = [...list];
@@ -405,6 +464,9 @@ export const useSyncStore = create()((set, get) => ({
405
464
  prevText.length > 0) {
406
465
  const isPrefixGrowth = incomingText.startsWith(prevText);
407
466
  if (!isPrefixGrowth) {
467
+ if (partSessionID && prevText.startsWith(incomingText)) {
468
+ setDeltaOffset(partSessionID, `${messageID}:${part.id}:text`, incomingText.length);
469
+ }
408
470
  if (!bridgeCleared)
409
471
  return s;
410
472
  const next = [...list];
@@ -413,9 +475,15 @@ export const useSyncStore = create()((set, get) => ({
413
475
  }
414
476
  }
415
477
  }
478
+ const followStream = () => {
479
+ if (partSessionID && isTextLikePart(part)) {
480
+ setDeltaOffset(partSessionID, `${messageID}:${part.id}:text`, part.text.length);
481
+ }
482
+ };
416
483
  const next = [...list];
417
484
  if (result.found) {
418
485
  next[result.index] = part;
486
+ followStream();
419
487
  }
420
488
  else {
421
489
  if (hasTrackedId(deltaActiveParts, partSessionID, part.id) && isTextLikePart(part)) {
@@ -427,6 +495,7 @@ export const useSyncStore = create()((set, get) => ({
427
495
  }
428
496
  }
429
497
  next.splice(result.index, 0, part);
498
+ followStream();
430
499
  }
431
500
  return { parts: { ...s.parts, [messageID]: next } };
432
501
  }),
@@ -729,7 +798,7 @@ export const useSyncStore = create()((set, get) => ({
729
798
  buildSessionMessages: (sessionID, msgs, parts) => {
730
799
  if (!msgs || msgs.length === 0)
731
800
  return EMPTY_MESSAGE_ROWS;
732
- const cached = sessionMessageRows.get(sessionID);
801
+ const cached = rowsByMessages.get(msgs) ?? sessionMessageRows.get(sessionID);
733
802
  if (cached && cached.msgs === msgs) {
734
803
  let same = cached.partRefs.length === msgs.length;
735
804
  if (same) {
@@ -775,7 +844,9 @@ export const useSyncStore = create()((set, get) => ({
775
844
  }
776
845
  result.push(reuse ?? { info, parts: messageParts ?? [] });
777
846
  }
778
- touchSessionMessageRows(sessionID, { msgs, partRefs, result });
847
+ const entry = { msgs, partRefs, result };
848
+ touchSessionMessageRows(sessionID, entry);
849
+ rowsByMessages.set(msgs, entry);
779
850
  return result;
780
851
  },
781
852
  hydrate: (sessionID, msgs, opts) => {
@@ -1008,8 +1079,9 @@ export const useSyncStore = create()((set, get) => ({
1008
1079
  clearBridge: (id) => untrackId(bridgedPartIds, sessionID, id),
1009
1080
  isDeltaActive: (id) => hasTrackedId(deltaActiveParts, sessionID, id),
1010
1081
  });
1082
+ const kept = keepUnchanged(s.messages[sessionID], merged, s.parts, newParts);
1011
1083
  return {
1012
- messages: { ...s.messages, [sessionID]: merged },
1084
+ messages: kept === s.messages[sessionID] ? s.messages : { ...s.messages, [sessionID]: kept },
1013
1085
  parts: newParts,
1014
1086
  };
1015
1087
  });
@@ -1025,6 +1097,7 @@ export const useSyncStore = create()((set, get) => ({
1025
1097
  bridgedPartIds.clear();
1026
1098
  deltaActiveParts.clear();
1027
1099
  deltaEventTails.clear();
1100
+ deltaOffsets.clear();
1028
1101
  stubAssistantIds.clear();
1029
1102
  sessionConsumers.clear();
1030
1103
  detachedSessions.clear();
@@ -1041,8 +1114,21 @@ export const useSyncStore = create()((set, get) => ({
1041
1114
  todos: {},
1042
1115
  sessionRevert: {},
1043
1116
  sessionRevertNeedsTailReconcile: {},
1117
+ sessionLogs: {},
1044
1118
  });
1045
1119
  },
1120
+ applySessionLogPage: (sessionId, page) => set((s) => {
1121
+ const current = s.sessionLogs[sessionId] ?? EMPTY_SESSION_LOG_STATE;
1122
+ const next = applySessionLogPage(current, { ...page, session: { session_id: sessionId, ...page.session } });
1123
+ return next === current ? s : { sessionLogs: { ...s.sessionLogs, [sessionId]: next } };
1124
+ }),
1125
+ applySessionLogEvent: (sessionId, event) => applySessionLogFrame(sessionId, event),
1126
+ dropSessionLog: (sessionId) => set((s) => {
1127
+ if (!(sessionId in s.sessionLogs))
1128
+ return s;
1129
+ const { [sessionId]: _dropped, ...rest } = s.sessionLogs;
1130
+ return { sessionLogs: rest };
1131
+ }),
1046
1132
  hasOptimisticMessages: (sessionID) => {
1047
1133
  const list = get().messages[sessionID];
1048
1134
  if (!list || list.length === 0)
@@ -1252,6 +1338,7 @@ export const useSyncStore = create()((set, get) => ({
1252
1338
  }
1253
1339
  deltaActiveParts.delete(sid);
1254
1340
  deltaEventTails.delete(sid);
1341
+ deltaOffsets.delete(sid);
1255
1342
  set((s) => {
1256
1343
  const msgs = s.messages[sid] ?? [];
1257
1344
  let userIdx = -1;
@@ -21,6 +21,10 @@ export interface PromptAttachmentControllerOptions {
21
21
  export interface PromptAttachmentController {
22
22
  add(file: File): string;
23
23
  addMany(files: readonly File[]): string[];
24
+ restore(uploads: readonly {
25
+ attachment: PromptAttachment;
26
+ file?: File;
27
+ }[]): (string | undefined)[];
24
28
  retry(id: string): void;
25
29
  remove(id: string): Promise<void>;
26
30
  abort(id: string): void;
@@ -216,6 +216,39 @@ export function createPromptAttachmentController(projectId, options = {}) {
216
216
  pump();
217
217
  return ids;
218
218
  }
219
+ function restore(uploads) {
220
+ const valid = uploads.map(({ attachment, file }) => {
221
+ try {
222
+ return { attachment: completedMetadata(attachment), file };
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ });
228
+ validateSizes(valid.flatMap((upload) => (upload ? [upload.attachment.size] : [])));
229
+ const ids = valid.map((upload) => {
230
+ if (!upload)
231
+ return undefined;
232
+ const id = `attachment-${++lastAttachmentId}`;
233
+ const { attachment, file } = upload;
234
+ entries.set(id, {
235
+ generation: 0,
236
+ item: {
237
+ id,
238
+ ...(file ? { file } : {}),
239
+ filename: attachment.filename,
240
+ mime: attachment.mime,
241
+ size: attachment.size,
242
+ status: 'ready',
243
+ receivedBytes: attachment.size,
244
+ attachment,
245
+ },
246
+ });
247
+ return id;
248
+ });
249
+ emit();
250
+ return ids;
251
+ }
219
252
  function abort(id) {
220
253
  const entry = entries.get(id);
221
254
  if (!entry || entry.item.status === 'ready')
@@ -284,6 +317,7 @@ export function createPromptAttachmentController(projectId, options = {}) {
284
317
  return {
285
318
  add: (file) => addMany([file])[0],
286
319
  addMany,
320
+ restore,
287
321
  retry(id) {
288
322
  const entry = entries.get(id);
289
323
  if (!entry?.handedOff)