@agent-native/core 0.78.1 → 0.78.3

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.
@@ -1,5 +1,17 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.78.3
4
+
5
+ ### Patch Changes
6
+
7
+ - a396d62: Resolve MCP tool caller org scope from the verified user email when a token has no explicit org claim, so org-scoped actions return the same resources agents can see in the UI.
8
+
9
+ ## 0.78.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 8a6522a: fix(agent): make the durable background-function worker reliably claim its run for heavy apps (analytics). Two changes: (1) the per-run context now carries `isBackgroundWorker`, set before the system prompt is built, so template `extraContext`/prompt builders can skip heavy, hang-prone enrichment in the worker — the analytics data-dictionary read+render (which ran eagerly during prompt construction, before any pre-send timeout could arm) is now skipped in the worker, while the foreground keeps the full dictionary; (2) the pre-send context cap now takes thunks instead of eagerly-created promises, so each step runs inside an already-armed timeout (an eager promise could start and stall the event loop before the cap wrapped it) and a stalled step is recorded as `presend_timeout:<label>` for attribution. Foreground behavior is unchanged.
14
+
3
15
  ## 0.78.1
4
16
 
5
17
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.78.1",
3
+ "version": "0.78.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -3978,6 +3978,11 @@ export function createProductionAgentHandler(
3978
3978
  if (requestRunCtx) {
3979
3979
  requestRunCtx.browserTabId = requestBrowserTabId;
3980
3980
  requestRunCtx.chatScope = requestChatScope;
3981
+ // Let template extraContext / system-prompt builders detect the durable
3982
+ // background worker so they can skip heavy hang-prone enrichment (e.g. the
3983
+ // analytics data-dictionary read) that otherwise stalls the worker before
3984
+ // it claims its run. Set early — before the system-prompt build runs.
3985
+ requestRunCtx.isBackgroundWorker = isBackgroundWorker;
3981
3986
  }
3982
3987
  const requestMode: AgentExecutionMode =
3983
3988
  body.mode === "plan" ? "plan" : "act";
@@ -4212,268 +4217,288 @@ export function createProductionAgentHandler(
4212
4217
  // Run all independent pre-send steps in parallel. Each of these hits
4213
4218
  // the DB or invokes an action; running them sequentially was the
4214
4219
  // single biggest contributor to pre-LLM latency.
4215
- const enrichedMessagePromise = enrichMessage(requestMessage, references);
4216
- const loopSettingsPromise = readAgentLoopSettings({
4217
- userEmail: ownerEmail ?? getRequestUserEmail() ?? null,
4218
- orgId: getRequestOrgId() ?? null,
4219
- }).catch(() => readAgentLoopSettings({}));
4220
+ const enrichedMessageThunk = () =>
4221
+ enrichMessage(requestMessage, references);
4222
+ const loopSettingsThunk = () =>
4223
+ readAgentLoopSettings({
4224
+ userEmail: ownerEmail ?? getRequestUserEmail() ?? null,
4225
+ orgId: getRequestOrgId() ?? null,
4226
+ }).catch(() => readAgentLoopSettings({}));
4220
4227
 
4221
4228
  let systemPromptError: any = null;
4222
- const systemPromptPromise = (async (): Promise<string> => {
4223
- const sysPromptStart = Date.now();
4224
- try {
4225
- return typeof options.systemPrompt === "function"
4226
- ? await options.systemPrompt(event)
4227
- : options.systemPrompt;
4228
- } catch (error) {
4229
- systemPromptError = error;
4230
- return "";
4231
- } finally {
4232
- setupMarks.sysPromptMs = Date.now() - sysPromptStart;
4233
- }
4234
- })();
4229
+ const systemPromptThunk = (): Promise<string> =>
4230
+ (async (): Promise<string> => {
4231
+ const sysPromptStart = Date.now();
4232
+ try {
4233
+ return typeof options.systemPrompt === "function"
4234
+ ? await options.systemPrompt(event)
4235
+ : options.systemPrompt;
4236
+ } catch (error) {
4237
+ systemPromptError = error;
4238
+ return "";
4239
+ } finally {
4240
+ setupMarks.sysPromptMs = Date.now() - sysPromptStart;
4241
+ }
4242
+ })();
4235
4243
 
4236
- const screenContextPromise = (async (): Promise<string> => {
4237
- const screenStart = Date.now();
4238
- try {
4239
- const viewScreenAction = resolvedActions["view-screen"];
4240
- if (viewScreenAction) {
4241
- const result = await viewScreenAction.run(
4242
- {},
4243
- {
4244
- userEmail: getRequestUserEmail(),
4245
- orgId: getRequestOrgId() ?? null,
4246
- caller: "tool",
4247
- },
4248
- );
4249
- if (result && result !== "(no output)") {
4250
- const screenText =
4251
- typeof result === "string"
4252
- ? result
4253
- : JSON.stringify(result, null, 2);
4254
- return `\n\n<current-screen>\n${capScreenContext(screenText)}\n</current-screen>`;
4255
- }
4256
- } else {
4257
- const navigation = await readAppStateForBrowserTab(
4258
- "navigation",
4259
- requestBrowserTabId,
4260
- );
4261
- if (navigation) {
4262
- return `\n\n<current-screen>\n${capScreenContext(JSON.stringify(navigation, null, 2))}\n</current-screen>`;
4244
+ const screenContextThunk = (): Promise<string> =>
4245
+ (async (): Promise<string> => {
4246
+ const screenStart = Date.now();
4247
+ try {
4248
+ const viewScreenAction = resolvedActions["view-screen"];
4249
+ if (viewScreenAction) {
4250
+ const result = await viewScreenAction.run(
4251
+ {},
4252
+ {
4253
+ userEmail: getRequestUserEmail(),
4254
+ orgId: getRequestOrgId() ?? null,
4255
+ caller: "tool",
4256
+ },
4257
+ );
4258
+ if (result && result !== "(no output)") {
4259
+ const screenText =
4260
+ typeof result === "string"
4261
+ ? result
4262
+ : JSON.stringify(result, null, 2);
4263
+ return `\n\n<current-screen>\n${capScreenContext(screenText)}\n</current-screen>`;
4264
+ }
4265
+ } else {
4266
+ const navigation = await readAppStateForBrowserTab(
4267
+ "navigation",
4268
+ requestBrowserTabId,
4269
+ );
4270
+ if (navigation) {
4271
+ return `\n\n<current-screen>\n${capScreenContext(JSON.stringify(navigation, null, 2))}\n</current-screen>`;
4272
+ }
4263
4273
  }
4274
+ } catch {
4275
+ // DB not ready or no navigation state — skip silently
4276
+ } finally {
4277
+ setupMarks.screenMs = Date.now() - screenStart;
4264
4278
  }
4265
- } catch {
4266
- // DB not ready or no navigation state — skip silently
4267
- } finally {
4268
- setupMarks.screenMs = Date.now() - screenStart;
4269
- }
4270
- return "";
4271
- })();
4279
+ return "";
4280
+ })();
4272
4281
 
4273
- const urlContextPromise = (async (): Promise<string> => {
4274
- try {
4275
- const url = (await readAppStateForBrowserTab(
4276
- "__url__",
4277
- requestBrowserTabId,
4278
- )) as {
4279
- pathname?: string;
4280
- search?: string;
4281
- hash?: string;
4282
- searchParams?: Record<string, string>;
4283
- } | null;
4284
- if (url && (url.pathname || url.search || url.hash)) {
4285
- const lines: string[] = [];
4286
- if (url.pathname) lines.push(`pathname: ${url.pathname}`);
4287
- const extensionId = url.pathname
4288
- ? extensionIdFromPathname(url.pathname)
4289
- : null;
4290
- if (extensionId) lines.push(`extensionId: ${extensionId}`);
4291
- if (url.search) lines.push(`search: ${url.search}`);
4292
- if (url.hash) lines.push(`hash: ${url.hash}`);
4293
- if (url.searchParams && Object.keys(url.searchParams).length > 0) {
4294
- lines.push("searchParams:");
4295
- for (const [k, v] of Object.entries(url.searchParams)) {
4296
- lines.push(` ${k}: ${v}`);
4282
+ const urlContextThunk = (): Promise<string> =>
4283
+ (async (): Promise<string> => {
4284
+ try {
4285
+ const url = (await readAppStateForBrowserTab(
4286
+ "__url__",
4287
+ requestBrowserTabId,
4288
+ )) as {
4289
+ pathname?: string;
4290
+ search?: string;
4291
+ hash?: string;
4292
+ searchParams?: Record<string, string>;
4293
+ } | null;
4294
+ if (url && (url.pathname || url.search || url.hash)) {
4295
+ const lines: string[] = [];
4296
+ if (url.pathname) lines.push(`pathname: ${url.pathname}`);
4297
+ const extensionId = url.pathname
4298
+ ? extensionIdFromPathname(url.pathname)
4299
+ : null;
4300
+ if (extensionId) lines.push(`extensionId: ${extensionId}`);
4301
+ if (url.search) lines.push(`search: ${url.search}`);
4302
+ if (url.hash) lines.push(`hash: ${url.hash}`);
4303
+ if (url.searchParams && Object.keys(url.searchParams).length > 0) {
4304
+ lines.push("searchParams:");
4305
+ for (const [k, v] of Object.entries(url.searchParams)) {
4306
+ lines.push(` ${k}: ${v}`);
4307
+ }
4297
4308
  }
4309
+ return `\n\n<current-url>\n${lines.join("\n")}\n</current-url>`;
4298
4310
  }
4299
- return `\n\n<current-url>\n${lines.join("\n")}\n</current-url>`;
4311
+ } catch {
4312
+ // DB not ready — skip silently
4300
4313
  }
4301
- } catch {
4302
- // DB not ready — skip silently
4303
- }
4304
- return "";
4305
- })();
4314
+ return "";
4315
+ })();
4306
4316
 
4307
4317
  // Selection context: written by the client when the user presses Cmd+I
4308
4318
  // with text selected on the page. Treat anything older than 5 minutes
4309
4319
  // as stale and ignore it.
4310
4320
  const SELECTION_TTL_MS = 5 * 60 * 1000;
4311
- const selectionContextPromise = (async (): Promise<string> => {
4312
- try {
4313
- const sel = (await readAppState("pending-selection-context")) as {
4314
- text?: string;
4315
- capturedAt?: number;
4316
- } | null;
4317
- if (!sel?.text) return "";
4318
- const capturedAt =
4319
- typeof sel.capturedAt === "number" ? sel.capturedAt : 0;
4320
- if (Date.now() - capturedAt > SELECTION_TTL_MS) return "";
4321
- return (
4322
- `\n\nThe user has selected the following text and pressed Cmd+I to focus the agent. ` +
4323
- `Treat this as the immediate context to act on:\n` +
4324
- `<selection>\n${capSelectionContext(sel.text)}\n</selection>`
4325
- );
4326
- } catch {
4327
- // DB not ready — skip silently
4328
- }
4329
- return "";
4330
- })();
4321
+ const selectionContextThunk = (): Promise<string> =>
4322
+ (async (): Promise<string> => {
4323
+ try {
4324
+ const sel = (await readAppState("pending-selection-context")) as {
4325
+ text?: string;
4326
+ capturedAt?: number;
4327
+ } | null;
4328
+ if (!sel?.text) return "";
4329
+ const capturedAt =
4330
+ typeof sel.capturedAt === "number" ? sel.capturedAt : 0;
4331
+ if (Date.now() - capturedAt > SELECTION_TTL_MS) return "";
4332
+ return (
4333
+ `\n\nThe user has selected the following text and pressed Cmd+I to focus the agent. ` +
4334
+ `Treat this as the immediate context to act on:\n` +
4335
+ `<selection>\n${capSelectionContext(sel.text)}\n</selection>`
4336
+ );
4337
+ } catch {
4338
+ // DB not ready — skip silently
4339
+ }
4340
+ return "";
4341
+ })();
4331
4342
 
4332
4343
  // On the first message of a conversation, inject workspace inventory
4333
4344
  // so the agent knows what files, skills, jobs, and custom agents exist.
4334
4345
  // Templates can opt out via `skipFilesContext: true` when the inventory
4335
4346
  // is unrelated to the app's job (e.g. a voice-first macro tracker).
4336
- const filesContextPromise = (async (): Promise<string> => {
4337
- let filesContext = "";
4338
- if (options.skipFilesContext) return filesContext;
4339
- if (history.length === 0) {
4340
- try {
4341
- const {
4342
- resourceListAccessible,
4343
- SHARED_OWNER,
4344
- WORKSPACE_OWNER,
4345
- resourceGet,
4346
- } = await import("../resources/store.js");
4347
- const {
4348
- getResourceKind,
4349
- parseCustomAgentProfile,
4350
- parseRemoteAgentManifest,
4351
- parseSkillMetadata,
4352
- } = await import("../resources/metadata.js");
4353
- const ownerEmail = getRequestUserEmail();
4354
- const orgId = getRequestOrgId();
4355
- if (!ownerEmail) throw new Error("no authenticated user");
4356
- const allResources = await resourceListAccessible(
4357
- ownerEmail,
4358
- undefined,
4359
- { userEmail: ownerEmail, orgId },
4360
- );
4347
+ const filesContextThunk = (): Promise<string> =>
4348
+ (async (): Promise<string> => {
4349
+ let filesContext = "";
4350
+ if (options.skipFilesContext) return filesContext;
4351
+ if (history.length === 0) {
4352
+ try {
4353
+ const {
4354
+ resourceListAccessible,
4355
+ SHARED_OWNER,
4356
+ WORKSPACE_OWNER,
4357
+ resourceGet,
4358
+ } = await import("../resources/store.js");
4359
+ const {
4360
+ getResourceKind,
4361
+ parseCustomAgentProfile,
4362
+ parseRemoteAgentManifest,
4363
+ parseSkillMetadata,
4364
+ } = await import("../resources/metadata.js");
4365
+ const ownerEmail = getRequestUserEmail();
4366
+ const orgId = getRequestOrgId();
4367
+ if (!ownerEmail) throw new Error("no authenticated user");
4368
+ const allResources = await resourceListAccessible(
4369
+ ownerEmail,
4370
+ undefined,
4371
+ { userEmail: ownerEmail, orgId },
4372
+ );
4361
4373
 
4362
- if (allResources.length > 0) {
4363
- const fileLines: string[] = [];
4364
- const skillLines: string[] = [];
4365
- const agentLines: string[] = [];
4366
- const jobLines: string[] = [];
4367
- for (const r of allResources) {
4368
- const scope =
4369
- r.owner === WORKSPACE_OWNER
4370
- ? "workspace"
4371
- : r.owner === SHARED_OWNER
4372
- ? "shared"
4373
- : "personal";
4374
- const kind = getResourceKind(r.path);
4375
- if (kind === "file") {
4376
- fileLines.push(` ${r.path} (${scope})`);
4377
- continue;
4378
- }
4374
+ if (allResources.length > 0) {
4375
+ const fileLines: string[] = [];
4376
+ const skillLines: string[] = [];
4377
+ const agentLines: string[] = [];
4378
+ const jobLines: string[] = [];
4379
+ for (const r of allResources) {
4380
+ const scope =
4381
+ r.owner === WORKSPACE_OWNER
4382
+ ? "workspace"
4383
+ : r.owner === SHARED_OWNER
4384
+ ? "shared"
4385
+ : "personal";
4386
+ const kind = getResourceKind(r.path);
4387
+ if (kind === "file") {
4388
+ fileLines.push(` ${r.path} (${scope})`);
4389
+ continue;
4390
+ }
4379
4391
 
4380
- if (kind === "job") {
4381
- jobLines.push(` ${r.path} (${scope})`);
4382
- continue;
4383
- }
4392
+ if (kind === "job") {
4393
+ jobLines.push(` ${r.path} (${scope})`);
4394
+ continue;
4395
+ }
4384
4396
 
4385
- if (
4386
- kind === "skill" ||
4387
- kind === "agent" ||
4388
- kind === "remote-agent"
4389
- ) {
4390
- const full = await resourceGet(r.id, {
4391
- userEmail: ownerEmail,
4392
- orgId,
4393
- });
4394
- if (!full) continue;
4395
- if (kind === "skill") {
4396
- const skill = parseSkillMetadata(full.content, r.path);
4397
- skillLines.push(
4398
- ` ${skill?.name || r.path} — ${compactInventoryDescription(skill?.description || r.path)} (${scope}, ${r.path})`,
4399
- );
4400
- } else if (kind === "agent") {
4401
- const agent = parseCustomAgentProfile(full.content, r.path);
4402
- agentLines.push(
4403
- ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Custom workspace agent")} (${scope}, ${r.path}${agent?.model ? `, model: ${agent.model}` : ""})`,
4404
- );
4405
- } else {
4406
- const agent = parseRemoteAgentManifest(full.content, r.path);
4407
- agentLines.push(
4408
- ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Connected A2A agent")} (${scope}, remote via ${r.path})`,
4409
- );
4397
+ if (
4398
+ kind === "skill" ||
4399
+ kind === "agent" ||
4400
+ kind === "remote-agent"
4401
+ ) {
4402
+ const full = await resourceGet(r.id, {
4403
+ userEmail: ownerEmail,
4404
+ orgId,
4405
+ });
4406
+ if (!full) continue;
4407
+ if (kind === "skill") {
4408
+ const skill = parseSkillMetadata(full.content, r.path);
4409
+ skillLines.push(
4410
+ ` ${skill?.name || r.path} — ${compactInventoryDescription(skill?.description || r.path)} (${scope}, ${r.path})`,
4411
+ );
4412
+ } else if (kind === "agent") {
4413
+ const agent = parseCustomAgentProfile(full.content, r.path);
4414
+ agentLines.push(
4415
+ ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Custom workspace agent")} (${scope}, ${r.path}${agent?.model ? `, model: ${agent.model}` : ""})`,
4416
+ );
4417
+ } else {
4418
+ const agent = parseRemoteAgentManifest(
4419
+ full.content,
4420
+ r.path,
4421
+ );
4422
+ agentLines.push(
4423
+ ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Connected A2A agent")} (${scope}, remote via ${r.path})`,
4424
+ );
4425
+ }
4410
4426
  }
4411
4427
  }
4428
+ const blocks: string[] = [];
4429
+ if (fileLines.length > 0) {
4430
+ const lines = limitInventoryLines(fileLines, "files");
4431
+ blocks.push(
4432
+ `<available-files>\nFiles in the workspace:\n${lines.join("\n")}\n\nTo read a resource file's contents, use the resources tool with action "read" and the file path.\n</available-files>`,
4433
+ );
4434
+ }
4435
+ if (skillLines.length > 0) {
4436
+ const lines = limitInventoryLines(skillLines, "skills");
4437
+ blocks.push(
4438
+ `<available-skills>\nSkills in the workspace:\n${lines.join("\n")}\n\nBefore using a matching workspace skill, read its path with the resources tool using action "read"; slash-selected skills are inlined automatically when available.\n</available-skills>`,
4439
+ );
4440
+ }
4441
+ if (agentLines.length > 0) {
4442
+ const lines = limitInventoryLines(agentLines, "agents");
4443
+ blocks.push(
4444
+ `<available-agents>\nCustom and connected agents in the workspace:\n${lines.join("\n")}\n\nCustom agents under agents/*.md can be mentioned or used via agent-teams (action: "spawn") with the agent parameter.\n</available-agents>`,
4445
+ );
4446
+ }
4447
+ if (jobLines.length > 0) {
4448
+ const lines = limitInventoryLines(jobLines, "jobs");
4449
+ blocks.push(
4450
+ `<available-jobs>\nScheduled tasks in the workspace:\n${lines.join("\n")}\n</available-jobs>`,
4451
+ );
4452
+ }
4453
+ filesContext =
4454
+ blocks.length > 0 ? `\n\n${blocks.join("\n\n")}` : "";
4412
4455
  }
4413
- const blocks: string[] = [];
4414
- if (fileLines.length > 0) {
4415
- const lines = limitInventoryLines(fileLines, "files");
4416
- blocks.push(
4417
- `<available-files>\nFiles in the workspace:\n${lines.join("\n")}\n\nTo read a resource file's contents, use the resources tool with action "read" and the file path.\n</available-files>`,
4418
- );
4419
- }
4420
- if (skillLines.length > 0) {
4421
- const lines = limitInventoryLines(skillLines, "skills");
4422
- blocks.push(
4423
- `<available-skills>\nSkills in the workspace:\n${lines.join("\n")}\n\nBefore using a matching workspace skill, read its path with the resources tool using action "read"; slash-selected skills are inlined automatically when available.\n</available-skills>`,
4424
- );
4425
- }
4426
- if (agentLines.length > 0) {
4427
- const lines = limitInventoryLines(agentLines, "agents");
4428
- blocks.push(
4429
- `<available-agents>\nCustom and connected agents in the workspace:\n${lines.join("\n")}\n\nCustom agents under agents/*.md can be mentioned or used via agent-teams (action: "spawn") with the agent parameter.\n</available-agents>`,
4430
- );
4431
- }
4432
- if (jobLines.length > 0) {
4433
- const lines = limitInventoryLines(jobLines, "jobs");
4434
- blocks.push(
4435
- `<available-jobs>\nScheduled tasks in the workspace:\n${lines.join("\n")}\n</available-jobs>`,
4436
- );
4437
- }
4438
- filesContext =
4439
- blocks.length > 0 ? `\n\n${blocks.join("\n\n")}` : "";
4456
+ } catch {
4457
+ // Resources not available — skip silently
4440
4458
  }
4441
- } catch {
4442
- // Resources not available — skip silently
4443
4459
  }
4444
- }
4445
- return filesContext;
4446
- })();
4447
-
4448
- // Durable bg worker: a pre-send DB read that HANGS (rather than erroring)
4449
- // would otherwise stall the worker until the foreground inline-recovery
4450
- // grace (~16s) — wasting the entire 15-min durable budget and leaving the
4451
- // run un-claimed (the exact analytics symptom: diag stuck at model_done,
4452
- // preStart≈18s). Cap each pre-send step so a hang degrades to a safe default
4453
- // and the worker proceeds to claim + run. The cap fires only when the event
4454
- // loop is free, so it also distinguishes an async hang (cap fires → worker
4455
- // claims) from an event-loop block (cap can't fire still frozen). The
4456
- // foreground keeps the un-capped path, so its behaviour is unchanged. On a
4457
- // rejection (e.g. enrichMessage has no .catch) the cap also resolves to the
4458
- // fallback rather than rejecting the whole Promise.all.
4460
+ return filesContext;
4461
+ })();
4462
+
4463
+ // Durable bg worker: a pre-send step that HANGS (rather than erroring) would
4464
+ // otherwise stall the worker until the foreground inline-recovery grace
4465
+ // (~16s) wasting the entire 15-min durable budget and leaving the run
4466
+ // un-claimed (the exact analytics symptom: diag stuck at model_done,
4467
+ // preStart≈18s). `presendCap` takes a THUNK (not an eagerly-started promise):
4468
+ // the work runs INSIDE the cap, after the timer is armed, so a step whose
4469
+ // own synchronous prefix is heavy can still be timed out an eagerly-created
4470
+ // promise would start (and could block the loop) before the cap ever wrapped
4471
+ // it. On timeout it records `presend_timeout:<label>` so a stalled phase is
4472
+ // attributable, then degrades to the fallback so the worker proceeds to
4473
+ // claim. Foreground keeps the un-capped path (thunk invoked immediately), so
4474
+ // its behaviour is unchanged. A rejected step (e.g. enrichMessage has no
4475
+ // .catch) resolves to the fallback instead of rejecting the whole batch.
4459
4476
  const presendCap = <T>(
4460
- p: Promise<T>,
4477
+ label: string,
4478
+ thunk: () => Promise<T>,
4461
4479
  fallback: T,
4462
4480
  ms: number,
4463
4481
  ): Promise<T> => {
4464
- if (!isBackgroundWorker) return p;
4482
+ if (!isBackgroundWorker) return thunk();
4465
4483
  return new Promise<T>((resolve) => {
4466
- const timer = setTimeout(() => resolve(fallback), ms);
4467
- p.then(
4468
- (v) => {
4469
- clearTimeout(timer);
4470
- resolve(v);
4471
- },
4472
- () => {
4473
- clearTimeout(timer);
4474
- resolve(fallback);
4475
- },
4476
- );
4484
+ const timer = setTimeout(() => {
4485
+ workerStep(`presend_timeout:${label}`);
4486
+ resolve(fallback);
4487
+ }, ms);
4488
+ // Defer invocation one microtask so every sibling cap arms its timer
4489
+ // before any thunk's synchronous prefix runs.
4490
+ void Promise.resolve()
4491
+ .then(thunk)
4492
+ .then(
4493
+ (v) => {
4494
+ clearTimeout(timer);
4495
+ resolve(v);
4496
+ },
4497
+ () => {
4498
+ clearTimeout(timer);
4499
+ resolve(fallback);
4500
+ },
4501
+ );
4477
4502
  });
4478
4503
  };
4479
4504
  const fallbackLoopSettings: AgentLoopSettings = {
@@ -4493,13 +4518,13 @@ export function createProductionAgentHandler(
4493
4518
  loopSettings,
4494
4519
  enrichedMessage,
4495
4520
  ] = await Promise.all([
4496
- presendCap(systemPromptPromise, "", 13000),
4497
- presendCap(screenContextPromise, "", 9000),
4498
- presendCap(urlContextPromise, "", 9000),
4499
- presendCap(selectionContextPromise, "", 9000),
4500
- presendCap(filesContextPromise, "", 12000),
4501
- presendCap(loopSettingsPromise, fallbackLoopSettings, 9000),
4502
- presendCap(enrichedMessagePromise, requestMessage, 9000),
4521
+ presendCap("systemPrompt", systemPromptThunk, "", 13000),
4522
+ presendCap("screen", screenContextThunk, "", 9000),
4523
+ presendCap("url", urlContextThunk, "", 9000),
4524
+ presendCap("selection", selectionContextThunk, "", 9000),
4525
+ presendCap("files", filesContextThunk, "", 12000),
4526
+ presendCap("loopSettings", loopSettingsThunk, fallbackLoopSettings, 9000),
4527
+ presendCap("enrichedMessage", enrichedMessageThunk, requestMessage, 9000),
4503
4528
  ]);
4504
4529
  setupMark("ctxAll");
4505
4530
  // DIAGNOSTIC-ONLY: all parallel context gathering (system prompt, screen,
@@ -5066,6 +5091,13 @@ export function createProductionAgentHandler(
5066
5091
  // DIAGNOSTIC-ONLY: last stage before startRun fires. A worker that reaches
5067
5092
  // prestart but never workerStarted is hanging inside startRun itself.
5068
5093
  workerStep("prestart");
5094
+ // DIAGNOSTIC-ONLY: peak-ish RSS (MB) + assembled system-prompt size (KB) at
5095
+ // prestart. The analytics bg worker dies right after model_done; if the
5096
+ // FOREGROUND (identical build, writes land) is already near the ~1024MB
5097
+ // Netlify function limit, an OOM kill in the heavier worker explains the
5098
+ // freeze. Both numbers ride along in the existing setup-timings detail.
5099
+ setupMarks.rssMB = Math.round(process.memoryUsage().rss / 1048576);
5100
+ setupMarks.promptKB = Math.round((systemPrompt?.length ?? 0) / 1024);
5069
5101
  const setupDetail =
5070
5102
  Object.entries(setupMarks)
5071
5103
  .map(([k, v]) => `${k}=${v}`)
@@ -1340,9 +1340,7 @@ export async function createMCPServerForRequest(
1340
1340
  // in that case we run with no userEmail/orgId, which makes downstream
1341
1341
  // tools that require per-user scope return empty results rather than
1342
1342
  // cross-tenant data (the safe default).
1343
- const orgIdPromise = effectiveIdentity?.orgId
1344
- ? Promise.resolve(effectiveIdentity.orgId)
1345
- : resolveOrgIdFromDomain(effectiveIdentity?.orgDomain);
1343
+ const orgIdPromise = resolveMcpIdentityOrgId(effectiveIdentity);
1346
1344
 
1347
1345
  /**
1348
1346
  * Wrap a callback in
@@ -2066,3 +2064,21 @@ export async function resolveOrgIdFromDomain(
2066
2064
  return undefined;
2067
2065
  }
2068
2066
  }
2067
+
2068
+ export async function resolveMcpIdentityOrgId(
2069
+ identity: MCPCallerIdentity | undefined,
2070
+ ): Promise<string | undefined> {
2071
+ if (identity?.orgId) return identity.orgId;
2072
+
2073
+ const orgIdFromDomain = await resolveOrgIdFromDomain(identity?.orgDomain);
2074
+ if (orgIdFromDomain) return orgIdFromDomain;
2075
+
2076
+ const userEmail = identity?.userEmail?.trim();
2077
+ if (!userEmail) return undefined;
2078
+ try {
2079
+ const { resolveOrgIdForEmail } = await import("../org/context.js");
2080
+ return (await resolveOrgIdForEmail(userEmail)) ?? undefined;
2081
+ } catch {
2082
+ return undefined;
2083
+ }
2084
+ }