@agent-native/core 0.210.1 → 0.211.0-nightly-20261020183616

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 (62) hide show
  1. package/dist/action.d.ts +8 -0
  2. package/dist/action.js +17 -0
  3. package/dist/agent/production-agent.js +11 -20
  4. package/dist/client/chat/agentkit-protocol.js +48 -5
  5. package/dist/client/chat/runtime.d.ts +2 -0
  6. package/dist/client/chat/runtime.js +6 -1
  7. package/dist/client/hooks/index.d.ts +1 -1
  8. package/dist/client/hooks/index.js +1 -1
  9. package/dist/client/resources/mcp-integration-logos.js +4 -0
  10. package/dist/client/session-identity.d.ts +8 -0
  11. package/dist/client/session-identity.js +13 -0
  12. package/dist/client/use-action.d.ts +14 -0
  13. package/dist/client/use-action.js +117 -28
  14. package/dist/client/use-agent-engine-configured.d.ts +2 -2
  15. package/dist/client/use-agent-engine-configured.js +3 -9
  16. package/dist/client/use-session.js +2 -0
  17. package/dist/collab/awareness.d.ts +2 -2
  18. package/dist/collab/struct-routes.d.ts +1 -1
  19. package/dist/deploy/build.js +24 -3
  20. package/dist/mcp/actions/revoke-org-service-token.js +5 -1
  21. package/dist/mcp/execute-action-call.js +8 -17
  22. package/dist/observability/routes.d.ts +5 -5
  23. package/dist/org/actions/set-app-member-roles.js +4 -0
  24. package/dist/org/actions/set-app-permission-roles.js +4 -0
  25. package/dist/org/actions/set-workspace-app-access.js +4 -0
  26. package/dist/org/index.d.ts +1 -1
  27. package/dist/provider-api/actions/custom-provider-registration.d.ts +17 -17
  28. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  29. package/dist/resources/handlers.d.ts +1 -1
  30. package/dist/review/suggestions/actions.d.ts +1 -0
  31. package/dist/review/suggestions/actions.js +54 -42
  32. package/dist/secrets/actions/delete-api-key.js +4 -0
  33. package/dist/secrets/routes.d.ts +4 -4
  34. package/dist/server/action-approval.d.ts +15 -0
  35. package/dist/server/action-approval.js +39 -0
  36. package/dist/server/action-routes.js +33 -28
  37. package/dist/server/edge.d.ts +1 -0
  38. package/dist/server/edge.js +1 -0
  39. package/dist/server/realtime-token.d.ts +1 -1
  40. package/dist/server/transcribe-voice.d.ts +1 -1
  41. package/dist/server/workspace-provider-oauth.js +1 -0
  42. package/dist/shared/action-error-codes.d.ts +5 -0
  43. package/dist/shared/action-error-codes.js +5 -0
  44. package/dist/shared/index.d.ts +1 -0
  45. package/dist/shared/index.js +1 -0
  46. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  47. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  48. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  49. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  50. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  51. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  52. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
  53. package/dist/vite/client.js +2 -1
  54. package/docs/content/actions-access-control.mdx +3 -3
  55. package/package.json +4 -4
  56. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +5 -3
  57. package/src/templates/default/.agents/skills/actions/references/action-fields.md +5 -3
  58. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +105 -33
  59. package/src/templates/factory/.agents/skills/review-latest-feedback/references/channel-inventory.md +128 -74
  60. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +39 -11
  61. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +5 -3
  62. package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +5 -3
package/dist/action.d.ts CHANGED
@@ -661,6 +661,14 @@ export declare function actionCallIntent(entry: {
661
661
  * share one, so none can change what another reads.
662
662
  */
663
663
  export declare function preparedCallArguments(call: PreparedActionCall): unknown;
664
+ /**
665
+ * Whether `call` needs a person's approval before it runs, by the action's
666
+ * `needsApproval`. Every surface that can run an action decides with this, so
667
+ * a predicate means the same thing to the agent loop, MCP, and HTTP.
668
+ */
669
+ export declare function preparedCallNeedsApproval(entry: {
670
+ needsApproval?: boolean | ((args: any, ctx?: ActionRunContext) => boolean | Promise<boolean>);
671
+ }, call: PreparedActionCall, ctx: ActionRunContext): Promise<boolean>;
664
672
  /**
665
673
  * @internal Executes a prepared call: `entry.run` receives its own copy of the
666
674
  * prepared arguments, which the action's input validation and classification
package/dist/action.js CHANGED
@@ -1398,6 +1398,23 @@ export function preparedCallArguments(call) {
1398
1398
  }
1399
1399
  return copyActionArguments(prepared.snapshot);
1400
1400
  }
1401
+ /**
1402
+ * Whether `call` needs a person's approval before it runs, by the action's
1403
+ * `needsApproval`. Every surface that can run an action decides with this, so
1404
+ * a predicate means the same thing to the agent loop, MCP, and HTTP.
1405
+ */
1406
+ export async function preparedCallNeedsApproval(entry, call, ctx) {
1407
+ const gate = entry.needsApproval;
1408
+ if (typeof gate !== "function")
1409
+ return gate === true;
1410
+ try {
1411
+ return Boolean(await gate(preparedCallArguments(call), ctx));
1412
+ }
1413
+ catch {
1414
+ // coercion-ok: a predicate that cannot decide fails closed and asks.
1415
+ return true;
1416
+ }
1417
+ }
1401
1418
  /**
1402
1419
  * @internal Executes a prepared call: `entry.run` receives its own copy of the
1403
1420
  * prepared arguments, which the action's input validation and classification
@@ -8,7 +8,7 @@ import { parseA2AAgentActivityPart } from "../a2a/activity.js";
8
8
  import { actionCallIsRetrySafe, actionChangeResource, classifyActionCall, } from "../action-call-classification.js";
9
9
  import { runOutsideActionInvocation, vouchedOutcome } from "../action-frame.js";
10
10
  import { ACTION_CHAT_UI_RECORD_CHANGE_RENDERER, normalizeActionChangeResult, } from "../action-ui.js";
11
- import { AgentConnectionRequiredError, describeToolParameterSignature, isActionContractError, isActionHiddenFromEveryAgentSurface, isAgentActionStopError, isAgentConnectionRequiredError, actionCallIntent, invocationEffect, runPreparedActionCall, boundActionCallIntent, preparedCallArguments, recordInvalidActionCall, stripUnsupportedSchemaKeywords, } from "../action.js";
11
+ import { AgentConnectionRequiredError, describeToolParameterSignature, isActionContractError, isActionHiddenFromEveryAgentSurface, isAgentActionStopError, isAgentConnectionRequiredError, actionCallIntent, invocationEffect, runPreparedActionCall, boundActionCallIntent, preparedCallArguments, preparedCallNeedsApproval, recordInvalidActionCall, stripUnsupportedSchemaKeywords, } from "../action.js";
12
12
  import { getAppConfig } from "../app-config/index.js";
13
13
  import { MAX_BACKGROUND_RUN_CONTINUATIONS, MAX_CONSECUTIVE_NO_PROGRESS_CONTINUATIONS, MAX_TURN_WALL_CLOCK_MS, } from "../app-config/run-lifecycle-invariants.js";
14
14
  import { readAppState } from "../application-state/script-helpers.js";
@@ -6131,25 +6131,16 @@ export async function runAgentLoop(opts) {
6131
6131
  ? await opts.consumeApproval(approvalBinding())
6132
6132
  : requestedApproval;
6133
6133
  if (actionEntry.needsApproval && !wasApproved) {
6134
- let mustApprove = false;
6135
- try {
6136
- mustApprove =
6137
- typeof actionEntry.needsApproval === "function"
6138
- ? Boolean(await actionEntry.needsApproval(preparedCallArguments(intent), {
6139
- userEmail: getRequestUserEmail(),
6140
- orgId: getRequestOrgId() ?? null,
6141
- appId: opts.appId,
6142
- caller: opts.actionCaller ?? "tool",
6143
- automation: opts.automation,
6144
- networkProtocol: opts.networkProtocol,
6145
- networkId: opts.networkId,
6146
- networkPeer: opts.networkPeer,
6147
- }))
6148
- : actionEntry.needsApproval === true;
6149
- }
6150
- catch {
6151
- mustApprove = true;
6152
- }
6134
+ let mustApprove = await preparedCallNeedsApproval(actionEntry, intent, {
6135
+ userEmail: getRequestUserEmail(),
6136
+ orgId: getRequestOrgId() ?? null,
6137
+ appId: opts.appId,
6138
+ caller: opts.actionCaller ?? "tool",
6139
+ automation: opts.automation,
6140
+ networkProtocol: opts.networkProtocol,
6141
+ networkId: opts.networkId,
6142
+ networkPeer: opts.networkPeer,
6143
+ });
6153
6144
  if (mustApprove &&
6154
6145
  actionEntry.allowPersistentApproval !== false &&
6155
6146
  opts.isToolAlwaysAllowed) {
@@ -1077,9 +1077,15 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1077
1077
  }
1078
1078
  return activeOwners[0] ?? owners.at(-1);
1079
1079
  }
1080
- function indexRun(run, runtimeRunId = run.turn.runId ?? run.runId) {
1080
+ function indexRun(run, runtimeRunId = run.turn.runId ?? run.runId, allowReplay = false) {
1081
1081
  const existingRun = runs.get(run.runId);
1082
1082
  if (existingRun && existingRun !== run) {
1083
+ if (allowReplay &&
1084
+ existingRun.threadId === run.threadId &&
1085
+ existingRun.turn.id !== undefined &&
1086
+ existingRun.turn.id === run.turn.id) {
1087
+ return existingRun;
1088
+ }
1083
1089
  throw new Error(`Duplicate AgentKit run id: ${run.runId}`);
1084
1090
  }
1085
1091
  if (runtimeRunId && runtimeRunId !== run.runId) {
@@ -1098,6 +1104,7 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1098
1104
  runtimeRunAliases.set(aliasKey, run);
1099
1105
  }
1100
1106
  runs.set(run.runId, run);
1107
+ return run;
1101
1108
  }
1102
1109
  async function resolveRuntimeRunOwner(threadId, runtimeRunId, sessionId, authorityRead) {
1103
1110
  if (!runAuthority)
@@ -1468,7 +1475,11 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1468
1475
  : {}),
1469
1476
  listeners: new Set(),
1470
1477
  };
1471
- indexRun(run, turn.runId ?? input.runId);
1478
+ const indexedRun = indexRun(run, turn.runId ?? input.runId, true);
1479
+ if (indexedRun !== run) {
1480
+ await closeDiscardedTurnEvents(turn);
1481
+ return indexedRun;
1482
+ }
1472
1483
  if (restoreRead)
1473
1484
  run.initialAuthorityRead = restoreRead;
1474
1485
  if (state && "pendingConnectionRequestId" in state) {
@@ -1488,7 +1499,7 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1488
1499
  * Registers a run the runtime just started and begins reading it. A disposed
1489
1500
  * adapter keeps no run it can no longer read.
1490
1501
  */
1491
- async function openStartedRun(threadId, session, turn, turnMetadata, observability) {
1502
+ async function openStartedRun(threadId, session, turn, turnMetadata, observability, start = {}) {
1492
1503
  const runId = turn.runId ?? turn.id ?? createId("run");
1493
1504
  const runMetadata = mergeTrustedProtocolMetadata(options.metadata, turnMetadata, turn.metadata, {
1494
1505
  [AGENT_NATIVE_PROTOCOL_METADATA_KEY]: {
@@ -1535,8 +1546,34 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1535
1546
  await disposeSession(session).catch(() => undefined);
1536
1547
  throw new Error("The AgentKit adapter was disposed during turn creation.");
1537
1548
  }
1549
+ const existingRun = start.attachReplayedRun
1550
+ ? findRun(threadId, runId)
1551
+ : undefined;
1552
+ if (existingRun) {
1553
+ // A duplicate queued submission is answered with the run that already
1554
+ // took it, replayed from its first event. This adapter reads that run
1555
+ // already, so the replay is drained rather than indexed a second time.
1556
+ // The replayed stream stays open until the run ends; closing it early
1557
+ // needs a transport abort, because turn.cancel would cancel the run.
1558
+ void (async () => {
1559
+ for await (const _event of turn.events) {
1560
+ // Discarded: the existing run delivers these events.
1561
+ }
1562
+ })().catch((error) => {
1563
+ console.warn(`[agentkit] replayed stream for run ${runId} failed`, error);
1564
+ });
1565
+ touchRun(existingRun);
1566
+ ensurePump(existingRun);
1567
+ // A runtime alias resolves to the run that owns it, which may use a
1568
+ // different protocol id; the caller must address that run.
1569
+ return existingRun.runId;
1570
+ }
1538
1571
  pruneRetainedRuns(run.lastAccessedAtMs);
1539
- indexRun(run, turn.runId);
1572
+ const indexedRun = indexRun(run, turn.runId, true);
1573
+ if (indexedRun !== run) {
1574
+ await closeDiscardedTurnEvents(turn);
1575
+ return indexedRun.runId;
1576
+ }
1540
1577
  append(run, {
1541
1578
  type: "run.started",
1542
1579
  agentId: runtime.id,
@@ -1552,6 +1589,10 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
1552
1589
  disposedSessions.add(session);
1553
1590
  await session.dispose?.();
1554
1591
  }
1592
+ async function closeDiscardedTurnEvents(turn) {
1593
+ const iterator = turn.events[Symbol.asyncIterator]();
1594
+ await Promise.all([turn.closeEvents?.(), iterator.return?.()]);
1595
+ }
1555
1596
  function append(run, event) {
1556
1597
  const lastType = run.events.at(-1)?.type;
1557
1598
  const terminalStatus = event.type === "run.completed" ||
@@ -3082,7 +3123,9 @@ export function createAgentKitProtocolAdapter(runtime, options = {}) {
3082
3123
  metadata: turnMetadata,
3083
3124
  abortSignal: readers.signal,
3084
3125
  });
3085
- return openStartedRun(input.threadId, session, turn, turnMetadata);
3126
+ return openStartedRun(input.threadId, session, turn, turnMetadata, undefined, {
3127
+ attachReplayedRun: true,
3128
+ });
3086
3129
  };
3087
3130
  if (!queuePromotionKey) {
3088
3131
  return { runId: await createRun(), capabilities };
@@ -550,6 +550,8 @@ export interface AgentChatRuntimeTurn<TEvent extends AgentChatRuntimeEventBase =
550
550
  readonly runId?: string;
551
551
  readonly metadata?: AgentChatRuntimeMetadata;
552
552
  readonly events: AsyncIterable<TEvent>;
553
+ /** Close the local event stream without cancelling the underlying run. */
554
+ closeEvents?(): Promise<void> | void;
553
555
  cancel?(input?: AgentChatRuntimeCancelInput): Promise<AgentChatRuntimeCancelResult>;
554
556
  }
555
557
  export interface AgentChatRuntimeSendMessageInput extends AgentChatRuntimeTurnInput {
@@ -504,13 +504,18 @@ export function createHttpAgentChatRuntime(options) {
504
504
  cleanup();
505
505
  }
506
506
  })();
507
+ const closeEvents = () => {
508
+ controller.abort();
509
+ cleanup();
510
+ };
507
511
  return {
508
512
  id: turnId,
509
513
  sessionId,
510
514
  runId,
511
515
  events,
516
+ closeEvents,
512
517
  cancel: async (cancelInput) => {
513
- controller.abort();
518
+ closeEvents();
514
519
  if (!options.cancelEndpoint)
515
520
  return { status: "cancelled" };
516
521
  const endpoint = typeof options.cancelEndpoint === "function"
@@ -6,7 +6,7 @@ export { useDemoModeStatus, type DemoModeStatus, } from "../use-demo-mode-status
6
6
  export { useReconciledState } from "../use-external-value.js";
7
7
  export { beginSignOut, isSigningOut, notifySessionInvalidated, useSession, type AuthSession, } from "../use-session.js";
8
8
  export { signOut, type SignOutOptions } from "../sign-out.js";
9
- export { ACTION_KEEPALIVE_BODY_BUDGET_BYTES, actionErrorMessage, callAction, callActionBlob, callActionWithRetry, tryCallActionKeepalive, useActionQuery, useActionMutation, type ActionRegistry, type ClientActionCallOptions, type ClientActionMethod, type RetriedActionCallOptions, type KeepaliveActionCallRejectionReason, type KeepaliveActionCallResult, } from "../use-action.js";
9
+ export { ACTION_KEEPALIVE_BODY_BUDGET_BYTES, actionErrorMessage, callAction, callActionBlob, callActionWithRetry, currentActionQueryCacheBinding, recordActionQueryBrowserPersist, tryCallActionKeepalive, useActionQuery, useActionMutation, type ActionRegistry, type ClientActionCallOptions, type ClientActionMethod, type RetriedActionCallOptions, type KeepaliveActionCallRejectionReason, type KeepaliveActionCallResult, } from "../use-action.js";
10
10
  export { actionErrorCode, isTerminalActionError, } from "../action-failure-circuit.js";
11
11
  export { createAgentNativeQueryClient } from "../create-query-client.js";
12
12
  export { APP_CHAT_SIDEBAR_STATE_EVENT, APP_CHAT_SIDEBAR_STATE_MESSAGE, APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE, buildAppChatSidebarStateMessage, buildAppChatSidebarStateRequest, isPerAppChatStorageKey, requestPerAppChatCommand, usePerAppChatState, usePerAppChatOpen, type AppChatSidebarCommand, type AppChatSidebarState, } from "../app-chat-sidebar.js";
@@ -6,7 +6,7 @@ export { useDemoModeStatus, } from "../use-demo-mode-status.js";
6
6
  export { useReconciledState } from "../use-external-value.js";
7
7
  export { beginSignOut, isSigningOut, notifySessionInvalidated, useSession, } from "../use-session.js";
8
8
  export { signOut } from "../sign-out.js";
9
- export { ACTION_KEEPALIVE_BODY_BUDGET_BYTES, actionErrorMessage, callAction, callActionBlob, callActionWithRetry, tryCallActionKeepalive, useActionQuery, useActionMutation, } from "../use-action.js";
9
+ export { ACTION_KEEPALIVE_BODY_BUDGET_BYTES, actionErrorMessage, callAction, callActionBlob, callActionWithRetry, currentActionQueryCacheBinding, recordActionQueryBrowserPersist, tryCallActionKeepalive, useActionQuery, useActionMutation, } from "../use-action.js";
10
10
  export { actionErrorCode, isTerminalActionError, } from "../action-failure-circuit.js";
11
11
  export { createAgentNativeQueryClient } from "../create-query-client.js";
12
12
  export { APP_CHAT_SIDEBAR_STATE_EVENT, APP_CHAT_SIDEBAR_STATE_MESSAGE, APP_CHAT_SIDEBAR_STATE_REQUEST_MESSAGE, buildAppChatSidebarStateMessage, buildAppChatSidebarStateRequest, isPerAppChatStorageKey, requestPerAppChatCommand, usePerAppChatState, usePerAppChatOpen, } from "../app-chat-sidebar.js";
@@ -223,6 +223,10 @@ const LOGOS = {
223
223
  mime: "image/svg+xml",
224
224
  data: "PHN2ZyBmaWxsPSIjRTM3NDAwIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R29vZ2xlIEFuYWx5dGljczwvdGl0bGU+PHBhdGggZD0iTTIyLjg0IDIuOTk4MnYxNy45OTg3Yy4wMDg2IDEuNjQ3My0xLjMxOTcgMi45ODk3LTIuOTY3IDIuOTk4NGEyLjk4MDggMi45ODA4IDAgMDEtLjM2NzctLjAyMDhjLTEuNTI4LS4yMjYtMi42NDc3LTEuNTU1OC0yLjYxMDUtMy4xVjMuMTIwNGMtLjAzNjktMS41NDU4IDEuMDg1Ni0yLjg3NjIgMi42MTU3LTMuMSAxLjYzNjEtLjE5MTUgMy4xMTc4Ljk3OTYgMy4zMDkzIDIuNjE1OC4wMTQuMTIwMS4wMjA4LjI0MS4wMjAyLjM2MTl6TTQuMTMyNiAxOC4wNTQ4Yy0xLjY0MTcgMC0yLjk3MjYgMS4zMzEtMi45NzI2IDIuOTcyNkMxLjE2IDIyLjY2OTEgMi40OTA5IDI0IDQuMTMyNiAyNHMyLjk3MjYtMS4zMzA5IDIuOTcyNi0yLjk3MjYtMS4zMzEtMi45NzI2LTIuOTcyNi0yLjk3MjZ6bTcuODcyOC05LjAwOThjLS4wMTcxIDAtLjAzNDIgMC0uMDUxMy4wMDAzLTEuNjQ5NS4wOTA0LTIuOTI5MyAxLjQ3NC0yLjg5MSAzLjEyNTZ2Ny45ODQ2YzAgMi4xNjcuOTUzNSAzLjQ4MjUgMi4zNTA1IDMuNzYzIDEuNjExOC4zMjY2IDMuMTgzMi0uNzE1MiAzLjUwOTgtMi4zMjcuMDQtLjE5NzQuMDYtLjM5ODMuMDU5My0uNTk5OHYtOC45NTg1Yy4wMDMtMS42NDc0LTEuMzMtMi45ODUyLTIuOTc3My0yLjk4ODJ6Ii8+PC9zdmc+",
225
225
  },
226
+ "google-search-console": {
227
+ mime: "image/svg+xml",
228
+ data: "PHN2ZyBmaWxsPSIjNDU4Q0Y1IiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R29vZ2xlIFNlYXJjaCBDb25zb2xlPC90aXRsZT48cGF0aCBkPSJNOC41NDggMS4xNTZMNi44MzIgMi44NzJ2MS42ODJoMS43MTZ6bTAgMy4zOTh2LjAzNUg2LjgzMnYtLjAzNUgzLjM4NkwwIDcuODQ0djMuNTc3aDIuODI2VjguOTRjMC0uNTI1LjQyOS0uOTU0Ljk1NC0uOTU0aDE2LjQ3NmMuNTI1IDAgLjk1NC40My45NTQuOTU0djIuNDhoMi43NTRWNy44NDRsLTMuMzg2LTMuMjlIMTcuM3YuMDM1aC0xLjcxN3YtLjAzNXptNy4wMzUgMEgxNy4zVjIuODcybC0xLjcxNy0xLjcxNnpNOC42NzkgMS4xODhWMi44NGg2Ljc3M1YxLjE4OHptMTEuNDcxIDcuMDdhLjgzNC44MzQgMCAwMC0uMTMyLjAxbC0uNTQzLjAwMmMtNS4yMTYuMDE0LTEwLjQzMi0uMDA4LTE1LjY0OC4wMS0uNDM1LS4wNjMtLjc5NC40MzYtLjcxNi44ODN2Mi4yNjRoMTcuODEyYy0uMDE2LS44ODguMDQ1LTEuNzgyLS4wMzQtMi42NjYtLjEwNC0uMzQyLS40MjctLjUwMi0uNzM5LS41MDJ6bS0xNS40MjIuNjM0YS42ODkuNjk4IDAgMDEuNjg5LjY5OC42ODkuNjk4IDAgMDEtLjY4OS42OTcuNjg5LjY5OCAwIDAxLS42ODgtLjY5Ny42ODkuNjk4IDAgMDEuNjg4LS42OTh6bTIuMTM0IDBhLjY4OS42OTggMCAwMS42ODkuNjk4LjY4OS42OTggMCAwMS0uNjg5LjY5Ny42ODkuNjk4IDAgMDEtLjY4OC0uNjk3LjY4OS42OTggMCAwMS42ODgtLjY5OHpNLjAzNiAxMS42NDV2OS4xNTZjMCAxLjA1Ljg1OCAxLjkwOCAxLjkwNyAxLjkwOGguODgzVjExLjY0NXptMjEuMTc0IDB2MTEuMDY0aC44ODJjMS4wNSAwIDEuOTA4LS44NTggMS45MDgtMS45MDh2LTkuMTU2ek00LjA1NyAxMy4xMzN2Ni44NWg2LjEzN3YtNi44NXptMTMuMjQzLjAyMXYzLjc3N2wtMS43MDguOTc3LTEuNzA4LS45Nzd2LTMuNzU4YTQuMDA2IDQuMDA2IDAgMDAwIDcuMjN2Mi40NDFoMy40NTd2LTIuNDQyYTQuMDA2IDQuMDA2IDAgMDAtLjA0MS03LjI0OHptLTEzLjI0MyA4LjI2djEuNDNoNy45MjV2LTEuNDN6Ii8+PC9zdmc+",
229
+ },
226
230
  "google-sheets": {
227
231
  mime: "image/svg+xml",
228
232
  data: "PHN2ZyBmaWxsPSIjMzRBODUzIiByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+R29vZ2xlIFNoZWV0czwvdGl0bGU+PHBhdGggZD0iTTExLjMxOCAxMi41NDVINy45MXYtMS45MDloMy40MXYxLjkxek0xNC43MjggMHY2aDZsLTYtNnptMS4zNjMgMTAuNjM2aC0zLjQxdjEuOTFoMy40MXYtMS45MXptMCAzLjI3M2gtMy40MXYxLjkxaDMuNDF2LTEuOTF6TTIwLjcyNyA2LjV2MTUuODY0YzAgLjkwNC0uNzMyIDEuNjM2LTEuNjM2IDEuNjM2SDQuOTA5YTEuNjM2IDEuNjM2IDAgMCAxLTEuNjM2LTEuNjM2VjEuNjM2QzMuMjczLjczMiA0LjAwNSAwIDQuOTA5IDBoOS4zMTh2Ni41aDYuNXptLTMuMjczIDIuNzczSDYuNTQ1djcuOTA5aDEwLjkxdi03Ljkxem0tNi4xMzYgNC42MzZINy45MXYxLjkxaDMuNDF2LTEuOTF6Ii8+PC9zdmc+",
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The identity of the session this tab last read: a string when signed in,
3
+ * `null` when known to be signed out (or signing out), `undefined` before the
4
+ * first read. Kept apart from `use-session` so action fetches can read it
5
+ * without depending on the session hook.
6
+ */
7
+ export declare function currentSessionIdentity(): string | null | undefined;
8
+ export declare function recordSessionIdentity(value: string | null | undefined): void;
@@ -0,0 +1,13 @@
1
+ let identity;
2
+ /**
3
+ * The identity of the session this tab last read: a string when signed in,
4
+ * `null` when known to be signed out (or signing out), `undefined` before the
5
+ * first read. Kept apart from `use-session` so action fetches can read it
6
+ * without depending on the session hook.
7
+ */
8
+ export function currentSessionIdentity() {
9
+ return identity;
10
+ }
11
+ export function recordSessionIdentity(value) {
12
+ identity = value;
13
+ }
@@ -44,6 +44,8 @@ export interface ClientActionCallOptions {
44
44
  headers?: Record<string, string>;
45
45
  /** Set when the action is declared `uiOnly`, so its first request carries the UI capability. */
46
46
  uiOnly?: boolean;
47
+ /** Sees each HTTP response, e.g. to pass to `recordActionQueryBrowserPersist`. */
48
+ onResponse?: (response: Response) => void;
47
49
  }
48
50
  export declare function serializeActionQueryParams(params: Record<string, any>): string;
49
51
  export interface ActionFetchOptions {
@@ -56,6 +58,8 @@ export interface ActionFetchOptions {
56
58
  headers?: Record<string, string>;
57
59
  uiOnly?: boolean;
58
60
  }
61
+ /** Drop the remembered capability so the next call that needs one mints it. */
62
+ export declare function forgetUiActionCapability(): void;
59
63
  export declare const ACTION_KEEPALIVE_BODY_BUDGET_BYTES = 48000;
60
64
  /**
61
65
  * `undefined` when there is no document to ask (SSR, a non-browser caller) —
@@ -121,6 +125,16 @@ export declare function isBrowserPersistableActionQueryForBinding(query: Query,
121
125
  /** @internal used only after a persisted action result is accepted and hydrated. */
122
126
  export declare function markRestoredBrowserPersistableActionQuery(query: Query, token?: object): void;
123
127
  export declare function revokeBrowserPersistableActionQuery(query: Query): void;
128
+ /** The persisted-cache binding to capture when a fetch starts, for `recordActionQueryBrowserPersist`. */
129
+ export declare function currentActionQueryCacheBinding(): object | undefined;
130
+ /**
131
+ * Lets an `["action", ...]` query filled by a hand-written `queryFn` or
132
+ * `setQueryData` join the browser's persisted query cache the way a
133
+ * `useActionQuery` result does: only when its response carried the persist
134
+ * header, and only into the binding that was active when the fetch began, so a
135
+ * result fetched for one user is never written into another user's record.
136
+ */
137
+ export declare function recordActionQueryBrowserPersist(query: Query, response: Response | undefined, bindingTokenAtFetch: object | undefined): void;
124
138
  export declare function useActionQuery<TResult = undefined, TName extends ActionName = ActionName>(actionName: TName, params?: ActionParams<TName>, options?: Omit<UseQueryOptions<TResult extends undefined ? ActionResult<TName> : TResult>, "queryKey" | "queryFn"> & {
125
139
  queryKeyScope?: readonly unknown[];
126
140
  /**
@@ -3,6 +3,7 @@ import { useContext } from "react";
3
3
  import { ACTION_BATCH_ACTION_NAME } from "../shared/action-batch.js";
4
4
  import { ACTION_BROWSER_PERSIST_ALLOW, ACTION_BROWSER_PERSIST_HEADER, } from "../shared/action-browser-persist.js";
5
5
  import { ACTION_CHANGE_MARKER_FAILED, ACTION_CHANGE_MARKER_HEADER, } from "../shared/action-change-marker-header.js";
6
+ import { UI_CAPABILITY_REQUIRED_ERROR_CODE } from "../shared/action-error-codes.js";
6
7
  import { SLOW_ACTION_RESPONSE_MS } from "../shared/analytics-events.js";
7
8
  import { ANALYTICS_CLIENT_PLATFORM_HEADER } from "../shared/analytics-platform.js";
8
9
  import { actionCircuitRemainingMs, actionErrorCode, actionErrorRetryAfterMs, actionErrorStatus, assertActionCircuitClosed, endsActionPolling, isActionCircuitOpenError, isTerminalActionError, recordActionFailure, resetActionFailureCircuit, resetActionFailureCircuits, } from "./action-failure-circuit.js";
@@ -19,6 +20,7 @@ import { clientBuildId, clientCompatibilityVersion, reloadForClientCompatibility
19
20
  import { ensureEmbedAuthFetchInterceptor, isEmbedAuthActive, } from "./embed-auth.js";
20
21
  import { recordActionStart } from "./recorder.js";
21
22
  import { currentRouteTemplate } from "./route-template.js";
23
+ import { currentSessionIdentity } from "./session-identity.js";
22
24
  import { recheckSessionAfterUnauthorized } from "./use-session.js";
23
25
  function actionPrefix() {
24
26
  return agentNativePath("/_agent-native/actions");
@@ -119,20 +121,54 @@ function appendActionQueryParam(qs, key, value) {
119
121
  }
120
122
  qs.append(key, String(value));
121
123
  }
124
+ // The server's capability lives 10 minutes; re-mint a minute early so a call
125
+ // never leaves with a capability that expires in flight.
126
+ const UI_CAPABILITY_REUSE_MS = 9 * 60_000;
127
+ // Every signed-in write waits on the shared mint, so a mint that never answers
128
+ // must settle on its own instead of holding writes until their own timeouts.
129
+ const UI_CAPABILITY_MINT_TIMEOUT_MS = 10_000;
122
130
  let uiCapabilityRequest;
131
+ let uiCapability = null;
132
+ function hasFreshUiCapability() {
133
+ return (uiCapability !== null &&
134
+ uiCapability.identity === currentSessionIdentity() &&
135
+ Date.now() - uiCapability.at < UI_CAPABILITY_REUSE_MS);
136
+ }
137
+ /** Drop the remembered capability so the next call that needs one mints it. */
138
+ export function forgetUiActionCapability() {
139
+ uiCapability = null;
140
+ }
123
141
  // The mint is shared by concurrent callers, so one caller's abort rejects only
124
142
  // its own wait and never cancels the request the others are waiting on.
125
143
  function ensureUiActionCapability(signal) {
126
- const request = uiCapabilityRequest ??
127
- (uiCapabilityRequest = fetch(agentNativePath("/_agent-native/ui-capability"), { credentials: "same-origin", cache: "no-store" })
128
- .then((response) => {
129
- if (!response.ok) {
144
+ if (hasFreshUiCapability())
145
+ return Promise.resolve();
146
+ const identity = currentSessionIdentity();
147
+ const request = uiCapabilityRequest ?? (uiCapabilityRequest = mint());
148
+ function mint() {
149
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
150
+ let timer;
151
+ const timedOut = new Promise((_resolve, reject) => {
152
+ timer = setTimeout(() => {
153
+ controller?.abort();
154
+ reject(new Error("Timed out establishing the browser UI capability."));
155
+ }, UI_CAPABILITY_MINT_TIMEOUT_MS);
156
+ });
157
+ const response = fetch(agentNativePath("/_agent-native/ui-capability"), {
158
+ credentials: "same-origin",
159
+ cache: "no-store",
160
+ signal: controller?.signal,
161
+ }).then((res) => {
162
+ if (!res.ok) {
130
163
  throw new Error("Could not establish the browser UI capability.");
131
164
  }
132
- })
133
- .finally(() => {
165
+ uiCapability = { identity, at: Date.now() };
166
+ });
167
+ return Promise.race([response, timedOut]).finally(() => {
168
+ clearTimeout(timer);
134
169
  uiCapabilityRequest = undefined;
135
- }));
170
+ });
171
+ }
136
172
  if (!signal)
137
173
  return request;
138
174
  return new Promise((resolve, reject) => {
@@ -145,6 +181,45 @@ function ensureUiActionCapability(signal) {
145
181
  .finally(() => signal.removeEventListener("abort", onAbort));
146
182
  });
147
183
  }
184
+ /**
185
+ * Every write from the signed-in app carries the UI capability, so an action
186
+ * that needs approval runs on its first request instead of failing once and
187
+ * retrying. The mint waits for a known signed-in session: a public page or an
188
+ * embed has none to mint from, and its writes go out as they are. A failed
189
+ * mint never holds a write back; the server decides what the write may do.
190
+ * A keepalive write never waits on a mint: it is sent as the page goes away,
191
+ * and a deferred send can be lost with it.
192
+ */
193
+ function presentUiCapability(method, options, signal) {
194
+ const identity = currentSessionIdentity();
195
+ let pending;
196
+ if (options?.uiOnly) {
197
+ pending = ensureUiActionCapability(signal);
198
+ }
199
+ else if (method === "GET" ||
200
+ options?.keepalive ||
201
+ typeof window === "undefined" ||
202
+ isEmbedAuthActive() ||
203
+ typeof identity !== "string") {
204
+ return undefined;
205
+ }
206
+ else {
207
+ pending = ensureUiActionCapability(signal).catch((error) => {
208
+ if (signal?.aborted)
209
+ throw error;
210
+ });
211
+ }
212
+ if (typeof identity !== "string")
213
+ return pending;
214
+ // The write leaves only after the mint, and the browser sends whichever
215
+ // session's cookies it holds by then: a user who changed in between must not
216
+ // receive the first user's write.
217
+ return pending.then(() => {
218
+ if (currentSessionIdentity() !== identity) {
219
+ throw new Error("The signed-in user changed before this write was sent.");
220
+ }
221
+ });
222
+ }
148
223
  export const ACTION_KEEPALIVE_BODY_BUDGET_BYTES = 48_000;
149
224
  let reservedKeepaliveBodyBytes = 0;
150
225
  function utf8ByteLength(value) {
@@ -271,9 +346,8 @@ async function performActionFetch(name, method, params, options) {
271
346
  batchUrl: `${actionPrefix()}/${ACTION_BATCH_ACTION_NAME}`,
272
347
  })
273
348
  : fetch(url, init);
274
- const request = options?.uiOnly
275
- ? ensureUiActionCapability(controller?.signal).then(send)
276
- : send();
349
+ const capability = presentUiCapability(method, options, controller?.signal);
350
+ const request = capability ? capability.then(send) : send();
277
351
  res = await Promise.race([request, timedOutSignal]);
278
352
  throwIfAborted(outerSignal);
279
353
  options?.onResponse?.(res);
@@ -340,9 +414,10 @@ async function performActionFetch(name, method, params, options) {
340
414
  }
341
415
  if (!res.ok) {
342
416
  if (res.status === 403 &&
343
- data?.errorCode === "ui_capability_required" &&
417
+ data?.errorCode === UI_CAPABILITY_REQUIRED_ERROR_CODE &&
344
418
  !options?.uiCapabilityRetry &&
345
419
  typeof window !== "undefined") {
420
+ forgetUiActionCapability();
346
421
  return performActionFetch(name, method, params, {
347
422
  ...options,
348
423
  uiOnly: true,
@@ -616,6 +691,7 @@ export function callAction(actionName, params, options = {}) {
616
691
  includeRequestSource: false,
617
692
  headers: options.headers,
618
693
  uiOnly: options.uiOnly,
694
+ onResponse: options.onResponse,
619
695
  });
620
696
  }
621
697
  export function callActionBlob(actionName, params, options = {}) {
@@ -626,6 +702,7 @@ export function callActionBlob(actionName, params, options = {}) {
626
702
  headers: options.headers,
627
703
  responseType: "blob",
628
704
  uiOnly: options.uiOnly,
705
+ onResponse: options.onResponse,
629
706
  });
630
707
  }
631
708
  function isAbortError(error) {
@@ -854,6 +931,32 @@ export function revokeBrowserPersistableActionQuery(query) {
854
931
  browserPersistableActionQueries.delete(query);
855
932
  browserPersistableActionQueryBindings.delete(query);
856
933
  }
934
+ /** The persisted-cache binding to capture when a fetch starts, for `recordActionQueryBrowserPersist`. */
935
+ export function currentActionQueryCacheBinding() {
936
+ return actionQueryCacheBindingToken;
937
+ }
938
+ /**
939
+ * Lets an `["action", ...]` query filled by a hand-written `queryFn` or
940
+ * `setQueryData` join the browser's persisted query cache the way a
941
+ * `useActionQuery` result does: only when its response carried the persist
942
+ * header, and only into the binding that was active when the fetch began, so a
943
+ * result fetched for one user is never written into another user's record.
944
+ */
945
+ export function recordActionQueryBrowserPersist(query, response, bindingTokenAtFetch) {
946
+ if (response?.headers.get(ACTION_BROWSER_PERSIST_HEADER) !==
947
+ ACTION_BROWSER_PERSIST_ALLOW) {
948
+ revokeBrowserPersistableActionQuery(query);
949
+ return;
950
+ }
951
+ browserPersistableActionQueries.add(query);
952
+ if (bindingTokenAtFetch !== undefined &&
953
+ actionQueryCacheBindingToken === bindingTokenAtFetch) {
954
+ browserPersistableActionQueryBindings.set(query, bindingTokenAtFetch);
955
+ }
956
+ else {
957
+ browserPersistableActionQueryBindings.delete(query);
958
+ }
959
+ }
857
960
  const UNBATCHED_REFETCHES_META_KEY = "actionUnbatchedRefetches";
858
961
  /**
859
962
  * A first fetch is what the page is waiting on, so it goes alone. On
@@ -887,16 +990,14 @@ export function useActionQuery(actionName, params, options) {
887
990
  const query = client.getQueryCache().find({ queryKey, exact: true });
888
991
  const bindingTokenAtFetch = actionQueryCacheBindingToken;
889
992
  assertActionCircuitClosed(key);
890
- let browserPersist = false;
993
+ let lastResponse;
891
994
  let result;
892
995
  try {
893
996
  result = await actionFetch(actionName, "GET", params, {
894
997
  signal,
895
998
  batch: canBatchActionQueryFetch(query),
896
999
  onResponse: (response) => {
897
- browserPersist =
898
- response.headers.get(ACTION_BROWSER_PERSIST_HEADER) ===
899
- ACTION_BROWSER_PERSIST_ALLOW;
1000
+ lastResponse = response;
900
1001
  },
901
1002
  });
902
1003
  }
@@ -916,19 +1017,7 @@ export function useActionQuery(actionName, params, options) {
916
1017
  resetActionFailureCircuit(key);
917
1018
  if (query && !signal.aborted) {
918
1019
  clearAccessDeniedActionQueryBinding(query);
919
- if (browserPersist) {
920
- browserPersistableActionQueries.add(query);
921
- if (bindingTokenAtFetch !== undefined &&
922
- actionQueryCacheBindingToken === bindingTokenAtFetch) {
923
- browserPersistableActionQueryBindings.set(query, bindingTokenAtFetch);
924
- }
925
- else {
926
- browserPersistableActionQueryBindings.delete(query);
927
- }
928
- }
929
- else {
930
- revokeBrowserPersistableActionQuery(query);
931
- }
1020
+ recordActionQueryBrowserPersist(query, lastResponse, bindingTokenAtFetch);
932
1021
  }
933
1022
  return result;
934
1023
  },
@@ -14,7 +14,7 @@ export interface UseAgentEngineConfiguredOptions {
14
14
  }
15
15
  /**
16
16
  * Readiness is shared by every composer and dispatch path in this client
17
- * bundle. The initial probe starts on mount, and cached answers make sends
18
- * instant until the short readiness window expires.
17
+ * bundle. The status route resolves identity from the request, so its probe
18
+ * can overlap the hidden session preload instead of waiting for it to finish.
19
19
  */
20
20
  export declare function useAgentEngineConfigured(enabled?: boolean, options?: UseAgentEngineConfiguredOptions): UseAgentEngineConfiguredResult;
@@ -1,22 +1,16 @@
1
1
  import { useEffect, useReducer } from "react";
2
2
  import { ensureAgentEngineReadiness, getAgentEngineReadiness, subscribeAgentEngineReadiness, } from "./agent-engine-readiness.js";
3
- import { useSessionPreloading } from "./use-session.js";
4
3
  export { fetchAgentEngineConfiguredState, } from "./agent-engine-readiness.js";
5
4
  const RETRY_BASE_MS = 2000;
6
5
  const RETRY_MAX_MS = 30000;
7
6
  /**
8
7
  * Readiness is shared by every composer and dispatch path in this client
9
- * bundle. The initial probe starts on mount, and cached answers make sends
10
- * instant until the short readiness window expires.
8
+ * bundle. The status route resolves identity from the request, so its probe
9
+ * can overlap the hidden session preload instead of waiting for it to finish.
11
10
  */
12
11
  export function useAgentEngineConfigured(enabled = true, options) {
13
12
  const [, forceRender] = useReducer((revision) => revision + 1, 0);
14
- // A hinted load mounts the app before the session is known, and the status
15
- // probe answers for one account. It waits for the session as onboarding does.
16
- const preloading = useSessionPreloading();
17
13
  useEffect(() => {
18
- if (preloading)
19
- return;
20
14
  let cancelled = false;
21
15
  let retryTimer;
22
16
  let retryAttempt = 0;
@@ -69,7 +63,7 @@ export function useAgentEngineConfigured(enabled = true, options) {
69
63
  clearTimeout(retryTimer);
70
64
  document.removeEventListener("visibilitychange", onVisibilityChange);
71
65
  };
72
- }, [enabled, preloading, options?.source, options?.tabId, options?.threadId]);
66
+ }, [enabled, options?.source, options?.tabId, options?.threadId]);
73
67
  const effectiveState = enabled
74
68
  ? getAgentEngineReadiness(options?.source)
75
69
  : "configured";
@@ -4,6 +4,7 @@ import { setSentryUser, trackEvent, trackSessionStatus } from "./analytics.js";
4
4
  import { agentNativeApiDisabledReason } from "./api-surface.js";
5
5
  import { expireClientStatusResult, fetchAuthSessionStatus, invalidateClientStatusRequest, SESSION_RESULT_LIFETIME_MS, } from "./client-status-requests.js";
6
6
  import { getFrameOrigin, getFramePostMessageTargetOrigin } from "./frame.js";
7
+ import { recordSessionIdentity } from "./session-identity.js";
7
8
  export { hasSessionHint, isSessionNavigationPending, } from "../shared/ssr-session-bootstrap.js";
8
9
  /**
9
10
  * True while the app tree is mounted hidden ahead of the session read (a hinted
@@ -105,6 +106,7 @@ function hasFreshSessionCache() {
105
106
  function publishSessionIdentity(session) {
106
107
  const identity = session?.userId ?? session?.email ?? null;
107
108
  const authUserId = session?.authUserId;
109
+ recordSessionIdentity(identity);
108
110
  if (trackedSessionIdentity !== identity ||
109
111
  trackedSessionAuthUserId !== authUserId) {
110
112
  trackedSessionIdentity = identity;
@@ -38,19 +38,19 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
38
38
  error: string;
39
39
  states?: undefined;
40
40
  } | {
41
- error?: undefined;
42
41
  states: {
43
42
  clientId: number;
44
43
  state: string;
45
44
  }[];
45
+ error?: undefined;
46
46
  }>>;
47
47
  export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
48
48
  error: string;
49
49
  users?: undefined;
50
50
  } | {
51
- error?: undefined;
52
51
  users: {
53
52
  clientId: number;
54
53
  lastSeen: number;
55
54
  }[];
55
+ error?: undefined;
56
56
  }>>;
@@ -1,6 +1,6 @@
1
1
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
2
- error: string;
3
2
  ok?: undefined;
3
+ error: string;
4
4
  } | {
5
5
  error?: undefined;
6
6
  ok: boolean;