@agent-native/core 0.90.10 → 0.90.11

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 (49) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +8 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +5 -5
  5. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +40 -8
  6. package/corpus/core/src/agent/engine/anthropic-engine.ts +25 -7
  7. package/corpus/core/src/agent/engine/output-tokens.ts +94 -0
  8. package/corpus/core/src/agent/production-agent.ts +34 -6
  9. package/corpus/core/src/agent/types.ts +3 -2
  10. package/corpus/core/src/db/client.ts +12 -9
  11. package/corpus/core/src/shared/reasoning-effort.ts +23 -0
  12. package/corpus/templates/clips/actions/regenerate-chapters.ts +7 -0
  13. package/corpus/templates/clips/app/hooks/use-auto-title.ts +15 -5
  14. package/corpus/templates/clips/app/routes/download.tsx +12 -4
  15. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -0
  16. package/corpus/templates/clips/changelog/2026-07-07-auto-generated-chapter-requests-now-open-the-matching-agent-.md +6 -0
  17. package/corpus/templates/clips/changelog/2026-07-07-the-clips-desktop-download-page-now-uses-the-agent-native-ap.md +6 -0
  18. package/dist/agent/durable-background.d.ts +1 -1
  19. package/dist/agent/durable-background.d.ts.map +1 -1
  20. package/dist/agent/durable-background.js +5 -3
  21. package/dist/agent/durable-background.js.map +1 -1
  22. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  23. package/dist/agent/engine/ai-sdk-engine.js +24 -4
  24. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  25. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  26. package/dist/agent/engine/anthropic-engine.js +13 -3
  27. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  28. package/dist/agent/engine/output-tokens.d.ts +27 -0
  29. package/dist/agent/engine/output-tokens.d.ts.map +1 -1
  30. package/dist/agent/engine/output-tokens.js +70 -0
  31. package/dist/agent/engine/output-tokens.js.map +1 -1
  32. package/dist/agent/production-agent.d.ts.map +1 -1
  33. package/dist/agent/production-agent.js +28 -7
  34. package/dist/agent/production-agent.js.map +1 -1
  35. package/dist/agent/types.d.ts +3 -2
  36. package/dist/agent/types.d.ts.map +1 -1
  37. package/dist/agent/types.js.map +1 -1
  38. package/dist/collab/routes.d.ts +1 -1
  39. package/dist/db/client.d.ts.map +1 -1
  40. package/dist/db/client.js +12 -7
  41. package/dist/db/client.js.map +1 -1
  42. package/dist/observability/routes.d.ts +5 -5
  43. package/dist/progress/routes.d.ts +1 -1
  44. package/dist/secrets/routes.d.ts +9 -9
  45. package/dist/shared/reasoning-effort.d.ts +1 -0
  46. package/dist/shared/reasoning-effort.d.ts.map +1 -1
  47. package/dist/shared/reasoning-effort.js +18 -0
  48. package/dist/shared/reasoning-effort.js.map +1 -1
  49. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2145
31
- - template files: 4841
31
+ - template files: 4843
@@ -1,5 +1,13 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.90.11
4
+
5
+ ### Patch Changes
6
+
7
+ - bba7332: Keep agent-chat workers on the hosted foreground timeout unless they are actually running inside a background function, preventing misrouted workers from being killed as stale runs.
8
+ - bba7332: Fix "The model returned an empty response" on hard/long-context chat turns: interactive chat now resolves max_output_tokens to min(model ceiling, 32K) instead of the flat 4096-8192 per-engine defaults, Anthropic/Gemini numeric thinking budgets are clamped to always leave real output headroom under max_tokens, and the empty-final-response retry now raises the token ceiling and steps reasoning effort down a tier (with the retry budget raised from 1 to 2 attempts) instead of re-issuing the identical doomed request.
9
+ - bba7332: Stop misrouted agent-chat workers from taking the large background Neon connection pool. `isBackgroundFunctionPoolContext()` no longer trusts the dispatch marker (`__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__`) — a worker dispatched toward a `-background` URL but routed onto the ~60s synchronous function would otherwise open the 8-connection worker pool while running as one of many warm sync-function instances, exhausting the Neon pooled endpoint (connection terminated / statement timeouts / failed heartbeat writes surfacing as stale runs). Only the genuine `-background` runtime marker (set at cold start) unlocks the larger pool now, mirroring the same proof-of-landing tightening applied to the worker soft-timeout.
10
+
3
11
  ## 0.90.10
4
12
 
5
13
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.90.10",
3
+ "version": "0.90.11",
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": {
@@ -260,12 +260,12 @@ export function backgroundRunMarkerExpectsBackgroundRuntime(
260
260
  }
261
261
 
262
262
  export function shouldUseBackgroundFunctionTimeoutForWorker(
263
- marker: unknown,
263
+ _marker: unknown,
264
264
  ): boolean {
265
- return (
266
- isInBackgroundFunctionRuntime() ||
267
- backgroundRunMarkerExpectsBackgroundRuntime(marker)
268
- );
265
+ // The dispatch marker says which URL the foreground targeted, not where the
266
+ // request actually landed. Only the worker runtime proof can safely lift the
267
+ // hosted 40s clamp to the 15-minute background-function budget.
268
+ return isInBackgroundFunctionRuntime();
269
269
  }
270
270
 
271
271
  export function backgroundRuntimeDiagnosticDetail(marker: unknown): string {
@@ -19,7 +19,10 @@ import {
19
19
  } from "../../server/credential-provider.js";
20
20
  import { normalizeReasoningEffortForModel } from "../../shared/reasoning-effort.js";
21
21
  import { AI_SDK_MODEL_CONFIG, type AISDKProvider } from "../model-config.js";
22
- import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
22
+ import {
23
+ clampThinkingBudgetTokens,
24
+ resolveMaxOutputTokensForEngine,
25
+ } from "./output-tokens.js";
23
26
  import {
24
27
  engineToolsToAISDK,
25
28
  engineMessagesToAISDK,
@@ -262,16 +265,34 @@ class AISDKEngine implements AgentEngine {
262
265
  toolResultImages: this.capabilities.vision,
263
266
  });
264
267
 
268
+ // Resolved once so both `maxOutputTokens` (below, in the streamText call)
269
+ // and the thinking-budget headroom clamp agree on the same ceiling.
270
+ const resolvedMaxOutputTokens = resolveMaxOutputTokensForEngine(
271
+ this.name,
272
+ opts.maxOutputTokens,
273
+ opts.model,
274
+ );
275
+
265
276
  // Build providerOptions for Anthropic-native features when using Anthropic provider
266
277
  const providerOpts: Record<string, unknown> = {};
267
278
  if (this.provider === "anthropic" && opts.providerOptions?.anthropic) {
268
279
  const anthropicOpts = opts.providerOptions.anthropic;
269
280
  if (anthropicOpts.thinking) {
281
+ // Only the "enabled" config carries a numeric budgetTokens; clamp it
282
+ // so thinking can't consume the entire maxOutputTokens budget and
283
+ // leave zero room for the actual response ("adaptive" thinking has
284
+ // no budgetTokens field at all per @ai-sdk/anthropic's schema).
270
285
  providerOpts.anthropic = {
271
286
  ...((providerOpts.anthropic as object) ?? {}),
272
287
  thinking: {
273
288
  type: "enabled",
274
- budgetTokens: anthropicOpts.thinking.budgetTokens,
289
+ budgetTokens:
290
+ typeof anthropicOpts.thinking.budgetTokens === "number"
291
+ ? clampThinkingBudgetTokens(
292
+ anthropicOpts.thinking.budgetTokens,
293
+ resolvedMaxOutputTokens,
294
+ )
295
+ : anthropicOpts.thinking.budgetTokens,
275
296
  },
276
297
  };
277
298
  }
@@ -309,11 +330,26 @@ class AISDKEngine implements AgentEngine {
309
330
  // Gemini 3.x models reject thinkingBudget — they require thinkingLevel.
310
331
  // Gemini 2.5.x models use thinkingBudget (integer token count or -1).
311
332
  const isGemini3 = /^gemini-3/.test(opts.model);
333
+ const thinkingBudget = googleThinkingBudget(reasoningEffort);
312
334
  providerOpts.google = {
313
335
  ...((providerOpts.google as object) ?? {}),
314
336
  thinkingConfig: isGemini3
315
337
  ? { thinkingLevel: gemini3ThinkingLevel(reasoningEffort) }
316
- : { thinkingBudget: googleThinkingBudget(reasoningEffort) },
338
+ : {
339
+ // Unlike Anthropic's adaptive thinking, Gemini 2.5's
340
+ // thinkingBudget IS a concrete numeric token count, so the
341
+ // same headroom clamp applies: at "max" effort this maps to
342
+ // 32000 tokens, which can equal (or exceed) a small
343
+ // maxOutputTokens cap and leave zero room for the actual
344
+ // response. Preserve Gemini's -1 "dynamic" sentinel.
345
+ thinkingBudget:
346
+ thinkingBudget > 0
347
+ ? clampThinkingBudgetTokens(
348
+ thinkingBudget,
349
+ resolvedMaxOutputTokens,
350
+ )
351
+ : thinkingBudget,
352
+ },
317
353
  };
318
354
  }
319
355
  }
@@ -326,11 +362,7 @@ class AISDKEngine implements AgentEngine {
326
362
  system: opts.systemPrompt,
327
363
  messages,
328
364
  tools: aiSdkTools,
329
- maxOutputTokens: resolveMaxOutputTokensForEngine(
330
- this.name,
331
- opts.maxOutputTokens,
332
- opts.model,
333
- ),
365
+ maxOutputTokens: resolvedMaxOutputTokens,
334
366
  ...(opts.temperature !== undefined
335
367
  ? { temperature: opts.temperature }
336
368
  : {}),
@@ -19,7 +19,10 @@ import {
19
19
  LLM_MISSING_CREDENTIALS_ERROR_CODE,
20
20
  LLM_MISSING_CREDENTIALS_MESSAGE,
21
21
  } from "./credential-errors.js";
22
- import { resolveMaxOutputTokensForEngine } from "./output-tokens.js";
22
+ import {
23
+ clampThinkingBudgetTokens,
24
+ resolveMaxOutputTokensForEngine,
25
+ } from "./output-tokens.js";
23
26
  import {
24
27
  engineToolsToAnthropic,
25
28
  engineMessagesToAnthropic,
@@ -67,12 +70,31 @@ class AnthropicEngine implements AgentEngine {
67
70
  const messages = engineMessagesToAnthropic(opts.messages);
68
71
  const anthropicOpts = opts.providerOptions?.anthropic;
69
72
 
73
+ // Resolved once so both max_tokens and the thinking-budget headroom
74
+ // clamp below agree on the same ceiling.
75
+ const resolvedMaxOutputTokens = resolveMaxOutputTokensForEngine(
76
+ this.name,
77
+ opts.maxOutputTokens,
78
+ opts.model,
79
+ );
80
+
70
81
  // Build extra body params for Anthropic-native features
71
82
  const extra: Record<string, unknown> = {};
72
83
  if (anthropicOpts?.thinking) {
73
84
  extra.thinking = {
74
85
  type: anthropicOpts.thinking.type,
75
- budget_tokens: anthropicOpts.thinking.budgetTokens,
86
+ // Only the "enabled" config carries a numeric budget_tokens; clamp it
87
+ // so thinking can't consume the entire max_tokens budget and leave
88
+ // zero room for the actual response ("adaptive" thinking has no
89
+ // budget_tokens field at all, so it passes through unclamped).
90
+ budget_tokens:
91
+ anthropicOpts.thinking.type === "enabled" &&
92
+ typeof anthropicOpts.thinking.budgetTokens === "number"
93
+ ? clampThinkingBudgetTokens(
94
+ anthropicOpts.thinking.budgetTokens,
95
+ resolvedMaxOutputTokens,
96
+ )
97
+ : anthropicOpts.thinking.budgetTokens,
76
98
  };
77
99
  }
78
100
  if (anthropicOpts?.topK !== undefined) {
@@ -139,11 +161,7 @@ class AnthropicEngine implements AgentEngine {
139
161
 
140
162
  const requestParams: any = {
141
163
  model: opts.model,
142
- max_tokens: resolveMaxOutputTokensForEngine(
143
- this.name,
144
- opts.maxOutputTokens,
145
- opts.model,
146
- ),
164
+ max_tokens: resolvedMaxOutputTokens,
147
165
  system: systemBlocks,
148
166
  tools: cachedTools.length > 0 ? cachedTools : undefined,
149
167
  messages: cachedMessages,
@@ -13,6 +13,100 @@ export const DEFAULT_AI_SDK_MAX_OUTPUT_TOKENS = 4096;
13
13
  export const DEFAULT_ANTHROPIC_MAX_OUTPUT_TOKENS = 8192;
14
14
  export const DEFAULT_BUILDER_MAX_OUTPUT_TOKENS = 8192;
15
15
 
16
+ // ---------------------------------------------------------------------------
17
+ // Main interactive chat path
18
+ //
19
+ // The per-engine defaults above (4096-8192) exist for internal/eval/
20
+ // observational-memory callers that intentionally want a small explicit cap.
21
+ // The main interactive chat turn (the one the end user is staring at) needs
22
+ // real headroom: on long-context or reasoning-heavy turns, a tiny completion
23
+ // budget means extended thinking alone can consume the entire response,
24
+ // leaving zero tokens for visible text/tool calls ("empty response" bug).
25
+ // These helpers give the chat path a much higher floor while staying at or
26
+ // under each model's documented ceiling — they never lower the ceiling.
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /** Cap for the first attempt of an interactive chat turn. */
30
+ export const MAIN_CHAT_MAX_OUTPUT_TOKENS_CAP = 32_000;
31
+ /**
32
+ * Cap used only when retrying a turn that came back with an empty final
33
+ * response (see production-agent.ts's empty-final-response retry). Higher
34
+ * than the first-attempt cap so the retry meaningfully raises the ceiling.
35
+ */
36
+ export const EMPTY_RESPONSE_RETRY_MAX_OUTPUT_TOKENS_CAP = 64_000;
37
+
38
+ /**
39
+ * Resolve the max_output_tokens floor for the first attempt of an
40
+ * interactive chat turn: min(model ceiling, 32K). Always at or above the
41
+ * flat per-engine defaults above, regardless of whether the model is known.
42
+ */
43
+ export function resolveMainChatMaxOutputTokens(modelId?: string): number {
44
+ return Math.min(
45
+ getMaxOutputTokensForModel(modelId),
46
+ MAIN_CHAT_MAX_OUTPUT_TOKENS_CAP,
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Resolve the max_output_tokens to use when retrying a turn after an empty
52
+ * final response: min(model ceiling, 64K).
53
+ */
54
+ export function resolveEmptyResponseRetryMaxOutputTokens(
55
+ modelId?: string,
56
+ ): number {
57
+ return Math.min(
58
+ getMaxOutputTokensForModel(modelId),
59
+ EMPTY_RESPONSE_RETRY_MAX_OUTPUT_TOKENS_CAP,
60
+ );
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Extended-thinking budget headroom
65
+ //
66
+ // Anthropic's `thinking: { type: "enabled", budget_tokens }` config requires
67
+ // budget_tokens >= 1024 and STRICTLY LESS THAN max_tokens (confirmed against
68
+ // the installed @anthropic-ai/sdk@0.90.0 type defs — see ThinkingConfigEnabled
69
+ // in resources/messages/messages.d.ts). budget_tokens counts toward
70
+ // max_tokens, so an unclamped large budget can leave too little (or zero)
71
+ // room for the actual visible completion. This clamp guarantees at least
72
+ // max(8000, 40% of maxOutputTokens) tokens of non-thinking headroom.
73
+ //
74
+ // Note: this only applies to the explicit numeric-budget "enabled" config.
75
+ // Anthropic's `type: "adaptive"` thinking config (used by the
76
+ // reasoningEffort -> output_config.effort mapping in anthropic-engine.ts /
77
+ // ai-sdk-engine.ts) has NO budget_tokens field at all per the SDK types, so
78
+ // there is nothing to clamp there — those callers rely on the raised
79
+ // maxOutputTokens ceiling above instead.
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /** Anthropic's documented minimum extended-thinking budget. */
83
+ export const ANTHROPIC_MIN_THINKING_BUDGET_TOKENS = 1024;
84
+
85
+ /**
86
+ * Clamp a requested Anthropic thinking budget so it leaves guaranteed
87
+ * headroom under `maxOutputTokens` for non-thinking output, and stays within
88
+ * the provider's valid range (>= 1024, < maxOutputTokens).
89
+ */
90
+ export function clampThinkingBudgetTokens(
91
+ requestedBudgetTokens: number,
92
+ maxOutputTokens: number,
93
+ ): number | undefined {
94
+ if (maxOutputTokens <= ANTHROPIC_MIN_THINKING_BUDGET_TOKENS) {
95
+ return undefined;
96
+ }
97
+ const headroom = Math.max(8000, Math.round(0.4 * maxOutputTokens));
98
+ const budgetCapForHeadroom = Math.max(
99
+ ANTHROPIC_MIN_THINKING_BUDGET_TOKENS,
100
+ maxOutputTokens - headroom,
101
+ );
102
+ // budget_tokens must stay strictly below max_tokens per the API contract.
103
+ const strictUpperBound = maxOutputTokens - 1;
104
+ return Math.max(
105
+ ANTHROPIC_MIN_THINKING_BUDGET_TOKENS,
106
+ Math.min(requestedBudgetTokens, budgetCapForHeadroom, strictUpperBound),
107
+ );
108
+ }
109
+
16
110
  function parsePositiveInteger(value: unknown): number | null {
17
111
  if (typeof value === "string" && value.trim() === "") return null;
18
112
  const n =
@@ -45,6 +45,7 @@ import { fireInternalDispatch } from "../server/self-dispatch.js";
45
45
  import {
46
46
  isReasoningEffort,
47
47
  normalizeReasoningEffortForModel,
48
+ stepDownReasoningEffort,
48
49
  type ReasoningEffort,
49
50
  } from "../shared/reasoning-effort.js";
50
51
  import { actionPreparationContinuationNote } from "./action-continuation-guidance.js";
@@ -77,7 +78,11 @@ import {
77
78
  normalizeModelForEngine,
78
79
  isResolvedEngineUsableForRequest,
79
80
  } from "./engine/index.js";
80
- import { resolveMaxOutputTokensForEngine } from "./engine/output-tokens.js";
81
+ import {
82
+ resolveEmptyResponseRetryMaxOutputTokens,
83
+ resolveMainChatMaxOutputTokens,
84
+ resolveMaxOutputTokensForEngine,
85
+ } from "./engine/output-tokens.js";
81
86
  import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js";
82
87
  import {
83
88
  backfillEngineMessagesToolResults,
@@ -1015,7 +1020,10 @@ const TOOL_INPUT_ACTIVITY_INTERVAL_MS = 1500;
1015
1020
  const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
1016
1021
  const ACTION_PREPARATION_ZERO_BYTE_RESTART_LIMIT = 2;
1017
1022
  const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90_000;
1018
- const EMPTY_FINAL_RESPONSE_RETRY_LIMIT = 1;
1023
+ // Raised from 1 -> 2 now that each retry actually adapts (raises the token
1024
+ // ceiling and steps reasoning effort down a tier) instead of re-issuing the
1025
+ // exact same doomed request twice.
1026
+ const EMPTY_FINAL_RESPONSE_RETRY_LIMIT = 2;
1019
1027
  const MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT = 6;
1020
1028
  const RUN_BUDGET_EXHAUSTED_ERROR_CODE = "run_budget_exhausted";
1021
1029
  const RUN_BUDGET_EXHAUSTED_MESSAGE =
@@ -2736,6 +2744,12 @@ export async function runAgentLoop(opts: {
2736
2744
  let finalGuardRetries = 0;
2737
2745
  let emptyFinalResponseRetries = 0;
2738
2746
  let iterations = 0;
2747
+ // Overridden (raised tokens, lowered effort) only after an empty-final-
2748
+ // response retry below — kept separate from `opts.maxOutputTokens`/
2749
+ // `opts.reasoningEffort` so the very first attempt is unaffected and later
2750
+ // tool-loop turns revert to the caller's original request after a success.
2751
+ let effectiveMaxOutputTokens = opts.maxOutputTokens;
2752
+ let effectiveReasoningEffort = opts.reasoningEffort;
2739
2753
 
2740
2754
  // Set when an in-loop processor aborts via `abort()` / throws a `TripWire`.
2741
2755
  // The loop emits the `tripwire` event, surfaces the reason as a final
@@ -2845,10 +2859,10 @@ export async function runAgentLoop(opts: {
2845
2859
  abortSignal: signal,
2846
2860
  maxOutputTokens: resolveMaxOutputTokensForEngine(
2847
2861
  engine.name,
2848
- opts.maxOutputTokens,
2862
+ effectiveMaxOutputTokens,
2849
2863
  model,
2850
2864
  ),
2851
- reasoningEffort: opts.reasoningEffort,
2865
+ reasoningEffort: effectiveReasoningEffort,
2852
2866
  providerOptions: opts.providerOptions,
2853
2867
  };
2854
2868
 
@@ -3351,8 +3365,10 @@ export async function runAgentLoop(opts: {
3351
3365
  // text — typically when reasoning consumes the entire output-token
3352
3366
  // budget. Without a final text part the SSE stream still ends with a
3353
3367
  // clean `done`, which renders as a totally empty assistant bubble.
3354
- // Retry once so a transient reasoning-budget miss can still finish; if
3355
- // the retry also has no visible content, surface a plain-language error.
3368
+ // Retry so a reasoning-budget miss can still finish; each retry raises
3369
+ // the token ceiling and steps reasoning effort down a tier so it's not
3370
+ // just re-issuing the identical doomed request. If retries also come
3371
+ // back empty, surface a plain-language error.
3356
3372
  const hasEmptyFinalResponse =
3357
3373
  !guardEmittedFallback &&
3358
3374
  collectTextParts(assistantContentForHistory).trim().length === 0 &&
@@ -3360,6 +3376,11 @@ export async function runAgentLoop(opts: {
3360
3376
  if (hasEmptyFinalResponse) {
3361
3377
  if (emptyFinalResponseRetries < EMPTY_FINAL_RESPONSE_RETRY_LIMIT) {
3362
3378
  emptyFinalResponseRetries += 1;
3379
+ effectiveMaxOutputTokens =
3380
+ resolveEmptyResponseRetryMaxOutputTokens(model);
3381
+ effectiveReasoningEffort = stepDownReasoningEffort(
3382
+ effectiveReasoningEffort,
3383
+ );
3363
3384
  appendAgentLoopContinuation(messages, "max_tokens");
3364
3385
  continue;
3365
3386
  }
@@ -3369,6 +3390,8 @@ export async function runAgentLoop(opts: {
3369
3390
  });
3370
3391
  } else {
3371
3392
  emptyFinalResponseRetries = 0;
3393
+ effectiveMaxOutputTokens = opts.maxOutputTokens;
3394
+ effectiveReasoningEffort = opts.reasoningEffort;
3372
3395
  }
3373
3396
  break;
3374
3397
  }
@@ -6795,6 +6818,11 @@ export function createProductionAgentHandler(
6795
6818
  orgId: getRequestOrgId() ?? null,
6796
6819
  attachments: requestAttachments,
6797
6820
  reasoningEffort,
6821
+ // The interactive chat turn needs real completion headroom — the
6822
+ // flat per-engine defaults (4096-8192) exist for internal/eval
6823
+ // callers and are far below what a hard, long-context turn needs
6824
+ // once extended thinking is in play. See output-tokens.ts.
6825
+ maxOutputTokens: resolveMainChatMaxOutputTokens(effectiveModel),
6798
6826
  providerOptions: options.providerOptions,
6799
6827
  executionMode: requestMode,
6800
6828
  maxIterations: loopSettings.maxIterations,
@@ -180,8 +180,9 @@ export interface AgentChatRequest {
180
180
  continuationCount?: number;
181
181
  /**
182
182
  * True when the dispatcher expects the self-POST to land in a real
183
- * Netlify `-background` function (15-min budget) rather than the ~60s
184
- * synchronous function. See `shouldUseBackgroundFunctionTimeoutForWorker`.
183
+ * Netlify `-background` function rather than the ~60s synchronous function.
184
+ * This is diagnostic only; the 15-minute budget is unlocked by the worker's
185
+ * actual runtime marker.
185
186
  */
186
187
  backgroundFunctionRuntimeExpected?: boolean;
187
188
  /**
@@ -856,15 +856,18 @@ export function isBackgroundFunctionPoolContext(): boolean {
856
856
  ) {
857
857
  return true;
858
858
  }
859
- // Set by the HMAC-authenticated agent-chat `_process-run` route before it
860
- // re-enters the normal chat handler. This mirrors the marker-only runtime
861
- // proof used by durable-background.ts without importing agent code into db/.
862
- if (
863
- (globalThis as Record<string, unknown>)
864
- .__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__ === true
865
- ) {
866
- return true;
867
- }
859
+ // NOTE: we deliberately do NOT trust `__AGENT_NATIVE_BACKGROUND_RUNTIME_EXPECTED__`
860
+ // here. That flag is set from the dispatch MARKER (which URL the foreground
861
+ // targeted), not from proof the request actually LANDED on a background
862
+ // function. A worker dispatched toward `-background` but routed onto the ~60s
863
+ // synchronous function would otherwise take the 8-connection background pool
864
+ // while running as one of MANY warm sync-function instances — multiplying
865
+ // Neon connections and exhausting the pooled endpoint ("connection
866
+ // terminated" / statement timeouts / failed heartbeat writes → stale runs).
867
+ // The genuine `-background` function sets `__AGENT_NATIVE_BACKGROUND_RUNTIME__`
868
+ // as its first cold-start statement, so a real background worker still gets
869
+ // the larger pool via the check above. Mirrors the same proof-of-landing
870
+ // tightening applied to `shouldUseBackgroundFunctionTimeoutForWorker`.
868
871
  const lambdaName = process.env.AWS_LAMBDA_FUNCTION_NAME;
869
872
  if (
870
873
  typeof lambdaName === "string" &&
@@ -98,6 +98,29 @@ export function reasoningEffortLabel(effort: ReasoningEffort | undefined) {
98
98
  return REASONING_EFFORT_LABELS[effort ?? "auto"];
99
99
  }
100
100
 
101
+ /**
102
+ * One tier down from each effort, stopping at "minimal" — "none"/"auto"
103
+ * (not really "tiers") and "minimal" itself are left unchanged. Used by the
104
+ * empty-final-response retry so a retried turn asks for meaningfully less
105
+ * reasoning instead of repeating the exact request that came back empty.
106
+ */
107
+ const REASONING_EFFORT_STEP_DOWN: Partial<
108
+ Record<ReasoningEffort, ReasoningEffort>
109
+ > = {
110
+ max: "xhigh",
111
+ xhigh: "high",
112
+ high: "medium",
113
+ medium: "low",
114
+ low: "minimal",
115
+ };
116
+
117
+ export function stepDownReasoningEffort(
118
+ effort: ReasoningEffort | undefined,
119
+ ): ReasoningEffort | undefined {
120
+ if (!effort) return effort;
121
+ return REASONING_EFFORT_STEP_DOWN[effort] ?? effort;
122
+ }
123
+
101
124
  function isGPTReasoningModel(model: string) {
102
125
  return /^gpt-5/.test(model) || /^o\d/.test(model);
103
126
  }
@@ -23,6 +23,12 @@ export default defineAction({
23
23
  "Ask the agent to generate chapters for this recording based on its transcript (and the full video when Include full video is enabled). The agent identifies topic transitions and calls set-chapters.",
24
24
  schema: z.object({
25
25
  recordingId: z.string().describe("Recording ID"),
26
+ openInChat: z
27
+ .boolean()
28
+ .optional()
29
+ .describe(
30
+ "When true, focus the queued generation request in the agent chat instead of keeping it hidden.",
31
+ ),
26
32
  }),
27
33
  run: async (args) => {
28
34
  await assertAccess("recording", args.recordingId, "editor");
@@ -57,6 +63,7 @@ export default defineAction({
57
63
  segmentsJson: transcript?.segmentsJson ?? "[]",
58
64
  transcriptText: transcript?.fullText ?? "",
59
65
  includeFullVideoInAi,
66
+ openInChat: args.openInChat === true,
60
67
  message: withFullVideoAiInstructions(
61
68
  baseMessage,
62
69
  args.recordingId,
@@ -15,6 +15,7 @@ import {
15
15
  agentNativePath,
16
16
  callAction,
17
17
  sendToAgentChat,
18
+ type AgentChatMessage,
18
19
  } from "@agent-native/core/client";
19
20
  import { fullVideoAiModelSelection } from "@shared/clips-ai-prefs";
20
21
  import { useEffect, useRef } from "react";
@@ -55,6 +56,7 @@ interface AiRequest {
55
56
  thresholdMs?: number;
56
57
  message?: string;
57
58
  includeFullVideoInAi?: boolean;
59
+ openInChat?: boolean;
58
60
  }
59
61
 
60
62
  const DISPATCHABLE_REQUESTS = new Set([
@@ -201,25 +203,33 @@ function buildRequestContext(rec: RecordingSummary, request: AiRequest) {
201
203
  };
202
204
  }
203
205
 
204
- function dispatchAiRequest(rec: RecordingSummary, request: AiRequest) {
206
+ export function buildAiRequestChatOptions(
207
+ rec: RecordingSummary,
208
+ request: AiRequest,
209
+ ): AgentChatMessage {
205
210
  const includeFullVideo = request.includeFullVideoInAi === true;
206
211
  const gemini = includeFullVideo ? fullVideoAiModelSelection() : null;
207
- sendToAgentChat({
212
+ const openInChat = request.openInChat === true;
213
+ return {
208
214
  message:
209
215
  request.message ??
210
216
  `Handle queued ${request.kind} work for recording ${rec.id}.`,
211
217
  context: JSON.stringify(buildRequestContext(rec, request)),
212
218
  submit: true,
213
- openSidebar: false,
219
+ openSidebar: openInChat ? true : false,
214
220
  newTab: true,
215
- background: true,
221
+ background: !openInChat,
216
222
  ...(gemini
217
223
  ? {
218
224
  engine: gemini.engine,
219
225
  model: gemini.model,
220
226
  }
221
227
  : {}),
222
- });
228
+ };
229
+ }
230
+
231
+ function dispatchAiRequest(rec: RecordingSummary, request: AiRequest) {
232
+ sendToAgentChat(buildAiRequestChatOptions(rec, request));
223
233
  }
224
234
 
225
235
  function parseJsonArray(raw: string | undefined): unknown[] {
@@ -4,7 +4,6 @@ import {
4
4
  IconBrandApple,
5
5
  IconBrandWindows,
6
6
  IconExternalLink,
7
- IconPlayerRecord,
8
7
  } from "@tabler/icons-react";
9
8
  import { useEffect, useState } from "react";
10
9
 
@@ -201,9 +200,18 @@ export default function DownloadPage() {
201
200
  href={appPath("/")}
202
201
  className="flex items-center gap-2 font-semibold"
203
202
  >
204
- <span className="grid h-7 w-7 place-items-center rounded-md bg-primary text-primary-foreground">
205
- <IconPlayerRecord className="h-4 w-4" />
206
- </span>
203
+ <img
204
+ src={appPath("/agent-native-icon-light.svg")}
205
+ alt=""
206
+ aria-hidden="true"
207
+ className="block h-4 w-auto shrink-0 dark:hidden"
208
+ />
209
+ <img
210
+ src={appPath("/agent-native-icon-dark.svg")}
211
+ alt=""
212
+ aria-hidden="true"
213
+ className="hidden h-4 w-auto shrink-0 dark:block"
214
+ />
207
215
  <span>Clips</span>
208
216
  </a>
209
217
  <a
@@ -1096,6 +1096,7 @@ export default function RecordingPage() {
1096
1096
  onSelect={() =>
1097
1097
  regenerateChapters.mutate({
1098
1098
  recordingId: recording.id,
1099
+ openInChat: true,
1099
1100
  } as any)
1100
1101
  }
1101
1102
  >
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ Auto-generated chapter requests now open the matching agent chat tab.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-07
4
+ ---
5
+
6
+ The Clips Desktop download page now uses the Agent Native app icon in its header.
@@ -103,7 +103,7 @@ export declare function isHostedRuntimeForDurableBackground(): boolean;
103
103
  */
104
104
  export declare function isInBackgroundFunctionRuntime(): boolean;
105
105
  export declare function backgroundRunMarkerExpectsBackgroundRuntime(marker: unknown): boolean;
106
- export declare function shouldUseBackgroundFunctionTimeoutForWorker(marker: unknown): boolean;
106
+ export declare function shouldUseBackgroundFunctionTimeoutForWorker(_marker: unknown): boolean;
107
107
  export declare function backgroundRuntimeDiagnosticDetail(marker: unknown): string;
108
108
  /**
109
109
  * The single gate. True when the env flag is explicitly enabled, or a workspace
@@ -1 +1 @@
1
- {"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AAgDA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,kCAAkC,gDAA0D,CAAC;AAiC1G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,sCAAsC,IAAI,MAAM,CAQ/D;AAED,wBAAgB,4CAA4C,CAC1D,YAAY,EAAE,MAAM,GACnB,OAAO,CAET;AAED;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAsBvD;AAED,wBAAgB,2CAA2C,CACzD,MAAM,EAAE,OAAO,GACd,OAAO,CAOT;AAED,wBAAgB,2CAA2C,CACzD,MAAM,EAAE,OAAO,GACd,OAAO,CAKT;AAED,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAQzE;AAwCD;;;;;;;GAOG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,CAAC,EAAE;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAWV;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,qCACb,CAAC;AAiBrC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qCAAqC,IAAI,OAAO,CAM/D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAahE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CAiDvB"}
1
+ {"version":3,"file":"durable-background.d.ts","sourceRoot":"","sources":["../../src/agent/durable-background.ts"],"names":[],"mappings":"AAgDA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,4BAA4B,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,kCAAkC,gDAA0D,CAAC;AAiC1G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,sCAAsC,IAAI,MAAM,CAQ/D;AAED,wBAAgB,4CAA4C,CAC1D,YAAY,EAAE,MAAM,GACnB,OAAO,CAET;AAED;;;GAGG;AACH,eAAO,MAAM,iCAAiC,kCACb,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AAEjE;;;;GAIG;AACH,wBAAgB,mCAAmC,IAAI,OAAO,CAsB7D;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAsBvD;AAED,wBAAgB,2CAA2C,CACzD,MAAM,EAAE,OAAO,GACd,OAAO,CAOT;AAED,wBAAgB,2CAA2C,CACzD,OAAO,EAAE,OAAO,GACf,OAAO,CAKT;AAED,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAQzE;AAwCD;;;;;;;GAOG;AACH,wBAAgB,mCAAmC,CAAC,OAAO,CAAC,EAAE;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAWV;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,qCACb,CAAC;AAiBrC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,qCAAqC,IAAI,OAAO,CAM/D;AAED,uDAAuD;AACvD,MAAM,MAAM,qBAAqB,GAC7B;IACE,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,GACD;IACE,EAAE,EAAE,KAAK,CAAC;IACV,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAahE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,qBAAqB,CAiDvB"}
@@ -225,9 +225,11 @@ export function backgroundRunMarkerExpectsBackgroundRuntime(marker) {
225
225
  marker
226
226
  .backgroundFunctionRuntimeExpected === true);
227
227
  }
228
- export function shouldUseBackgroundFunctionTimeoutForWorker(marker) {
229
- return (isInBackgroundFunctionRuntime() ||
230
- backgroundRunMarkerExpectsBackgroundRuntime(marker));
228
+ export function shouldUseBackgroundFunctionTimeoutForWorker(_marker) {
229
+ // The dispatch marker says which URL the foreground targeted, not where the
230
+ // request actually landed. Only the worker runtime proof can safely lift the
231
+ // hosted 40s clamp to the 15-minute background-function budget.
232
+ return isInBackgroundFunctionRuntime();
231
233
  }
232
234
  export function backgroundRuntimeDiagnosticDetail(marker) {
233
235
  return [