@agent-native/core 0.159.5 → 0.160.1

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 (81) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/server/lib/generation.ts +10 -6
  3. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
  4. package/corpus/templates/calendar/actions/create-event.ts +4 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
  6. package/corpus/templates/calendar/actions/update-event.ts +1 -0
  7. package/corpus/templates/calendar/agent-native.config.ts +1 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
  11. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
  12. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
  13. package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
  14. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
  15. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
  16. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
  17. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  18. package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
  19. package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
  20. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
  21. package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
  22. package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
  23. package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
  25. package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
  26. package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
  27. package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
  28. package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
  29. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
  30. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
  33. package/dist/agent/engine/builder-engine.d.ts +16 -2
  34. package/dist/agent/engine/builder-engine.js +178 -115
  35. package/dist/agent/engine/builtin.js +13 -4
  36. package/dist/agent/engine/credential-errors.d.ts +23 -0
  37. package/dist/agent/engine/credential-errors.js +19 -0
  38. package/dist/agent/engine/error-detail.d.ts +15 -0
  39. package/dist/agent/engine/error-detail.js +29 -0
  40. package/dist/agent/engine/registry.d.ts +14 -3
  41. package/dist/agent/engine/registry.js +106 -61
  42. package/dist/agent/engine/types.d.ts +16 -0
  43. package/dist/agent/engine/types.js +9 -0
  44. package/dist/agent/production-agent.d.ts +7 -0
  45. package/dist/agent/production-agent.js +68 -31
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/tool-approval-migrations.d.ts +1 -0
  49. package/dist/agent/tool-approval-migrations.js +10 -0
  50. package/dist/agent/tool-approval-store.d.ts +5 -3
  51. package/dist/agent/tool-approval-store.js +11 -6
  52. package/dist/agent/types.d.ts +9 -0
  53. package/dist/cli/code-agent-connector.js +79 -7
  54. package/dist/cli/portal-transfer.d.ts +43 -0
  55. package/dist/cli/portal-transfer.js +201 -0
  56. package/dist/client/agent-chat-adapter.js +46 -15
  57. package/dist/client/error-format.js +25 -2
  58. package/dist/client/sse-event-processor.d.ts +2 -0
  59. package/dist/client/sse-event-processor.js +12 -1
  60. package/dist/collab/awareness.d.ts +2 -2
  61. package/dist/collab/struct-routes.d.ts +1 -1
  62. package/dist/jobs/background-automation-runner.js +9 -15
  63. package/dist/notifications/routes.d.ts +6 -6
  64. package/dist/observability/routes.d.ts +5 -5
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/register-framework-secrets.js +25 -0
  67. package/dist/secrets/routes.d.ts +9 -9
  68. package/dist/server/agent-chat-plugin.js +9 -2
  69. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  70. package/dist/server/credential-provider.d.ts +80 -0
  71. package/dist/server/credential-provider.js +168 -1
  72. package/dist/server/google-realtime-session.d.ts +1 -1
  73. package/dist/server/google-realtime-session.js +6 -6
  74. package/dist/server/index.d.ts +1 -1
  75. package/dist/server/index.js +6 -1
  76. package/dist/server/realtime-token.d.ts +1 -1
  77. package/dist/server/realtime-voice.js +12 -6
  78. package/dist/server/transcribe-voice.d.ts +1 -1
  79. package/dist/server/transcribe-voice.js +18 -16
  80. package/dist/transcription/builder-transcription.js +8 -4
  81. package/package.json +3 -3
@@ -14,7 +14,7 @@ import { isMcpToolAllowedForRequest } from "../mcp-client/visibility.js";
14
14
  import { shouldInferSentimentForTurn } from "../observability/sentiment.js";
15
15
  import { completeRun as completeProgressRun, startRun as startProgressRun, updateRunProgress, } from "../progress/registry.js";
16
16
  import { isRuntimeVisibleScope, parseSkillFrontmatter, } from "../server/agent-chat/skill-frontmatter.js";
17
- import { canUseDeployCredentialFallbackForRequest, getProviderCredentialAuthFailure, readDeployCredentialEnv, } from "../server/credential-provider.js";
17
+ import { canUseDeployCredentialFallbackForRequest, getProviderCredentialAuthFailure, isBuilderGatewayDeployConfigured, readDeployCredentialEnv, } from "../server/credential-provider.js";
18
18
  import { readBody } from "../server/h3-helpers.js";
19
19
  import { resolveHostedHarnessPolicy } from "../server/hosted-harness-policy.js";
20
20
  import { assertRequestActionSurfaceIsolation, getRequestRunContext, ensureRequestRunContext, getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
@@ -27,8 +27,8 @@ import { buildSystemManifestSections, readContextXraySystemSections, } from "./c
27
27
  import { AGENT_CHAT_BACKGROUND_RUN_FIELD, AGENT_CHAT_PROCESS_RUN_PATH, backgroundRuntimeDiagnosticDetail, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, isAgentChatForegroundSelfChainEnabled, isInBackgroundFunctionRuntime, resolveAgentChatProcessRunDispatchPath, shouldUseBackgroundFunctionTimeoutForWorker, } from "./durable-background.js";
28
28
  import { applyContextXrayTransformForIteration } from "./engine/context-directives-transform.js";
29
29
  import { attemptContinuationDispatch } from "./engine/continuation-dispatch-retry.js";
30
- import { LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, userFacingLlmCredentialError, } from "./engine/credential-errors.js";
31
- import { isProviderConnectionErrorMessage } from "./engine/error-detail.js";
30
+ import { formatLlmCredentialErrorMessage, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, userFacingLlmCredentialError, } from "./engine/credential-errors.js";
31
+ import { isContextOverflowCode, isContextOverflowMessage, isProviderConnectionErrorMessage, } from "./engine/error-detail.js";
32
32
  import { resolveEngine, explicitEngineName, registerBuiltinEngines, getStoredModelForEngine, normalizeModelForEngine, isResolvedEngineUsableForRequest, } from "./engine/index.js";
33
33
  import { resolveEmptyResponseRetryMaxOutputTokens, resolveMainChatMaxOutputTokens, resolveMaxOutputTokensForEngine, } from "./engine/output-tokens.js";
34
34
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
@@ -840,20 +840,17 @@ function toolInputActivityLabel(toolName) {
840
840
  export function isContextTooLongError(err) {
841
841
  if (!(err instanceof Error))
842
842
  return false;
843
- const msg = err.message.toLowerCase();
844
- if (msg.includes("context_length_exceeded") ||
845
- msg.includes("input_too_long") ||
846
- msg.includes("too many tokens") ||
847
- msg.includes("prompt is too long") ||
848
- msg.includes("reduce the length") ||
849
- // Gemini phrasing
850
- msg.includes("input token count exceeds") ||
851
- msg.includes("request too large"))
843
+ // The engine's structural verdict comes first: it was read off the provider's
844
+ // own reply, which is not always the message delivered here — a
845
+ // Builder-credits deployment answers every gateway rejection with one visitor
846
+ // line, and the gateway reports an overflow as a plain `invalid_request_error`
847
+ // whose prose was the only signal.
848
+ if (err instanceof EngineError && err.contextOverflow === true)
849
+ return true;
850
+ if (isContextOverflowMessage(err.message))
851
+ return true;
852
+ if (err instanceof EngineError && isContextOverflowCode(err.errorCode)) {
852
853
  return true;
853
- if (err instanceof EngineError) {
854
- const code = (err.errorCode ?? "").toLowerCase();
855
- if (code.includes("context_length") || code.includes("input_too_long"))
856
- return true;
857
854
  }
858
855
  return false;
859
856
  }
@@ -1425,8 +1422,10 @@ export async function callConnectedAgentReference(input) {
1425
1422
  orgSecret: callerAuth.orgSecret,
1426
1423
  onUpdate: relay.observePollUpdate,
1427
1424
  });
1428
- const responseText = userFacingLlmCredentialError(response, { agentName: input.agent }) ??
1429
- response;
1425
+ const responseText = userFacingLlmCredentialError(response, {
1426
+ agentName: input.agent,
1427
+ visitorFacing: isBuilderGatewayDeployConfigured(),
1428
+ }) ?? response;
1430
1429
  relay.emitResponseText(responseText);
1431
1430
  relay.finish("done");
1432
1431
  return responseText;
@@ -1564,6 +1563,11 @@ export function isResumableEngineError(err) {
1564
1563
  const code = err instanceof EngineError ? (err.errorCode ?? "").toLowerCase() : "";
1565
1564
  if (code === "builder_gateway_timeout" ||
1566
1565
  code === "builder_gateway_network_error" ||
1566
+ // A gateway stream that stopped mid-turn. It reached here as
1567
+ // `builder_gateway_network_error` until the engine named it, and a
1568
+ // continuation is precisely its recovery: the partial turn is preserved and
1569
+ // the agent is nudged to finish it, where an in-call retry would `clear` it.
1570
+ code === "builder_gateway_stream_ended" ||
1567
1571
  code === "provider_network_error") {
1568
1572
  return true;
1569
1573
  }
@@ -1626,6 +1630,13 @@ export function continuationReasonForResumableError(err) {
1626
1630
  const code = err instanceof EngineError ? (err.errorCode ?? "").toLowerCase() : "";
1627
1631
  if (code === "builder_gateway_timeout")
1628
1632
  return "gateway_timeout";
1633
+ // A gateway/proxy timeout status, read structurally. The prose below says the
1634
+ // same thing on most lanes, but not on a Builder-credits deployment, where the
1635
+ // message is one visitor line.
1636
+ if (err instanceof EngineError &&
1637
+ (err.statusCode === 408 || err.statusCode === 504)) {
1638
+ return "gateway_timeout";
1639
+ }
1629
1640
  const text = err instanceof Error ? err.message.toLowerCase() : "";
1630
1641
  if (text.includes("gateway timeout") ||
1631
1642
  text.includes("upstream timeout") ||
@@ -3737,6 +3748,7 @@ export async function runAgentLoop(opts) {
3737
3748
  upgradeUrl: event.upgradeUrl,
3738
3749
  statusCode: event.statusCode,
3739
3750
  providerRetryable: event.providerRetryable,
3751
+ contextOverflow: event.contextOverflow,
3740
3752
  });
3741
3753
  }
3742
3754
  }
@@ -5132,7 +5144,8 @@ function backgroundChatProgressRunId(turnId) {
5132
5144
  .slice(0, 160);
5133
5145
  return `agent-chat-${normalized || "turn"}`;
5134
5146
  }
5135
- function isRecoverableContinuationError(event) {
5147
+ /** @internal exported for unit tests only */
5148
+ export function isRecoverableContinuationError(event) {
5136
5149
  const code = String(event.errorCode ?? "").toLowerCase();
5137
5150
  const message = event.error.toLowerCase();
5138
5151
  if (code === "builder_gateway_error")
@@ -5147,12 +5160,26 @@ function isRecoverableContinuationError(event) {
5147
5160
  return (event.recoverable === true ||
5148
5161
  code === "builder_gateway_timeout" ||
5149
5162
  code === "builder_gateway_network_error" ||
5163
+ // Server-driven background continuation for a truncated stream. The message
5164
+ // used to carry this ("stream ended without a stop event", classified into
5165
+ // `builder_gateway_network_error` above); the code carries it now, and on a
5166
+ // Builder-credits deployment the code is all that is left.
5167
+ code === "builder_gateway_stream_ended" ||
5150
5168
  code === "provider_network_error" ||
5151
5169
  code === "stale_run" ||
5152
5170
  code === "timeout" ||
5153
5171
  code === "timeout_error" ||
5154
5172
  code === "http_408" ||
5155
5173
  code === "http_429" ||
5174
+ // The 5xx family the message clauses below used to reach by prose alone
5175
+ // ("temporarily unavailable", "gateway timeout"). The client's own
5176
+ // continuation list (sse-event-processor) has always carried these codes;
5177
+ // the server read the sentence instead, which a Builder-credits deployment
5178
+ // replaces with one visitor line.
5179
+ code === "http_500" ||
5180
+ code === "http_502" ||
5181
+ code === "http_503" ||
5182
+ code === "http_504" ||
5156
5183
  code === "http_529" ||
5157
5184
  code === "run_timeout" ||
5158
5185
  message.includes("timeout") ||
@@ -6514,11 +6541,20 @@ export function createProductionAgentHandler(options) {
6514
6541
  setResponseHeader(event, "Cache-Control", "no-cache");
6515
6542
  setResponseHeader(event, "Connection", "keep-alive");
6516
6543
  const encoder = new TextEncoder();
6544
+ // A Builder-credits deployment serves this chat to visitors with no
6545
+ // account, so the owner-facing "connect a provider" copy is unactionable
6546
+ // for them. It is also what they see most of the time once a gateway auth
6547
+ // failure arms the 15-minute marker: the credits lane stops resolving,
6548
+ // engine selection falls back to the anthropic default, and this branch —
6549
+ // not the engine's own error — is what answers the turn.
6550
+ const missingCredentialsError = formatLlmCredentialErrorMessage({
6551
+ visitorFacing: isBuilderGatewayDeployConfigured(),
6552
+ });
6517
6553
  return new ReadableStream({
6518
6554
  start(controller) {
6519
6555
  controller.enqueue(encoder.encode(`data: ${JSON.stringify({
6520
6556
  type: "error",
6521
- error: LLM_MISSING_CREDENTIALS_MESSAGE,
6557
+ error: missingCredentialsError,
6522
6558
  errorCode: LLM_MISSING_CREDENTIALS_ERROR_CODE,
6523
6559
  })}\n\n`));
6524
6560
  controller.close();
@@ -6887,6 +6923,11 @@ export function createProductionAgentHandler(options) {
6887
6923
  .filter((key) => typeof key === "string")
6888
6924
  .slice(0, 200)
6889
6925
  : undefined;
6926
+ // The durable approval row is the authorization boundary. Do not require
6927
+ // the client to reproduce the original structured history exactly: the UI
6928
+ // may truncate tool arguments and intentionally assigns fresh replay ids.
6929
+ // The loop still consumes only a matching server-created grant for the
6930
+ // current owner/org/thread/turn/tool/input tuple.
6890
6931
  const exactApprovedToolCall = findApprovedStructuredToolCall(structuredHistory, requestedApprovedToolCalls);
6891
6932
  const firstRequestPayloadDetail = buildFirstRequestPayloadDetail({
6892
6933
  isFirstRequest: history.length === 0,
@@ -6965,14 +7006,7 @@ export function createProductionAgentHandler(options) {
6965
7006
  return consumeAgentToolApproval(approvalStoreBinding(binding));
6966
7007
  },
6967
7008
  };
6968
- const exactApprovedToolEntry = exactApprovedToolCall
6969
- ? requestActions[exactApprovedToolCall.name]
6970
- : undefined;
6971
- const approvedToolCallsForExecution = exactApprovedToolCall && exactApprovedToolEntry?.needsApproval
6972
- ? [
6973
- toolCallCacheKey(exactApprovedToolCall.name, exactApprovedToolCall.input),
6974
- ]
6975
- : undefined;
7009
+ const approvedToolCallsForExecution = requestedApprovedToolCalls;
6976
7010
  if (isBackgroundWorker &&
6977
7011
  (await isTurnAborted(effectiveThreadId, effectiveTurnId))) {
6978
7012
  await markRunAborted(runId, "user").catch(() => { });
@@ -7630,6 +7664,7 @@ export function createProductionAgentHandler(options) {
7630
7664
  });
7631
7665
  const message = userFacingLlmCredentialError(err, {
7632
7666
  agentName: ref.name,
7667
+ visitorFacing: isBuilderGatewayDeployConfigured(),
7633
7668
  }) ?? `Failed to run ${ref.name}: ${err?.message}`;
7634
7669
  return `<agent-response name="${ref.name}" id="${ref.refId}" type="custom-agent" error="true">\n${message}\n</agent-response>`;
7635
7670
  }
@@ -7670,6 +7705,7 @@ export function createProductionAgentHandler(options) {
7670
7705
  catch (err) {
7671
7706
  const message = userFacingLlmCredentialError(err, {
7672
7707
  agentName: ref.name,
7708
+ visitorFacing: isBuilderGatewayDeployConfigured(),
7673
7709
  }) ?? `Failed to reach ${ref.name}: ${err?.message}`;
7674
7710
  return `<agent-response name="${ref.name}" id="${ref.refId}" error="true">\n${message}\n</agent-response>`;
7675
7711
  }
@@ -7750,9 +7786,10 @@ export function createProductionAgentHandler(options) {
7750
7786
  ...(threadId
7751
7787
  ? { threadId: effectiveThreadId, turnId: effectiveTurnId }
7752
7788
  : {}),
7753
- // Human-in-the-loop approval grants for this turn (sanitized the
7754
- // request is untrusted; only the exact structured call is passed to
7755
- // the loop, where the durable grant is consumed atomically.
7789
+ // Human-in-the-loop approval grants for this turn. The request is
7790
+ // untrusted; the durable approval consumer below validates every key
7791
+ // against the authenticated owner/org/thread/turn and consumes it
7792
+ // atomically for the exact tool/input tuple.
7756
7793
  ...(approvedToolCallsForExecution
7757
7794
  ? { approvedToolCalls: approvedToolCallsForExecution }
7758
7795
  : {}),
@@ -323,6 +323,10 @@ function shouldCaptureRunError(err) {
323
323
  const normalizedCode = errorCode.toLowerCase();
324
324
  return (!normalizedCode.startsWith("credits-limit") &&
325
325
  normalizedCode !== "builder_gateway_network_error" &&
326
+ // A truncated gateway stream, which the client continues. It arrived here as
327
+ // `builder_gateway_network_error` until the engine gave it its own code, so
328
+ // omitting it would turn a routine recovered interruption into Sentry noise.
329
+ normalizedCode !== "builder_gateway_stream_ended" &&
326
330
  normalizedCode !== PROVIDER_NETWORK_ERROR_CODE &&
327
331
  normalizedCode !== "provider_rate_limited" &&
328
332
  normalizedCode !== "rate_limit_exceeded");
@@ -1173,6 +1177,32 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1173
1177
  ...(err instanceof EngineError && err.upgradeUrl
1174
1178
  ? { upgradeUrl: err.upgradeUrl }
1175
1179
  : {}),
1180
+ // The engine's own retry verdict, carried structurally. The client
1181
+ // decides auto-continue from this, `recoverable` and the error code, and
1182
+ // a deployment whose visitor copy replaces the message (Builder credits)
1183
+ // leaves it nothing else to read: without this a provider throttle the
1184
+ // engine marked retryable ends the turn on those sites only.
1185
+ //
1186
+ // NOT `recoverable`. That field is the server's own "this run stopped at
1187
+ // an internal continuation boundary" signal, read by
1188
+ // `isInternalContinuationError` (thread-data-builder) to drop the error
1189
+ // from the persisted turn and by `isRecoverableContinuationError`
1190
+ // (production-agent) to self-chain the next background chunk. Neither
1191
+ // lists `rate_limited` or `too_many_concurrent_requests` today, so
1192
+ // feeding them from the engine verdict would newly chain up to
1193
+ // MAX_BACKGROUND_RUN_CONTINUATIONS invocations into a live throttle — on
1194
+ // every lane, not just Builder credits.
1195
+ ...(err instanceof EngineError && err.providerRetryable === true
1196
+ ? { providerRetryable: true }
1197
+ : {}),
1198
+ // Same reasoning as `providerRetryable`: on the credits lane the message
1199
+ // is the visitor line and the code is `invalid_request_error`, so this
1200
+ // flag is the only thing left that says "trim and retry once". Dropping
1201
+ // it here turns a recoverable overflow into a terminal failure on exactly
1202
+ // the sites that cannot read the real reason.
1203
+ ...(err instanceof EngineError && err.contextOverflow === true
1204
+ ? { contextOverflow: true }
1205
+ : {}),
1176
1206
  });
1177
1207
  })
1178
1208
  .finally(async () => {
@@ -10,6 +10,10 @@ function isInternalContinuationError(event) {
10
10
  return false;
11
11
  return (event.recoverable === true ||
12
12
  code === "builder_gateway_timeout" ||
13
+ // Carries what `msg.includes("stream ended")` below used to: a
14
+ // Builder-credits deployment replaces that sentence with one visitor line,
15
+ // and the code is the only thing left that says the turn was truncated.
16
+ code === "builder_gateway_stream_ended" ||
13
17
  code === "stale_run" ||
14
18
  code === "timeout" ||
15
19
  code === "timeout_error" ||
@@ -33,6 +33,7 @@ export declare const AGENT_TOOL_APPROVAL_TABLE_SQL: {
33
33
  )`;
34
34
  };
35
35
  export declare const AGENT_TOOL_APPROVAL_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_binding\n ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)";
36
+ export declare const AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL = "CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_logical\n ON agent_tool_approvals(owner_email, org_id, thread_id, turn_id, tool_name, approval_key_hash, status)";
36
37
  /**
37
38
  * Durable approval grants are created and consumed on request paths, but their
38
39
  * schema belongs to the release migration boundary in production.
@@ -33,6 +33,8 @@ export const AGENT_TOOL_APPROVAL_TABLE_SQL = {
33
33
  };
34
34
  export const AGENT_TOOL_APPROVAL_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_binding
35
35
  ON agent_tool_approvals(owner_email, org_id, thread_id, tool_name, call_id, status)`;
36
+ export const AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_agent_tool_approvals_logical
37
+ ON agent_tool_approvals(owner_email, org_id, thread_id, turn_id, tool_name, approval_key_hash, status)`;
36
38
  /**
37
39
  * Durable approval grants are created and consumed on request paths, but their
38
40
  * schema belongs to the release migration boundary in production.
@@ -48,4 +50,12 @@ ${AGENT_TOOL_APPROVAL_INDEX_SQL}`,
48
50
  ${AGENT_TOOL_APPROVAL_INDEX_SQL}`,
49
51
  },
50
52
  },
53
+ {
54
+ version: 2,
55
+ name: "agent-tool-approvals-logical-binding-index",
56
+ sql: {
57
+ postgres: AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL,
58
+ sqlite: AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL,
59
+ },
60
+ },
51
61
  ];
@@ -11,8 +11,10 @@ export interface AgentToolApprovalBinding {
11
11
  export declare function hashAgentToolApprovalKey(approvalKey: string): string;
12
12
  export declare function createAgentToolApproval(binding: AgentToolApprovalBinding): Promise<void>;
13
13
  /**
14
- * Atomically consume the server-created approval for one exact tool call.
15
- * Client history and approval keys are only lookup input; the pending row is
16
- * the authorization boundary and cannot be manufactured by the client.
14
+ * Atomically consume the server-created approval for one logical tool call.
15
+ * The model's call id is transport metadata and can change when Dispatch
16
+ * reconstructs a paused turn, so it is deliberately not part of authorization.
17
+ * The pending row remains the boundary: client history and approval keys alone
18
+ * cannot manufacture a grant.
17
19
  */
18
20
  export declare function consumeAgentToolApproval(binding: AgentToolApprovalBinding): Promise<boolean>;
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { getDbExec, isPostgres, retryOnDdlRace } from "../db/client.js";
3
3
  import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
4
- import { AGENT_TOOL_APPROVAL_INDEX_SQL, AGENT_TOOL_APPROVAL_TABLE_SQL, } from "./tool-approval-migrations.js";
4
+ import { AGENT_TOOL_APPROVAL_INDEX_SQL, AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL, AGENT_TOOL_APPROVAL_TABLE_SQL, } from "./tool-approval-migrations.js";
5
5
  const APPROVAL_TTL_MS = 15 * 60_000;
6
6
  const APPROVAL_CLEANUP_AGE_MS = 24 * 60 * 60_000;
7
7
  let initPromise;
@@ -12,11 +12,13 @@ async function ensureAgentToolApprovalTable() {
12
12
  if (isPostgres()) {
13
13
  await ensureTableExists("agent_tool_approvals", createSql);
14
14
  await ensureIndexExists("idx_agent_tool_approvals_binding", AGENT_TOOL_APPROVAL_INDEX_SQL);
15
+ await ensureIndexExists("idx_agent_tool_approvals_logical", AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL);
15
16
  return;
16
17
  }
17
18
  const client = getDbExec();
18
19
  await retryOnDdlRace(() => client.execute(createSql));
19
20
  await retryOnDdlRace(() => client.execute(AGENT_TOOL_APPROVAL_INDEX_SQL));
21
+ await retryOnDdlRace(() => client.execute(AGENT_TOOL_APPROVAL_LOGICAL_INDEX_SQL));
20
22
  })().catch((error) => {
21
23
  initPromise = undefined;
22
24
  throw error;
@@ -65,9 +67,11 @@ export async function createAgentToolApproval(binding) {
65
67
  }
66
68
  }
67
69
  /**
68
- * Atomically consume the server-created approval for one exact tool call.
69
- * Client history and approval keys are only lookup input; the pending row is
70
- * the authorization boundary and cannot be manufactured by the client.
70
+ * Atomically consume the server-created approval for one logical tool call.
71
+ * The model's call id is transport metadata and can change when Dispatch
72
+ * reconstructs a paused turn, so it is deliberately not part of authorization.
73
+ * The pending row remains the boundary: client history and approval keys alone
74
+ * cannot manufacture a grant.
71
75
  */
72
76
  export async function consumeAgentToolApproval(binding) {
73
77
  await ensureAgentToolApprovalTable();
@@ -80,8 +84,8 @@ export async function consumeAgentToolApproval(binding) {
80
84
  WHERE owner_email = ?
81
85
  AND ((org_id IS NULL AND CAST(? AS TEXT) IS NULL) OR org_id = ?)
82
86
  AND ((thread_id IS NULL AND CAST(? AS TEXT) IS NULL) OR thread_id = ?)
87
+ AND ((turn_id IS NULL AND CAST(? AS TEXT) IS NULL) OR turn_id = ?)
83
88
  AND tool_name = ?
84
- AND call_id = ?
85
89
  AND approval_key_hash = ?
86
90
  AND status = 'pending'
87
91
  AND expires_at > ?
@@ -97,8 +101,9 @@ export async function consumeAgentToolApproval(binding) {
97
101
  binding.orgId ?? null,
98
102
  binding.threadId ?? null,
99
103
  binding.threadId ?? null,
104
+ binding.turnId ?? null,
105
+ binding.turnId ?? null,
100
106
  binding.toolName,
101
- binding.callId,
102
107
  hashAgentToolApprovalKey(binding.approvalKey),
103
108
  now,
104
109
  ],
@@ -375,6 +375,15 @@ export type AgentChatEvent = {
375
375
  details?: string;
376
376
  /** True when the user can reasonably continue/retry from partial work. */
377
377
  recoverable?: boolean;
378
+ /**
379
+ * The engine's own verdict that another attempt at the same request may
380
+ * succeed (`EngineError.providerRetryable`). Distinct from `recoverable`,
381
+ * which the server's continuation classifiers read as "this run ended at
382
+ * an internal boundary, fold it and chain the next chunk". A provider
383
+ * throttle is retryable without being a boundary — conflating them makes
384
+ * a rate limit self-chain background continuations.
385
+ */
386
+ providerRetryable?: boolean;
378
387
  }
379
388
  /**
380
389
  * Legacy SSE terminal event. New streams emit
@@ -6,6 +6,7 @@ import { assertValidComputerCommandEnvelope } from "../integrations/computer-sup
6
6
  import { serializeBoundedRemoteJson } from "../integrations/remote-json-safety.js";
7
7
  import { executeDenyCodeAgentApproval, executePendingCodeAgentApproval, } from "./code-agent-executor.js";
8
8
  import { appendCodeAgentTranscriptEvent, codeAgentRunTranscriptPath, codeAgentStoreRoot, createCodeAgentRunRecord, getCodeAgentRunRecord, listCodeAgentRunRecords, normalizeCodeAgentPermissionMode, queueCodeAgentFollowUp, updateCodeAgentRunRecord, } from "./code-agent-runs.js";
9
+ import { appendPortalTransferTranscript, parsePortalTransferContext, } from "./portal-transfer.js";
9
10
  import { loadPortalEnvironment, preparePortalWorkspace, parsePortalHandoff, } from "./portal-workspace.js";
10
11
  const DEVICE_PATH_ENV = "AGENT_NATIVE_REMOTE_DEVICE_PATH";
11
12
  const COMPUTER_BRIDGE_URL_ENV = "AGENT_NATIVE_COMPUTER_BRIDGE_URL";
@@ -203,6 +204,16 @@ class RemoteCodeAgentConnector {
203
204
  const metadata = isObject(command.params.metadata)
204
205
  ? command.params.metadata
205
206
  : {};
207
+ let portalTransfer = null;
208
+ try {
209
+ portalTransfer = parsePortalTransferContext(metadata.portalTransfer);
210
+ }
211
+ catch (error) {
212
+ return {
213
+ ok: false,
214
+ error: error instanceof Error ? error.message : String(error),
215
+ };
216
+ }
206
217
  let cwd = resolveCommandCwd(command.params.cwd ?? this.config.workspacePath);
207
218
  let portalWorkspace;
208
219
  let portalEnvironment;
@@ -226,6 +237,12 @@ class RemoteCodeAgentConnector {
226
237
  if (portalWorkspace && requestedRunId && !isSafeRunId(requestedRunId)) {
227
238
  return { ok: false, error: "Portal run id is invalid." };
228
239
  }
240
+ if (portalTransfer && (!portalWorkspace || !requestedRunId)) {
241
+ return {
242
+ ok: false,
243
+ error: "Portal transfer context requires a Portal run id and workspace.",
244
+ };
245
+ }
229
246
  if (portalWorkspace && requestedRunId) {
230
247
  const existing = getCodeAgentRunRecord(requestedRunId);
231
248
  const existingRemote = isObject(existing?.metadata?.remote)
@@ -240,12 +257,28 @@ class RemoteCodeAgentConnector {
240
257
  }
241
258
  return { ok: true, runId: existing.id, run: existing, resumed: true };
242
259
  }
260
+ const existingTransfer = isObject(existing?.metadata?.portalTransfer)
261
+ ? existing.metadata.portalTransfer
262
+ : undefined;
263
+ if (portalTransfer &&
264
+ existing &&
265
+ firstStringValue(existingTransfer?.sourceRunId) ===
266
+ portalTransfer.sourceRunId) {
267
+ this.remoteRunIds.add(existing.id);
268
+ this.transcriptCursors.set(existing.id, { offset: 0, seq: 0 });
269
+ if (!activeRunners.has(existing.id)) {
270
+ this.spawnRunner(existing.id, existing.cwd, existing.permissionMode, portalEnvironment?.values);
271
+ }
272
+ return { ok: true, runId: existing.id, run: existing, resumed: true };
273
+ }
243
274
  }
244
275
  const permissionMode = normalizeCodeAgentPermissionMode(command.params.permissionMode) ??
245
276
  "full-auto";
246
277
  const engine = firstStringValue(command.params.engine);
247
278
  const model = firstStringValue(command.params.model);
248
279
  const effort = firstStringValue(command.params.effort, command.params.reasoningEffort);
280
+ const metadataWithoutTransfer = { ...metadata };
281
+ delete metadataWithoutTransfer.portalTransfer;
249
282
  const run = createCodeAgentRunRecord({
250
283
  ...(portalWorkspace && requestedRunId ? { id: requestedRunId } : {}),
251
284
  goalId,
@@ -267,6 +300,14 @@ class RemoteCodeAgentConnector {
267
300
  { label: "Prompt", value: truncateForDisplay(prompt, 160) },
268
301
  { label: "Agent", value: "Remote connector" },
269
302
  { label: "Mode", value: permissionMode },
303
+ ...(portalTransfer
304
+ ? [
305
+ {
306
+ label: "Context",
307
+ value: `Imported ${portalTransfer.events.length} transcript events`,
308
+ },
309
+ ]
310
+ : []),
270
311
  ...(portalWorkspace
271
312
  ? [
272
313
  {
@@ -283,7 +324,7 @@ class RemoteCodeAgentConnector {
283
324
  : []),
284
325
  ],
285
326
  metadata: {
286
- ...metadata,
327
+ ...metadataWithoutTransfer,
287
328
  prompt,
288
329
  source: "remote-connector",
289
330
  engine,
@@ -313,14 +354,45 @@ class RemoteCodeAgentConnector {
313
354
  },
314
355
  }
315
356
  : {}),
357
+ ...(portalTransfer
358
+ ? {
359
+ portalTransfer: {
360
+ schemaVersion: 1,
361
+ sourceRunId: portalTransfer.sourceRunId,
362
+ ...(portalTransfer.sourceStatus
363
+ ? { sourceStatus: portalTransfer.sourceStatus }
364
+ : {}),
365
+ ...(portalTransfer.sourcePhase
366
+ ? { sourcePhase: portalTransfer.sourcePhase }
367
+ : {}),
368
+ eventCount: portalTransfer.events.length,
369
+ transferredAt: new Date().toISOString(),
370
+ },
371
+ }
372
+ : {}),
316
373
  },
317
374
  });
318
- appendCodeAgentTranscriptEvent({
319
- runId: run.id,
320
- kind: "user",
321
- message: prompt,
322
- metadata: { source: "remote-initial-prompt", commandId: command.id },
323
- });
375
+ if (portalTransfer) {
376
+ appendPortalTransferTranscript(portalTransfer, run.id);
377
+ appendCodeAgentTranscriptEvent({
378
+ runId: run.id,
379
+ kind: "user",
380
+ message: prompt,
381
+ metadata: {
382
+ source: "portal-transfer-continuation",
383
+ commandId: command.id,
384
+ sourceRunId: portalTransfer.sourceRunId,
385
+ },
386
+ });
387
+ }
388
+ else {
389
+ appendCodeAgentTranscriptEvent({
390
+ runId: run.id,
391
+ kind: "user",
392
+ message: prompt,
393
+ metadata: { source: "remote-initial-prompt", commandId: command.id },
394
+ });
395
+ }
324
396
  appendCodeAgentTranscriptEvent({
325
397
  runId: run.id,
326
398
  kind: "status",
@@ -0,0 +1,43 @@
1
+ export declare const PORTAL_TRANSFER_SCHEMA_VERSION: 1;
2
+ export declare const PORTAL_TRANSFER_MAX_CONTEXT_BYTES = 900000;
3
+ export type PortalTransferEventKind = "user" | "system" | "note" | "artifact" | "status";
4
+ export interface PortalTransferTranscriptEvent {
5
+ schemaVersion: 1;
6
+ id: string;
7
+ kind: PortalTransferEventKind;
8
+ message: string;
9
+ createdAt: string;
10
+ metadata?: Record<string, unknown>;
11
+ signal?: "credential-gap";
12
+ }
13
+ export interface PortalTransferContext {
14
+ schemaVersion: 1;
15
+ sourceRunId: string;
16
+ sourceStatus?: string;
17
+ sourcePhase?: string;
18
+ events: PortalTransferTranscriptEvent[];
19
+ }
20
+ export interface PortalTransferSourceEvent {
21
+ id?: unknown;
22
+ runId?: unknown;
23
+ kind?: unknown;
24
+ type?: unknown;
25
+ message?: unknown;
26
+ text?: unknown;
27
+ createdAt?: unknown;
28
+ metadata?: unknown;
29
+ signal?: unknown;
30
+ }
31
+ export declare function createPortalTransferContext(input: {
32
+ sourceRunId: string;
33
+ sourceStatus?: string;
34
+ sourcePhase?: string;
35
+ events: readonly PortalTransferSourceEvent[];
36
+ }): PortalTransferContext;
37
+ export declare function parsePortalTransferContext(value: unknown): PortalTransferContext | null;
38
+ export declare function appendPortalTransferTranscript(context: PortalTransferContext, runId: string): number;
39
+ export declare function portalTransferContinuationPrompt(input: {
40
+ hostLabel: string;
41
+ handoffId: string;
42
+ eventCount: number;
43
+ }): string;