@agent-native/core 0.221.0 → 0.221.1-nightly-20261028032829

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.
@@ -2,20 +2,20 @@ import { EMBED_MODE_QUERY_PARAM, EMBED_START_PATH, EMBED_TARGET_HEADER, EMBED_TA
2
2
  import { isMcpDirectoryWidgetReadCapabilityScope, isMcpDirectoryWidgetWriteCapabilityScope, } from "../shared/embed-auth.js";
3
3
  import { FRAMEWORK_INTERNAL_ROUTE_PREFIX } from "../shared/framework-route-prefix.js";
4
4
  import { MCP_APP_HOST_FILL_ATTRIBUTE } from "../shared/mcp-app-display.js";
5
- import { EMBED_TOKEN_STORAGE_KEY, MCP_CHAT_BRIDGE_STORAGE_KEY, } from "../shared/mcp-app-widget-embed.js";
5
+ import { embedTokenStorageKey, mcpChatBridgeStorageKey, } from "../shared/mcp-app-widget-embed.js";
6
6
  import { SIGN_IN_ENTRY_PATH, SIGN_IN_LEGACY_ENTRY_PATH, } from "../shared/sign-in-journey.js";
7
- import { frameworkRoutePrefix } from "./api-path.js";
7
+ import { frameworkRoutePrefix, isWorkspaceRuntime, resolveAppBasePath, resolveExternalEmbedTargetAppBasePath, WorkspaceAppMountResolutionError, } from "./api-path.js";
8
8
  let installed = false;
9
- let memoryToken = null;
9
+ const memoryTokens = new Map();
10
10
  let renewedFromUrlToken = null;
11
11
  let pendingWidgetSessionRenewal = null;
12
12
  let widgetSessionRenewalRequestId = 0;
13
13
  let mcpChatBridgeActive = false;
14
14
  let mcpChatBridgeScope = null;
15
+ let mcpChatBridgeStorageKeyAtState = null;
15
16
  const AUTH_FAILURE_COOLDOWN_MS = 60_000;
16
17
  const GUARDED_METHODS = new Set(["GET", "HEAD"]);
17
18
  const AUTH_FAILURE_HEADER = "x-agent-native-auth-circuit-breaker";
18
- const EMBED_TOKEN_RENEWED_FROM_STORAGE_KEY = `${EMBED_TOKEN_STORAGE_KEY}:renewed-from`;
19
19
  const EMBED_SESSION_RENEWAL_TIMEOUT_MS = 15_000;
20
20
  const MCP_CHAT_BRIDGE_VIEWPORT_STYLE_ID = "agent-native-mcp-chat-bridge-viewport";
21
21
  const MCP_CHAT_BRIDGE_VIEWPORT_HEIGHT = 560;
@@ -24,6 +24,48 @@ const authFailureCache = new Map();
24
24
  function browserWindow() {
25
25
  return typeof window === "undefined" ? null : window;
26
26
  }
27
+ function currentEmbedBasePath() {
28
+ try {
29
+ return resolveAppBasePath();
30
+ }
31
+ catch (error) {
32
+ if (error instanceof WorkspaceAppMountResolutionError)
33
+ return null;
34
+ throw error;
35
+ }
36
+ }
37
+ function currentEmbedTokenStorageKey() {
38
+ const basePath = currentEmbedBasePath();
39
+ return basePath === null
40
+ ? null
41
+ : embedTokenStorageKey(basePath, isWorkspaceRuntime());
42
+ }
43
+ function currentMcpChatBridgeStorageKey() {
44
+ const basePath = currentEmbedBasePath();
45
+ return basePath === null
46
+ ? null
47
+ : mcpChatBridgeStorageKey(basePath, isWorkspaceRuntime());
48
+ }
49
+ function memoryTokenFor(storageKey) {
50
+ return memoryTokens.get(storageKey) ?? null;
51
+ }
52
+ function externalEmbedToken(win) {
53
+ const config = win.__AGENT_NATIVE_EXTERNAL_EMBED;
54
+ return typeof config?.token === "string" && config.token
55
+ ? config.token
56
+ : null;
57
+ }
58
+ function externalEmbedTokenStorageKey(win) {
59
+ const config = win.__AGENT_NATIVE_EXTERNAL_EMBED;
60
+ const target = config?.target;
61
+ if (typeof target !== "string" || !target.startsWith("/"))
62
+ return null;
63
+ const workspaceRuntime = isWorkspaceRuntime();
64
+ const basePath = resolveExternalEmbedTargetAppBasePath(target);
65
+ if (workspaceRuntime && basePath === undefined)
66
+ return null;
67
+ return embedTokenStorageKey(basePath, workspaceRuntime);
68
+ }
27
69
  function currentUrl(win) {
28
70
  try {
29
71
  return new URL(win.location.href);
@@ -48,13 +90,20 @@ export function readEmbedMcpChatBridgeFlagFromUrl() {
48
90
  return value === "1" || value === "true";
49
91
  }
50
92
  function currentMcpChatBridgeScope(win) {
51
- return getEmbedAuthToken() ?? memoryToken ?? storedToken(win);
93
+ const storageKey = currentEmbedTokenStorageKey();
94
+ if (storageKey === null)
95
+ return null;
96
+ return getEmbedAuthToken() ?? storedToken(win, storageKey);
52
97
  }
53
98
  function clearMcpChatBridge(win) {
54
99
  mcpChatBridgeActive = false;
55
100
  mcpChatBridgeScope = null;
101
+ mcpChatBridgeStorageKeyAtState = null;
102
+ const storageKey = currentMcpChatBridgeStorageKey();
103
+ if (storageKey === null)
104
+ return;
56
105
  try {
57
- win.sessionStorage?.removeItem(MCP_CHAT_BRIDGE_STORAGE_KEY);
106
+ win.sessionStorage.removeItem(storageKey);
58
107
  }
59
108
  catch {
60
109
  // ignore unavailable session storage
@@ -62,15 +111,19 @@ function clearMcpChatBridge(win) {
62
111
  }
63
112
  export function markEmbedMcpChatBridgeActive() {
64
113
  const win = browserWindow();
65
- const scope = win ? currentMcpChatBridgeScope(win) : null;
114
+ const storageKey = currentMcpChatBridgeStorageKey();
115
+ const scope = win && storageKey !== null ? currentMcpChatBridgeScope(win) : null;
66
116
  mcpChatBridgeActive = true;
67
117
  mcpChatBridgeScope = scope;
118
+ mcpChatBridgeStorageKeyAtState = storageKey;
119
+ if (storageKey === null)
120
+ return;
68
121
  try {
69
122
  if (scope) {
70
- win?.sessionStorage?.setItem(MCP_CHAT_BRIDGE_STORAGE_KEY, scope);
123
+ win?.sessionStorage.setItem(storageKey, scope);
71
124
  }
72
125
  else {
73
- win?.sessionStorage?.removeItem(MCP_CHAT_BRIDGE_STORAGE_KEY);
126
+ win?.sessionStorage.removeItem(storageKey);
74
127
  }
75
128
  }
76
129
  catch {
@@ -82,6 +135,23 @@ export function isEmbedMcpChatBridgeActive() {
82
135
  const win = browserWindow();
83
136
  if (!win)
84
137
  return false;
138
+ const storageKey = currentMcpChatBridgeStorageKey();
139
+ if (storageKey === null) {
140
+ if (isEmbedAuthActive() && readEmbedMcpChatBridgeFlagFromUrl()) {
141
+ markEmbedMcpChatBridgeActive();
142
+ }
143
+ // A temporary route without a declared mount does not prove the host
144
+ // unenrolled this document from the bridge.
145
+ return mcpChatBridgeActive;
146
+ }
147
+ if (mcpChatBridgeActive && mcpChatBridgeStorageKeyAtState === null) {
148
+ markEmbedMcpChatBridgeActive();
149
+ }
150
+ if (mcpChatBridgeActive && mcpChatBridgeStorageKeyAtState !== storageKey) {
151
+ mcpChatBridgeActive = false;
152
+ mcpChatBridgeScope = null;
153
+ mcpChatBridgeStorageKeyAtState = null;
154
+ }
85
155
  if (!isEmbedAuthActive()) {
86
156
  clearMcpChatBridge(win);
87
157
  return false;
@@ -108,14 +178,15 @@ export function isEmbedMcpChatBridgeActive() {
108
178
  return false;
109
179
  }
110
180
  try {
111
- const storedScope = win.sessionStorage?.getItem(MCP_CHAT_BRIDGE_STORAGE_KEY);
181
+ const storedScope = win.sessionStorage.getItem(storageKey);
112
182
  if (storedScope && (scope == null || storedScope === scope)) {
113
183
  mcpChatBridgeActive = true;
114
184
  mcpChatBridgeScope = storedScope;
185
+ mcpChatBridgeStorageKeyAtState = storageKey;
115
186
  return true;
116
187
  }
117
188
  if (storedScope && scope != null && storedScope !== scope) {
118
- win.sessionStorage?.removeItem(MCP_CHAT_BRIDGE_STORAGE_KEY);
189
+ win.sessionStorage.removeItem(storageKey);
119
190
  }
120
191
  return false;
121
192
  }
@@ -123,18 +194,46 @@ export function isEmbedMcpChatBridgeActive() {
123
194
  return false;
124
195
  }
125
196
  }
126
- function storedToken(win) {
197
+ function storedToken(win, storageKey) {
127
198
  try {
128
- return win.sessionStorage?.getItem(EMBED_TOKEN_STORAGE_KEY) ?? null;
199
+ return win.sessionStorage.getItem(storageKey) ?? null;
129
200
  }
130
201
  catch {
131
202
  return null;
132
203
  }
133
204
  }
134
- function storeToken(token, win) {
135
- memoryToken = token;
205
+ function embedTokenTargetStorageKey(token) {
206
+ const payload = token.split(".")[0] ?? "";
207
+ let decodedClaims;
136
208
  try {
137
- win.sessionStorage?.setItem(EMBED_TOKEN_STORAGE_KEY, token);
209
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
210
+ const bytes = Uint8Array.from(atob(base64 + "=".repeat((4 - (base64.length % 4)) % 4)), (char) => char.charCodeAt(0));
211
+ decodedClaims = JSON.parse(new TextDecoder().decode(bytes));
212
+ }
213
+ catch {
214
+ // coercion-ok: an unreadable token has no mount binding; null makes the caller reject it.
215
+ return null;
216
+ }
217
+ if (!decodedClaims ||
218
+ typeof decodedClaims !== "object" ||
219
+ Array.isArray(decodedClaims)) {
220
+ return null;
221
+ }
222
+ const claims = decodedClaims;
223
+ if (typeof claims.audienceBasePath === "string") {
224
+ return embedTokenStorageKey(claims.audienceBasePath, true);
225
+ }
226
+ if (claims.audienceBasePath !== undefined)
227
+ return null;
228
+ if (typeof claims.targetPath !== "string")
229
+ return null;
230
+ const basePath = resolveExternalEmbedTargetAppBasePath(claims.targetPath);
231
+ return basePath === undefined ? null : embedTokenStorageKey(basePath, true);
232
+ }
233
+ function storeToken(token, win, storageKey) {
234
+ memoryTokens.set(storageKey, token);
235
+ try {
236
+ win.sessionStorage.setItem(storageKey, token);
138
237
  }
139
238
  catch {
140
239
  // Session storage may be unavailable in some sandboxed hosts. The
@@ -145,37 +244,56 @@ export function getEmbedAuthToken() {
145
244
  const win = browserWindow();
146
245
  if (!win)
147
246
  return null;
247
+ const storageKey = currentEmbedTokenStorageKey();
248
+ if (storageKey === null)
249
+ return null;
250
+ const renewedFromStorageKey = `${storageKey}:renewed-from`;
148
251
  const fromUrl = readTokenFromUrl(win);
149
252
  if (fromUrl) {
253
+ if (isWorkspaceRuntime() &&
254
+ embedTokenTargetStorageKey(fromUrl) !== storageKey) {
255
+ return null;
256
+ }
150
257
  let renewedFrom = renewedFromUrlToken;
151
258
  if (!renewedFrom) {
152
259
  try {
153
- renewedFrom =
154
- win.sessionStorage?.getItem(EMBED_TOKEN_RENEWED_FROM_STORAGE_KEY) ??
155
- null;
260
+ renewedFrom = win.sessionStorage.getItem(renewedFromStorageKey) ?? null;
156
261
  }
157
262
  catch {
158
263
  renewedFrom = null;
159
264
  }
160
265
  }
161
266
  if (renewedFrom === fromUrl) {
162
- const refreshed = memoryToken ?? storedToken(win);
267
+ const refreshed = memoryTokenFor(storageKey) ?? storedToken(win, storageKey);
163
268
  if (refreshed && refreshed !== fromUrl)
164
269
  return refreshed;
165
270
  }
166
271
  if (renewedFrom && renewedFrom !== fromUrl) {
167
272
  renewedFromUrlToken = null;
168
273
  try {
169
- win.sessionStorage?.removeItem(EMBED_TOKEN_RENEWED_FROM_STORAGE_KEY);
274
+ win.sessionStorage.removeItem(renewedFromStorageKey);
170
275
  }
171
276
  catch {
172
277
  // coercion-ok: cleanup is best-effort; the refreshed URL token still supersedes this marker.
173
278
  }
174
279
  }
175
- storeToken(fromUrl, win);
280
+ storeToken(fromUrl, win, storageKey);
176
281
  return fromUrl;
177
282
  }
178
- return memoryToken ?? storedToken(win);
283
+ const inMemoryToken = memoryTokenFor(storageKey);
284
+ if (inMemoryToken)
285
+ return inMemoryToken;
286
+ const stored = storedToken(win, storageKey);
287
+ if (stored)
288
+ return stored;
289
+ const external = externalEmbedToken(win);
290
+ if (!external)
291
+ return null;
292
+ // The frame config outlives navigation, but its token belongs to one mount.
293
+ if (externalEmbedTokenStorageKey(win) !== storageKey)
294
+ return null;
295
+ storeToken(external, win, storageKey);
296
+ return external;
179
297
  }
180
298
  function readEmbedTokenScope(token) {
181
299
  const payload = token.split(".")[0] ?? "";
@@ -362,13 +480,14 @@ export function _resetEmbedAuthForTests() {
362
480
  pending.win.clearTimeout(id);
363
481
  }
364
482
  installed = false;
365
- memoryToken = null;
483
+ memoryTokens.clear();
366
484
  renewedFromUrlToken = null;
367
485
  pendingWidgetSessionRenewal = null;
368
486
  widgetSessionRenewalRequestId = 0;
369
487
  widgetScopeCache = null;
370
488
  mcpChatBridgeActive = false;
371
489
  mcpChatBridgeScope = null;
490
+ mcpChatBridgeStorageKeyAtState = null;
372
491
  authFailureCache.clear();
373
492
  }
374
493
  /**
@@ -654,8 +773,8 @@ export function ensureEmbedAuthFetchInterceptor() {
654
773
  markEmbedMcpChatBridgeActive();
655
774
  const urlToken = readTokenFromUrl(win);
656
775
  if (urlToken) {
657
- getEmbedAuthToken();
658
- stripTokenFromUrl(win);
776
+ if (getEmbedAuthToken())
777
+ stripTokenFromUrl(win);
659
778
  }
660
779
  ensureMcpChatBridgeViewportClamp(win);
661
780
  if (installed)
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef } from "react";
3
3
  import { useLocation, useNavigate, } from "react-router";
4
4
  import { isWorkspaceAppPath } from "./api-path.js";
5
5
  import { deleteClientAppState, readClientAppState, setClientAppState, } from "./application-state.js";
6
- import { getBrowserTabId } from "./browser-tab-id.js";
6
+ import { getBrowserTabId, isBrowserTabIdForCurrentDocument, } from "./browser-tab-id.js";
7
7
  import { navigateWithAgentChatViewTransition, } from "./chat-view-transition.js";
8
8
  import { postAgentNativeWorkspaceAppRoute } from "./workspace-app-navigation.js";
9
9
  const SAFE_BROWSER_TAB_ID_RE = /^[A-Za-z0-9_-]{1,96}$/;
@@ -195,8 +195,19 @@ export function useAgentRouteState(options) {
195
195
  const { navigationKey = "navigation", commandKey = "navigate" } = options;
196
196
  const location = useLocation();
197
197
  const navigate = useNavigate();
198
- const browserTabId = useMemo(() => normalizeBrowserTabId(options.browserTabId) ??
199
- (options.browserTabId === undefined ? defaultBrowserTabId() : undefined), [options.browserTabId]);
198
+ const defaultTabId = defaultBrowserTabId();
199
+ const explicitTabId = normalizeBrowserTabId(options.browserTabId);
200
+ // Templates expose TAB_ID as a module-level constant. If it matches the
201
+ // current document's generated IDs, keep following the active mount.
202
+ const browserTabId = explicitTabId === undefined ||
203
+ isBrowserTabIdForCurrentDocument(explicitTabId)
204
+ ? defaultTabId
205
+ : explicitTabId;
206
+ const requestSourceFollowsDefault = options.requestSource === undefined ||
207
+ isBrowserTabIdForCurrentDocument(options.requestSource);
208
+ const requestSource = requestSourceFollowsDefault
209
+ ? browserTabId
210
+ : options.requestSource;
200
211
  const writeGlobalNavigation = options.writeGlobalNavigation ?? false;
201
212
  const readGlobalCommandFallback = options.readGlobalCommandFallback ?? false;
202
213
  useEffect(() => {
@@ -237,7 +248,7 @@ export function useAgentRouteState(options) {
237
248
  commandKeys,
238
249
  commandQueryKey,
239
250
  browserTabId,
240
- requestSource: options.requestSource ?? browserTabId,
251
+ requestSource,
241
252
  commandRefetchInterval: options.refetchInterval,
242
253
  enabled: options.enabled,
243
254
  keepalive: options.keepalive,
@@ -3,6 +3,7 @@ import { BUILDER_GATEWAY_INTERNAL_ERROR_CODE, PROVIDER_TRANSIENT_REJECTION_ERROR
3
3
  import { normalizeConnectRequiredResult } from "../shared/connect-required.js";
4
4
  import { isNonProgressEventType } from "../shared/progress-events.js";
5
5
  import { toolInputsMatch } from "../shared/tool-inputs.js";
6
+ import { getBrowserTabId } from "./browser-tab-id.js";
6
7
  import { emitChatFirstOpenApp } from "./chat-first-state.js";
7
8
  import { formatChatErrorText, normalizeChatError } from "./error-format.js";
8
9
  import { humanizeToolLabelText, humanizeToolName, isToolCallActive, runningToolLabel, } from "./tool-display.js";
@@ -1443,6 +1444,8 @@ export function processEvent(ev, content, toolCallCounter, tabId, state, context
1443
1444
  isError: ev.isError === true,
1444
1445
  completedSideEffect: ev.completedSideEffect === true,
1445
1446
  tabId,
1447
+ browserTabId: getBrowserTabId(),
1448
+ runId: context?.runId,
1446
1449
  eventId: ev.eventId ?? ev.id,
1447
1450
  },
1448
1451
  }));
@@ -87,7 +87,12 @@ class ChangeVersionStore {
87
87
  }
88
88
  }
89
89
  }
90
- const store = new ChangeVersionStore();
90
+ const CHANGE_VERSION_STORE_SYMBOL = Symbol.for("@agent-native/core/client/change-version-store");
91
+ const globalChangeVersionStore = globalThis;
92
+ // Vite can load an optimized package root beside a directly served deep import.
93
+ const store = globalChangeVersionStore[CHANGE_VERSION_STORE_SYMBOL] ??
94
+ (globalChangeVersionStore[CHANGE_VERSION_STORE_SYMBOL] =
95
+ new ChangeVersionStore());
91
96
  export function bumpChangeVersion(source, version) {
92
97
  return store.bump(source, version);
93
98
  }
@@ -5,7 +5,7 @@ export { REALTIME_CAP_POLL_LIVE } from "../realtime-protocol.js";
5
5
  import { addSurfaceVisibilityListener, isHostSurfaceHidden, isSurfaceHidden, } from "../shared/surface-visibility.js";
6
6
  import { actionQueryAffectedByResources } from "./action-query-scope.js";
7
7
  import { agentNativePath } from "./api-path.js";
8
- import { getBrowserTabId } from "./browser-tab-id.js";
8
+ import { getBrowserTabId, resolveBrowserTabIdAlias } from "./browser-tab-id.js";
9
9
  import { isTerminalAuthFailure } from "./create-query-client.js";
10
10
  import { ensureEmbedAuthFetchInterceptor, isEmbedAuthActive, } from "./embed-auth.js";
11
11
  import { bumpChangeVersion, bumpActiveLocalChangeVersions, bumpLocalChangeVersion, } from "./use-change-version.js";
@@ -1372,10 +1372,7 @@ export function useDbSync(options = {}) {
1372
1372
  .map((key) => `${key.length}:${key}`)
1373
1373
  .join("|");
1374
1374
  const needsBroadAppStateRefresh = appStateKeys.some((key) => key === "*" ||
1375
- isInteractionCriticalSyncEvent({
1376
- source: "app-state",
1377
- key,
1378
- }));
1375
+ INTERACTION_CRITICAL_APP_STATE_KEYS.includes(key));
1379
1376
  invalidateWithoutCancel(needsBroadAppStateRefresh
1380
1377
  ? { queryKey: ["app-state"] }
1381
1378
  : {
@@ -1432,11 +1429,19 @@ export function useDbSync(options = {}) {
1432
1429
  subscriberCursor = maxSyncCursor(subscriberCursor, cursor);
1433
1430
  }
1434
1431
  const sideEffectToolsByTab = new Map();
1435
- const eventsForTool = (tool, completedSideEffect, failed) => {
1432
+ const browserTabIdsByTab = new Map();
1433
+ const eventsForTool = (tool, completedSideEffect, failed, browserTabId) => {
1436
1434
  const events = [];
1437
1435
  if (completedSideEffect) {
1438
1436
  events.push({ source: "action", key: tool, version: 0 });
1439
1437
  }
1438
+ if (tool === "navigate" && completedSideEffect) {
1439
+ events.push({
1440
+ source: "app-state",
1441
+ key: browserTabId ? `navigate:${browserTabId}` : "navigate",
1442
+ version: 0,
1443
+ });
1444
+ }
1440
1445
  if (["__set_url__", "set-url", "set-search-params"].includes(tool)) {
1441
1446
  events.push({ source: "app-state", key: "__set_url__", version: 0 });
1442
1447
  }
@@ -1478,13 +1483,36 @@ export function useDbSync(options = {}) {
1478
1483
  return;
1479
1484
  const tool = detail.tool;
1480
1485
  const completedSideEffect = detail.completedSideEffect === true;
1481
- const events = eventsForTool(tool, completedSideEffect, detail.isError === true);
1486
+ const tabId = typeof detail.tabId === "string" && detail.tabId
1487
+ ? detail.tabId
1488
+ : "__default__";
1489
+ const runId = typeof detail.runId === "string" && detail.runId
1490
+ ? detail.runId
1491
+ : undefined;
1492
+ const runStart = browserTabIdsByTab.get(tabId);
1493
+ if (runId && runStart?.runId && runId !== runStart.runId)
1494
+ return;
1495
+ const aliasedRunStartId = runStart
1496
+ ? resolveBrowserTabIdAlias(runStart.browserTabId)
1497
+ : undefined;
1498
+ const browserTabId = aliasedRunStartId ??
1499
+ runStart?.browserTabId ??
1500
+ (typeof detail.browserTabId === "string"
1501
+ ? detail.browserTabId
1502
+ : getBrowserTabId());
1503
+ if (runStart && browserTabId !== runStart.browserTabId) {
1504
+ browserTabIdsByTab.set(tabId, { ...runStart, browserTabId });
1505
+ }
1506
+ if (completedSideEffect && !runStart) {
1507
+ browserTabIdsByTab.set(tabId, {
1508
+ browserTabId,
1509
+ ...(runId ? { runId } : {}),
1510
+ });
1511
+ }
1512
+ const events = eventsForTool(tool, completedSideEffect, detail.isError === true, browserTabId);
1482
1513
  if (events.length === 0)
1483
1514
  return;
1484
1515
  if (completedSideEffect) {
1485
- const tabId = typeof detail.tabId === "string" && detail.tabId
1486
- ? detail.tabId
1487
- : "__default__";
1488
1516
  const tools = sideEffectToolsByTab.get(tabId) ?? new Map();
1489
1517
  const previouslyFailed = tools.get(tool);
1490
1518
  tools.set(tool, previouslyFailed === false ? false : detail.isError === true);
@@ -1498,6 +1526,22 @@ export function useDbSync(options = {}) {
1498
1526
  const tabId = typeof detail.tabId === "string" && detail.tabId
1499
1527
  ? detail.tabId
1500
1528
  : "__default__";
1529
+ const browserTabId = typeof detail.browserTabId === "string"
1530
+ ? detail.browserTabId
1531
+ : getBrowserTabId();
1532
+ const runId = typeof detail.runId === "string" && detail.runId
1533
+ ? detail.runId
1534
+ : undefined;
1535
+ const runStart = browserTabIdsByTab.get(tabId);
1536
+ if (!runId) {
1537
+ browserTabIdsByTab.set(tabId, { browserTabId });
1538
+ }
1539
+ else if (!runStart || (runStart.runId && runStart.runId !== runId)) {
1540
+ browserTabIdsByTab.set(tabId, { browserTabId, runId });
1541
+ }
1542
+ else if (!runStart.runId) {
1543
+ browserTabIdsByTab.set(tabId, { ...runStart, runId });
1544
+ }
1501
1545
  sideEffectToolsByTab.delete(tabId);
1502
1546
  return;
1503
1547
  }
@@ -1506,11 +1550,26 @@ export function useDbSync(options = {}) {
1506
1550
  const tabId = typeof detail.tabId === "string" && detail.tabId
1507
1551
  ? detail.tabId
1508
1552
  : "__default__";
1553
+ const runStart = browserTabIdsByTab.get(tabId);
1554
+ const runId = typeof detail.runId === "string" && detail.runId
1555
+ ? detail.runId
1556
+ : undefined;
1557
+ if (runId && runStart?.runId && runId !== runStart.runId)
1558
+ return;
1559
+ const aliasedRunStartId = runStart
1560
+ ? resolveBrowserTabIdAlias(runStart.browserTabId)
1561
+ : undefined;
1562
+ const browserTabId = aliasedRunStartId ??
1563
+ runStart?.browserTabId ??
1564
+ (typeof detail.browserTabId === "string"
1565
+ ? detail.browserTabId
1566
+ : getBrowserTabId());
1567
+ browserTabIdsByTab.delete(tabId);
1509
1568
  const tools = sideEffectToolsByTab.get(tabId);
1510
1569
  if (!tools?.size)
1511
1570
  return;
1512
1571
  sideEffectToolsByTab.delete(tabId);
1513
- applyRunEvents(event, [...tools].flatMap(([tool, failed]) => eventsForTool(tool, true, failed)), processedRunEndEvents);
1572
+ applyRunEvents(event, [...tools].flatMap(([tool, failed]) => eventsForTool(tool, true, failed, browserTabId)), processedRunEndEvents);
1514
1573
  };
1515
1574
  window.addEventListener("agent-native:tool-done", handleRunToolDone);
1516
1575
  window.addEventListener("agentNative.chatRunning", handleRunStatus);
@@ -18,6 +18,29 @@ export interface UseCollaborativeDocOptions {
18
18
  };
19
19
  /** Application events received alongside document updates on the existing transport. */
20
20
  onEvent?: (event: SyncEvent) => void;
21
+ /**
22
+ * Keeps this tab's unacknowledged updates in localStorage until the server
23
+ * acknowledges them, and sends the ones a closed or reloaded tab left
24
+ * behind when the doc next opens for the same user. Needs `user`, whose
25
+ * email scopes what is kept.
26
+ */
27
+ durableUpdates?: boolean;
28
+ /** New updates need a known live-copy outcome handled by a current strict subscriber. */
29
+ requireLiveCopyOutcome?: boolean;
30
+ /**
31
+ * Called for each update the server acknowledged, with the live-copy
32
+ * outcome it reported. With it, an acknowledgment whose body cannot be read
33
+ * counts as undelivered and the update is sent again.
34
+ */
35
+ onUpdateAcknowledged?: (ack: CollabUpdateAck) => void;
36
+ }
37
+ /**
38
+ * What the server reported for an acknowledged update: whether the app's rows
39
+ * were derived from its live copy, or null when the server defines none.
40
+ */
41
+ export interface CollabUpdateAck {
42
+ liveCopy: "derived" | "not_derived" | "unchanged" | null;
43
+ liveCopyReason?: string;
21
44
  }
22
45
  export type CollabInitializationErrorCategory = "forbidden-or-not-found" | "server" | "network" | "invalid-payload";
23
46
  export type CollabInitializationState = {
@@ -51,6 +74,17 @@ export interface UseCollaborativeDocResult {
51
74
  * can already receive it through the document instead of inserting it twice.
52
75
  */
53
76
  flushUpdates: () => Promise<boolean>;
77
+ /**
78
+ * True once keeping this tab's unacknowledged updates, or reading the ones
79
+ * another tab left, failed. Updates still send while the tab stays open.
80
+ */
81
+ durableUpdatesFailed: boolean;
82
+ /**
83
+ * True once the server refused one of this tab's updates as too large.
84
+ * Resending it cannot succeed, so this tab stops sending updates; they stay
85
+ * unacknowledged, and kept with `durableUpdates`.
86
+ */
87
+ updatesRefused: boolean;
54
88
  activeUsers: CollabUser[];
55
89
  agentActive: boolean;
56
90
  agentPresent: boolean;