@agent-native/core 0.222.0 → 0.222.1-nightly-20261029092708

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 (43) hide show
  1. package/dist/agent/production-agent.d.ts +2 -0
  2. package/dist/agent/production-agent.js +86 -6
  3. package/dist/agent/run-store.js +3 -2
  4. package/dist/app-config/agent.d.ts +9 -0
  5. package/dist/app-config/agent.js +31 -0
  6. package/dist/app-config/schema.d.ts +9 -0
  7. package/dist/app-config/store.js +48 -5
  8. package/dist/collab/routes.d.ts +1 -1
  9. package/dist/collab/struct-routes.d.ts +1 -1
  10. package/dist/file-upload/actions/upload-image.d.ts +4 -4
  11. package/dist/jobs/run-history.js +3 -2
  12. package/dist/observability/hosted-model-experiment.d.ts +1 -1
  13. package/dist/observability/hosted-model-experiment.js +7 -1
  14. package/dist/observability/question-intake-experiment.d.ts +34 -0
  15. package/dist/observability/question-intake-experiment.js +114 -0
  16. package/dist/observability/traces.d.ts +4 -0
  17. package/dist/observability/traces.js +5 -4
  18. package/dist/observability/tracing.d.ts +1 -0
  19. package/dist/observability/tracing.js +14 -4
  20. package/dist/org/index.d.ts +1 -1
  21. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  22. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  23. package/dist/server/agent-chat/browser-team-tools.js +2 -2
  24. package/dist/server/agent-engine-api-key-route.d.ts +3 -3
  25. package/dist/server/better-auth-instance.js +43 -0
  26. package/dist/server/builder-branch-files.d.ts +3 -1
  27. package/dist/server/builder-branch-files.js +74 -33
  28. package/dist/server/builder-branch-git.d.ts +1 -1
  29. package/dist/server/builder-branch-git.js +2 -0
  30. package/dist/server/builder-browser.d.ts +17 -0
  31. package/dist/server/builder-browser.js +33 -0
  32. package/dist/server/core-routes-plugin.js +42 -8
  33. package/dist/server/index.d.ts +1 -1
  34. package/dist/server/index.js +1 -1
  35. package/dist/server/request-context.d.ts +8 -0
  36. package/dist/server/transcribe-voice.d.ts +1 -1
  37. package/dist/shared/first-run-builder-credits-experiment.d.ts +1 -0
  38. package/dist/shared/first-run-builder-credits-experiment.js +19 -0
  39. package/dist/tracking/index.d.ts +1 -1
  40. package/dist/tracking/index.js +1 -1
  41. package/dist/tracking/registry.d.ts +7 -0
  42. package/dist/tracking/registry.js +84 -4
  43. package/package.json +4 -4
@@ -35,6 +35,7 @@ export interface TrackingEventScope {
35
35
  pending: Set<Promise<void>>;
36
36
  }
37
37
  export declare function createTrackingEventScope(): TrackingEventScope;
38
+ export declare function registerTrackingWork(pending: Promise<void>, scope?: TrackingEventScope): void;
38
39
  export declare function recordTrackingEvent(name: string, properties?: Record<string, unknown>, origin?: TrackingEventOrigin): Promise<void>;
39
40
  export declare function queueTrackingEvent(name: string, properties?: Record<string, unknown>, origin?: TrackingEventOrigin, scope?: TrackingEventScope): void;
40
41
  export declare function flushTrackingEvents(scope?: TrackingEventScope): Promise<void>;
@@ -59,6 +59,11 @@ const pendingTrackingEvents = new Set();
59
59
  export function createTrackingEventScope() {
60
60
  return { pending: new Set() };
61
61
  }
62
+ export function registerTrackingWork(pending, scope) {
63
+ const pendingEvents = scope?.pending ?? pendingTrackingEvents;
64
+ pendingEvents.add(pending);
65
+ void pending.then(() => pendingEvents.delete(pending), () => pendingEvents.delete(pending));
66
+ }
62
67
  function numericDuration(properties) {
63
68
  const duration = properties.duration_ms;
64
69
  return typeof duration === "number" &&
@@ -194,12 +199,17 @@ export async function recordTrackingEvent(name, properties = {}, origin) {
194
199
  }
195
200
  export function queueTrackingEvent(name, properties = {}, origin = "server", scope) {
196
201
  const pending = recordTrackingEvent(name, properties, origin);
197
- const pendingEvents = scope?.pending ?? pendingTrackingEvents;
198
- pendingEvents.add(pending);
199
- void pending.then(() => pendingEvents.delete(pending), () => pendingEvents.delete(pending));
202
+ registerTrackingWork(pending, scope);
200
203
  }
201
204
  export async function flushTrackingEvents(scope) {
202
- await Promise.allSettled([...(scope?.pending ?? pendingTrackingEvents)]);
205
+ const pendingEvents = scope?.pending ?? pendingTrackingEvents;
206
+ if (!scope) {
207
+ await Promise.allSettled([...pendingEvents]);
208
+ return;
209
+ }
210
+ while (pendingEvents.size > 0) {
211
+ await Promise.allSettled([...pendingEvents]);
212
+ }
203
213
  }
204
214
  export async function startAgentSpan(name, attributes = {}, parentSpan = null, startTime) {
205
215
  const runtime = await resolveRuntime();
@@ -180,10 +180,10 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
180
180
  soloDeploymentAdmin: boolean | undefined;
181
181
  }>>;
182
182
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
183
- id?: undefined;
184
183
  name?: undefined;
185
184
  success: boolean;
186
185
  role?: undefined;
186
+ id?: undefined;
187
187
  } | {
188
188
  success?: undefined;
189
189
  id: string;
@@ -76,9 +76,9 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
76
76
  }>>;
77
77
  }, z.core.$strip>>, {
78
78
  id?: undefined;
79
+ provider?: undefined;
79
80
  deleted?: undefined;
80
81
  message?: undefined;
81
- found?: undefined;
82
82
  providers: {
83
83
  id: string;
84
84
  label: string;
@@ -89,7 +89,7 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
89
89
  updatedAt: number;
90
90
  }[];
91
91
  count: number;
92
- provider?: undefined;
92
+ found?: undefined;
93
93
  registered?: undefined;
94
94
  label?: undefined;
95
95
  } | {
@@ -103,31 +103,31 @@ export declare function createCustomProviderRegistrationAction<TSchema extends Z
103
103
  registered?: undefined;
104
104
  label?: undefined;
105
105
  } | {
106
+ provider?: undefined;
106
107
  count?: undefined;
107
108
  deleted?: undefined;
108
109
  message?: undefined;
109
110
  providers?: undefined;
110
- provider?: undefined;
111
111
  found: boolean;
112
112
  id: string;
113
113
  registered?: undefined;
114
114
  label?: undefined;
115
115
  } | {
116
+ provider?: undefined;
116
117
  count?: undefined;
117
118
  message?: undefined;
118
- found?: undefined;
119
119
  providers?: undefined;
120
- provider?: undefined;
120
+ found?: undefined;
121
121
  deleted: boolean;
122
122
  id: string;
123
123
  registered?: undefined;
124
124
  label?: undefined;
125
125
  } | {
126
+ provider?: undefined;
126
127
  count?: undefined;
127
128
  deleted?: undefined;
128
- found?: undefined;
129
129
  providers?: undefined;
130
- provider?: undefined;
130
+ found?: undefined;
131
131
  registered: boolean;
132
132
  id: string;
133
133
  label: string;
@@ -316,9 +316,9 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
316
316
  notes?: string | undefined;
317
317
  scope?: "org" | "user" | undefined;
318
318
  }, {
319
+ provider?: undefined;
319
320
  id?: undefined;
320
321
  message?: undefined;
321
- found?: undefined;
322
322
  providers: {
323
323
  id: string;
324
324
  label: string;
@@ -329,7 +329,7 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
329
329
  updatedAt: number;
330
330
  }[];
331
331
  count: number;
332
- provider?: undefined;
332
+ found?: undefined;
333
333
  deleted?: undefined;
334
334
  registered?: undefined;
335
335
  label?: undefined;
@@ -344,30 +344,30 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
344
344
  registered?: undefined;
345
345
  label?: undefined;
346
346
  } | {
347
+ provider?: undefined;
347
348
  count?: undefined;
348
349
  message?: undefined;
349
350
  providers?: undefined;
350
- provider?: undefined;
351
351
  found: boolean;
352
352
  id: string;
353
353
  deleted?: undefined;
354
354
  registered?: undefined;
355
355
  label?: undefined;
356
356
  } | {
357
+ provider?: undefined;
357
358
  count?: undefined;
358
359
  message?: undefined;
359
- found?: undefined;
360
360
  providers?: undefined;
361
- provider?: undefined;
361
+ found?: undefined;
362
362
  deleted: boolean;
363
363
  id: string;
364
364
  registered?: undefined;
365
365
  label?: undefined;
366
366
  } | {
367
+ provider?: undefined;
367
368
  count?: undefined;
368
- found?: undefined;
369
369
  providers?: undefined;
370
- provider?: undefined;
370
+ found?: undefined;
371
371
  deleted?: undefined;
372
372
  registered: boolean;
373
373
  id: string;
@@ -5,7 +5,7 @@ import { fail, isActionContractError, } from "../../action.js";
5
5
  import { getActiveFileUploadProviderForRequest } from "../../file-upload/registry.js";
6
6
  import { areBuiltinMcpCapabilitiesSupported, setBuiltinMcpCapabilityEnabled, } from "../../mcp-client/index.js";
7
7
  import { frameworkMcpTools } from "../../mcp/framework-tools.js";
8
- import { getBuilderBrowserConnectUrlForOwner, resolveBuilderBranchProjectId, } from "../builder-browser.js";
8
+ import { getBuilderBrowserConnectUrlForOwner, resolveBuilderHandoffProjectId, } from "../builder-browser.js";
9
9
  import { getRequestOrgId, getRequestUserEmail } from "../request-context.js";
10
10
  import { getMcpManagerForCurrentRequest } from "./mcp-glue.js";
11
11
  const MAX_EXTENSION_PROMOTION_CONTENT_CHARS = 200_000;
@@ -199,7 +199,7 @@ export function createBuilderBrowserTool(deps) {
199
199
  // re-authorizing renders unconfigured, so the card offers to connect.
200
200
  const configured = (await builderConnectionForRequest(creds, BUILDER_OAUTH_SCOPE))
201
201
  .status === "connected";
202
- const branchProjectId = await resolveBuilderBranchProjectId();
202
+ const branchProjectId = await resolveBuilderHandoffProjectId();
203
203
  let prompt = typeof args?.prompt === "string" ? args.prompt : "";
204
204
  const extensionId = typeof args?.extensionId === "string" ? args.extensionId.trim() : "";
205
205
  let promotion;
@@ -54,21 +54,21 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
54
54
  error: string;
55
55
  }>;
56
56
  export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
+ error: string;
57
58
  errorCode?: undefined;
58
59
  code?: undefined;
59
- error: string;
60
60
  } | {
61
- code?: undefined;
62
61
  error: string;
63
62
  errorCode: string;
63
+ code?: undefined;
64
64
  } | {
65
65
  errorCode?: undefined;
66
66
  error: string;
67
67
  code: ProviderKeyCheckCode;
68
68
  } | {
69
+ error?: undefined;
69
70
  errorCode?: undefined;
70
71
  code?: undefined;
71
- error?: undefined;
72
72
  ok: boolean;
73
73
  key: string;
74
74
  baseUrlKey?: string | undefined;
@@ -26,6 +26,7 @@ import { CORE_CHANGE_EMAIL_CONFIRMATION_EMAIL_ID, CORE_CHANGE_EMAIL_VERIFICATION
26
26
  import { renderTransactionalEmail } from "../email-catalog/templates.js";
27
27
  import { executeIdentityRekey, rekeyIdentity, resumePendingIdentityRekeys, verifiedEmailChangeFromToken, } from "../identity/rekey.js";
28
28
  import { saveOAuthTokens } from "../oauth-tokens/store.js";
29
+ import { assignQuestionIntakeExperimentOnSignup } from "../observability/question-intake-experiment.js";
29
30
  import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
30
31
  import { getAuthEmailForUserId, getRequiredAuthProviderForEmail, } from "../org/auth-policy.js";
31
32
  import { autoJoinDomainMatchingOrgs } from "../org/auto-join-domain.js";
@@ -1849,6 +1850,48 @@ async function createBetterAuthInstance(config) {
1849
1850
  if (!email)
1850
1851
  return;
1851
1852
  await emitSignupEventForCreatedUser(user, context);
1853
+ const actingUserId = context?.context?.session?.user?.id;
1854
+ if (user.id && (!actingUserId || actingUserId === user.id)) {
1855
+ let experimentId;
1856
+ try {
1857
+ const questionIntakeConfig = getAppConfig().agent.questionIntake;
1858
+ experimentId = questionIntakeConfig?.experimentId;
1859
+ // raw-sql-ok: the assignment store binds the authenticated user ID as a query argument.
1860
+ await assignQuestionIntakeExperimentOnSignup(questionIntakeConfig, user.id);
1861
+ }
1862
+ catch (error) {
1863
+ console.error("[auth] question-intake assignment failed after retry", {
1864
+ code: "question_intake_assignment_failed",
1865
+ experimentId,
1866
+ });
1867
+ if (experimentId) {
1868
+ try {
1869
+ track("agent_experiment_assignment_failed", {
1870
+ experiment_id: experimentId,
1871
+ failure_stage: "signup",
1872
+ error_code: "question_intake_assignment_failed",
1873
+ }, { authUserId: user.id, userId: email });
1874
+ }
1875
+ catch {
1876
+ console.error("[auth] failed to track question-intake assignment failure", { code: "question_intake_assignment_tracking_failed" });
1877
+ }
1878
+ }
1879
+ try {
1880
+ const { captureError } = await import("./capture-error.js");
1881
+ captureError(error, {
1882
+ route: "auth.signup",
1883
+ tags: {
1884
+ failureClass: "question-intake-assignment",
1885
+ errorCode: "question_intake_assignment_failed",
1886
+ },
1887
+ extra: { experimentId },
1888
+ });
1889
+ }
1890
+ catch {
1891
+ console.error("[auth] failed to report question-intake assignment failure", { code: "question_intake_assignment_reporting_failed" });
1892
+ }
1893
+ }
1894
+ }
1852
1895
  if (user.emailVerified !== true)
1853
1896
  return;
1854
1897
  try {
@@ -1,6 +1,8 @@
1
+ export type BuilderBranchReadbackStage = "status_request" | "status_shape" | "status_binding" | "status_currentness" | "post_read_status_request" | "post_read_status_shape" | "post_read_status_binding" | "post_read_status_currentness" | "download_url_request" | "download_url_shape" | "download_url_binding" | "bundle_download" | "bundle_read" | "bundle_digest" | "bundle_parse" | "target_subtree_missing" | "post_read_currentness";
1
2
  export declare class BuilderBranchFilesError extends Error {
2
3
  readonly code: "invalid_request" | "not_connected" | "forbidden" | "unavailable" | "unverified" | "integrity_failed";
3
- constructor(code: "invalid_request" | "not_connected" | "forbidden" | "unavailable" | "unverified" | "integrity_failed", message: string);
4
+ readonly readbackStage?: BuilderBranchReadbackStage | undefined;
5
+ constructor(code: "invalid_request" | "not_connected" | "forbidden" | "unavailable" | "unverified" | "integrity_failed", message: string, readbackStage?: BuilderBranchReadbackStage | undefined);
4
6
  }
5
7
  type BranchRef = {
6
8
  projectId: string;
@@ -10,9 +10,11 @@ const MAX_BUNDLE_BYTES = 32 * 1024 * 1024;
10
10
  const API_TIMEOUT_MS = 30_000;
11
11
  export class BuilderBranchFilesError extends Error {
12
12
  code;
13
- constructor(code, message) {
13
+ readbackStage;
14
+ constructor(code, message, readbackStage) {
14
15
  super(message);
15
16
  this.code = code;
17
+ this.readbackStage = readbackStage;
16
18
  this.name = "BuilderBranchFilesError";
17
19
  }
18
20
  }
@@ -56,6 +58,20 @@ const downloadSchema = z.object({
56
58
  partial: z.literal(false),
57
59
  expiresAt: z.string(),
58
60
  });
61
+ const statusFailureStages = {
62
+ before_read: {
63
+ request: "status_request",
64
+ shape: "status_shape",
65
+ binding: "status_binding",
66
+ currentness: "status_currentness",
67
+ },
68
+ after_read: {
69
+ request: "post_read_status_request",
70
+ shape: "post_read_status_shape",
71
+ binding: "post_read_status_binding",
72
+ currentness: "post_read_status_currentness",
73
+ },
74
+ };
59
75
  function backupIdentityFingerprint(backup) {
60
76
  return JSON.stringify([
61
77
  backup.projectId,
@@ -67,10 +83,10 @@ function backupIdentityFingerprint(backup) {
67
83
  backup.size,
68
84
  ]);
69
85
  }
70
- function unverified() {
71
- throw new BuilderBranchFilesError("unverified", "Builder has no complete, current, identity-bound branch backup to verify.");
86
+ function unverified(stage) {
87
+ throw new BuilderBranchFilesError("unverified", "Builder has no complete, current, identity-bound branch backup to verify.", stage);
72
88
  }
73
- function primaryCommit(snapshot) {
89
+ function primaryCommit(snapshot, failureStage) {
74
90
  if (SHA.test(snapshot))
75
91
  return snapshot;
76
92
  const pairs = snapshot.split(",");
@@ -78,25 +94,25 @@ function primaryCommit(snapshot) {
78
94
  for (const pair of pairs) {
79
95
  const separator = pair.indexOf(":");
80
96
  if (separator < 1)
81
- unverified();
97
+ unverified(failureStage);
82
98
  let folder;
83
99
  try {
84
100
  folder = decodeURIComponent(pair.slice(0, separator));
85
101
  }
86
102
  catch {
87
- return unverified();
103
+ return unverified(failureStage);
88
104
  }
89
105
  const commit = pair.slice(separator + 1);
90
106
  if (!SHA.test(commit))
91
- unverified();
107
+ unverified(failureStage);
92
108
  if (folder === "code")
93
109
  entries.push(commit);
94
110
  }
95
111
  return entries.length === 1 ? entries[0] : undefined;
96
112
  }
97
- async function readBytes(response, maxBytes) {
113
+ async function readBytes(response, maxBytes, stage) {
98
114
  if (!response.body) {
99
- throw new BuilderBranchFilesError("unavailable", "Builder response has no body.");
115
+ throw new BuilderBranchFilesError("unavailable", "Builder response has no body.", stage);
100
116
  }
101
117
  const reader = response.body.getReader();
102
118
  const chunks = [];
@@ -108,7 +124,7 @@ async function readBytes(response, maxBytes) {
108
124
  break;
109
125
  size += part.value.byteLength;
110
126
  if (size > maxBytes) {
111
- throw new BuilderBranchFilesError("integrity_failed", "Builder readback exceeds its byte limit.");
127
+ throw new BuilderBranchFilesError("integrity_failed", "Builder readback exceeds its byte limit.", stage);
112
128
  }
113
129
  chunks.push(part.value);
114
130
  }
@@ -116,7 +132,7 @@ async function readBytes(response, maxBytes) {
116
132
  catch (error) {
117
133
  if (error instanceof BuilderBranchFilesError)
118
134
  throw error;
119
- throw new BuilderBranchFilesError("unavailable", "Builder response could not be read completely.");
135
+ throw new BuilderBranchFilesError("unavailable", "Builder response could not be read completely.", stage);
120
136
  }
121
137
  finally {
122
138
  await reader.cancel().catch(() => { });
@@ -190,31 +206,54 @@ export async function readBuilderBranchFiles(input) {
190
206
  throw new BuilderBranchFilesError("unavailable", "Builder returned an invalid readback response.");
191
207
  }
192
208
  }
193
- async function readStatus() {
194
- const result = statusSchema.safeParse(await request("/projects/branch/status"));
209
+ async function readStatus(phase) {
210
+ let rawStatus;
211
+ try {
212
+ rawStatus = await request("/projects/branch/status");
213
+ }
214
+ catch (error) {
215
+ if (error instanceof BuilderBranchFilesError) {
216
+ throw new BuilderBranchFilesError(error.code, error.message, statusFailureStages[phase].request);
217
+ }
218
+ throw error;
219
+ }
220
+ const result = statusSchema.safeParse(rawStatus);
195
221
  if (!result.success)
196
- return unverified();
222
+ return unverified(statusFailureStages[phase].shape);
197
223
  const { branch, backupStatuses } = result.data;
198
224
  const backup = backupStatuses.code.backup;
199
225
  if (branch.projectId !== input.projectId ||
200
226
  branch.name !== input.branchName ||
201
227
  backup.projectId !== input.projectId ||
202
228
  backup.branchName !== input.branchName ||
203
- branch.ownerId !== backup.ownerId ||
204
- backup.at < branch.lastActivityAt ||
205
- primaryCommit(branch.lastCommitHash) !== backup.lastCommitHash)
206
- return unverified();
229
+ branch.ownerId !== backup.ownerId) {
230
+ return unverified(statusFailureStages[phase].binding);
231
+ }
232
+ const branchCommit = primaryCommit(branch.lastCommitHash, statusFailureStages[phase].currentness);
233
+ if (backup.at < branch.lastActivityAt ||
234
+ branchCommit !== backup.lastCommitHash)
235
+ return unverified(statusFailureStages[phase].currentness);
207
236
  return backup;
208
237
  }
209
238
  // Export can fall back to an older backup after a refresh error. Only accept
210
239
  // the bytes whose digest and commit still match the branch's recorded state.
211
- const download = downloadSchema.safeParse(await request("/projects/branch/bundle-download-url", {
212
- projectId: input.projectId,
213
- branchName: input.branchName,
214
- }));
240
+ let rawDownload;
241
+ try {
242
+ rawDownload = await request("/projects/branch/bundle-download-url", {
243
+ projectId: input.projectId,
244
+ branchName: input.branchName,
245
+ });
246
+ }
247
+ catch (error) {
248
+ if (error instanceof BuilderBranchFilesError) {
249
+ throw new BuilderBranchFilesError(error.code, error.message, "download_url_request");
250
+ }
251
+ throw error;
252
+ }
253
+ const download = downloadSchema.safeParse(rawDownload);
215
254
  if (!download.success)
216
- return unverified();
217
- const before = await readStatus();
255
+ return unverified("download_url_shape");
256
+ const before = await readStatus("before_read");
218
257
  const beforeFingerprint = backupIdentityFingerprint(before);
219
258
  const filePath = before.ownerId +
220
259
  "/" +
@@ -229,7 +268,7 @@ export async function readBuilderBranchFiles(input) {
229
268
  storagePath = decodeURIComponent(url.pathname);
230
269
  }
231
270
  catch {
232
- return unverified();
271
+ return unverified("download_url_binding");
233
272
  }
234
273
  if (download.data.filePath !== filePath ||
235
274
  url.origin !== "https://storage.googleapis.com" ||
@@ -240,7 +279,7 @@ export async function readBuilderBranchFiles(input) {
240
279
  !Number.isFinite(expiresAt) ||
241
280
  expiresAt <= Date.now() ||
242
281
  expiresAt > Date.now() + 25 * 60 * 60 * 1000)
243
- return unverified();
282
+ return unverified("download_url_binding");
244
283
  let response;
245
284
  try {
246
285
  response = await ssrfSafeFetch(url.toString(), { signal: AbortSignal.timeout(API_TIMEOUT_MS) }, {
@@ -254,16 +293,16 @@ export async function readBuilderBranchFiles(input) {
254
293
  });
255
294
  }
256
295
  catch {
257
- throw new BuilderBranchFilesError("unavailable", "Builder branch bundle could not be downloaded.");
296
+ throw new BuilderBranchFilesError("unavailable", "Builder branch bundle could not be downloaded.", "bundle_download");
258
297
  }
259
298
  if (!response.ok) {
260
299
  await response.body?.cancel().catch(() => { });
261
- throw new BuilderBranchFilesError("unavailable", "Builder branch bundle is unavailable.");
300
+ throw new BuilderBranchFilesError("unavailable", "Builder branch bundle is unavailable.", "bundle_download");
262
301
  }
263
- const bundle = await readBytes(response, MAX_BUNDLE_BYTES);
302
+ const bundle = await readBytes(response, MAX_BUNDLE_BYTES, "bundle_read");
264
303
  if (bundle.byteLength !== before.size ||
265
304
  createHash("md5").update(bundle).digest("base64") !== before.contentMd5) {
266
- throw new BuilderBranchFilesError("integrity_failed", "Builder branch bundle failed its recorded byte integrity check.");
305
+ throw new BuilderBranchFilesError("integrity_failed", "Builder branch bundle failed its recorded byte integrity check.", "bundle_digest");
267
306
  }
268
307
  let snapshot;
269
308
  try {
@@ -276,11 +315,13 @@ export async function readBuilderBranchFiles(input) {
276
315
  catch (error) {
277
316
  throw new BuilderBranchFilesError(error instanceof BuilderGitBundleError && error.code === "unavailable"
278
317
  ? "unavailable"
279
- : "integrity_failed", "Builder branch files could not be verified from the bundle.");
318
+ : "integrity_failed", "Builder branch files could not be verified from the bundle.", error instanceof BuilderGitBundleError && error.code === "target_missing"
319
+ ? "target_subtree_missing"
320
+ : "bundle_parse");
280
321
  }
281
- const after = await readStatus();
322
+ const after = await readStatus("after_read");
282
323
  if (backupIdentityFingerprint(after) !== beforeFingerprint)
283
- return unverified();
324
+ return unverified("post_read_currentness");
284
325
  return {
285
326
  projectId: input.projectId,
286
327
  branchName: input.branchName,
@@ -1,4 +1,4 @@
1
- export type BuilderGitBundleErrorCode = "invalid_request" | "integrity_failed" | "unavailable";
1
+ export type BuilderGitBundleErrorCode = "invalid_request" | "integrity_failed" | "target_missing" | "unavailable";
2
2
  export declare class BuilderGitBundleError extends Error {
3
3
  readonly code: BuilderGitBundleErrorCode;
4
4
  constructor(code: BuilderGitBundleErrorCode);
@@ -100,6 +100,8 @@ async function findTargetTree(gitApi, root, gitdir, rootTreeOid, appPath, cache)
100
100
  const current = await readTree(gitApi, root, gitdir, treeOid, MAX_TREE_ENTRIES - traversedEntries, cache);
101
101
  traversedEntries += current.entryCount;
102
102
  const matching = current.tree.tree.filter((entry) => entry.path === segment);
103
+ if (matching.length === 0)
104
+ fail("target_missing");
103
105
  if (matching.length !== 1)
104
106
  fail("integrity_failed");
105
107
  const entry = matching.at(0);
@@ -192,7 +192,24 @@ export declare function getBuilderAppHost(): string;
192
192
  export declare function getBuilderApiHost(): string;
193
193
  export declare function getBuilderBranchProjectId(): string;
194
194
  export declare function isBuilderBranchingEnabled(): boolean;
195
+ type BuilderBranchProjectProvider = () => string | undefined | Promise<string | undefined>;
196
+ /**
197
+ * A host that binds each view to a Builder project (Design, mounted inside
198
+ * Builder) names that project here, so a code-change handoff targets the
199
+ * project the user is looking at rather than one deployment-wide setting.
200
+ * Return `undefined` when the request has no bound project; the usual
201
+ * Dispatch, env and secret lookups then apply. See
202
+ * `resolveBuilderHandoffProjectId`.
203
+ */
204
+ export declare function registerBuilderBranchProject(provider: BuilderBranchProjectProvider | null): void;
195
205
  export declare function resolveBuilderBranchProjectId(): Promise<string>;
206
+ /**
207
+ * The project a code-change handoff targets: the one the host binds to the
208
+ * current view when it names one, otherwise the deployment's configured
209
+ * project. Only the handoff uses this. Realtime channels and Builder status
210
+ * keep the deployment's project, so a view bound elsewhere cannot move them.
211
+ */
212
+ export declare function resolveBuilderHandoffProjectId(): Promise<string>;
196
213
  export declare function resolveIsBuilderBranchingEnabled(): Promise<boolean>;
197
214
  export declare const BUILDER_OPENER_PARAM = "_an_opener";
198
215
  /**
@@ -766,6 +766,18 @@ async function resolveDispatchBuilderProjectId() {
766
766
  return { status: "unreadable" };
767
767
  }
768
768
  }
769
+ let builderBranchProjectProvider = null;
770
+ /**
771
+ * A host that binds each view to a Builder project (Design, mounted inside
772
+ * Builder) names that project here, so a code-change handoff targets the
773
+ * project the user is looking at rather than one deployment-wide setting.
774
+ * Return `undefined` when the request has no bound project; the usual
775
+ * Dispatch, env and secret lookups then apply. See
776
+ * `resolveBuilderHandoffProjectId`.
777
+ */
778
+ export function registerBuilderBranchProject(provider) {
779
+ builderBranchProjectProvider = provider;
780
+ }
769
781
  export async function resolveBuilderBranchProjectId() {
770
782
  const dispatchProject = await resolveDispatchBuilderProjectId();
771
783
  if (dispatchProject.status === "unreadable")
@@ -792,6 +804,27 @@ export async function resolveBuilderBranchProjectId() {
792
804
  }
793
805
  return "";
794
806
  }
807
+ /**
808
+ * The project a code-change handoff targets: the one the host binds to the
809
+ * current view when it names one, otherwise the deployment's configured
810
+ * project. Only the handoff uses this. Realtime channels and Builder status
811
+ * keep the deployment's project, so a view bound elsewhere cannot move them.
812
+ */
813
+ export async function resolveBuilderHandoffProjectId() {
814
+ if (builderBranchProjectProvider) {
815
+ try {
816
+ const hostProjectId = (await builderBranchProjectProvider())?.trim();
817
+ if (hostProjectId)
818
+ return hostProjectId;
819
+ }
820
+ catch {
821
+ // coercion-ok: fails closed like an unreadable Dispatch setting, so no
822
+ // other project stands in for the one the host could not name.
823
+ return "";
824
+ }
825
+ }
826
+ return resolveBuilderBranchProjectId();
827
+ }
795
828
  export async function resolveIsBuilderBranchingEnabled() {
796
829
  return !!(await resolveBuilderBranchProjectId());
797
830
  }