@beeuni/sdk 0.2.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 (231) hide show
  1. package/README.md +19 -10
  2. package/dist/beeuni.esm.min.js +37 -35
  3. package/dist/beeuni.global.js +37 -35
  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 +1 -0
  12. package/dist/browser/stores/sync-store.js +72 -16
  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 +92 -5
  18. package/dist/core/client/beeuni.js +2 -0
  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-identity.d.ts +1 -0
  22. package/dist/core/client/facade-identity.js +1 -0
  23. package/dist/core/client/project-access-resources.d.ts +3 -1
  24. package/dist/core/client/project-access-resources.js +1 -1
  25. package/dist/core/client/project-operations-admin.d.ts +4 -4
  26. package/dist/core/client/project-operations-review.d.ts +2 -3
  27. package/dist/core/client/project-operations-review.js +1 -0
  28. package/dist/core/client/project-operations-workspace.d.ts +4 -4
  29. package/dist/core/client/project-operations-workspace.js +1 -1
  30. package/dist/core/client/project-operations.d.ts +5 -4
  31. package/dist/core/client/session-context.d.ts +2 -0
  32. package/dist/core/client/session-handle.d.ts +13 -0
  33. package/dist/core/client/session-lifecycle-resources.d.ts +11 -0
  34. package/dist/core/client/session-lifecycle-resources.js +4 -0
  35. package/dist/core/client/session-lifecycle-security.d.ts +2 -0
  36. package/dist/core/client/session-lifecycle-security.js +1 -0
  37. package/dist/core/client/session-readiness.d.ts +2 -0
  38. package/dist/core/client/session-readiness.js +20 -1
  39. package/dist/core/http/api-client.d.ts +1 -0
  40. package/dist/core/http/api-client.js +1 -0
  41. package/dist/core/http/config.d.ts +2 -0
  42. package/dist/core/http/response-error.js +26 -11
  43. package/dist/core/http/transport.js +6 -1
  44. package/dist/core/models/composer-model.d.ts +14 -0
  45. package/dist/core/models/composer-model.js +21 -1
  46. package/dist/core/rest/platform-client/capture.d.ts +363 -0
  47. package/dist/core/rest/platform-client/capture.js +116 -0
  48. package/dist/core/rest/platform-client/index.d.ts +1 -0
  49. package/dist/core/rest/platform-client/index.js +1 -0
  50. package/dist/core/rest/platform-client/lifecycle.js +18 -12
  51. package/dist/core/rest/platform-client/shared.d.ts +5 -0
  52. package/dist/core/rest/platform-client/shared.js +18 -0
  53. package/dist/core/rest/projects-client/account-secret-resources.d.ts +2 -1
  54. package/dist/core/rest/projects-client/accounts.d.ts +34 -0
  55. package/dist/core/rest/projects-client/accounts.js +10 -0
  56. package/dist/core/rest/projects-client/admin-providers.d.ts +0 -61
  57. package/dist/core/rest/projects-client/admin-providers.js +0 -6
  58. package/dist/core/rest/projects-client/agent-config.d.ts +3 -0
  59. package/dist/core/rest/projects-client/apps-fleet.d.ts +5 -1
  60. package/dist/core/rest/projects-client/apps-fleet.js +2 -2
  61. package/dist/core/rest/projects-client/apps.d.ts +3 -0
  62. package/dist/core/rest/projects-client/change-requests.d.ts +2 -0
  63. package/dist/core/rest/projects-client/change-requests.js +2 -0
  64. package/dist/core/rest/projects-client/connectors.d.ts +7 -0
  65. package/dist/core/rest/projects-client/connectors.js +3 -0
  66. package/dist/core/rest/projects-client/files.d.ts +3 -0
  67. package/dist/core/rest/projects-client/files.js +4 -0
  68. package/dist/core/rest/projects-client/gateway.d.ts +5 -0
  69. package/dist/core/rest/projects-client/iam-actions.d.ts +76 -0
  70. package/dist/core/rest/projects-client/iam-actions.js +101 -0
  71. package/dist/core/rest/projects-client/iam.d.ts +1 -0
  72. package/dist/core/rest/projects-client/iam.js +1 -0
  73. package/dist/core/rest/projects-client/notifications.d.ts +1 -1
  74. package/dist/core/rest/projects-client/notifications.js +4 -0
  75. package/dist/core/rest/projects-client/projects.d.ts +3 -2
  76. package/dist/core/rest/projects-client/projects.js +2 -3
  77. package/dist/core/rest/projects-client/review-items.d.ts +2 -0
  78. package/dist/core/rest/projects-client/review-items.js +4 -0
  79. package/dist/core/rest/projects-client/review.d.ts +17 -3
  80. package/dist/core/rest/projects-client/review.js +11 -0
  81. package/dist/core/rest/projects-client/secrets.d.ts +3 -0
  82. package/dist/core/rest/projects-client/session-attachments.js +14 -1
  83. package/dist/core/rest/projects-client/sessions.d.ts +29 -2
  84. package/dist/core/rest/projects-client/sessions.js +34 -4
  85. package/dist/core/runtime/runtime-rest-client.js +1 -1
  86. package/dist/core/runtime/runtime-types.d.ts +1 -0
  87. package/dist/core/session/open-bundle.d.ts +4 -4
  88. package/dist/core/session/open-bundle.js +29 -46
  89. package/dist/core/session/turn-end-watch.d.ts +47 -0
  90. package/dist/core/session/turn-end-watch.js +141 -0
  91. package/dist/core/session-log/index.d.ts +4 -0
  92. package/dist/core/session-log/index.js +3 -0
  93. package/dist/core/session-log/tool-call.d.ts +7 -2
  94. package/dist/core/session-log/tool-call.js +25 -6
  95. package/dist/core/session-log/turns/blocks.d.ts +60 -0
  96. package/dist/core/session-log/turns/blocks.js +124 -0
  97. package/dist/core/session-log/turns/history.d.ts +32 -0
  98. package/dist/core/session-log/turns/history.js +99 -0
  99. package/dist/core/session-log/turns/index.d.ts +4 -0
  100. package/dist/core/session-log/turns/index.js +4 -0
  101. package/dist/core/session-log/turns/metrics.d.ts +21 -0
  102. package/dist/core/session-log/turns/metrics.js +29 -0
  103. package/dist/core/session-log/turns/turn.d.ts +55 -0
  104. package/dist/core/session-log/turns/turn.js +279 -0
  105. package/dist/core/session-log/turns/v1-parts.d.ts +8 -0
  106. package/dist/core/session-log/turns/v1-parts.js +43 -0
  107. package/dist/core/session-log/v1-view.d.ts +5 -2
  108. package/dist/core/session-log/v1-view.js +86 -22
  109. package/dist/core/session-log/v1.js +10 -3
  110. package/dist/core/session-sync/saved-transcript.d.ts +15 -1
  111. package/dist/core/session-sync/saved-transcript.js +55 -0
  112. package/dist/core/session-sync/session-sync-controller.js +3 -0
  113. package/dist/core/stream/event-stream.js +3 -3
  114. package/dist/core/turns/classify.js +4 -2
  115. package/dist/core/turns/edit-blocks.d.ts +1 -0
  116. package/dist/core/turns/edit-blocks.js +15 -0
  117. package/dist/core/turns/segments/burst-summary.d.ts +1 -0
  118. package/dist/core/turns/segments/burst-summary.js +24 -0
  119. package/dist/core/turns/segments/group-steps.d.ts +4 -3
  120. package/dist/core/turns/segments/group-steps.js +9 -6
  121. package/dist/core/turns/segments/narration.d.ts +5 -4
  122. package/dist/core/turns/segments/narration.js +12 -3
  123. package/dist/core/turns/segments/session-activity-groups.d.ts +2 -1
  124. package/dist/core/turns/segments/session-activity-groups.js +3 -1
  125. package/dist/core/turns/segments/step-label.d.ts +2 -1
  126. package/dist/core/turns/segments/step-label.js +3 -1
  127. package/dist/core/turns/segments/working-turn.d.ts +4 -2
  128. package/dist/core/turns/segments/working-turn.js +9 -8
  129. package/dist/core/turns/tool-kind.d.ts +1 -1
  130. package/dist/core/turns/tool-kind.js +3 -2
  131. package/dist/core/turns/tool-registry.js +1 -0
  132. package/dist/core/turns/tools/tool-meta.d.ts +1 -1
  133. package/dist/core/turns/tools/tool-meta.js +3 -2
  134. package/dist/core/turns/view-model.js +1 -13
  135. package/dist/genui/catalog.d.ts +28 -0
  136. package/dist/genui/catalog.js +396 -0
  137. package/dist/genui/expansion.d.ts +3 -0
  138. package/dist/genui/expansion.js +295 -0
  139. package/dist/genui/fence-entry.d.ts +2 -0
  140. package/dist/genui/fence-entry.js +1 -0
  141. package/dist/genui/fence.d.ts +21 -0
  142. package/dist/genui/fence.js +138 -0
  143. package/dist/genui/index.d.ts +6 -0
  144. package/dist/genui/index.js +6 -0
  145. package/dist/genui/markdown.d.ts +12 -0
  146. package/dist/genui/markdown.js +104 -0
  147. package/dist/genui/parse.d.ts +6 -0
  148. package/dist/genui/parse.js +75 -0
  149. package/dist/genui/prompt.d.ts +2 -0
  150. package/dist/genui/prompt.js +206 -0
  151. package/dist/genui/react/default-components.d.ts +2 -0
  152. package/dist/genui/react/default-components.js +72 -0
  153. package/dist/genui/react/default-components.test.d.ts +1 -0
  154. package/dist/genui/react/default-components.test.js +94 -0
  155. package/dist/genui/react/genui-block-disabled.test.d.ts +1 -0
  156. package/dist/genui/react/genui-block-disabled.test.js +50 -0
  157. package/dist/genui/react/genui-block.d.ts +28 -0
  158. package/dist/genui/react/genui-block.js +91 -0
  159. package/dist/genui/react/genui-block.test.d.ts +1 -0
  160. package/dist/genui/react/genui-block.test.js +205 -0
  161. package/dist/genui/react/index.d.ts +2 -0
  162. package/dist/genui/react/index.js +2 -0
  163. package/dist/genui/share.d.ts +5 -0
  164. package/dist/genui/share.js +30 -0
  165. package/dist/genui/test-fixtures.d.ts +2 -0
  166. package/dist/genui/test-fixtures.js +16 -0
  167. package/dist/genui/types.d.ts +29 -0
  168. package/dist/genui/types.js +1 -0
  169. package/dist/genui/urls.d.ts +1 -0
  170. package/dist/genui/urls.js +18 -0
  171. package/dist/genui/validate.d.ts +9 -0
  172. package/dist/genui/validate.js +116 -0
  173. package/dist/index.d.ts +2 -2
  174. package/dist/index.js +2 -2
  175. package/dist/internal/connector-catalog.d.ts +1 -0
  176. package/dist/platform/ui.d.ts +8 -2
  177. package/dist/platform/ui.js +19 -4
  178. package/dist/react/chat/use-chat-turns.d.ts +1 -1
  179. package/dist/react/human-presence.d.ts +9 -0
  180. package/dist/react/human-presence.js +12 -0
  181. package/dist/react/index.d.ts +4 -0
  182. package/dist/react/index.js +4 -0
  183. package/dist/react/opencode.d.ts +1 -1
  184. package/dist/react/opencode.js +1 -1
  185. package/dist/react/query-contracts.d.ts +6 -0
  186. package/dist/react/query-contracts.js +6 -0
  187. package/dist/react/query-keys.d.ts +17 -1
  188. package/dist/react/query-keys.js +24 -1
  189. package/dist/react/reset-identity-state.js +2 -0
  190. package/dist/react/session-cache-write.d.ts +1 -0
  191. package/dist/react/session-cache-write.js +20 -0
  192. package/dist/react/session-transcript-subscription.js +1 -1
  193. package/dist/react/sign-in-with-beeuni.d.ts +5 -5
  194. package/dist/react/turn-end-handoff.d.ts +13 -0
  195. package/dist/react/turn-end-handoff.js +101 -0
  196. package/dist/react/use-admin-providers.d.ts +0 -3
  197. package/dist/react/use-admin-providers.js +1 -11
  198. package/dist/react/use-capture.d.ts +47 -0
  199. package/dist/react/use-capture.js +173 -0
  200. package/dist/react/use-feature-flag.js +1 -0
  201. package/dist/react/use-opencode-events/handle-interaction-event.js +8 -3
  202. package/dist/react/use-opencode-events/handle-session-event.js +13 -8
  203. package/dist/react/use-opencode-events/handler-context.d.ts +1 -0
  204. package/dist/react/use-opencode-events/helpers.d.ts +1 -0
  205. package/dist/react/use-opencode-events/helpers.js +11 -1
  206. package/dist/react/use-opencode-local.d.ts +5 -1
  207. package/dist/react/use-opencode-local.js +15 -2
  208. package/dist/react/use-opencode-sessions/commands.d.ts +11 -1
  209. package/dist/react/use-opencode-sessions/commands.js +21 -1
  210. package/dist/react/use-opencode-sessions/permissions.js +28 -1
  211. package/dist/react/use-project-sessions.d.ts +1 -1
  212. package/dist/react/use-project-sessions.js +6 -5
  213. package/dist/react/use-prompt-attachments.js +1 -0
  214. package/dist/react/use-review-summary.d.ts +13 -0
  215. package/dist/react/use-review-summary.js +32 -0
  216. package/dist/react/use-runtime-reconnect.js +2 -2
  217. package/dist/react/use-session-list-state.d.ts +6 -0
  218. package/dist/react/use-session-list-state.js +33 -0
  219. package/dist/react/use-session-log.d.ts +3 -2
  220. package/dist/react/use-session-log.js +26 -17
  221. package/dist/react/use-session-messages.d.ts +2 -1
  222. package/dist/react/use-session-messages.js +15 -14
  223. package/dist/react/use-session-stream.js +12 -1
  224. package/dist/react/use-session-sync.d.ts +2 -2
  225. package/dist/react/use-session-sync.js +10 -8
  226. package/dist/react/use-session-transcript-history.js +2 -12
  227. package/dist/react/use-session-working.d.ts +2 -0
  228. package/dist/react/use-session-working.js +10 -0
  229. package/dist/react/use-session.d.ts +6 -0
  230. package/dist/react/use-session.js +87 -7
  231. package/package.json +49 -3
@@ -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
+ }
@@ -68,4 +68,5 @@ interface SyncState {
68
68
  statuses: Record<string, SessionStatus>;
69
69
  }
70
70
  export declare function hasOnlyCacheSourcedMessages(sessionID: string): boolean;
71
+ export declare function hasRuntimeConfirmedMessage(sessionID: string, messageID: string): boolean;
71
72
  export declare const useSyncStore: import("zustand").UseBoundStore<import("zustand").StoreApi<SyncState>>;
@@ -7,6 +7,7 @@ import { ascendingId } from "./sync-store/ascending-id.js";
7
7
  import { Binary } from "./sync-store/binary.js";
8
8
  import { DELTA_EVENT_TAIL_LIMIT } from "./sync-store/delta-event-window.js";
9
9
  import { reconcileHydratedParts } from "./sync-store/reconcile-parts.js";
10
+ import { keepUnchanged } from "./sync-store/keep-unchanged.js";
10
11
  export { ascendingId, Binary };
11
12
  export function sameSessionStatus(a, b) {
12
13
  if (a === b)
@@ -82,6 +83,16 @@ export function hasOnlyCacheSourcedMessages(sessionID) {
82
83
  return false;
83
84
  return messages.every((message) => cached.has(message.id));
84
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();
85
96
  function recordOptimisticEcho(sessionID, optimisticID, echoID) {
86
97
  if (optimisticID === echoID)
87
98
  return;
@@ -139,6 +150,7 @@ function forgetSessionIds(sessionID) {
139
150
  deltaActiveParts.delete(sessionID);
140
151
  bridgedPartIds.delete(sessionID);
141
152
  deltaEventTails.delete(sessionID);
153
+ deltaOffsets.delete(sessionID);
142
154
  stubAssistantIds.delete(sessionID);
143
155
  }
144
156
  const isOptimistic = (sessionID, messageID) => hasTrackedId(optimisticIds, sessionID, messageID);
@@ -161,6 +173,15 @@ const awaitsUserEcho = (sessionID, messageID) => {
161
173
  const bridgedPartIds = new Map();
162
174
  const deltaActiveParts = new Map();
163
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
+ }
164
185
  const stubAssistantIds = new Map();
165
186
  function trackStub(sessionID, stubId, parentId) {
166
187
  const bucket = stubAssistantIds.get(sessionID);
@@ -224,6 +245,7 @@ const DETACHED_SESSION_LIMIT = 8;
224
245
  const MESSAGE_ROWS_LIMIT = 20;
225
246
  const sessionMessageRows = new Map();
226
247
  const EMPTY_MESSAGE_ROWS = [];
248
+ const rowsByMessages = new WeakMap();
227
249
  function touchSessionMessageRows(sessionID, entry) {
228
250
  sessionMessageRows.delete(sessionID);
229
251
  sessionMessageRows.set(sessionID, entry);
@@ -326,19 +348,38 @@ function appendPartDeltas(sessionID, messageID, partID, field, pieces) {
326
348
  const result = Binary.search(list, partID, (p) => p.id);
327
349
  if (!result.found)
328
350
  return s;
329
- 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;
330
362
  for (const piece of pieces) {
331
- if (piece.id && !takeDeltaEventId(sessionID, `${messageID}:${partID}:${field}`, piece.id))
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;
367
+ }
368
+ if (offset < text.length && text.startsWith(piece.delta, offset)) {
369
+ offset += piece.delta.length;
332
370
  continue;
333
- delta += piece.delta;
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;
334
375
  applied = true;
335
376
  }
377
+ setDeltaOffset(sessionID, offsetKey, offset);
336
378
  if (!applied)
337
379
  return s;
338
380
  const next = [...list];
339
381
  const part = { ...next[result.index] };
340
- const existing = part[field];
341
- part[field] = (existing ?? "") + delta;
382
+ part[field] = text;
342
383
  next[result.index] = part;
343
384
  return { parts: { ...s.parts, [messageID]: next } };
344
385
  });
@@ -364,15 +405,15 @@ export const useSyncStore = create()((set, get) => ({
364
405
  const list = s.messages[sessionID] ?? [];
365
406
  const result = Binary.search(list, message.id, (m) => m.id);
366
407
  const bsValid = result.found && list[result.index]?.id === message.id;
367
- if (bsValid) {
368
- const next = [...list];
369
- next[result.index] = message;
370
- return { messages: { ...s.messages, [sessionID]: next } };
371
- }
372
- const linearIdx = list.findIndex((m) => m.id === message.id);
373
- 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;
374
415
  const next = [...list];
375
- next[linearIdx] = message;
416
+ next[index] = kept;
376
417
  return { messages: { ...s.messages, [sessionID]: next } };
377
418
  }
378
419
  const next = [...list];
@@ -423,6 +464,9 @@ export const useSyncStore = create()((set, get) => ({
423
464
  prevText.length > 0) {
424
465
  const isPrefixGrowth = incomingText.startsWith(prevText);
425
466
  if (!isPrefixGrowth) {
467
+ if (partSessionID && prevText.startsWith(incomingText)) {
468
+ setDeltaOffset(partSessionID, `${messageID}:${part.id}:text`, incomingText.length);
469
+ }
426
470
  if (!bridgeCleared)
427
471
  return s;
428
472
  const next = [...list];
@@ -431,9 +475,15 @@ export const useSyncStore = create()((set, get) => ({
431
475
  }
432
476
  }
433
477
  }
478
+ const followStream = () => {
479
+ if (partSessionID && isTextLikePart(part)) {
480
+ setDeltaOffset(partSessionID, `${messageID}:${part.id}:text`, part.text.length);
481
+ }
482
+ };
434
483
  const next = [...list];
435
484
  if (result.found) {
436
485
  next[result.index] = part;
486
+ followStream();
437
487
  }
438
488
  else {
439
489
  if (hasTrackedId(deltaActiveParts, partSessionID, part.id) && isTextLikePart(part)) {
@@ -445,6 +495,7 @@ export const useSyncStore = create()((set, get) => ({
445
495
  }
446
496
  }
447
497
  next.splice(result.index, 0, part);
498
+ followStream();
448
499
  }
449
500
  return { parts: { ...s.parts, [messageID]: next } };
450
501
  }),
@@ -747,7 +798,7 @@ export const useSyncStore = create()((set, get) => ({
747
798
  buildSessionMessages: (sessionID, msgs, parts) => {
748
799
  if (!msgs || msgs.length === 0)
749
800
  return EMPTY_MESSAGE_ROWS;
750
- const cached = sessionMessageRows.get(sessionID);
801
+ const cached = rowsByMessages.get(msgs) ?? sessionMessageRows.get(sessionID);
751
802
  if (cached && cached.msgs === msgs) {
752
803
  let same = cached.partRefs.length === msgs.length;
753
804
  if (same) {
@@ -793,7 +844,9 @@ export const useSyncStore = create()((set, get) => ({
793
844
  }
794
845
  result.push(reuse ?? { info, parts: messageParts ?? [] });
795
846
  }
796
- touchSessionMessageRows(sessionID, { msgs, partRefs, result });
847
+ const entry = { msgs, partRefs, result };
848
+ touchSessionMessageRows(sessionID, entry);
849
+ rowsByMessages.set(msgs, entry);
797
850
  return result;
798
851
  },
799
852
  hydrate: (sessionID, msgs, opts) => {
@@ -1026,8 +1079,9 @@ export const useSyncStore = create()((set, get) => ({
1026
1079
  clearBridge: (id) => untrackId(bridgedPartIds, sessionID, id),
1027
1080
  isDeltaActive: (id) => hasTrackedId(deltaActiveParts, sessionID, id),
1028
1081
  });
1082
+ const kept = keepUnchanged(s.messages[sessionID], merged, s.parts, newParts);
1029
1083
  return {
1030
- messages: { ...s.messages, [sessionID]: merged },
1084
+ messages: kept === s.messages[sessionID] ? s.messages : { ...s.messages, [sessionID]: kept },
1031
1085
  parts: newParts,
1032
1086
  };
1033
1087
  });
@@ -1043,6 +1097,7 @@ export const useSyncStore = create()((set, get) => ({
1043
1097
  bridgedPartIds.clear();
1044
1098
  deltaActiveParts.clear();
1045
1099
  deltaEventTails.clear();
1100
+ deltaOffsets.clear();
1046
1101
  stubAssistantIds.clear();
1047
1102
  sessionConsumers.clear();
1048
1103
  detachedSessions.clear();
@@ -1283,6 +1338,7 @@ export const useSyncStore = create()((set, get) => ({
1283
1338
  }
1284
1339
  deltaActiveParts.delete(sid);
1285
1340
  deltaEventTails.delete(sid);
1341
+ deltaOffsets.delete(sid);
1286
1342
  set((s) => {
1287
1343
  const msgs = s.messages[sid] ?? [];
1288
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)
@@ -0,0 +1,17 @@
1
+ export declare const CAPTURE_LAYERS: readonly ["screen", "actions", "audio"];
2
+ export type CaptureLayer = (typeof CAPTURE_LAYERS)[number];
3
+ export declare const CAPTURE_EXCLUSION_GROUPS: readonly ["password_managers", "messaging", "banking"];
4
+ export type CaptureExclusionGroup = (typeof CAPTURE_EXCLUSION_GROUPS)[number];
5
+ export declare const CAPTURE_RETENTION_LIMITS: {
6
+ readonly localHours: number;
7
+ readonly remoteDays: 3650;
8
+ };
9
+ export declare const CAPTURE_FRAME_INTERVALS_MS: readonly [500, 1000, 2000, 5000, 10000];
10
+ export declare const CAPTURE_MAX_HEIGHTS: readonly [720, 1080, 1440, 2160];
11
+ export type CaptureMaxHeight = (typeof CAPTURE_MAX_HEIGHTS)[number];
12
+ export declare const CAPTURE_DEVICE_DEFAULTS: {
13
+ readonly intervalMs: 2000;
14
+ readonly maxHeight: 1440;
15
+ };
16
+ export declare function normalizeCaptureDomain(input: string): string | null;
17
+ export declare function normalizeCaptureAppId(input: string): string | null;
@@ -0,0 +1,27 @@
1
+ export const CAPTURE_LAYERS = ['screen', 'actions', 'audio'];
2
+ export const CAPTURE_EXCLUSION_GROUPS = ['password_managers', 'messaging', 'banking'];
3
+ export const CAPTURE_RETENTION_LIMITS = { localHours: 24 * 365, remoteDays: 3650 };
4
+ export const CAPTURE_FRAME_INTERVALS_MS = [500, 1000, 2000, 5000, 10000];
5
+ export const CAPTURE_MAX_HEIGHTS = [720, 1080, 1440, 2160];
6
+ export const CAPTURE_DEVICE_DEFAULTS = { intervalMs: 2000, maxHeight: 1440 };
7
+ const HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
8
+ export function normalizeCaptureDomain(input) {
9
+ const raw = input.trim().toLowerCase();
10
+ if (!raw)
11
+ return null;
12
+ let host;
13
+ try {
14
+ host = new URL(raw.includes('://') ? raw : `https://${raw}`).hostname;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ host = host.replace(/^www\./, '').replace(/\.$/, '');
20
+ return HOST.test(host) ? host : null;
21
+ }
22
+ export function normalizeCaptureAppId(input) {
23
+ const id = input.trim();
24
+ if (!id || id.length > 255 || id.startsWith('-') || /[\u0000-\u001f\u007f]/.test(id))
25
+ return null;
26
+ return /\s/.test(id) && !id.toLowerCase().endsWith('.exe') ? null : id;
27
+ }