@agent-native/core 0.77.14 → 0.77.15

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,20 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.77.15
4
+
5
+ ### Patch Changes
6
+
7
+ - d5ec2d0: Diagnostic + defensive instrumentation for the durable background-agent worker's
8
+ pre-send setup. Adds stdout breadcrumbs (`bgLog`, gated on the worker → Netlify
9
+ function logs, independent of DB writes which stall in the failing case) across
10
+ the post-`model_done` / pre-claim sequence and each parallel pre-send branch
11
+ (enrich, loop settings, system prompt, view-screen, url, selection, files
12
+ inventory) with start/done/error/timeout. The OPTIONAL context reads
13
+ (view-screen, url, selection, files) now race a short timeout with a safe `""`
14
+ fallback so one stuck read cannot block the worker from reaching
15
+ `claimBackgroundRun`. Used to localize the analytics-only worker freeze that the
16
+ DB-based diagnostic can't see.
17
+
3
18
  ## 0.77.14
4
19
 
5
20
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.77.14",
3
+ "version": "0.77.15",
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": {
@@ -3950,6 +3950,58 @@ export function createProductionAgentHandler(
3950
3950
  `${s}=${Date.now() - setupT0}ms`,
3951
3951
  ).catch(() => {});
3952
3952
  };
3953
+ // DIAGNOSTIC-ONLY: non-DB breadcrumb to the function log drain. `workerStep`
3954
+ // writes to the DB, which is exactly what stalls after `model_done` in the bg
3955
+ // worker (no diag write lands), so this logs to stdout (Netlify function
3956
+ // logs) instead — used to name the exact post-model_done / pre-claim async
3957
+ // branch that hangs. Gated on the worker so foreground logs stay clean.
3958
+ const bgLog = (label: string) => {
3959
+ if (!isBackgroundWorker) return;
3960
+ try {
3961
+ console.log(
3962
+ `[bg-presend] +${Date.now() - setupT0}ms run=${(bgRunId ?? "").slice(-6)} ${label}`,
3963
+ );
3964
+ } catch {}
3965
+ };
3966
+ // DIAGNOSTIC + DEFENSIVE: race an optional context read against a short
3967
+ // timeout and fall back to a safe default, logging start/done/error/timeout.
3968
+ // A single stuck optional read can no longer block the worker from reaching
3969
+ // claim, and the TIMEOUT breadcrumb names the culprit in the function logs.
3970
+ const withBgFallback = <T>(
3971
+ label: string,
3972
+ ms: number,
3973
+ fallback: T,
3974
+ fn: () => Promise<T>,
3975
+ ): Promise<T> => {
3976
+ bgLog(`${label}:start`);
3977
+ return new Promise<T>((resolve) => {
3978
+ let done = false;
3979
+ const timer = setTimeout(() => {
3980
+ if (done) return;
3981
+ done = true;
3982
+ bgLog(`${label}:TIMEOUT@${ms}ms`);
3983
+ resolve(fallback);
3984
+ }, ms);
3985
+ fn().then(
3986
+ (v) => {
3987
+ if (done) return;
3988
+ done = true;
3989
+ clearTimeout(timer);
3990
+ bgLog(`${label}:done`);
3991
+ resolve(v);
3992
+ },
3993
+ (e) => {
3994
+ if (done) return;
3995
+ done = true;
3996
+ clearTimeout(timer);
3997
+ bgLog(
3998
+ `${label}:error ${(e as { message?: string })?.message ?? e}`,
3999
+ );
4000
+ resolve(fallback);
4001
+ },
4002
+ );
4003
+ });
4004
+ };
3953
4005
  // Whether this worker is REALLY executing inside a 15-min Netlify
3954
4006
  // `-background` function (proven by the runtime function name), not merely a
3955
4007
  // `_process-run` re-entry that may have landed on the ~60s synchronous
@@ -4160,6 +4212,7 @@ export function createProductionAgentHandler(
4160
4212
  engine.defaultModel;
4161
4213
  // DIAGNOSTIC-ONLY: stored-model resolution finished.
4162
4214
  workerStep("model_done");
4215
+ bgLog("model_done");
4163
4216
  const model = normalizeModelForEngine(engine, modelCandidate);
4164
4217
  const reasoningEffort = normalizeReasoningEffortForModel(
4165
4218
  model,
@@ -4168,7 +4221,9 @@ export function createProductionAgentHandler(
4168
4221
  : options.reasoningEffort,
4169
4222
  );
4170
4223
 
4224
+ bgLog("onEngineResolved:before");
4171
4225
  options.onEngineResolved?.(engine, model);
4226
+ bgLog("onEngineResolved:after");
4172
4227
 
4173
4228
  // One-line per-turn resolution log so it's obvious in dev which engine
4174
4229
  // is actually handling the request. `requestEngine` is what the client
@@ -4206,241 +4261,290 @@ export function createProductionAgentHandler(
4206
4261
  // reached db_request_ctx but not env_config hung in attachment upload or
4207
4262
  // engine/model resolution.
4208
4263
  workerStep("env_config");
4264
+ bgLog("env_config");
4265
+ bgLog("presend:creating");
4209
4266
  // Run all independent pre-send steps in parallel. Each of these hits
4210
4267
  // the DB or invokes an action; running them sequentially was the
4211
- // single biggest contributor to pre-LLM latency.
4212
- const enrichedMessagePromise = enrichMessage(requestMessage, references);
4268
+ // single biggest contributor to pre-LLM latency. Each branch is bracketed
4269
+ // with a log-drain breadcrumb (bgLog) so the function logs name any branch
4270
+ // that starts but never finishes; the OPTIONAL context reads additionally
4271
+ // race a short timeout + "" fallback (withBgFallback) so one stuck read
4272
+ // cannot block the worker from reaching claim.
4273
+ bgLog("enrich:start");
4274
+ const enrichedMessagePromise = Promise.resolve(
4275
+ enrichMessage(requestMessage, references),
4276
+ ).then((v) => {
4277
+ bgLog("enrich:done");
4278
+ return v;
4279
+ });
4280
+ bgLog("loop:start");
4213
4281
  const loopSettingsPromise = readAgentLoopSettings({
4214
4282
  userEmail: ownerEmail ?? getRequestUserEmail() ?? null,
4215
4283
  orgId: getRequestOrgId() ?? null,
4216
- }).catch(() => readAgentLoopSettings({}));
4284
+ })
4285
+ .catch(() => readAgentLoopSettings({}))
4286
+ .then((v) => {
4287
+ bgLog("loop:done");
4288
+ return v;
4289
+ });
4217
4290
 
4218
4291
  let systemPromptError: any = null;
4219
4292
  const systemPromptPromise = (async (): Promise<string> => {
4220
4293
  const sysPromptStart = Date.now();
4294
+ bgLog("sysprompt:start");
4221
4295
  try {
4222
- return typeof options.systemPrompt === "function"
4223
- ? await options.systemPrompt(event)
4224
- : options.systemPrompt;
4296
+ const sp =
4297
+ typeof options.systemPrompt === "function"
4298
+ ? await options.systemPrompt(event)
4299
+ : options.systemPrompt;
4300
+ bgLog("sysprompt:done");
4301
+ return sp;
4225
4302
  } catch (error) {
4226
4303
  systemPromptError = error;
4304
+ bgLog(
4305
+ "sysprompt:error " +
4306
+ ((error as { message?: string })?.message ?? error),
4307
+ );
4227
4308
  return "";
4228
4309
  } finally {
4229
4310
  setupMarks.sysPromptMs = Date.now() - sysPromptStart;
4230
4311
  }
4231
4312
  })();
4232
4313
 
4233
- const screenContextPromise = (async (): Promise<string> => {
4234
- const screenStart = Date.now();
4235
- try {
4236
- const viewScreenAction = resolvedActions["view-screen"];
4237
- if (viewScreenAction) {
4238
- const result = await viewScreenAction.run(
4239
- {},
4240
- {
4241
- userEmail: getRequestUserEmail(),
4242
- orgId: getRequestOrgId() ?? null,
4243
- caller: "tool",
4244
- },
4245
- );
4246
- if (result && result !== "(no output)") {
4247
- const screenText =
4248
- typeof result === "string"
4249
- ? result
4250
- : JSON.stringify(result, null, 2);
4251
- return `\n\n<current-screen>\n${capScreenContext(screenText)}\n</current-screen>`;
4252
- }
4253
- } else {
4254
- const navigation = await readAppStateForBrowserTab(
4255
- "navigation",
4256
- requestBrowserTabId,
4257
- );
4258
- if (navigation) {
4259
- return `\n\n<current-screen>\n${capScreenContext(JSON.stringify(navigation, null, 2))}\n</current-screen>`;
4314
+ const screenContextPromise = withBgFallback(
4315
+ "screen",
4316
+ 6000,
4317
+ "",
4318
+ async (): Promise<string> => {
4319
+ const screenStart = Date.now();
4320
+ try {
4321
+ const viewScreenAction = resolvedActions["view-screen"];
4322
+ if (viewScreenAction) {
4323
+ const result = await viewScreenAction.run(
4324
+ {},
4325
+ {
4326
+ userEmail: getRequestUserEmail(),
4327
+ orgId: getRequestOrgId() ?? null,
4328
+ caller: "tool",
4329
+ },
4330
+ );
4331
+ if (result && result !== "(no output)") {
4332
+ const screenText =
4333
+ typeof result === "string"
4334
+ ? result
4335
+ : JSON.stringify(result, null, 2);
4336
+ return `\n\n<current-screen>\n${capScreenContext(screenText)}\n</current-screen>`;
4337
+ }
4338
+ } else {
4339
+ const navigation = await readAppStateForBrowserTab(
4340
+ "navigation",
4341
+ requestBrowserTabId,
4342
+ );
4343
+ if (navigation) {
4344
+ return `\n\n<current-screen>\n${capScreenContext(JSON.stringify(navigation, null, 2))}\n</current-screen>`;
4345
+ }
4260
4346
  }
4347
+ } catch {
4348
+ // DB not ready or no navigation state — skip silently
4349
+ } finally {
4350
+ setupMarks.screenMs = Date.now() - screenStart;
4261
4351
  }
4262
- } catch {
4263
- // DB not ready or no navigation state — skip silently
4264
- } finally {
4265
- setupMarks.screenMs = Date.now() - screenStart;
4266
- }
4267
- return "";
4268
- })();
4352
+ return "";
4353
+ },
4354
+ );
4269
4355
 
4270
- const urlContextPromise = (async (): Promise<string> => {
4271
- try {
4272
- const url = (await readAppStateForBrowserTab(
4273
- "__url__",
4274
- requestBrowserTabId,
4275
- )) as {
4276
- pathname?: string;
4277
- search?: string;
4278
- hash?: string;
4279
- searchParams?: Record<string, string>;
4280
- } | null;
4281
- if (url && (url.pathname || url.search || url.hash)) {
4282
- const lines: string[] = [];
4283
- if (url.pathname) lines.push(`pathname: ${url.pathname}`);
4284
- const extensionId = url.pathname
4285
- ? extensionIdFromPathname(url.pathname)
4286
- : null;
4287
- if (extensionId) lines.push(`extensionId: ${extensionId}`);
4288
- if (url.search) lines.push(`search: ${url.search}`);
4289
- if (url.hash) lines.push(`hash: ${url.hash}`);
4290
- if (url.searchParams && Object.keys(url.searchParams).length > 0) {
4291
- lines.push("searchParams:");
4292
- for (const [k, v] of Object.entries(url.searchParams)) {
4293
- lines.push(` ${k}: ${v}`);
4356
+ const urlContextPromise = withBgFallback(
4357
+ "url",
4358
+ 6000,
4359
+ "",
4360
+ async (): Promise<string> => {
4361
+ try {
4362
+ const url = (await readAppStateForBrowserTab(
4363
+ "__url__",
4364
+ requestBrowserTabId,
4365
+ )) as {
4366
+ pathname?: string;
4367
+ search?: string;
4368
+ hash?: string;
4369
+ searchParams?: Record<string, string>;
4370
+ } | null;
4371
+ if (url && (url.pathname || url.search || url.hash)) {
4372
+ const lines: string[] = [];
4373
+ if (url.pathname) lines.push(`pathname: ${url.pathname}`);
4374
+ const extensionId = url.pathname
4375
+ ? extensionIdFromPathname(url.pathname)
4376
+ : null;
4377
+ if (extensionId) lines.push(`extensionId: ${extensionId}`);
4378
+ if (url.search) lines.push(`search: ${url.search}`);
4379
+ if (url.hash) lines.push(`hash: ${url.hash}`);
4380
+ if (url.searchParams && Object.keys(url.searchParams).length > 0) {
4381
+ lines.push("searchParams:");
4382
+ for (const [k, v] of Object.entries(url.searchParams)) {
4383
+ lines.push(` ${k}: ${v}`);
4384
+ }
4294
4385
  }
4386
+ return `\n\n<current-url>\n${lines.join("\n")}\n</current-url>`;
4295
4387
  }
4296
- return `\n\n<current-url>\n${lines.join("\n")}\n</current-url>`;
4388
+ } catch {
4389
+ // DB not ready — skip silently
4297
4390
  }
4298
- } catch {
4299
- // DB not ready — skip silently
4300
- }
4301
- return "";
4302
- })();
4391
+ return "";
4392
+ },
4393
+ );
4303
4394
 
4304
4395
  // Selection context: written by the client when the user presses Cmd+I
4305
4396
  // with text selected on the page. Treat anything older than 5 minutes
4306
4397
  // as stale and ignore it.
4307
4398
  const SELECTION_TTL_MS = 5 * 60 * 1000;
4308
- const selectionContextPromise = (async (): Promise<string> => {
4309
- try {
4310
- const sel = (await readAppState("pending-selection-context")) as {
4311
- text?: string;
4312
- capturedAt?: number;
4313
- } | null;
4314
- if (!sel?.text) return "";
4315
- const capturedAt =
4316
- typeof sel.capturedAt === "number" ? sel.capturedAt : 0;
4317
- if (Date.now() - capturedAt > SELECTION_TTL_MS) return "";
4318
- return (
4319
- `\n\nThe user has selected the following text and pressed Cmd+I to focus the agent. ` +
4320
- `Treat this as the immediate context to act on:\n` +
4321
- `<selection>\n${capSelectionContext(sel.text)}\n</selection>`
4322
- );
4323
- } catch {
4324
- // DB not ready skip silently
4325
- }
4326
- return "";
4327
- })();
4399
+ const selectionContextPromise = withBgFallback(
4400
+ "selection",
4401
+ 6000,
4402
+ "",
4403
+ async (): Promise<string> => {
4404
+ try {
4405
+ const sel = (await readAppState("pending-selection-context")) as {
4406
+ text?: string;
4407
+ capturedAt?: number;
4408
+ } | null;
4409
+ if (!sel?.text) return "";
4410
+ const capturedAt =
4411
+ typeof sel.capturedAt === "number" ? sel.capturedAt : 0;
4412
+ if (Date.now() - capturedAt > SELECTION_TTL_MS) return "";
4413
+ return (
4414
+ `\n\nThe user has selected the following text and pressed Cmd+I to focus the agent. ` +
4415
+ `Treat this as the immediate context to act on:\n` +
4416
+ `<selection>\n${capSelectionContext(sel.text)}\n</selection>`
4417
+ );
4418
+ } catch {
4419
+ // DB not ready — skip silently
4420
+ }
4421
+ return "";
4422
+ },
4423
+ );
4328
4424
 
4329
4425
  // On the first message of a conversation, inject workspace inventory
4330
4426
  // so the agent knows what files, skills, jobs, and custom agents exist.
4331
4427
  // Templates can opt out via `skipFilesContext: true` when the inventory
4332
4428
  // is unrelated to the app's job (e.g. a voice-first macro tracker).
4333
- const filesContextPromise = (async (): Promise<string> => {
4334
- let filesContext = "";
4335
- if (options.skipFilesContext) return filesContext;
4336
- if (history.length === 0) {
4337
- try {
4338
- const {
4339
- resourceListAccessible,
4340
- SHARED_OWNER,
4341
- WORKSPACE_OWNER,
4342
- resourceGet,
4343
- } = await import("../resources/store.js");
4344
- const {
4345
- getResourceKind,
4346
- parseCustomAgentProfile,
4347
- parseRemoteAgentManifest,
4348
- parseSkillMetadata,
4349
- } = await import("../resources/metadata.js");
4350
- const ownerEmail = getRequestUserEmail();
4351
- const orgId = getRequestOrgId();
4352
- if (!ownerEmail) throw new Error("no authenticated user");
4353
- const allResources = await resourceListAccessible(
4354
- ownerEmail,
4355
- undefined,
4356
- { userEmail: ownerEmail, orgId },
4357
- );
4429
+ const filesContextPromise = withBgFallback(
4430
+ "files",
4431
+ 8000,
4432
+ "",
4433
+ async (): Promise<string> => {
4434
+ let filesContext = "";
4435
+ if (options.skipFilesContext) return filesContext;
4436
+ if (history.length === 0) {
4437
+ try {
4438
+ const {
4439
+ resourceListAccessible,
4440
+ SHARED_OWNER,
4441
+ WORKSPACE_OWNER,
4442
+ resourceGet,
4443
+ } = await import("../resources/store.js");
4444
+ const {
4445
+ getResourceKind,
4446
+ parseCustomAgentProfile,
4447
+ parseRemoteAgentManifest,
4448
+ parseSkillMetadata,
4449
+ } = await import("../resources/metadata.js");
4450
+ const ownerEmail = getRequestUserEmail();
4451
+ const orgId = getRequestOrgId();
4452
+ if (!ownerEmail) throw new Error("no authenticated user");
4453
+ const allResources = await resourceListAccessible(
4454
+ ownerEmail,
4455
+ undefined,
4456
+ { userEmail: ownerEmail, orgId },
4457
+ );
4358
4458
 
4359
- if (allResources.length > 0) {
4360
- const fileLines: string[] = [];
4361
- const skillLines: string[] = [];
4362
- const agentLines: string[] = [];
4363
- const jobLines: string[] = [];
4364
- for (const r of allResources) {
4365
- const scope =
4366
- r.owner === WORKSPACE_OWNER
4367
- ? "workspace"
4368
- : r.owner === SHARED_OWNER
4369
- ? "shared"
4370
- : "personal";
4371
- const kind = getResourceKind(r.path);
4372
- if (kind === "file") {
4373
- fileLines.push(` ${r.path} (${scope})`);
4374
- continue;
4375
- }
4459
+ if (allResources.length > 0) {
4460
+ const fileLines: string[] = [];
4461
+ const skillLines: string[] = [];
4462
+ const agentLines: string[] = [];
4463
+ const jobLines: string[] = [];
4464
+ for (const r of allResources) {
4465
+ const scope =
4466
+ r.owner === WORKSPACE_OWNER
4467
+ ? "workspace"
4468
+ : r.owner === SHARED_OWNER
4469
+ ? "shared"
4470
+ : "personal";
4471
+ const kind = getResourceKind(r.path);
4472
+ if (kind === "file") {
4473
+ fileLines.push(` ${r.path} (${scope})`);
4474
+ continue;
4475
+ }
4376
4476
 
4377
- if (kind === "job") {
4378
- jobLines.push(` ${r.path} (${scope})`);
4379
- continue;
4380
- }
4477
+ if (kind === "job") {
4478
+ jobLines.push(` ${r.path} (${scope})`);
4479
+ continue;
4480
+ }
4381
4481
 
4382
- if (
4383
- kind === "skill" ||
4384
- kind === "agent" ||
4385
- kind === "remote-agent"
4386
- ) {
4387
- const full = await resourceGet(r.id, {
4388
- userEmail: ownerEmail,
4389
- orgId,
4390
- });
4391
- if (!full) continue;
4392
- if (kind === "skill") {
4393
- const skill = parseSkillMetadata(full.content, r.path);
4394
- skillLines.push(
4395
- ` ${skill?.name || r.path} — ${compactInventoryDescription(skill?.description || r.path)} (${scope}, ${r.path})`,
4396
- );
4397
- } else if (kind === "agent") {
4398
- const agent = parseCustomAgentProfile(full.content, r.path);
4399
- agentLines.push(
4400
- ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Custom workspace agent")} (${scope}, ${r.path}${agent?.model ? `, model: ${agent.model}` : ""})`,
4401
- );
4402
- } else {
4403
- const agent = parseRemoteAgentManifest(full.content, r.path);
4404
- agentLines.push(
4405
- ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Connected A2A agent")} (${scope}, remote via ${r.path})`,
4406
- );
4482
+ if (
4483
+ kind === "skill" ||
4484
+ kind === "agent" ||
4485
+ kind === "remote-agent"
4486
+ ) {
4487
+ const full = await resourceGet(r.id, {
4488
+ userEmail: ownerEmail,
4489
+ orgId,
4490
+ });
4491
+ if (!full) continue;
4492
+ if (kind === "skill") {
4493
+ const skill = parseSkillMetadata(full.content, r.path);
4494
+ skillLines.push(
4495
+ ` ${skill?.name || r.path} — ${compactInventoryDescription(skill?.description || r.path)} (${scope}, ${r.path})`,
4496
+ );
4497
+ } else if (kind === "agent") {
4498
+ const agent = parseCustomAgentProfile(full.content, r.path);
4499
+ agentLines.push(
4500
+ ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Custom workspace agent")} (${scope}, ${r.path}${agent?.model ? `, model: ${agent.model}` : ""})`,
4501
+ );
4502
+ } else {
4503
+ const agent = parseRemoteAgentManifest(
4504
+ full.content,
4505
+ r.path,
4506
+ );
4507
+ agentLines.push(
4508
+ ` ${agent?.name || r.path} — ${compactInventoryDescription(agent?.description || "Connected A2A agent")} (${scope}, remote via ${r.path})`,
4509
+ );
4510
+ }
4407
4511
  }
4408
4512
  }
4513
+ const blocks: string[] = [];
4514
+ if (fileLines.length > 0) {
4515
+ const lines = limitInventoryLines(fileLines, "files");
4516
+ blocks.push(
4517
+ `<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>`,
4518
+ );
4519
+ }
4520
+ if (skillLines.length > 0) {
4521
+ const lines = limitInventoryLines(skillLines, "skills");
4522
+ blocks.push(
4523
+ `<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>`,
4524
+ );
4525
+ }
4526
+ if (agentLines.length > 0) {
4527
+ const lines = limitInventoryLines(agentLines, "agents");
4528
+ blocks.push(
4529
+ `<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>`,
4530
+ );
4531
+ }
4532
+ if (jobLines.length > 0) {
4533
+ const lines = limitInventoryLines(jobLines, "jobs");
4534
+ blocks.push(
4535
+ `<available-jobs>\nScheduled tasks in the workspace:\n${lines.join("\n")}\n</available-jobs>`,
4536
+ );
4537
+ }
4538
+ filesContext =
4539
+ blocks.length > 0 ? `\n\n${blocks.join("\n\n")}` : "";
4409
4540
  }
4410
- const blocks: string[] = [];
4411
- if (fileLines.length > 0) {
4412
- const lines = limitInventoryLines(fileLines, "files");
4413
- blocks.push(
4414
- `<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>`,
4415
- );
4416
- }
4417
- if (skillLines.length > 0) {
4418
- const lines = limitInventoryLines(skillLines, "skills");
4419
- blocks.push(
4420
- `<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>`,
4421
- );
4422
- }
4423
- if (agentLines.length > 0) {
4424
- const lines = limitInventoryLines(agentLines, "agents");
4425
- blocks.push(
4426
- `<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>`,
4427
- );
4428
- }
4429
- if (jobLines.length > 0) {
4430
- const lines = limitInventoryLines(jobLines, "jobs");
4431
- blocks.push(
4432
- `<available-jobs>\nScheduled tasks in the workspace:\n${lines.join("\n")}\n</available-jobs>`,
4433
- );
4434
- }
4435
- filesContext =
4436
- blocks.length > 0 ? `\n\n${blocks.join("\n\n")}` : "";
4541
+ } catch {
4542
+ // Resources not available — skip silently
4437
4543
  }
4438
- } catch {
4439
- // Resources not available — skip silently
4440
4544
  }
4441
- }
4442
- return filesContext;
4443
- })();
4545
+ return filesContext;
4546
+ },
4547
+ );
4444
4548
 
4445
4549
  // DIAGNOSTIC-ONLY: the background worker freezes between `env_config` and
4446
4550
  // `context_all` when one of the parallel pre-send promises hangs (observed
@@ -4468,6 +4572,7 @@ export function createProductionAgentHandler(
4468
4572
  .finally(() => __psMark("enrich"))
4469
4573
  .catch(() => {});
4470
4574
 
4575
+ bgLog("presend:awaiting Promise.all");
4471
4576
  const [
4472
4577
  systemPrompt,
4473
4578
  screenBlock,
@@ -4489,6 +4594,7 @@ export function createProductionAgentHandler(
4489
4594
  // DIAGNOSTIC-ONLY: all parallel context gathering (system prompt, screen,
4490
4595
  // files, loop settings, enriched message) resolved.
4491
4596
  workerStep("context_all");
4597
+ bgLog("context_all");
4492
4598
 
4493
4599
  if (systemPromptError) {
4494
4600
  setResponseHeader(event, "Content-Type", "text/event-stream");
@@ -4519,6 +4625,7 @@ export function createProductionAgentHandler(
4519
4625
  setupMark("actions");
4520
4626
  // DIAGNOSTIC-ONLY: action/tool resolution + engine-tool filtering finished.
4521
4627
  workerStep("action_tool_setup");
4628
+ bgLog("action_tool_setup");
4522
4629
  const requestSystemPrompt =
4523
4630
  requestMode === "plan"
4524
4631
  ? `${systemPrompt}\n\n${PLAN_MODE_SYSTEM_PROMPT}`
@@ -4633,6 +4740,7 @@ export function createProductionAgentHandler(
4633
4740
  // DIAGNOSTIC-ONLY: owner/thread resolution + runId/effectiveThreadId +
4634
4741
  // chained-continuation thread fetch finished.
4635
4742
  workerStep("owner_thread");
4743
+ bgLog("owner_thread");
4636
4744
 
4637
4745
  // Persist the user's turn exactly once. The foreground POST does this
4638
4746
  // before dispatching; the background worker must NOT repeat it (it re-enters
@@ -5025,7 +5133,9 @@ export function createProductionAgentHandler(
5025
5133
  dispatchMode: "background",
5026
5134
  }).catch(() => {});
5027
5135
  }
5136
+ bgLog("claim:before");
5028
5137
  const won = await claimBackgroundRun(runId);
5138
+ bgLog("claim:after won=" + won);
5029
5139
  if (!won) {
5030
5140
  // Already claimed by an earlier delivery — return a benign ack so
5031
5141
  // Netlify doesn't retry a successful handoff.
@@ -5050,11 +5160,13 @@ export function createProductionAgentHandler(
5050
5160
  // DIAGNOSTIC-ONLY: last stage before startRun fires. A worker that reaches
5051
5161
  // prestart but never workerStarted is hanging inside startRun itself.
5052
5162
  workerStep("prestart");
5163
+ bgLog("prestart");
5053
5164
  const setupDetail =
5054
5165
  Object.entries(setupMarks)
5055
5166
  .map(([k, v]) => `${k}=${v}`)
5056
5167
  .join(" ") + ` total=${Date.now() - setupT0}`;
5057
5168
 
5169
+ bgLog("startRun:invoked");
5058
5170
  startRun(
5059
5171
  runId,
5060
5172
  effectiveThreadId,