@agent-native/core 0.225.0 → 0.225.2-nightly-20261106152255

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.
@@ -571,6 +571,8 @@ export async function sendAgentHarnessFollowUp(input) {
571
571
  let providerDispatchStarted = false;
572
572
  const hasFollowUpProviderDispatchStarted = () => providerDispatchStarted;
573
573
  let activeRun;
574
+ let finalizedRejected = false;
575
+ const hasFinalizedRejection = () => finalizedRejected;
574
576
  try {
575
577
  const dispatchIntent = await markAgentHarnessSessionFollowUpDispatched(session.id, claimed.generation, claimId);
576
578
  if (!dispatchIntent) {
@@ -801,7 +803,10 @@ export async function sendAgentHarnessFollowUp(input) {
801
803
  rejectFirstDispatch(runFailure ??
802
804
  new Error("The AgentHarness follow-up run ended before provider dispatch."));
803
805
  }
804
- }, rejectFirstDispatch);
806
+ }, (error) => {
807
+ finalizedRejected = true;
808
+ rejectFirstDispatch(error);
809
+ });
805
810
  if (followUpAbort.signal.aborted)
806
811
  abortActiveRun();
807
812
  return firstDispatch;
@@ -819,7 +824,13 @@ export async function sendAgentHarnessFollowUp(input) {
819
824
  refreshLiveAgentHarnessSessionGeneration(session.id, acquired.live.session, dispatch.session.generation);
820
825
  if (isAbortRequested(followUpAbort.signal))
821
826
  abortActiveRun();
822
- await activeRun.finalized;
827
+ try {
828
+ await activeRun.finalized;
829
+ }
830
+ catch (error) {
831
+ finalizedRejected = true;
832
+ throw error;
833
+ }
823
834
  }
824
835
  finally {
825
836
  followUpAbort.signal.removeEventListener("abort", abortActiveRun);
@@ -994,13 +1005,22 @@ export async function sendAgentHarnessFollowUp(input) {
994
1005
  if (isAbortRequested(followUpAbort.signal)) {
995
1006
  const reason = followUpAbort.signal.reason;
996
1007
  if (isAgentHarnessSessionConflictError(reason)) {
997
- const latest = await getAgentHarnessSessionByRunId(input.runId);
998
1008
  const cleanupErrors = [];
999
- if (!latest ||
1000
- (latest.status === "idle" && latest.followUpClaimId == null) ||
1001
- latest.status === "stopped" ||
1002
- latest.status === "errored" ||
1003
- latest.status === "destroyed") {
1009
+ let lookupFailed = false;
1010
+ let latest;
1011
+ try {
1012
+ latest = await getAgentHarnessSessionByRunId(input.runId);
1013
+ }
1014
+ catch (lookupError) {
1015
+ lookupFailed = true;
1016
+ cleanupErrors.push(lookupError);
1017
+ }
1018
+ if (latest !== undefined &&
1019
+ (!latest ||
1020
+ (latest.status === "idle" && latest.followUpClaimId == null) ||
1021
+ latest.status === "stopped" ||
1022
+ latest.status === "errored" ||
1023
+ latest.status === "destroyed")) {
1004
1024
  try {
1005
1025
  await stopAcquiredHarnessSession(session.id, acquired.live.session);
1006
1026
  }
@@ -1016,12 +1036,26 @@ export async function sendAgentHarnessFollowUp(input) {
1016
1036
  cleanupErrors.push(closeError);
1017
1037
  }
1018
1038
  }
1039
+ if (lookupFailed &&
1040
+ !hasFinalizedRejection() &&
1041
+ cleanupErrors.length === 1) {
1042
+ throw cleanupErrors[0];
1043
+ }
1019
1044
  if (cleanupErrors.length > 0) {
1020
- throw new AggregateError([reason, ...cleanupErrors], "Harness follow-up was interrupted and stream cleanup was incomplete.");
1045
+ throw new AggregateError([
1046
+ reason,
1047
+ ...(hasFinalizedRejection() ? [error] : []),
1048
+ ...cleanupErrors,
1049
+ ], "Harness follow-up was interrupted and stream cleanup was incomplete.");
1021
1050
  }
1051
+ if (hasFinalizedRejection())
1052
+ throw error;
1022
1053
  return failure(input.runId, latest ?? session, "unavailable", "The harness follow-up was interrupted because its session claim changed or stopped.");
1023
1054
  }
1024
- const failures = [reason ?? error];
1055
+ const failures = [
1056
+ reason ?? error,
1057
+ ...(hasFinalizedRejection() ? [error] : []),
1058
+ ];
1025
1059
  let failedSession = null;
1026
1060
  try {
1027
1061
  failedSession = await markOwnedFollowUpErrored(session.id, claimId, acquired.live.session, hasFollowUpProviderDispatchStarted() ? "unknown" : "not_dispatched");
@@ -1046,7 +1080,9 @@ export async function sendAgentHarnessFollowUp(input) {
1046
1080
  }
1047
1081
  }
1048
1082
  if (failures.length > 1) {
1049
- throw new AggregateError(failures, "Harness follow-up claim monitoring failed and cleanup was incomplete.");
1083
+ throw new AggregateError(failures, hasFinalizedRejection()
1084
+ ? "Harness follow-up claim monitoring failed and finalization or cleanup was incomplete."
1085
+ : "Harness follow-up claim monitoring failed and cleanup was incomplete.");
1050
1086
  }
1051
1087
  const latest = failedSession ?? (await getAgentHarnessSessionByRunId(input.runId));
1052
1088
  return failure(input.runId, latest ?? session, "unavailable", "The harness follow-up was stopped because its session claim could not be verified.");
@@ -83,14 +83,14 @@ function resolveObjectSchemaBranch(schema, value) {
83
83
  const index = discriminatedBranchIndex(branches, value);
84
84
  return index >= 0 ? branches[index] : undefined;
85
85
  }
86
- function stripOptionalToolPlaceholders(schema, value, emptyValues) {
86
+ function stripOptionalToolPlaceholders(schema, value, emptyValues, depth = 0) {
87
87
  if (!schema)
88
88
  return { value, changed: false };
89
89
  if (Array.isArray(value)) {
90
90
  const itemSchema = leafBranches(schema).find((branch) => branch.items)?.items;
91
91
  if (!itemSchema)
92
92
  return { value, changed: false };
93
- const results = value.map((item) => stripOptionalToolPlaceholders(itemSchema, item, emptyValues));
93
+ const results = value.map((item) => stripOptionalToolPlaceholders(itemSchema, item, emptyValues, depth + 1));
94
94
  return results.some((result) => result.changed)
95
95
  ? { value: results.map((result) => result.value), changed: true }
96
96
  : { value, changed: false };
@@ -119,6 +119,14 @@ function stripOptionalToolPlaceholders(schema, value, emptyValues) {
119
119
  }
120
120
  continue;
121
121
  }
122
+ // Top-level only: a nested null may be a deliberate clear.
123
+ if (emptyValues.stripNull &&
124
+ depth === 0 &&
125
+ entry === null &&
126
+ !required.has(key)) {
127
+ placeholders.push(key);
128
+ continue;
129
+ }
122
130
  if (!required.has(key) && isStructurallyEmptyToolValue(entry)) {
123
131
  if (schemaRejectsToolValue(propertySchema, entry)) {
124
132
  if (emptyValues.nullOnly && entry !== null)
@@ -128,9 +136,12 @@ function stripOptionalToolPlaceholders(schema, value, emptyValues) {
128
136
  continue;
129
137
  }
130
138
  // A valid `false` sets a value, and `[]` on an action that says an empty
131
- // list erases nothing is padding. Elsewhere `[]` could wipe a stored
139
+ // list erases nothing is padding. `null` on an optional field is absent:
140
+ // models fill every unused field with it, so it never marks a call as
141
+ // clearing data. Elsewhere `[]` could wipe a stored
132
142
  // list. An empty equal to the schema's own default changes nothing.
133
143
  if (entry !== false &&
144
+ entry !== null &&
134
145
  !(emptyValues.listsEraseNothing &&
135
146
  Array.isArray(entry) &&
136
147
  entry.length === 0) &&
@@ -139,7 +150,7 @@ function stripOptionalToolPlaceholders(schema, value, emptyValues) {
139
150
  JSON.stringify(propertySchema.default) === JSON.stringify(entry)))
140
151
  emptyValues.valid = true;
141
152
  }
142
- const nested = stripOptionalToolPlaceholders(propertySchema, entry, emptyValues);
153
+ const nested = stripOptionalToolPlaceholders(propertySchema, entry, emptyValues, depth + 1);
143
154
  if (nested.changed) {
144
155
  normalized ??= { ...record };
145
156
  normalized[key] = nested.value;
@@ -175,7 +186,15 @@ export function normalizeOptionalToolPlaceholders(schema, input, entry, { nullOn
175
186
  nullOnly,
176
187
  listsEraseNothing: entry.emptyListsEraseNothing === true,
177
188
  };
178
- const result = stripOptionalToolPlaceholders(schema, input, emptyValues);
189
+ let result = stripOptionalToolPlaceholders(schema, input, emptyValues);
190
+ // A padded call means its nulls are padding too, including on nullable
191
+ // fields where a lone null would be a deliberate clear.
192
+ if (emptyValues.invalid && !emptyValues.valid) {
193
+ result = stripOptionalToolPlaceholders(schema, input, {
194
+ ...emptyValues,
195
+ stripNull: true,
196
+ });
197
+ }
179
198
  // Only a declared read is exempt: a dynamic plan-mode effect classifies the
180
199
  // raw input, which defaults can turn into a write.
181
200
  if (emptyValues.valid &&
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
- import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY, MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS, parseQueueMessageInput, persistableFilePart, fingerprintAgentSubmissionAttachment, submissionAttachmentsMatch, } from "@agent-native/agentkit/protocol";
3
+ import { AGENT_NATIVE_SUBMISSION_EXPIRES_AT_METADATA_KEY, AGENT_NATIVE_SUBMISSION_TTL_MS_METADATA_KEY, MAX_AGENT_REQUEST_ATTACHMENT_DATA_CHARS, parseQueueMessageInput, persistableFilePart, fingerprintAgentSubmissionAttachment, isPersistableAttachmentUrl, submissionAttachmentsMatch, } from "@agent-native/agentkit/protocol";
4
4
  import Ajv from "ajv";
5
5
  import Ajv2020 from "ajv/dist/2020.js";
6
6
  import { defineEventHandler, getHeader, setResponseHeader, setResponseStatus, getMethod, } from "h3";
@@ -1412,23 +1412,6 @@ export { DurableAttachmentReferenceRequiredError };
1412
1412
  function isDataUrlReference(value) {
1413
1413
  return typeof value === "string" && /^\s*data:/i.test(value);
1414
1414
  }
1415
- function hasDurableAttachmentUrl(value) {
1416
- if (typeof value !== "string")
1417
- return false;
1418
- const normalized = value.trim();
1419
- if (!normalized ||
1420
- isDataUrlReference(normalized) ||
1421
- isRawBase64Payload(normalized) ||
1422
- !URL.canParse(normalized)) {
1423
- return false;
1424
- }
1425
- const url = new URL(normalized);
1426
- return (url.protocol === "https:" &&
1427
- !url.username &&
1428
- !url.password &&
1429
- !url.search &&
1430
- !url.hash);
1431
- }
1432
1415
  const OMIT_DURABLE_DISPATCH_VALUE = Symbol("omit-durable-dispatch-value");
1433
1416
  const DURABLE_ATTACHMENT_PAYLOAD_FIELDS = /^(?:base64|bytes|body|data|dataurl|image|payload)$/i;
1434
1417
  const DURABLE_ATTACHMENT_REFERENCE_FIELDS = /^(?:preview|referenceUrl|src|thumbnail|url)$/i;
@@ -1516,7 +1499,7 @@ function sanitizeDurableAttachment(value, attachmentContext = false, requiredAtt
1516
1499
  (attachmentReferenceField &&
1517
1500
  typeof value === "string" &&
1518
1501
  (isInlineBase64Payload(value) ||
1519
- (value.trim().length > 0 && !hasDurableAttachmentUrl(value)))))) {
1502
+ (value.trim().length > 0 && !isPersistableAttachmentUrl(value)))))) {
1520
1503
  return OMIT_DURABLE_DISPATCH_VALUE;
1521
1504
  }
1522
1505
  if (Array.isArray(value)) {
@@ -1537,12 +1520,12 @@ function sanitizeDurableAttachment(value, attachmentContext = false, requiredAtt
1537
1520
  const isAttachment = attachmentContext || typedAttachment;
1538
1521
  const hasInlinePayload = isAttachment && containsDurableAttachmentPayload(item, true);
1539
1522
  const hasDurableReference = item.type === "image"
1540
- ? hasDurableAttachmentUrl(item.url)
1541
- : hasDurableAttachmentUrl(item.url) ||
1542
- hasDurableAttachmentUrl(item.referenceUrl);
1523
+ ? isPersistableAttachmentUrl(item.url)
1524
+ : isPersistableAttachmentUrl(item.url) ||
1525
+ isPersistableAttachmentUrl(item.referenceUrl);
1543
1526
  const hasInlineReference = [item.url, item.referenceUrl].some((reference) => typeof reference === "string" &&
1544
1527
  reference.trim().length > 0 &&
1545
- !hasDurableAttachmentUrl(reference));
1528
+ !isPersistableAttachmentUrl(reference));
1546
1529
  if ((requiredAttachment || typedAttachment) &&
1547
1530
  (hasInlinePayload || hasInlineReference) &&
1548
1531
  !hasDurableReference) {
@@ -9040,11 +9023,12 @@ async function queueableAttachments(attachments, ownerEmail, requestAttachmentIn
9040
9023
  ? { fileId: record.id }
9041
9024
  : {}),
9042
9025
  });
9043
- if (!part.fileId &&
9044
- inlineData &&
9026
+ let uploadAttempted = false;
9027
+ if (inlineData &&
9045
9028
  (!part.url || !record.uploadProvider || record.storageUploadFailed)) {
9046
9029
  const parsedData = parseBase64DataUrl(inlineData);
9047
9030
  if (parsedData) {
9031
+ uploadAttempted = true;
9048
9032
  const uploaded = await uploadFile({
9049
9033
  data: new Uint8Array(Buffer.from(parsedData.data, "base64")),
9050
9034
  filename: attachment.name,
@@ -9059,15 +9043,16 @@ async function queueableAttachments(attachments, ownerEmail, requestAttachmentIn
9059
9043
  name: attachment.name,
9060
9044
  ...(mediaType ? { mediaType } : {}),
9061
9045
  url: uploaded.url,
9046
+ ...(part.fileId ? { fileId: part.fileId } : {}),
9062
9047
  });
9063
9048
  }
9064
9049
  }
9065
9050
  }
9066
9051
  if (requestAttachmentIndexes.has(index)) {
9067
- if (!part.url || !hasDurableAttachmentUrl(part.url))
9052
+ if (!part.url || !isPersistableAttachmentUrl(part.url))
9068
9053
  return null;
9069
9054
  const referenceUrl = attachment.referenceUrl;
9070
- if (referenceUrl && !hasDurableAttachmentUrl(referenceUrl))
9055
+ if (referenceUrl && !isPersistableAttachmentUrl(referenceUrl))
9071
9056
  return null;
9072
9057
  requestFiles.push({
9073
9058
  type: "image",
@@ -9078,8 +9063,13 @@ async function queueableAttachments(attachments, ownerEmail, requestAttachmentIn
9078
9063
  });
9079
9064
  continue;
9080
9065
  }
9081
- if (!part.url && !part.fileId)
9082
- return null;
9066
+ if (!part.url) {
9067
+ // A failed upload is transient (null -> retryable); an id with no
9068
+ // uploadable bytes can never become a reference.
9069
+ if (uploadAttempted)
9070
+ return null;
9071
+ throw new DurableAttachmentReferenceRequiredError();
9072
+ }
9083
9073
  files.push(part);
9084
9074
  }
9085
9075
  return { attachments: files, requestAttachments: requestFiles };
@@ -9128,12 +9118,12 @@ function validateQueuedPromotionRequestAttachments(value) {
9128
9118
  return null;
9129
9119
  }
9130
9120
  }
9131
- if ((typeof url === "string" && !hasDurableAttachmentUrl(url)) ||
9121
+ if ((typeof url === "string" && !isPersistableAttachmentUrl(url)) ||
9132
9122
  (typeof attachment.referenceUrl === "string" &&
9133
- !hasDurableAttachmentUrl(attachment.referenceUrl)) ||
9123
+ !isPersistableAttachmentUrl(attachment.referenceUrl)) ||
9134
9124
  (typeof data !== "string" &&
9135
- !hasDurableAttachmentUrl(url) &&
9136
- !hasDurableAttachmentUrl(attachment.referenceUrl))) {
9125
+ !isPersistableAttachmentUrl(url) &&
9126
+ !isPersistableAttachmentUrl(attachment.referenceUrl))) {
9137
9127
  return null;
9138
9128
  }
9139
9129
  attachments.push(attachment);
@@ -10339,7 +10329,21 @@ export function createProductionAgentHandler(options) {
10339
10329
  setResponseStatus(event, 404);
10340
10330
  return { error: "Thread not found" };
10341
10331
  }
10342
- const attachmentsForQueue = await queueableAttachments(requestAttachments, ownerEmail, submissionRequestAttachmentIndexes);
10332
+ let attachmentsForQueue;
10333
+ try {
10334
+ attachmentsForQueue = await queueableAttachments(requestAttachments, ownerEmail, submissionRequestAttachmentIndexes);
10335
+ }
10336
+ catch (error) {
10337
+ if (!(error instanceof DurableAttachmentReferenceRequiredError)) {
10338
+ throw error;
10339
+ }
10340
+ setResponseStatus(event, 503);
10341
+ return {
10342
+ error: error.message,
10343
+ code: error.code,
10344
+ retryable: false,
10345
+ };
10346
+ }
10343
10347
  if (!attachmentsForQueue) {
10344
10348
  if (requestAttachments.some((attachment) => attachment.displayOnly !== true &&
10345
10349
  (attachment.type === "image" ||
@@ -741,6 +741,12 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
741
741
  let persistenceFailure;
742
742
  let persistenceFailureBlocked = false;
743
743
  const flushEventWrites = (retryAfterFailure) => {
744
+ if (persistenceFailure instanceof AgentRunEventNotPersistedError &&
745
+ persistenceFailure.terminalRun) {
746
+ const failed = Promise.reject(persistenceFailure);
747
+ persistenceChain = failed;
748
+ return failed;
749
+ }
744
750
  if (persistenceDrain) {
745
751
  const activeDrain = persistenceDrain;
746
752
  const settledDrain = retryAfterFailure
@@ -779,8 +785,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
779
785
  catch (error) {
780
786
  const terminalRun = error instanceof AgentRunEventNotPersistedError &&
781
787
  error.terminalRun;
782
- persistenceFailure = terminalRun ? undefined : error;
783
- persistenceFailureBlocked = !terminalRun;
788
+ persistenceFailure = error;
789
+ persistenceFailureBlocked = true;
784
790
  for (const queued of pendingEventWrites) {
785
791
  for (const publication of queued.publications) {
786
792
  if (publication.settled)
@@ -1527,6 +1533,14 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1527
1533
  return publication;
1528
1534
  };
1529
1535
  const emitRunEvent = (runEvent, options) => {
1536
+ if (persistenceFailure instanceof AgentRunEventNotPersistedError &&
1537
+ persistenceFailure.terminalRun) {
1538
+ const publication = Promise.reject(persistenceFailure);
1539
+ if (!options?.surfacePersistenceError)
1540
+ publication.catch(() => { });
1541
+ void checkSqlAbort();
1542
+ return publication;
1543
+ }
1530
1544
  if (privateTurn && runEvent.event.omitFromPersistedHistory !== true) {
1531
1545
  runEvent = {
1532
1546
  ...runEvent,
@@ -1722,7 +1736,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1722
1736
  completionInProgress = true;
1723
1737
  let completionError = null;
1724
1738
  let terminalPersistenceError = null;
1725
- let eventPersistenceError = null;
1739
+ let eventPersistenceFailure = null;
1726
1740
  let runTerminalErrorCode;
1727
1741
  let runTerminalBlockedTool;
1728
1742
  let runTerminalBlockedBy;
@@ -1731,21 +1745,26 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1731
1745
  let suppressContinuationEvent = false;
1732
1746
  try {
1733
1747
  await persistenceChain;
1748
+ }
1749
+ catch (error) {
1750
+ eventPersistenceFailure = { error };
1751
+ }
1752
+ try {
1734
1753
  await run.abortPersistencePromise;
1735
1754
  }
1736
1755
  catch (error) {
1737
- eventPersistenceError = error;
1738
- if (run.status !== "aborted") {
1739
- run.status = "errored";
1740
- pendingTerminalEvent = {
1741
- seq: allocateRunEventSeq(run),
1742
- event: {
1743
- type: "error",
1744
- error: "Agent run ended unexpectedly",
1745
- errorCode: "run_event_persistence_failed",
1746
- },
1747
- };
1748
- }
1756
+ eventPersistenceFailure ??= { error };
1757
+ }
1758
+ if (eventPersistenceFailure && run.status !== "aborted") {
1759
+ run.status = "errored";
1760
+ pendingTerminalEvent = {
1761
+ seq: allocateRunEventSeq(run),
1762
+ event: {
1763
+ type: "error",
1764
+ error: "Agent run ended unexpectedly",
1765
+ errorCode: "run_event_persistence_failed",
1766
+ },
1767
+ };
1749
1768
  }
1750
1769
  if (checkpointStatusReadFailed || checkpointUnknownStatus !== null) {
1751
1770
  const event = checkpointStatusReadFailed
@@ -1754,7 +1773,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1754
1773
  const abortAlreadyWon = run.status === "aborted" && run.abortReason !== undefined;
1755
1774
  if (!abortAlreadyWon) {
1756
1775
  run.status = "errored";
1757
- if (!eventPersistenceError) {
1776
+ if (!eventPersistenceFailure) {
1758
1777
  pendingTerminalEvent = {
1759
1778
  seq: peekRunEventSeq(run),
1760
1779
  event: { ...event },
@@ -1768,7 +1787,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1768
1787
  const resolveTerminalEventForCompletion = () => {
1769
1788
  if (run.status === "aborted" && run.abortReason !== undefined)
1770
1789
  return null;
1771
- if (eventPersistenceError) {
1790
+ if (eventPersistenceFailure) {
1772
1791
  return pendingTerminalEvent
1773
1792
  ? { ...pendingTerminalEvent, seq: run.events.length }
1774
1793
  : null;
@@ -1886,7 +1905,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1886
1905
  if (unfinishedTurnContinuationEvent) {
1887
1906
  terminalEvent = unfinishedTurnContinuationEvent;
1888
1907
  }
1889
- const terminalReason = eventPersistenceError && finalStatus !== "aborted"
1908
+ const terminalReason = eventPersistenceFailure && finalStatus !== "aborted"
1890
1909
  ? "error:run_event_persistence_failed"
1891
1910
  : privateTurn && terminalEvent?.type === "error"
1892
1911
  ? "error:private_content_omitted"
@@ -1989,7 +2008,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1989
2008
  : error);
1990
2009
  };
1991
2010
  if (finalStatus === "completed" || finalStatus === "errored") {
1992
- const terminalEventToEmit = eventPersistenceError
2011
+ const terminalEventToEmit = eventPersistenceFailure
1993
2012
  ? {
1994
2013
  type: "error",
1995
2014
  error: "Agent run ended unexpectedly",
@@ -2048,7 +2067,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
2048
2067
  ? { ...terminalEventToEmit, omitFromPersistedHistory: true }
2049
2068
  : terminalEventToEmit,
2050
2069
  };
2051
- if (eventPersistenceError) {
2070
+ if (eventPersistenceFailure) {
2052
2071
  volatileTerminalEvent = terminal;
2053
2072
  }
2054
2073
  else {
@@ -2100,8 +2119,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
2100
2119
  terminalPersistenceEstablished = false;
2101
2120
  }
2102
2121
  else {
2122
+ let abortStatusAttempted = false;
2103
2123
  try {
2104
2124
  await insertRunPromise;
2125
+ abortStatusAttempted = true;
2105
2126
  const persistAtomicAbort = () => markRunAborted(runId, run.abortReason ?? "execution-lease-claim-failed");
2106
2127
  const aborted = executionLeaseClaimFailed
2107
2128
  ? await persistAtomicAbort()
@@ -2113,6 +2134,10 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
2113
2134
  catch (error) {
2114
2135
  terminalPersistenceEstablished = false;
2115
2136
  reportTerminalStatusPersistenceError(error);
2137
+ if (eventPersistenceFailure &&
2138
+ (abortStatusAttempted || eventPersistenceFailure.error !== error)) {
2139
+ throw new AggregateError([eventPersistenceFailure.error, error], "Run event and abort status persistence both failed");
2140
+ }
2116
2141
  throw error;
2117
2142
  }
2118
2143
  }
@@ -2136,7 +2161,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
2136
2161
  await insertRunPromise;
2137
2162
  }
2138
2163
  if (!continuationStatusPersistedBeforeEvent &&
2139
- (!terminalPersistenceError || eventPersistenceError)) {
2164
+ (!terminalPersistenceError || eventPersistenceFailure)) {
2140
2165
  let statusUpdated = false;
2141
2166
  try {
2142
2167
  statusUpdated = await persistOwnTerminalStatus();
@@ -2208,7 +2233,7 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
2208
2233
  }
2209
2234
  }
2210
2235
  if (terminalPersistenceError) {
2211
- const reconciled = eventPersistenceError
2236
+ const reconciled = eventPersistenceFailure
2212
2237
  ? false
2213
2238
  : await reconcileTerminalRun();
2214
2239
  if (!reconciled)
@@ -2242,8 +2267,8 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
2242
2267
  attemptCount: options?.attemptCount,
2243
2268
  });
2244
2269
  }
2245
- if (eventPersistenceError && finalStatus !== "aborted") {
2246
- throw eventPersistenceError;
2270
+ if (eventPersistenceFailure) {
2271
+ throw eventPersistenceFailure.error;
2247
2272
  }
2248
2273
  })
2249
2274
  .finally(async () => {
@@ -3069,8 +3094,6 @@ export async function abortRunDurably(runId, reason = "user", options) {
3069
3094
  }
3070
3095
  if (run)
3071
3096
  run.abortPersistenceManaged = abortPersistedDurably;
3072
- if (eventFlushFailure)
3073
- throw eventFlushFailure;
3074
3097
  }
3075
3098
  catch (error) {
3076
3099
  if (run)
@@ -3090,8 +3113,13 @@ export async function abortRunDurably(runId, reason = "user", options) {
3090
3113
  },
3091
3114
  });
3092
3115
  console.error("[run-manager] durable abort persistence failed:", error instanceof Error ? error.message : error);
3116
+ if (eventFlushFailure) {
3117
+ throw new AggregateError([eventFlushFailure, error], "Run abort event flush and marker persistence failed");
3118
+ }
3093
3119
  throw error;
3094
3120
  }
3121
+ if (eventFlushFailure)
3122
+ throw eventFlushFailure;
3095
3123
  return abortedInMemory;
3096
3124
  })();
3097
3125
  if (run) {
@@ -3147,7 +3175,7 @@ export async function abortRunAtSafeBoundaryDurably(runId, reason = "user") {
3147
3175
  throw result.eventFlushFailure;
3148
3176
  return result.aborted;
3149
3177
  }
3150
- export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
3178
+ async function abortTurnByRefDurablyInternal(threadId, turnId, reason, reportedFlushFailure) {
3151
3179
  const fence = currentExecutionFence();
3152
3180
  if (fence && (fence.threadId !== threadId || fence.turnId !== turnId)) {
3153
3181
  throw new StaleExecutionFenceError(fence);
@@ -3161,24 +3189,49 @@ export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
3161
3189
  runsToFlush.push(run);
3162
3190
  }
3163
3191
  }
3164
- const flushFailures = await Promise.all(runsToFlush.map((run) => flushRunEventsBeforeAbort(run)));
3192
+ const flushResults = await Promise.allSettled(runsToFlush.map((run) => flushRunEventsBeforeAbort(run)));
3193
+ // Only the same run's repeated flush is already reported by the caller;
3194
+ // a marker write can fail with the very same error object independently.
3195
+ const unreportedFlushFailures = [];
3196
+ let hasRejectedFlush = false;
3197
+ for (const [index, result] of flushResults.entries()) {
3198
+ if (result.status === "rejected") {
3199
+ hasRejectedFlush = true;
3200
+ unreportedFlushFailures.push(result.reason);
3201
+ }
3202
+ else if (result.value &&
3203
+ !(reportedFlushFailure &&
3204
+ runsToFlush[index]?.runId === reportedFlushFailure.runId &&
3205
+ result.value === reportedFlushFailure.error)) {
3206
+ unreportedFlushFailures.push(result.value);
3207
+ }
3208
+ }
3209
+ if (hasRejectedFlush) {
3210
+ if (unreportedFlushFailures.length === 1)
3211
+ throw unreportedFlushFailures[0];
3212
+ throw new AggregateError(unreportedFlushFailures, "Turn abort event flushes failed");
3213
+ }
3165
3214
  let result;
3166
3215
  try {
3167
3216
  result = await markTurnAborted(threadId, turnId, reason);
3168
3217
  }
3169
3218
  catch (error) {
3170
- const eventFlushFailure = flushFailures.find(Boolean);
3171
- if (eventFlushFailure) {
3172
- throw new AggregateError([eventFlushFailure, error], "Turn abort event flush and marker persistence failed");
3219
+ if (unreportedFlushFailures.length) {
3220
+ throw new AggregateError([...unreportedFlushFailures, error], "Turn abort event flush and marker persistence failed");
3173
3221
  }
3174
3222
  throw error;
3175
3223
  }
3176
- const eventFlushFailure = flushFailures.find(Boolean);
3177
- if (eventFlushFailure)
3178
- throw eventFlushFailure;
3224
+ if (unreportedFlushFailures.length === 1)
3225
+ throw unreportedFlushFailures[0];
3226
+ if (unreportedFlushFailures.length > 1) {
3227
+ throw new AggregateError(unreportedFlushFailures, "Turn abort event flushes failed");
3228
+ }
3179
3229
  return result;
3180
3230
  }
3181
- export async function abortTurnDurably(runId, reason = "user") {
3231
+ export async function abortTurnByRefDurably(threadId, turnId, reason = "user") {
3232
+ return abortTurnByRefDurablyInternal(threadId, turnId, reason);
3233
+ }
3234
+ async function abortTurnDurablyInternal(runId, reason, reportedFlushFailure) {
3182
3235
  const memRun = activeRuns.get(runId);
3183
3236
  try {
3184
3237
  const ref = memRun
@@ -3187,7 +3240,7 @@ export async function abortTurnDurably(runId, reason = "user") {
3187
3240
  if (!ref) {
3188
3241
  throw new Error(`Cannot durably abort turn for missing run ${runId}`);
3189
3242
  }
3190
- return await abortTurnByRefDurably(ref.threadId, ref.turnId, reason);
3243
+ return await abortTurnByRefDurablyInternal(ref.threadId, ref.turnId, reason, reportedFlushFailure);
3191
3244
  }
3192
3245
  catch (error) {
3193
3246
  captureError(error, {
@@ -3199,6 +3252,9 @@ export async function abortTurnDurably(runId, reason = "user") {
3199
3252
  throw error;
3200
3253
  }
3201
3254
  }
3255
+ export async function abortTurnDurably(runId, reason = "user") {
3256
+ return abortTurnDurablyInternal(runId, reason);
3257
+ }
3202
3258
  /** Persist a Stop's turn marker even when a terminal event flush must be reported. */
3203
3259
  export async function abortRunAndTurnDurably(runId, reason = "user", options) {
3204
3260
  let abortedInMemory = false;
@@ -3222,7 +3278,9 @@ export async function abortRunAndTurnDurably(runId, reason = "user", options) {
3222
3278
  }
3223
3279
  if (!options?.safeBoundary || abortedInMemory) {
3224
3280
  try {
3225
- await abortTurnDurably(runId, reason);
3281
+ await abortTurnDurablyInternal(runId, reason, terminalFlushFailure
3282
+ ? { runId, error: terminalFlushFailure }
3283
+ : undefined);
3226
3284
  }
3227
3285
  catch (error) {
3228
3286
  if (terminalFlushFailure) {
@@ -5138,13 +5138,16 @@ async function retryPendingStaleRunThreadProjections(client) {
5138
5138
  const projection = row;
5139
5139
  if (projection.thread_projection_error_code != null) {
5140
5140
  failed += 1;
5141
+ console.error(`[run-store] stale projection stuck for run ${String(projection.id)}:`, projection.thread_projection_error_code);
5141
5142
  continue;
5142
5143
  }
5143
5144
  const runId = projection.id;
5144
5145
  if (typeof runId !== "string")
5145
5146
  continue;
5146
- if (!(await finalizeStaleRun(runId, "stale-projection-retry")))
5147
+ if (!(await finalizeStaleRun(runId, "stale-projection-retry"))) {
5147
5148
  failed += 1;
5149
+ console.error(`[run-store] stale projection retry failed for run ${runId}`);
5150
+ }
5148
5151
  }
5149
5152
  return { failed, truncated };
5150
5153
  }
@@ -15,4 +15,28 @@ export interface WorkspaceProviderOAuthOptions {
15
15
  targetAccountEmail?: string;
16
16
  }
17
17
  export declare function workspaceProviderOAuthUrl(provider: string, options: WorkspaceProviderOAuthOptions): string;
18
+ /** True when this app runs inside another page's iframe (for example Dispatch). */
19
+ export declare function isFramedContext(): boolean;
20
+ /**
21
+ * The completion route the OAuth popup returns to. Written in the
22
+ * mount-relative, internal `/_agent-native` form: the app's server joins it to
23
+ * its own mount (see `getAppUrl`), so a path-mounted deployment does not double
24
+ * the prefix. The completion page posts the workspace-connection message back
25
+ * to the opener.
26
+ */
27
+ export declare const WORKSPACE_CONNECTION_COMPLETION_RETURN = "/_agent-native/oauth/popup?complete=workspace-connection";
28
+ /**
29
+ * The start URL for a popup that returns to the framework completion page, so
30
+ * an embedded app can learn the flow finished instead of navigating its frame
31
+ * to the provider (which providers refuse to be framed). The caller's own
32
+ * `returnPath` is preserved in the options and applied on completion.
33
+ */
34
+ export declare function workspaceProviderOAuthPopupStartUrl(provider: string, options: WorkspaceProviderOAuthOptions): string;
35
+ /**
36
+ * Resolve when the popup posts the framework completion message, or when it is
37
+ * closed without one. Accepts the completion page's origin, which can be a
38
+ * configured API origin distinct from the embedded UI origin, while still
39
+ * binding to the exact popup window. Returns a stop function.
40
+ */
41
+ export declare function watchWorkspaceConnectionCompletion(popup: Window, onComplete: () => void, expectedOrigin?: string): () => void;
18
42
  export declare function startWorkspaceProviderOAuth(provider: string, options: WorkspaceProviderOAuthOptions): void;
@@ -1,4 +1,6 @@
1
+ import { FRAMEWORK_INTERNAL_ROUTE_PREFIX } from "../../shared/framework-route-prefix.js";
1
2
  import { agentNativePath } from "../api-path.js";
3
+ import { openOAuthPopup } from "../oauth-popup.js";
2
4
  import { serverMountReturnPath } from "../sign-in-return.js";
3
5
  export function workspaceProviderOAuthUrl(provider, options) {
4
6
  const params = new URLSearchParams({
@@ -22,6 +24,114 @@ export function workspaceProviderOAuthUrl(provider, options) {
22
24
  }
23
25
  return agentNativePath(`/_agent-native/connections/oauth/${provider}/start?${params.toString()}`);
24
26
  }
27
+ /** True when this app runs inside another page's iframe (for example Dispatch). */
28
+ export function isFramedContext() {
29
+ if (typeof window === "undefined")
30
+ return false;
31
+ try {
32
+ return window.top !== window.self;
33
+ }
34
+ catch {
35
+ // A cross-origin ancestor makes `window.top` throw; that means framed.
36
+ return true;
37
+ }
38
+ }
39
+ /**
40
+ * The completion route the OAuth popup returns to. Written in the
41
+ * mount-relative, internal `/_agent-native` form: the app's server joins it to
42
+ * its own mount (see `getAppUrl`), so a path-mounted deployment does not double
43
+ * the prefix. The completion page posts the workspace-connection message back
44
+ * to the opener.
45
+ */
46
+ export const WORKSPACE_CONNECTION_COMPLETION_RETURN = `${FRAMEWORK_INTERNAL_ROUTE_PREFIX}/oauth/popup?complete=workspace-connection`;
47
+ /** The origin that will serve the completion page and post the message. */
48
+ function completionOrigin(startUrl) {
49
+ if (!URL.canParse(startUrl, window.location.href))
50
+ return undefined;
51
+ return new URL(startUrl, window.location.href).origin;
52
+ }
53
+ /**
54
+ * The start URL for a popup that returns to the framework completion page, so
55
+ * an embedded app can learn the flow finished instead of navigating its frame
56
+ * to the provider (which providers refuse to be framed). The caller's own
57
+ * `returnPath` is preserved in the options and applied on completion.
58
+ */
59
+ export function workspaceProviderOAuthPopupStartUrl(provider, options) {
60
+ return workspaceProviderOAuthUrl(provider, {
61
+ ...options,
62
+ serverReturnPath: WORKSPACE_CONNECTION_COMPLETION_RETURN,
63
+ });
64
+ }
65
+ /**
66
+ * Resolve when the popup posts the framework completion message, or when it is
67
+ * closed without one. Accepts the completion page's origin, which can be a
68
+ * configured API origin distinct from the embedded UI origin, while still
69
+ * binding to the exact popup window. Returns a stop function.
70
+ */
71
+ export function watchWorkspaceConnectionCompletion(popup, onComplete, expectedOrigin) {
72
+ let closedTicks = 0;
73
+ const stop = () => {
74
+ window.removeEventListener("message", onMessage);
75
+ window.clearInterval(closedTimer);
76
+ };
77
+ const onMessage = (event) => {
78
+ const originAccepted = event.origin === window.location.origin ||
79
+ (expectedOrigin !== undefined && event.origin === expectedOrigin);
80
+ if (!originAccepted || event.source !== popup)
81
+ return;
82
+ const data = event.data;
83
+ if (typeof data !== "object" ||
84
+ data === null ||
85
+ data.type !==
86
+ "agent-native:workspace-connection-complete")
87
+ return;
88
+ stop();
89
+ onComplete();
90
+ };
91
+ // The completion message is posted just before the popup closes; wait one
92
+ // extra tick so a close never wins the race against it.
93
+ const closedTimer = window.setInterval(() => {
94
+ if (!popup.closed) {
95
+ closedTicks = 0;
96
+ return;
97
+ }
98
+ closedTicks += 1;
99
+ if (closedTicks > 1)
100
+ stop();
101
+ }, 500);
102
+ window.addEventListener("message", onMessage);
103
+ return stop;
104
+ }
105
+ /**
106
+ * Apply the caller's return target on completion, so resume state carried in
107
+ * `returnPath` survives an embedded flow. Falls back to a reload.
108
+ */
109
+ function applyReturnPath(options) {
110
+ if (options.returnPath) {
111
+ window.location.assign(options.returnPath);
112
+ return;
113
+ }
114
+ window.location.reload();
115
+ }
25
116
  export function startWorkspaceProviderOAuth(provider, options) {
117
+ if (isFramedContext()) {
118
+ // Providers refuse to be framed (X-Frame-Options / frame-ancestors), so a
119
+ // same-window navigation inside Dispatch's iframe is refused. Leave the
120
+ // frame: run the flow in a popup whose completion page posts back.
121
+ const startUrl = workspaceProviderOAuthPopupStartUrl(provider, options);
122
+ const popup = openOAuthPopup({
123
+ initialUrl: startUrl,
124
+ features: "width=640,height=760",
125
+ });
126
+ if (popup) {
127
+ watchWorkspaceConnectionCompletion(popup, () => applyReturnPath(options), completionOrigin(startUrl));
128
+ return;
129
+ }
130
+ // Popup blocked while embedded. Never navigate this frame into the
131
+ // provider — the frame refuses it and the user cannot connect at all.
132
+ // Surface the recovery; the caller retries once popups are allowed.
133
+ console.warn("[agent-native] OAuth popup was blocked while embedded in another page; allow popups for this site to connect the provider.");
134
+ return;
135
+ }
26
136
  window.location.assign(workspaceProviderOAuthUrl(provider, options));
27
137
  }
@@ -1,17 +1,17 @@
1
1
  {
2
- "@agent-native/agentkit": "0.225.0",
2
+ "@agent-native/agentkit": "0.225.2-nightly-20261106152255",
3
3
  "@agent-native/code-agents-ui": "0.1.75",
4
- "@agent-native/core": "0.225.0",
5
- "@agent-native/core-corpus": "0.225.0",
6
- "@agent-native/creative-context": "0.8.66",
7
- "@agent-native/dispatch": "0.42.1",
4
+ "@agent-native/core": "0.225.2-nightly-20261106152255",
5
+ "@agent-native/core-corpus": "0.225.2-nightly-20261106152255",
6
+ "@agent-native/creative-context": "0.8.67",
7
+ "@agent-native/dispatch": "0.42.3-nightly-20261106152255",
8
8
  "@agent-native/embedding": "0.2.0",
9
9
  "@agent-native/migrate": "0.0.7",
10
- "@agent-native/otel": "0.1.33",
11
- "@agent-native/pinpoint": "0.1.109",
12
- "@agent-native/recap-cli": "0.5.97",
13
- "@agent-native/scheduling": "0.2.53",
10
+ "@agent-native/otel": "0.1.34",
11
+ "@agent-native/pinpoint": "0.1.110",
12
+ "@agent-native/recap-cli": "0.5.98",
13
+ "@agent-native/scheduling": "0.2.54",
14
14
  "@agent-native/shared-app-config": "0.1.2",
15
- "@agent-native/skills": "0.4.1",
16
- "@agent-native/toolkit": "0.225.0"
15
+ "@agent-native/skills": "0.4.3-nightly-20261106152255",
16
+ "@agent-native/toolkit": "0.225.2-nightly-20261106152255"
17
17
  }
@@ -8167,28 +8167,38 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8167
8167
  jobsSkippedReason: "unclaimed-background-sweep-failed",
8168
8168
  };
8169
8169
  }
8170
+ const incompleteSteps = [
8171
+ staleRunReapIncomplete && "staleRunsReaped",
8172
+ agentHarnessFollowUpRecoveryIncomplete &&
8173
+ "expiredAgentHarnessFollowUps",
8174
+ agentHarnessApprovalRecoveryIncomplete &&
8175
+ "expiredAgentHarnessApprovals",
8176
+ appSweepHandlers.failed.length > 0 && "appSweepHandlers",
8177
+ automationFailureAlerts === null && "automationFailureAlerts",
8178
+ (queuedSubmissionDrain === null ||
8179
+ queuedSubmissionDrain.failed > 0) &&
8180
+ "queuedSubmissionDrain",
8181
+ (staleAgentTeamRuns === null || staleAgentTeamRuns.failed > 0) &&
8182
+ "staleAgentTeamRuns",
8183
+ ].filter((step) => typeof step === "string");
8184
+ if (incompleteSteps.length > 0) {
8185
+ console.error("[recurring-jobs] Sweep incomplete:", {
8186
+ incompleteSteps,
8187
+ staleRunsReaped,
8188
+ expiredAgentHarnessFollowUps,
8189
+ expiredAgentHarnessApprovals,
8190
+ staleAgentTeamRuns,
8191
+ queuedSubmissionDrain,
8192
+ automationFailureAlerts,
8193
+ appSweepHandlers,
8194
+ });
8195
+ }
8170
8196
  if (!triggerAvailability.available) {
8171
- if (staleRunReapIncomplete ||
8172
- agentHarnessFollowUpRecoveryIncomplete ||
8173
- agentHarnessApprovalRecoveryIncomplete ||
8174
- appSweepHandlers.failed.length > 0 ||
8175
- automationFailureAlerts === null ||
8176
- queuedSubmissionDrain === null ||
8177
- queuedSubmissionDrain.failed > 0 ||
8178
- staleAgentTeamRuns === null ||
8179
- staleAgentTeamRuns.failed > 0) {
8197
+ if (incompleteSteps.length > 0) {
8180
8198
  setResponseStatus(event, 500);
8181
8199
  }
8182
8200
  return {
8183
- ok: !staleRunReapIncomplete &&
8184
- !agentHarnessFollowUpRecoveryIncomplete &&
8185
- !agentHarnessApprovalRecoveryIncomplete &&
8186
- appSweepHandlers.failed.length === 0 &&
8187
- automationFailureAlerts !== null &&
8188
- queuedSubmissionDrain !== null &&
8189
- queuedSubmissionDrain.failed === 0 &&
8190
- staleAgentTeamRuns !== null &&
8191
- staleAgentTeamRuns.failed === 0,
8201
+ ok: incompleteSteps.length === 0,
8192
8202
  staleRunsReaped,
8193
8203
  expiredAgentHarnessFollowUps,
8194
8204
  expiredAgentHarnessApprovals,
@@ -8204,15 +8214,7 @@ Non-code requests are still fine on this surface: read data, navigate the UI, su
8204
8214
  }
8205
8215
  try {
8206
8216
  await processRecurringJobs(schedulerDeps);
8207
- if (staleRunReapIncomplete ||
8208
- agentHarnessFollowUpRecoveryIncomplete ||
8209
- agentHarnessApprovalRecoveryIncomplete ||
8210
- appSweepHandlers.failed.length > 0 ||
8211
- automationFailureAlerts === null ||
8212
- queuedSubmissionDrain === null ||
8213
- queuedSubmissionDrain.failed > 0 ||
8214
- staleAgentTeamRuns === null ||
8215
- staleAgentTeamRuns.failed > 0) {
8217
+ if (incompleteSteps.length > 0) {
8216
8218
  setResponseStatus(event, 500);
8217
8219
  return {
8218
8220
  ok: false,
@@ -3211,11 +3211,38 @@ function createAuthGuardFn(app) {
3211
3211
  if (session) {
3212
3212
  const method = getMethod(event);
3213
3213
  if (p.startsWith("/api/") || p.startsWith("/_agent-native/")) {
3214
+ // A fresh loopback dev workspace has no session org yet, and the
3215
+ // read-only session resolver never creates one, so the app-access check
3216
+ // below can deny before the ordinary org initializer runs. Resolve (or
3217
+ // create) the existing default org through the normal policy first and
3218
+ // hand its id to the unchanged check. The check still owns the verdict;
3219
+ // a missing or failed org initialization also denies protected requests,
3220
+ // while null keeps the wrapper's org-recovery exemption.
3221
+ let workspaceOrgId = session.orgId;
3222
+ let workspaceOrgInitializationFailed = false;
3223
+ if (!workspaceOrgId &&
3224
+ !hasExplicitPersonalOrgScope(event) &&
3225
+ isSyntheticAutoDevIdentity(session.email) &&
3226
+ isLoopbackDevRequest(event) &&
3227
+ getAppConfig().access.orgCreation === "open" &&
3228
+ getAppConfig().access.autoCreateDefaultOrg) {
3229
+ const { getOrgContext } = await import("../org/context.js");
3230
+ try {
3231
+ const org = await getOrgContext(event);
3232
+ if (org.orgId)
3233
+ workspaceOrgId = org.orgId;
3234
+ else
3235
+ workspaceOrgInitializationFailed = true;
3236
+ }
3237
+ catch {
3238
+ workspaceOrgInitializationFailed = true;
3239
+ }
3240
+ }
3214
3241
  let workspaceAppAccess = await checkWorkspaceAppAccessForRequest({
3215
3242
  path: p,
3216
3243
  method,
3217
3244
  email: session.email,
3218
- ...(session.orgId === undefined ? {} : { orgId: session.orgId }),
3245
+ ...(workspaceOrgId === undefined ? {} : { orgId: workspaceOrgId }),
3219
3246
  });
3220
3247
  if (workspaceAppAccess === false &&
3221
3248
  !session.orgId &&
@@ -3246,7 +3273,8 @@ function createAuthGuardFn(app) {
3246
3273
  setResponseStatus(event, 503);
3247
3274
  return workspaceAppAccessUnavailableBody(workspaceAppAccess);
3248
3275
  }
3249
- if (workspaceAppAccess === false) {
3276
+ if ((workspaceOrgInitializationFailed && workspaceAppAccess !== null) ||
3277
+ workspaceAppAccess === false) {
3250
3278
  setResponseStatus(event, 403);
3251
3279
  return { error: "You do not have access to this workspace app." };
3252
3280
  }
@@ -3408,6 +3436,22 @@ export function isLocalDevAuthAllowed(event) {
3408
3436
  ((!isHostedPreviewRequest(event) && isLoopbackRequest(event)) ||
3409
3437
  isBuilderPreviewLocalDevRequest(event)));
3410
3438
  }
3439
+ /**
3440
+ * A real loopback dev/test request. `isLocalDevAuthAllowed` also admits the
3441
+ * Builder-preview branch, which does not require loopback, so org
3442
+ * auto-initialization additionally requires the actual socket peer and
3443
+ * excludes hosted-preview hosts. Builder preview keeps the ordinary
3444
+ * app-access path.
3445
+ */
3446
+ function isLoopbackDevRequest(event) {
3447
+ return (isLocalDevAuthAllowed(event) &&
3448
+ isLoopbackRequest(event) &&
3449
+ !isHostedPreviewRequest(event));
3450
+ }
3451
+ /** The framework's own throwaway local account, never a real signup. */
3452
+ function isSyntheticAutoDevIdentity(email) {
3453
+ return (email === AUTO_DEV_ACCOUNT_EMAIL || email === LEGACY_AUTO_DEV_ACCOUNT_EMAIL);
3454
+ }
3411
3455
  async function createOrReuseAutoDevSession(auth, config) {
3412
3456
  let session = await createBetterAuthSessionForEmail(AUTO_DEV_ACCOUNT_EMAIL, config);
3413
3457
  if (!session) {
@@ -1,3 +1,4 @@
1
+ import { isPersistableAttachmentUrl } from "@agent-native/agentkit/protocol";
2
+ export { isPersistableAttachmentUrl };
1
3
  export declare function isInlineDataUrl(value: unknown): value is string;
2
- export declare function isPersistableAttachmentUrl(value: unknown): value is string;
3
4
  export declare function stripInlineAttachmentPayloads(value: unknown, attachmentContext?: boolean): unknown;
@@ -1,3 +1,5 @@
1
+ import { isPersistableAttachmentUrl } from "@agent-native/agentkit/protocol";
2
+ export { isPersistableAttachmentUrl };
1
3
  const ATTACHMENT_BODY_FIELDS = new Set([
2
4
  "base64",
3
5
  "bytes",
@@ -30,19 +32,6 @@ const ATTACHMENT_CONTEXT_FIELDS = new Set([
30
32
  export function isInlineDataUrl(value) {
31
33
  return typeof value === "string" && /^\s*data:/i.test(value);
32
34
  }
33
- export function isPersistableAttachmentUrl(value) {
34
- if (typeof value !== "string" || !value.trim())
35
- return false;
36
- if (!URL.canParse(value))
37
- return false;
38
- const url = new URL(value);
39
- return (url.protocol === "https:" &&
40
- Boolean(url.hostname) &&
41
- !url.username &&
42
- !url.password &&
43
- !url.search &&
44
- !url.hash);
45
- }
46
35
  function isBase64Payload(value) {
47
36
  return (typeof value === "string" &&
48
37
  value.trim().length >= 64 &&
@@ -1,19 +1,6 @@
1
+ import { isPersistableAttachmentReferenceUrl } from "@agent-native/agentkit/protocol";
1
2
  import { isInlineDataUrl } from "./attachments.js";
2
3
  import { parseDataUrl } from "./data-url.js";
3
- function isPersistableAttachmentReferenceUrl(value) {
4
- if (typeof value !== "string" || !value.trim() || value.length > 8192) {
5
- return false;
6
- }
7
- if (!URL.canParse(value))
8
- return false;
9
- const url = new URL(value);
10
- return ((url.protocol === "http:" || url.protocol === "https:") &&
11
- Boolean(url.hostname) &&
12
- !url.username &&
13
- !url.password &&
14
- !url.search &&
15
- !url.hash);
16
- }
17
4
  /**
18
5
  * Inline file bytes reached a SQL write with no durable URL to stand in for
19
6
  * them. Raised only by the `reject` policy; a durable dispatch payload must
@@ -177,10 +164,10 @@ function sanitize(value, policy, inAttachmentList, inheritedUrl, inAttachmentMet
177
164
  const ownUrl = hasAttachmentContext ? durableUrl(item) : undefined;
178
165
  const fallbackUrl = ownUrl ?? (hasAttachmentContext ? inheritedUrl : undefined);
179
166
  const inline = hasAttachmentContext ? inlineKeys(item) : [];
180
- const unreferenced = isAttachment &&
181
- inline.length > 0 &&
182
- !fallbackUrl &&
183
- !(typeof item.fileId === "string" && item.fileId);
167
+ // A `fileId` names the attachment; nothing resolves it back to bytes, so it
168
+ // never stands in for bytes that need a URL. The dispatch serializer agrees
169
+ // for parts that carry bytes.
170
+ const unreferenced = isAttachment && inline.length > 0 && !fallbackUrl;
184
171
  if (unreferenced && policy === "reject") {
185
172
  throw new DurableAttachmentReferenceRequiredError();
186
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.225.0",
3
+ "version": "0.225.2-nightly-20261106152255",
4
4
  "description": "The agentic application framework for building autonomous agents with intuitive UIs",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -760,8 +760,8 @@
760
760
  "yaml": "^2.9.0",
761
761
  "yjs": "^13.6.33",
762
762
  "zod": "^4.3.6",
763
- "@agent-native/agentkit": "^0.225.0",
764
- "@agent-native/recap-cli": "^0.5.97"
763
+ "@agent-native/agentkit": "^0.225.2-nightly-20261106152255",
764
+ "@agent-native/recap-cli": "^0.5.98"
765
765
  },
766
766
  "devDependencies": {
767
767
  "@ai-sdk/anthropic": "^3.0.71",
@@ -833,7 +833,7 @@
833
833
  "xlsx": "^0.18.5"
834
834
  },
835
835
  "peerDependencies": {
836
- "@agent-native/core-corpus": ">=0.196.0",
836
+ "@agent-native/core-corpus": "0.225.2-nightly-20261106152255",
837
837
  "@ai-sdk/anthropic": ">=3",
838
838
  "@ai-sdk/cohere": ">=3",
839
839
  "@ai-sdk/google": ">=3",