@agent-native/core 0.79.6 → 0.79.9

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 (97) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +0 -93
  6. package/corpus/core/src/agent/run-manager.ts +9 -1
  7. package/corpus/core/src/client/agent-chat-adapter.ts +24 -16
  8. package/corpus/core/src/client/session-replay.ts +67 -13
  9. package/corpus/core/src/onboarding/default-steps.ts +3 -3
  10. package/corpus/core/src/server/agent-chat-plugin.ts +82 -162
  11. package/corpus/core/src/server/agent-run-context.ts +204 -0
  12. package/corpus/core/src/server/credential-provider.ts +20 -4
  13. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +14 -4
  14. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-uploads-now-accept-compressed-payloads-so-lar.md +6 -0
  15. package/corpus/templates/analytics/changelog/2026-06-26-sql-chart-tooltips-now-stay-stable-while-moving-across-dense.md +6 -0
  16. package/corpus/templates/analytics/server/handlers/session-replay.ts +56 -8
  17. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +1 -1
  18. package/corpus/templates/clips/app/components/recorder/storage-setup-card.tsx +44 -16
  19. package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -9
  20. package/corpus/templates/clips/app/i18n/de-DE.ts +12 -9
  21. package/corpus/templates/clips/app/i18n/en-US.ts +19 -16
  22. package/corpus/templates/clips/app/i18n/es-ES.ts +12 -9
  23. package/corpus/templates/clips/app/i18n/fr-FR.ts +12 -9
  24. package/corpus/templates/clips/app/i18n/hi-IN.ts +13 -9
  25. package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -9
  26. package/corpus/templates/clips/app/i18n/ko-KR.ts +12 -9
  27. package/corpus/templates/clips/app/i18n/pt-BR.ts +12 -9
  28. package/corpus/templates/clips/app/i18n/zh-CN.ts +19 -14
  29. package/corpus/templates/clips/app/i18n/zh-TW.ts +19 -14
  30. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +7 -0
  31. package/corpus/templates/clips/app/routes/record.tsx +2 -2
  32. package/corpus/templates/clips/changelog/2026-06-26-desktop-recordings-recover-more-reliably-when-macos-reports-.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-06-26-slack-unfurls-now-recognize-recording-dashboard-links-pasted.md +6 -0
  34. package/corpus/templates/clips/changelog/2026-06-26-storage-setup-now-explains-why-clips-needs-connected-storage.md +6 -0
  35. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  36. package/corpus/templates/clips/chrome-extension/src/background.ts +24 -3
  37. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +28 -2
  38. package/corpus/templates/clips/chrome-extension/src/popup.html +16 -0
  39. package/corpus/templates/clips/chrome-extension/src/popup.ts +59 -7
  40. package/corpus/templates/clips/chrome-extension/src/sentry.ts +28 -1
  41. package/corpus/templates/clips/chrome-extension/src/styles.css +74 -0
  42. package/corpus/templates/clips/desktop/src/app.tsx +85 -18
  43. package/corpus/templates/clips/desktop/src/styles.css +55 -0
  44. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +88 -51
  45. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +12 -3
  46. package/corpus/templates/clips/server/lib/slack-unfurls.ts +5 -2
  47. package/corpus/templates/slides/actions/_app-url.ts +1 -1
  48. package/corpus/templates/slides/changelog/2026-06-26-deck-links-now-use-the-workspace-gateway-when-available-so-g.md +6 -0
  49. package/dist/agent/production-agent.d.ts.map +1 -1
  50. package/dist/agent/production-agent.js +0 -79
  51. package/dist/agent/production-agent.js.map +1 -1
  52. package/dist/agent/run-manager.d.ts.map +1 -1
  53. package/dist/agent/run-manager.js +12 -1
  54. package/dist/agent/run-manager.js.map +1 -1
  55. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  56. package/dist/client/agent-chat-adapter.js +15 -8
  57. package/dist/client/agent-chat-adapter.js.map +1 -1
  58. package/dist/client/session-replay.d.ts.map +1 -1
  59. package/dist/client/session-replay.js +55 -14
  60. package/dist/client/session-replay.js.map +1 -1
  61. package/dist/collab/awareness.d.ts +2 -2
  62. package/dist/collab/awareness.d.ts.map +1 -1
  63. package/dist/collab/routes.d.ts +2 -2
  64. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  65. package/dist/notifications/routes.d.ts +1 -1
  66. package/dist/observability/routes.d.ts +7 -7
  67. package/dist/onboarding/default-steps.js +3 -3
  68. package/dist/onboarding/default-steps.js.map +1 -1
  69. package/dist/progress/routes.d.ts +1 -1
  70. package/dist/resources/handlers.d.ts +3 -3
  71. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  72. package/dist/server/agent-chat-plugin.js +69 -154
  73. package/dist/server/agent-chat-plugin.js.map +1 -1
  74. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  75. package/dist/server/agent-run-context.d.ts +35 -0
  76. package/dist/server/agent-run-context.d.ts.map +1 -0
  77. package/dist/server/agent-run-context.js +137 -0
  78. package/dist/server/agent-run-context.js.map +1 -0
  79. package/dist/server/credential-provider.d.ts.map +1 -1
  80. package/dist/server/credential-provider.js +15 -4
  81. package/dist/server/credential-provider.js.map +1 -1
  82. package/dist/server/transcribe-voice.d.ts +1 -1
  83. package/docs/content/locales/zh-TW/durable-background-runs.md +74 -0
  84. package/package.json +1 -1
  85. package/corpus/templates/.retired/README.md +0 -9
  86. package/corpus/templates/.retired/calls/package.json +0 -8
  87. package/corpus/templates/.retired/code/package.json +0 -8
  88. package/corpus/templates/.retired/contracts/package.json +0 -8
  89. package/corpus/templates/.retired/images/package.json +0 -8
  90. package/corpus/templates/.retired/issues/package.json +0 -8
  91. package/corpus/templates/.retired/meeting-notes/package.json +0 -8
  92. package/corpus/templates/.retired/migration/package.json +0 -8
  93. package/corpus/templates/.retired/recruiting/package.json +0 -8
  94. package/corpus/templates/.retired/scheduling/package.json +0 -8
  95. package/corpus/templates/.retired/visual-plans/package.json +0 -8
  96. package/corpus/templates/.retired/voice/package.json +0 -8
  97. package/corpus/templates/.retired/workbench/package.json +0 -8
package/corpus/README.md CHANGED
@@ -27,5 +27,5 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
27
27
 
28
28
  ## Generated Counts
29
29
 
30
- - core files: 2027
31
- - template files: 4380
30
+ - core files: 2029
31
+ - template files: 4373
@@ -1,5 +1,24 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.79.9
4
+
5
+ ### Patch Changes
6
+
7
+ - ca677a0: Reduce noisy error capture for expected provider connection failures and transient chat recovery probes.
8
+
9
+ ## 0.79.8
10
+
11
+ ### Patch Changes
12
+
13
+ - 4984f2b: Fail A2A agent tasks that end on terminal run errors without a final response, and keep app-native artifact actions ahead of generic extension creation in agent prompts.
14
+ - 4984f2b: Centralize agent chat request context resolution so foreground and durable background workers share owner, org, timezone, and background-run setup, harden hosted credential fallback detection, and support compressed session replay uploads.
15
+
16
+ ## 0.79.7
17
+
18
+ ### Patch Changes
19
+
20
+ - b7cfefe: Remove the temporary durable-background-worker hang-localizer diagnostics from the agent chat setup hot path. The analytics worker-stall bug they were added to debug is fixed and verified in prod, so the ~7 extra awaited DB round-trips per durable run setup (and their 8s-hang risk under DB stress) are no longer needed. The lasting fix and observability — the `worker_stage` column, cheap fire-and-forget `workerStep` markers, `readBackgroundRunClaim`, and the centralized request-context resolution — are retained.
21
+
3
22
  ## 0.79.6
4
23
 
5
24
  ### Patch Changes
@@ -0,0 +1,74 @@
1
+ ---
2
+ title: "持久背景執行"
3
+ description: "長時間的代理聊天回合可以在獨立的 15 分鐘 Netlify 背景函式中執行,而不是佔用同步請求,讓多步驟工作在伺服器端完成,不需要用戶端看守續跑。每個應用可自行選用;前景斷路器會讓聊天在背景 worker 未啟動時仍可繼續運作。"
4
+ ---
5
+
6
+ # 持久背景執行
7
+
8
+ > **適用對象:**部署 agent-native 應用、希望長時間多步驟代理回合在伺服器端完成的人,以及想了解 `AGENT_CHAT_DURABLE_BACKGROUND` 旗標和第二個 Netlify 函式用途的人。這是框架內建的選用功能,不需要撰寫任何應用程式碼。
9
+
10
+ 一般託管代理回合會在同步請求函式中執行,並受平台請求逾時限制(Netlify 免費方案約 10 秒,框架約 40 秒軟逾時,之後必須把控制權交回用戶端並「從上次中斷的地方繼續」)。這對快速回答沒有問題,但長時間的代理回合(許多工具呼叫、大量分析)會在多次由用戶端驅動的續跑之間來回跳轉。
11
+
12
+ 持久背景執行會把那個長回合移到**獨立的 Netlify 背景函式**,並提供 **15 分鐘**預算。該回合會在伺服器端的一次呼叫中執行完成;UI 只需要串流結果。
13
+
14
+ ## 兩個函式,不是一個 {#two-functions}
15
+
16
+ 已部署的應用正好有兩個伺服器函式,幾乎所有請求都會打到第一個。只有被分派的代理回合會在第二個函式上執行。
17
+
18
+ ```an-diagram title="server 處理所有事;只有被分派的代理回合在背景函式上執行" summary="同步 server 函式提供頁面、actions、API 和前景聊天 POST;它會用 HMAC 將長回合分派到有 15 分鐘預算的背景函式。"
19
+ {
20
+ "html": "<div class=\"diagram-bg-runs\"><div class=\"diagram-panel\" data-rough><div class=\"diagram-pill\">server &middot; synchronous</div><div class=\"diagram-box\">頁面 / SSR</div><div class=\"diagram-box\">Actions</div><div class=\"diagram-box\">API + 框架路由</div><div class=\"diagram-box\">前景聊天 POST <small class=\"diagram-muted\">決定 + 分派</small></div><small class=\"diagram-muted\">約 10 秒逾時 &middot; 幾乎所有請求</small></div><div class=\"diagram-arrow diagram-muted\" aria-hidden=\"true\">&rarr;<br><small>HMAC 202</small></div><div class=\"diagram-panel\" data-rough><div class=\"diagram-pill accent\">server-agent-background &middot; background:true</div><div class=\"diagram-box\">_process-run worker</div><div class=\"diagram-box\">代理回合:LLM + 工具呼叫</div><small class=\"diagram-ok\">15 分鐘預算 &middot; 僅限被分派的回合</small></div></div>",
21
+ "css": ".diagram-bg-runs{display:flex;align-items:stretch;gap:14px;flex-wrap:wrap}.diagram-bg-runs .diagram-panel{display:flex;flex-direction:column;gap:8px;padding:14px;flex:1;min-width:230px}.diagram-bg-runs .diagram-box{padding:8px 10px}.diagram-bg-runs .diagram-arrow{display:flex;flex-direction:column;justify-content:center;text-align:center;min-width:90px;font-size:20px}"
22
+ }
23
+ ```
24
+
25
+ 應用或範本程式碼中**沒有任何需要標記的地方**。當函式的 `export const config` 含有 `background: true`(或函式名稱以 `-background` 結尾)時,Netlify 就會把它視為背景函式。啟用此功能時,框架的建置步驟會自動產生 `server-agent-background` 包裝器;應用作者不需要自行宣告。
26
+
27
+ ## 背景中會執行什麼,以及如何選擇 {#what-runs}
28
+
29
+ 只有長時間的代理聊天回合,也就是會重新進入代理迴圈的內部 `_process-run` 分派(LLM 呼叫 + 工具執行)。頁面、actions 和一般 API 呼叫一律在 `server` 上同步執行。
30
+
31
+ 前景聊天 POST 會**逐回合**決定。只有下列三個條件同時成立時,才會分派到背景函式:
32
+
33
+ - `AGENT_CHAT_DURABLE_BACKGROUND` 為真值(`true` / `1` / `yes` / `on`),
34
+ - 應用正在託管環境中執行(在平台上,而非本機開發),以及
35
+ - 已設定 `A2A_SECRET`(分派是 HMAC 簽署的自我呼叫;沒有 secret 就無法簽署,因此該回合會留在線上同步執行)。
36
+
37
+ 若缺少任何條件,該回合就會和以前一樣在線上同步執行。這個旗標會在**建置時間**讀取(決定是否輸出背景函式),也會在**執行時間**讀取(決定是否分派),因此啟用後需要重新部署。
38
+
39
+ ## 生命週期與斷路器 {#lifecycle}
40
+
41
+ Netlify async function 會在**排入**呼叫的瞬間回傳 `202`;這不代表 worker 已實際執行。因此前景不會盲目相信 `202`:分派後會短暫輪詢,確認 worker 是否真的**認領**該 run;若沒有認領,前景會在線上同步復原。
42
+
43
+ ```an-diagram title="前景一律回傳可運作的回合" summary="分派時,前景會等待一段寬限時間讓 worker 認領 run;若認領成功,前景會訂閱它;若未認領,前景會自行認領並在線上同步執行該回合。"
44
+ {
45
+ "html": "<div class=\"diagram-bg-life\"><div class=\"diagram-box\" data-rough>前景 POST</div><div class=\"diagram-arrow diagram-muted\">&rarr;</div><div class=\"diagram-box\" data-rough>插入 run + HMAC 分派 &rarr; 202</div><div class=\"diagram-arrow diagram-muted\">&rarr;</div><div class=\"diagram-panel center\" data-rough><strong>Worker 是否在寬限時間內認領?</strong><small class=\"diagram-ok\">是 &rarr; 串流背景 worker(最多 15 分鐘)</small><small class=\"diagram-warn\">否 &rarr; 斷路器在線上同步復原該回合</small></div></div>",
46
+ "css": ".diagram-bg-life{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.diagram-bg-life .diagram-arrow{font-size:20px}.diagram-bg-life .center{display:flex;flex-direction:column;gap:4px;padding:12px}"
47
+ }
48
+ ```
49
+
50
+ 認領會透過 `dispatch_mode` 從 `background` 變成 `background-processing` 來觀察(只有 worker 的原子認領會這樣做)。寬限時間必須涵蓋 worker 冷啟動;較重的應用曾觀察到需要比原本 8 秒更久,因此目前是 **15 秒**(仍在前景約 40 秒軟逾時內)。如果 worker 從未認領,無論是故障或只是啟動太慢,都會在寬限時間後回退到線上同步執行,所以該回合仍會完成;代價是先等待寬限時間所增加的延遲。
51
+
52
+ ```an-callout
53
+ {
54
+ "tone": "success",
55
+ "body": "無論哪種情況,前景都會回傳可運作的回合:worker 及時認領時使用背景 worker,否則使用線上同步回合。因此,即使 worker 失敗,持久背景執行也應該保留聊天正確性;但設定錯誤或認領過慢的 worker 可能會在同步復原接手前增加回退延遲(最多到寬限時間)。"
56
+ }
57
+ ```
58
+
59
+ ## 成本與取捨 {#cost}
60
+
61
+ Netlify 背景函式是**付費方案**功能(Personal / Pro,不含免費方案),並依運算時間計費(GB-hour)。因此多分鐘的代理回合會比短同步呼叫花費更多運算量;但它大致上**取代**了舊模式,也就是回合撞到約 40 秒限制後,用戶端重新送出多次續跑請求。這是把同一份工作合併到一次長呼叫中,不是純粹額外增加。
62
+
63
+ ```an-callout
64
+ {
65
+ "tone": "info",
66
+ "body": "持久背景執行是每個應用可選用的功能,預設為關閉。使用 AGENT_CHAT_DURABLE_BACKGROUND=true(加上 A2A_SECRET)啟用並重新部署。若要驗證某個回合真的使用 worker,請檢查該 run 是否到達 dispatch_mode=background-processing,而不是回退到線上同步復原。"
67
+ }
68
+ ```
69
+
70
+ ## 相關
71
+
72
+ - [**持久恢復**](/docs/durable-resume) — 中斷的 run 如何恢復而不重複已完成的副作用(另一個永遠啟用的保護)。
73
+ - [**部署**](/docs/deployment) — 應用及其函式如何建置並部署到平台。
74
+ - [**A2A Protocol**](/docs/a2a-protocol) — `A2A_SECRET` 與持久 worker 重用的 HMAC 簽署自我分派。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.79.6",
3
+ "version": "0.79.9",
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": {
@@ -3972,25 +3972,6 @@ export function createProductionAgentHandler(
3972
3972
  `${s}=${Date.now() - setupT0}ms`,
3973
3973
  ).catch(() => {});
3974
3974
  };
3975
- // DIAGNOSTIC-ONLY: AWAITED worker mark. Fire-and-forget workerStep marks
3976
- // after `post_model_ok` stopped landing even though writes still work
3977
- // there — so the worker's MAIN FLOW stalls between post_model and pre_claim
3978
- // (not a write hang). Awaited marks block until they land (or withDbTimeout-
3979
- // reject), so `worker_stage` reliably advances to the last phase the main
3980
- // flow actually reached — pinpointing the stall. Bg worker only.
3981
- const awaitedWorkerMark = async (s: string) => {
3982
- if (!isBackgroundWorker || !bgRunId) return;
3983
- try {
3984
- await recordRunDiagnostic(
3985
- bgRunId,
3986
- RUN_DIAG_STAGE.workerSetupStep,
3987
- `${s}=${Date.now() - setupT0}ms`,
3988
- );
3989
- } catch {
3990
- // bounded by withDbTimeout; ignore — the absence of the next mark is
3991
- // itself the signal.
3992
- }
3993
- };
3994
3975
  // Whether this worker is REALLY executing inside a 15-min Netlify
3995
3976
  // `-background` function (proven by the runtime function name), not merely a
3996
3977
  // `_process-run` re-entry that may have landed on the ~60s synchronous
@@ -4215,35 +4196,6 @@ export function createProductionAgentHandler(
4215
4196
  );
4216
4197
 
4217
4198
  options.onEngineResolved?.(engine, model);
4218
- // DIAGNOSTIC-ONLY: localize where the worker stalls AFTER model_done. These
4219
- // land in `worker_stage` (foreground-independent), so even though the
4220
- // foreground overwrites `diag_stage` on inline recovery, the worker's last
4221
- // reached point survives. If the last worker_stage is `model_done`, writes
4222
- // hang right after model resolution (DB connection); if it's
4223
- // `engine_resolved`/`systemprompt_enter`, the stall is in the named step.
4224
- workerStep("engine_resolved");
4225
- // DIAGNOSTIC-ONLY: AWAITED probe (bg worker only). worker_stage stalls at
4226
- // `model_done` even though the code right after is trivial sync — this
4227
- // distinguishes the two causes: if `post_model_awaited` lands, DB writes
4228
- // still work after model_done and the stall is later in the main flow; if it
4229
- // never lands (stays `model_done`), the bg-fn DB connection itself is hung
4230
- // right after model resolution. `recordRunDiagnostic` is `withDbTimeout`-
4231
- // bounded, so a hung write rejects (caught) rather than blocking forever.
4232
- if (isBackgroundWorker && bgRunId) {
4233
- const probeStart = Date.now();
4234
- try {
4235
- await recordRunDiagnostic(
4236
- bgRunId,
4237
- RUN_DIAG_STAGE.workerSetupStep,
4238
- "post_model_awaited",
4239
- );
4240
- workerStep(`post_model_ok=${Date.now() - probeStart}ms`);
4241
- } catch (e) {
4242
- workerStep(
4243
- `post_model_threw=${Date.now() - probeStart}ms:${String((e as Error)?.message ?? e).slice(0, 80)}`,
4244
- );
4245
- }
4246
- }
4247
4199
 
4248
4200
  // One-line per-turn resolution log so it's obvious in dev which engine
4249
4201
  // is actually handling the request. `requestEngine` is what the client
@@ -4254,14 +4206,6 @@ export function createProductionAgentHandler(
4254
4206
  `[agent-chat] resolved engine=${engine.name} model=${model} requestEngine=${requestEngine ?? "(none)"}`,
4255
4207
  );
4256
4208
 
4257
- // DIAGNOSTIC-ONLY: the worker reaches post_model_ok but never aw_env — the
4258
- // only exit between them is this anthropic key check. Capture the exact
4259
- // resolution state so we know WHY effectiveApiKey is falsy in the worker
4260
- // (owner not resolved from the HMAC/thread? owner has no stored key? deploy
4261
- // fallback blocked?) vs the foreground which resolves it fine.
4262
- await awaitedWorkerMark(
4263
- `apikey_state:engine=${engine.name},owner=${ownerEmail ? "Y" : "N"},userKey=${userApiKey ? "Y" : "N"},effKey=${effectiveApiKey ? "Y" : "N"},block=${shouldBlockDeployCredentialFallback()}`,
4264
- );
4265
4209
  // Check for API key before starting a run (only for anthropic engine)
4266
4210
  if (engine.name === "anthropic" && !effectiveApiKey) {
4267
4211
  setResponseHeader(event, "Content-Type", "text/event-stream");
@@ -4289,7 +4233,6 @@ export function createProductionAgentHandler(
4289
4233
  // reached db_request_ctx but not env_config hung in attachment upload or
4290
4234
  // engine/model resolution.
4291
4235
  workerStep("env_config");
4292
- await awaitedWorkerMark("aw_env");
4293
4236
  // Run all independent pre-send steps in parallel. Each of these hits
4294
4237
  // the DB or invokes an action; running them sequentially was the
4295
4238
  // single biggest contributor to pre-LLM latency.
@@ -4305,17 +4248,11 @@ export function createProductionAgentHandler(
4305
4248
  const systemPromptThunk = (): Promise<string> =>
4306
4249
  (async (): Promise<string> => {
4307
4250
  const sysPromptStart = Date.now();
4308
- // Brackets the system-prompt build (which runs the template's
4309
- // extraContext / data-dictionary). If worker_stage stalls at
4310
- // `systemprompt_enter` with no `systemprompt_done`, the build itself is
4311
- // the stall point.
4312
- workerStep("systemprompt_enter");
4313
4251
  try {
4314
4252
  const built =
4315
4253
  typeof options.systemPrompt === "function"
4316
4254
  ? await options.systemPrompt(event)
4317
4255
  : options.systemPrompt;
4318
- workerStep("systemprompt_done");
4319
4256
  return built;
4320
4257
  } catch (error) {
4321
4258
  systemPromptError = error;
@@ -4614,7 +4551,6 @@ export function createProductionAgentHandler(
4614
4551
  // DIAGNOSTIC-ONLY: all parallel context gathering (system prompt, screen,
4615
4552
  // files, loop settings, enriched message) resolved.
4616
4553
  workerStep("context_all");
4617
- await awaitedWorkerMark("aw_presend");
4618
4554
 
4619
4555
  if (systemPromptError) {
4620
4556
  setResponseHeader(event, "Content-Type", "text/event-stream");
@@ -4645,7 +4581,6 @@ export function createProductionAgentHandler(
4645
4581
  setupMark("actions");
4646
4582
  // DIAGNOSTIC-ONLY: action/tool resolution + engine-tool filtering finished.
4647
4583
  workerStep("action_tool_setup");
4648
- await awaitedWorkerMark("aw_actions");
4649
4584
  const requestSystemPrompt =
4650
4585
  requestMode === "plan"
4651
4586
  ? `${systemPrompt}\n\n${PLAN_MODE_SYSTEM_PROMPT}`
@@ -4760,7 +4695,6 @@ export function createProductionAgentHandler(
4760
4695
  // DIAGNOSTIC-ONLY: owner/thread resolution + runId/effectiveThreadId +
4761
4696
  // chained-continuation thread fetch finished.
4762
4697
  workerStep("owner_thread");
4763
- await awaitedWorkerMark("aw_owner");
4764
4698
 
4765
4699
  // Persist the user's turn exactly once. The foreground POST does this
4766
4700
  // before dispatching; the background worker must NOT repeat it (it re-enters
@@ -5178,33 +5112,6 @@ export function createProductionAgentHandler(
5178
5112
  // DIAGNOSTIC-ONLY: last stage before startRun fires. A worker that reaches
5179
5113
  // prestart but never workerStarted is hanging inside startRun itself.
5180
5114
  workerStep("prestart");
5181
- // DIAGNOSTIC-ONLY: AWAITED probe right before startRun (bg worker only). If
5182
- // worker_stage reaches `pre_claim_ok` but the run never flips to
5183
- // `worker_started`, the stall is inside startRun's claim itself; if it never
5184
- // reaches `pre_claim_*`, the stall is somewhere in the setup between
5185
- // post_model and here.
5186
- if (isBackgroundWorker && bgRunId) {
5187
- const claimProbeStart = Date.now();
5188
- try {
5189
- await recordRunDiagnostic(
5190
- bgRunId,
5191
- RUN_DIAG_STAGE.workerSetupStep,
5192
- "pre_claim",
5193
- );
5194
- workerStep(`pre_claim_ok=${Date.now() - claimProbeStart}ms`);
5195
- } catch (e) {
5196
- workerStep(
5197
- `pre_claim_threw=${Date.now() - claimProbeStart}ms:${String((e as Error)?.message ?? e).slice(0, 80)}`,
5198
- );
5199
- }
5200
- }
5201
- // DIAGNOSTIC-ONLY: peak-ish RSS (MB) + assembled system-prompt size (KB) at
5202
- // prestart. The analytics bg worker dies right after model_done; if the
5203
- // FOREGROUND (identical build, writes land) is already near the ~1024MB
5204
- // Netlify function limit, an OOM kill in the heavier worker explains the
5205
- // freeze. Both numbers ride along in the existing setup-timings detail.
5206
- setupMarks.rssMB = Math.round(process.memoryUsage().rss / 1048576);
5207
- setupMarks.promptKB = Math.round((systemPrompt?.length ?? 0) / 1024);
5208
5115
  const setupDetail =
5209
5116
  Object.entries(setupMarks)
5210
5117
  .map(([k, v]) => `${k}=${v}`)
@@ -1,4 +1,5 @@
1
1
  import { captureError } from "../server/capture-error.js";
2
+ import { isLlmCredentialError } from "./engine/credential-errors.js";
2
3
  import { EngineError } from "./engine/types.js";
3
4
  import {
4
5
  insertRun,
@@ -149,10 +150,15 @@ function getEngineRunErrorDetails(err: EngineError): string | undefined {
149
150
  function shouldCaptureRunError(err: unknown): boolean {
150
151
  if (!(err instanceof EngineError)) return true;
151
152
  const errorCode = getEngineRunErrorCode(err);
153
+ if (isLlmCredentialError(err, errorCode)) return false;
154
+ if (err.statusCode === 401 || err.statusCode === 403) return false;
155
+ if (/^40[13] status code\b/i.test(err.message)) return false;
156
+ if (err.message.trim().toLowerCase() === "connection error.") return false;
152
157
  if (!errorCode) return true;
153
158
  const normalizedCode = errorCode.toLowerCase();
154
159
  return (
155
160
  !normalizedCode.startsWith("credits-limit") &&
161
+ normalizedCode !== "builder_gateway_network_error" &&
156
162
  normalizedCode !== "provider_rate_limited" &&
157
163
  normalizedCode !== "rate_limit_exceeded"
158
164
  );
@@ -423,6 +429,8 @@ export function startRun(
423
429
  let pendingTerminalEvent: RunEvent | null = null;
424
430
 
425
431
  const captureRunError = (error: unknown, phase: "run" | "completion") => {
432
+ const errorCode =
433
+ error instanceof EngineError ? getEngineRunErrorCode(error) : undefined;
426
434
  captureError(error, {
427
435
  route: "/_agent-native/agent-chat",
428
436
  tags: {
@@ -431,7 +439,7 @@ export function startRun(
431
439
  runStatus: run.status,
432
440
  softTimedOut: softTimedOut ? "true" : "false",
433
441
  abortReason: run.abortReason,
434
- errorCode: error instanceof EngineError ? error.errorCode : undefined,
442
+ errorCode,
435
443
  },
436
444
  extra: {
437
445
  runId,
@@ -717,7 +717,7 @@ function continuationRepeatSignature(content: ContentPart[]): string {
717
717
  .map((segment) => segment.replace(/[^a-z0-9]+/g, " ").trim())
718
718
  .filter((segment) => segment.length > 0);
719
719
  if (segments.length === 0) return "";
720
- return Array.from(new Set(segments)).sort().join("");
720
+ return Array.from(new Set(segments)).sort().join("\u0000");
721
721
  }
722
722
 
723
723
  /**
@@ -771,7 +771,7 @@ function inFlightToolInputSignature(
771
771
  part: Extract<ContentPart, { type: "tool-call" }>,
772
772
  ): string {
773
773
  const raw = part.argsText ?? stableJson(part.args);
774
- return `${raw.length}${raw.slice(0, 256)}`;
774
+ return `${raw.length}\u0000${raw.slice(0, 256)}`;
775
775
  }
776
776
 
777
777
  function toolContinuationKey(
@@ -964,6 +964,10 @@ function retryDelay(attempt: number, abortSignal: AbortSignal): Promise<void> {
964
964
  return delay(ms, abortSignal);
965
965
  }
966
966
 
967
+ function shouldCaptureRecoveryHttpStatus(status: number): boolean {
968
+ return status < 500 || status >= 600;
969
+ }
970
+
967
971
  function generateTurnId(): string {
968
972
  if (
969
973
  typeof crypto !== "undefined" &&
@@ -1518,15 +1522,17 @@ export function createAgentChatAdapter(
1518
1522
  lastReconnectError = new Error(
1519
1523
  `Reconnect failed: ${reconnectRes.status}`,
1520
1524
  );
1521
- captureChatClientError(
1522
- lastReconnectError,
1523
- "reconnect-current-response",
1524
- {
1525
- status: reconnectRes.status,
1526
- hasBody: Boolean(reconnectRes.body),
1527
- attempt,
1528
- },
1529
- );
1525
+ if (shouldCaptureRecoveryHttpStatus(reconnectRes.status)) {
1526
+ captureChatClientError(
1527
+ lastReconnectError,
1528
+ "reconnect-current-response",
1529
+ {
1530
+ status: reconnectRes.status,
1531
+ hasBody: Boolean(reconnectRes.body),
1532
+ attempt,
1533
+ },
1534
+ );
1535
+ }
1530
1536
  reconnectErrorCaptured = true;
1531
1537
  break;
1532
1538
  }
@@ -1610,11 +1616,13 @@ export function createAgentChatAdapter(
1610
1616
  lastActiveRunError = new Error(
1611
1617
  `Active run lookup failed: ${activeRes.status}`,
1612
1618
  );
1613
- captureChatClientError(
1614
- lastActiveRunError,
1615
- "reconnect-active-response",
1616
- { status: activeRes.status, attempt },
1617
- );
1619
+ if (shouldCaptureRecoveryHttpStatus(activeRes.status)) {
1620
+ captureChatClientError(
1621
+ lastActiveRunError,
1622
+ "reconnect-active-response",
1623
+ { status: activeRes.status, attempt },
1624
+ );
1625
+ }
1618
1626
  return false;
1619
1627
  }
1620
1628
  const active = await activeRes.json();
@@ -630,6 +630,72 @@ function buildReplayBody(
630
630
  return `${envelopeJson.slice(0, -1)},"events":[${eventJsonParts.join(",")}]}`;
631
631
  }
632
632
 
633
+ interface ReplayUploadBody {
634
+ body: BodyInit;
635
+ headers: Record<string, string>;
636
+ compressed: boolean;
637
+ }
638
+
639
+ async function gzipReplayBody(body: string): Promise<Blob | null> {
640
+ if (
641
+ typeof CompressionStream === "undefined" ||
642
+ typeof Blob === "undefined" ||
643
+ typeof Response === "undefined"
644
+ ) {
645
+ return null;
646
+ }
647
+ try {
648
+ const stream = new Blob([body], { type: "application/json" })
649
+ .stream()
650
+ .pipeThrough(new CompressionStream("gzip"));
651
+ const compressed = await new Response(stream).arrayBuffer();
652
+ return new Blob([compressed], { type: "application/json" });
653
+ } catch {
654
+ return null;
655
+ }
656
+ }
657
+
658
+ async function buildReplayUploadBody(body: string): Promise<ReplayUploadBody> {
659
+ const compressed = await gzipReplayBody(body);
660
+ if (compressed) {
661
+ return {
662
+ body: compressed,
663
+ compressed: true,
664
+ headers: {
665
+ "Content-Type": "application/json",
666
+ "Content-Encoding": "gzip",
667
+ },
668
+ };
669
+ }
670
+ return {
671
+ body,
672
+ compressed: false,
673
+ headers: {
674
+ "Content-Type": "text/plain;charset=UTF-8",
675
+ },
676
+ };
677
+ }
678
+
679
+ async function sendReplayUpload(
680
+ options: NormalizedSessionReplayOptions,
681
+ body: string,
682
+ ): Promise<void> {
683
+ const upload = await buildReplayUploadBody(body);
684
+ if (!upload.compressed && navigator.sendBeacon) {
685
+ const sent = navigator.sendBeacon(options.endpoint, body);
686
+ if (sent) return;
687
+ }
688
+ await fetch(options.endpoint, {
689
+ method: "POST",
690
+ body: upload.body,
691
+ keepalive: true,
692
+ headers: {
693
+ ...upload.headers,
694
+ "X-Agent-Native-Analytics-Key": options.publicKey,
695
+ },
696
+ }).catch(() => {});
697
+ }
698
+
633
699
  function isFinalFlushReason(reason: string): boolean {
634
700
  return [
635
701
  "manual",
@@ -649,19 +715,7 @@ export async function flushSessionReplay(reason = "manual"): Promise<void> {
649
715
  if (!body || !state.options) return;
650
716
  state.flushing = true;
651
717
  try {
652
- if (navigator.sendBeacon) {
653
- const sent = navigator.sendBeacon(state.options.endpoint, body);
654
- if (sent) return;
655
- }
656
- await fetch(state.options.endpoint, {
657
- method: "POST",
658
- body,
659
- keepalive: true,
660
- headers: {
661
- "Content-Type": "text/plain;charset=UTF-8",
662
- "X-Agent-Native-Analytics-Key": state.options.publicKey,
663
- },
664
- }).catch(() => {});
718
+ await sendReplayUpload(state.options, body);
665
719
  } finally {
666
720
  state.flushing = false;
667
721
  }
@@ -121,9 +121,9 @@ const llmStep: OnboardingStep = {
121
121
  await import("../server/credential-provider.js");
122
122
  if (await resolveHasCompleteBuilderConnection()) return true;
123
123
  } catch {
124
- if (process.env.BUILDER_PRIVATE_KEY && process.env.BUILDER_PUBLIC_KEY) {
125
- return true;
126
- }
124
+ // Credential storage may be unavailable during early boot. Do not fall
125
+ // back to deployment-level Builder env here; the scoped resolver owns the
126
+ // policy for when that is safe.
127
127
  }
128
128
  try {
129
129
  if (await detectEngineFromUserSecrets()) return true;