@agent-native/core 0.145.0 → 0.145.2

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 (72) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/brain/server/lib/capture-sanitization.ts +6 -2
  3. package/corpus/templates/clips/changelog/2026-08-07-full-screen-recordings-on-a-multi-monitor-mac-now-let-you-pi.md +6 -0
  4. package/corpus/templates/clips/chrome-extension/src/styles.css +13 -4
  5. package/corpus/templates/clips/desktop/src/app.tsx +38 -1
  6. package/corpus/templates/clips/desktop/src/components/MediaDeviceRow.tsx +123 -104
  7. package/corpus/templates/clips/desktop/src/lib/recorder.ts +165 -2
  8. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  9. package/corpus/templates/clips/desktop/src/overlays/monitor-picker.tsx +70 -0
  10. package/corpus/templates/clips/desktop/src/styles.css +70 -11
  11. package/corpus/templates/clips/desktop/src-tauri/capabilities/default.json +2 -1
  12. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +163 -1
  13. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +6 -1
  14. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +141 -22
  15. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +27 -1
  16. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +12 -1
  17. package/dist/agent/engine/index.d.ts +1 -1
  18. package/dist/agent/engine/index.js +1 -1
  19. package/dist/agent/engine/registry.d.ts +15 -0
  20. package/dist/agent/engine/registry.js +39 -10
  21. package/dist/agent/production-agent.d.ts +40 -1
  22. package/dist/agent/production-agent.js +81 -40
  23. package/dist/agent/run-manager.d.ts +44 -1
  24. package/dist/agent/run-manager.js +70 -8
  25. package/dist/chat-threads/store.js +6 -4
  26. package/dist/client/use-db-sync.js +9 -1
  27. package/dist/db/ddl-guard.d.ts +8 -0
  28. package/dist/db/ddl-guard.js +148 -0
  29. package/dist/deploy/build.d.ts +36 -0
  30. package/dist/deploy/build.js +78 -5
  31. package/dist/integrations/webhook-handler.d.ts +3 -2
  32. package/dist/integrations/webhook-handler.js +8 -38
  33. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  34. package/dist/notifications/routes.d.ts +3 -3
  35. package/dist/observability/routes.d.ts +5 -5
  36. package/dist/org/accept-pending.js +2 -2
  37. package/dist/org/auth-policy.js +4 -0
  38. package/dist/org/auto-join-domain.d.ts +9 -0
  39. package/dist/org/auto-join-domain.js +53 -4
  40. package/dist/org/context.js +12 -3
  41. package/dist/org/handlers.js +17 -5
  42. package/dist/org/request-org-cache.d.ts +24 -2
  43. package/dist/org/request-org-cache.js +57 -2
  44. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  45. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  46. package/dist/resources/store.d.ts +1 -1
  47. package/dist/resources/store.js +96 -7
  48. package/dist/secrets/routes.d.ts +6 -6
  49. package/dist/server/agent-chat-plugin.js +83 -37
  50. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  51. package/dist/server/auth.d.ts +3 -0
  52. package/dist/server/auth.js +22 -1
  53. package/dist/server/collab-plugin.js +10 -4
  54. package/dist/server/complete-text.js +14 -7
  55. package/dist/server/core-routes-plugin.js +3 -0
  56. package/dist/server/index.d.ts +1 -1
  57. package/dist/server/index.js +1 -1
  58. package/dist/server/request-context.d.ts +7 -1
  59. package/dist/server/session-email-cache.d.ts +14 -0
  60. package/dist/server/session-email-cache.js +48 -0
  61. package/dist/server/sweep-runtime.d.ts +22 -0
  62. package/dist/server/sweep-runtime.js +28 -0
  63. package/dist/server/transcribe-voice.d.ts +1 -1
  64. package/dist/settings/store.js +63 -7
  65. package/dist/shared/ttl-cache.d.ts +59 -0
  66. package/dist/shared/ttl-cache.js +43 -0
  67. package/dist/sharing/actions/list-resource-shares.d.ts +3 -3
  68. package/dist/sharing/actions/list-resource-shares.js +4 -1
  69. package/dist/triggers/condition-evaluator.js +11 -27
  70. package/docs/content/deployment.mdx +48 -0
  71. package/docs/content/environment-variables.mdx +11 -0
  72. package/package.json +1 -1
@@ -57,6 +57,7 @@ import { createPollHandler } from "./poll.js";
57
57
  import { createRealtimeTokenHandler } from "./realtime-token.js";
58
58
  import { getRequestContext, hasRequestContext, runWithRequestContext, } from "./request-context.js";
59
59
  import { findUnsupportedScopedKeyNames, saveKeyValuesToScopedSecrets, ScopedKeyStorageError, } from "./scoped-key-storage.js";
60
+ import { shouldDisableInProcessSweeps } from "./sweep-runtime.js";
60
61
  import { createTranscribeVoiceHandler } from "./transcribe-voice.js";
61
62
  import { createVoiceProvidersStatusHandler } from "./voice-providers-status.js";
62
63
  import { createWorkspaceProviderOAuthHandler } from "./workspace-provider-oauth.js";
@@ -1260,6 +1261,8 @@ export function createCoreRoutesPlugin(options = {}) {
1260
1261
  // poll-time drain in run-code covers deployments where warm-instance
1261
1262
  // timers rarely fire.
1262
1263
  (() => {
1264
+ if (shouldDisableInProcessSweeps())
1265
+ return;
1263
1266
  let lastSweep = 0;
1264
1267
  const SWEEP_INTERVAL_MS = 2 * 60 * 1000;
1265
1268
  setTimeout(() => {
@@ -14,7 +14,7 @@ export { assertCurrentRequestUserIsOrgAdmin, currentRequestUserIsOrgAdmin, } fro
14
14
  export { verifyCaptcha, type CaptchaVerifyResult } from "./captcha.js";
15
15
  export { getLocaleInitScript, parseAcceptLanguage, resolveLocaleFromRequest, type LocaleInitScriptOptions, type ResolveLocaleFromRequestOptions, type ResolvedRequestLocale, } from "../localization/server.js";
16
16
  export { createProductionAgentHandler, type ActionEntry, type ScriptEntry, type ProductionAgentOptions, type ActionTool, type ScriptTool, type AgentMessage, type AgentChatRequest, type AgentChatEvent, type AgentChatAttachment, type AgentChatReference, type MentionProvider, type MentionProviderItem, type AgentLoopFinalResponseGuard, type AgentLoopFinalResponseGuardContext, type AgentLoopFinalResponseGuardResult, type AgentLoopToolCallSummary, type AgentLoopToolResultSummary, } from "../agent/index.js";
17
- export { actionsToEngineTools, executeAgentToolCall, getOwnerActiveApiKey, runAgentLoop, type AgentToolCallExecutionResult, type ExecuteAgentToolCallOptions, } from "../agent/production-agent.js";
17
+ export { actionsToEngineTools, executeAgentToolCall, getOwnerActiveApiKey, getOwnerApiKeyForEngine, resolveOwnerEngineApiKey, runAgentLoop, type AgentToolCallExecutionResult, type ExecuteAgentToolCallOptions, type ResolvedOwnerApiKey, } from "../agent/production-agent.js";
18
18
  export { mountRealtimeVoiceRoutes, realtimeVoiceSafetyIdentifier, REALTIME_VOICE_MAX_SDP_BYTES, REALTIME_VOICE_MAX_TOOL_BODY_BYTES, REALTIME_VOICE_MAX_TOOL_OUTPUT_CHARS, REALTIME_VOICE_SESSION_PATH, REALTIME_VOICE_TOOL_PATH, type MountRealtimeVoiceRoutesOptions, type RealtimeVoiceRequestContext, type RealtimeVoiceToolExecutionRequest, type RealtimeVoiceToolExecutionResult, } from "./realtime-voice.js";
19
19
  export { getStoredModelForEngine, resolveEngine, } from "../agent/engine/index.js";
20
20
  export { completeText, type CompleteTextMessage, type CompleteTextOptions, type CompleteTextResult, type CompleteTextUsage, } from "./complete-text.js";
@@ -14,7 +14,7 @@ export { assertCurrentRequestUserIsOrgAdmin, currentRequestUserIsOrgAdmin, } fro
14
14
  export { verifyCaptcha } from "./captcha.js";
15
15
  export { getLocaleInitScript, parseAcceptLanguage, resolveLocaleFromRequest, } from "../localization/server.js";
16
16
  export { createProductionAgentHandler, } from "../agent/index.js";
17
- export { actionsToEngineTools, executeAgentToolCall, getOwnerActiveApiKey, runAgentLoop, } from "../agent/production-agent.js";
17
+ export { actionsToEngineTools, executeAgentToolCall, getOwnerActiveApiKey, getOwnerApiKeyForEngine, resolveOwnerEngineApiKey, runAgentLoop, } from "../agent/production-agent.js";
18
18
  export { mountRealtimeVoiceRoutes, realtimeVoiceSafetyIdentifier, REALTIME_VOICE_MAX_SDP_BYTES, REALTIME_VOICE_MAX_TOOL_BODY_BYTES, REALTIME_VOICE_MAX_TOOL_OUTPUT_CHARS, REALTIME_VOICE_SESSION_PATH, REALTIME_VOICE_TOOL_PATH, } from "./realtime-voice.js";
19
19
  export { getStoredModelForEngine, resolveEngine, } from "../agent/engine/index.js";
20
20
  export { completeText, } from "./complete-text.js";
@@ -41,8 +41,14 @@ export interface RequestRunContext {
41
41
  } | null;
42
42
  /** Resolved owner email (set by prepareRun). */
43
43
  owner?: string;
44
- /** Owner's active Anthropic API key (set by prepareRun). */
44
+ /** Owner's API key for this run's engine (set by prepareRun). */
45
45
  userApiKey?: string;
46
+ /**
47
+ * Env var `userApiKey` was issued for. Anything that hands the key to a
48
+ * fixed provider must check this first — the owner's active engine is not
49
+ * always Anthropic, and an unchecked key reaches the wrong endpoint.
50
+ */
51
+ userApiKeyEnvVar?: string;
46
52
  /** Thread ID for the current run (set by onRunStart). */
47
53
  threadId?: string;
48
54
  /** Run ID for the current run (set by onRunStart). */
@@ -0,0 +1,14 @@
1
+ export declare function getCachedSessionEmail(token: string): string | undefined;
2
+ export declare function setCachedSessionEmail(token: string, email: string): void;
3
+ export declare function forgetCachedSessionEmail(token: string): void;
4
+ /**
5
+ * Drop cached session resolutions. Call after ANY write to `sessions`.
6
+ *
7
+ * Clears every token rather than one: the bulk sign-out paths delete by email
8
+ * (`DELETE FROM sessions WHERE email = ?`) and do not know which tokens they
9
+ * removed, so a per-token eviction there would leave a signed-out session
10
+ * resolving for the rest of the TTL.
11
+ */
12
+ export declare function invalidateSessionEmailCache(): void;
13
+ /** Test seam — the cache is module state, so suites must be able to clear it. */
14
+ export declare function __resetSessionEmailCacheForTests(): void;
@@ -0,0 +1,48 @@
1
+ import { createTtlCache } from "../shared/ttl-cache.js";
2
+ /**
3
+ * Cross-request cache for resolved legacy session tokens.
4
+ *
5
+ * A session token is immutable for its (30-day) lifetime, yet every
6
+ * authenticated request re-read it: production showed 494,848 of these for 3
7
+ * users, 1:1 with the request count. The TTL is far shorter than the session
8
+ * lifetime, so expiry is enforced at most `SESSION_EMAIL_TTL_MS` late — a 30-day
9
+ * boundary observed up to 15s late changes nothing.
10
+ *
11
+ * ONLY successful resolutions belong in here. A `null` — no such token, expired,
12
+ * revoked — must stay uncached so a sign-out or a token that has just been
13
+ * written is never answered from a stale negative, and so an auth failure can
14
+ * never be made sticky by a cache.
15
+ *
16
+ * Lives in its own module rather than in `auth.ts` because `auth.ts` imports
17
+ * `org/auth-policy.ts`, which also revokes sessions and so needs to invalidate
18
+ * this. Importing back into `auth.ts` from there would be a cycle.
19
+ */
20
+ const SESSION_EMAIL_TTL_MS = 15_000;
21
+ const sessionEmailCache = createTtlCache({
22
+ ttlMs: SESSION_EMAIL_TTL_MS,
23
+ maxEntries: 4_096,
24
+ });
25
+ export function getCachedSessionEmail(token) {
26
+ return sessionEmailCache.get(token);
27
+ }
28
+ export function setCachedSessionEmail(token, email) {
29
+ sessionEmailCache.set(token, email);
30
+ }
31
+ export function forgetCachedSessionEmail(token) {
32
+ sessionEmailCache.delete(token);
33
+ }
34
+ /**
35
+ * Drop cached session resolutions. Call after ANY write to `sessions`.
36
+ *
37
+ * Clears every token rather than one: the bulk sign-out paths delete by email
38
+ * (`DELETE FROM sessions WHERE email = ?`) and do not know which tokens they
39
+ * removed, so a per-token eviction there would leave a signed-out session
40
+ * resolving for the rest of the TTL.
41
+ */
42
+ export function invalidateSessionEmailCache() {
43
+ sessionEmailCache.clear();
44
+ }
45
+ /** Test seam — the cache is module state, so suites must be able to clear it. */
46
+ export function __resetSessionEmailCacheForTests() {
47
+ sessionEmailCache.clear();
48
+ }
@@ -0,0 +1,22 @@
1
+ type SweepRuntimeEnvKey = "AGENT_NATIVE_DISABLE_INPROCESS_SWEEPS";
2
+ type SweepRuntimeEnv = Partial<Record<SweepRuntimeEnvKey, string | undefined>>;
3
+ /**
4
+ * Kill switch for the `setInterval` backstop sweeps.
5
+ *
6
+ * These timers are correct on a long-lived Node server: one process, one timer
7
+ * set, one query per tick. On a serverless host they are billed per WARM
8
+ * CONTAINER, not per deployment — every warm instance independently runs the
9
+ * whole timer set, so the sweep query rate scales with instance count, which no
10
+ * in-process backoff or `lastSweep` guard can observe. A production sample
11
+ * showed one 2-minute sweep issuing 237k queries, implying ~11 containers each
12
+ * ticking forever against a database that then never gets to autosuspend.
13
+ *
14
+ * Default OFF (sweeps run), because on every host where the timers are the only
15
+ * thing driving recovery, disabling them silently strands queued work. Turn this
16
+ * on ONLY where a durable platform scheduler already drives the same recovery —
17
+ * the shape `isNetlifyRecurringJobsRuntime` + the emitted scheduled sweep
18
+ * already implement for recurring jobs, and which these three sweeps should
19
+ * eventually adopt instead of needing this switch at all.
20
+ */
21
+ export declare function shouldDisableInProcessSweeps(env?: SweepRuntimeEnv): boolean;
22
+ export {};
@@ -0,0 +1,28 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Gating for the in-process backstop sweep timers (automation redispatch,
3
+ // agent-teams orphan reconciliation, durable sandbox executions).
4
+ // ---------------------------------------------------------------------------
5
+ function isTruthyEnv(value) {
6
+ return /^(1|true|yes|on)$/i.test(value?.trim() ?? "");
7
+ }
8
+ /**
9
+ * Kill switch for the `setInterval` backstop sweeps.
10
+ *
11
+ * These timers are correct on a long-lived Node server: one process, one timer
12
+ * set, one query per tick. On a serverless host they are billed per WARM
13
+ * CONTAINER, not per deployment — every warm instance independently runs the
14
+ * whole timer set, so the sweep query rate scales with instance count, which no
15
+ * in-process backoff or `lastSweep` guard can observe. A production sample
16
+ * showed one 2-minute sweep issuing 237k queries, implying ~11 containers each
17
+ * ticking forever against a database that then never gets to autosuspend.
18
+ *
19
+ * Default OFF (sweeps run), because on every host where the timers are the only
20
+ * thing driving recovery, disabling them silently strands queued work. Turn this
21
+ * on ONLY where a durable platform scheduler already drives the same recovery —
22
+ * the shape `isNetlifyRecurringJobsRuntime` + the emitted scheduled sweep
23
+ * already implement for recurring jobs, and which these three sweeps should
24
+ * eventually adopt instead of needing this switch at all.
25
+ */
26
+ export function shouldDisableInProcessSweeps(env = process.env) {
27
+ return isTruthyEnv(env.AGENT_NATIVE_DISABLE_INPROCESS_SWEEPS);
28
+ }
@@ -20,6 +20,6 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
@@ -25,6 +25,24 @@ function requestSettingsCache() {
25
25
  }
26
26
  return cache;
27
27
  }
28
+ /**
29
+ * Per-request memo of the WHOLE settings table for `getAllSettings`.
30
+ *
31
+ * The single-key path above was already request-cached; the full-table read was
32
+ * not, and production showed 98,479 of them — an entire `SELECT key, value FROM
33
+ * settings` per call, several times per request via the MCP client routes and
34
+ * org settings.
35
+ *
36
+ * Holds raw JSON strings, like the single-key cache, so callers can't mutate a
37
+ * shared parsed object. Dropped by every write path, because a request that
38
+ * writes a setting and then re-reads all of them must see its own write.
39
+ */
40
+ const _requestAllSettingsCache = new WeakMap();
41
+ function invalidateRequestAllSettings() {
42
+ const ctx = getRequestContext();
43
+ if (ctx && typeof ctx === "object")
44
+ _requestAllSettingsCache.delete(ctx);
45
+ }
28
46
  // Created lazily so this module can be evaluated in the browser dev graph
29
47
  // without a top-level `new EventEmitter()` tripping Vite's externalized
30
48
  // `node:events` stub. The emitter drives server-side SSE fan-out only.
@@ -152,6 +170,7 @@ export async function mutateSetting(key, updater, options) {
152
170
  if (result.rowsAffected === 0)
153
171
  continue;
154
172
  requestSettingsCache()?.set(key, nextRaw);
173
+ invalidateRequestAllSettings();
155
174
  settingsEmitter().emit("settings", {
156
175
  source: "settings",
157
176
  type: "change",
@@ -173,6 +192,7 @@ export async function putSetting(key, value, options) {
173
192
  args: [key, JSON.stringify(value), Date.now()],
174
193
  });
175
194
  requestSettingsCache()?.set(key, JSON.stringify(value));
195
+ invalidateRequestAllSettings();
176
196
  settingsEmitter().emit("settings", {
177
197
  source: "settings",
178
198
  type: "change",
@@ -189,6 +209,7 @@ export async function deleteSetting(key, options) {
189
209
  args: [key],
190
210
  });
191
211
  requestSettingsCache()?.set(key, null);
212
+ invalidateRequestAllSettings();
192
213
  if (result.rowsAffected > 0) {
193
214
  settingsEmitter().emit("settings", {
194
215
  source: "settings",
@@ -215,6 +236,7 @@ export async function deleteSettingsByPrefix(prefix, options) {
215
236
  args: [`${escaped}%`],
216
237
  });
217
238
  requestSettingsCache()?.clear();
239
+ invalidateRequestAllSettings();
218
240
  if (result.rowsAffected > 0) {
219
241
  settingsEmitter().emit("settings", {
220
242
  source: "settings",
@@ -226,13 +248,47 @@ export async function deleteSettingsByPrefix(prefix, options) {
226
248
  return result.rowsAffected;
227
249
  }
228
250
  export async function getAllSettings() {
229
- await ensureTable();
230
- const client = getDbExec();
231
- const table = settingsTable();
232
- const { rows } = await client.execute(`SELECT key, value FROM ${table}`);
251
+ const raw = await loadAllSettingsRaw();
233
252
  const result = {};
234
- for (const row of rows) {
235
- result[row.key] = JSON.parse(row.value);
236
- }
253
+ for (const [key, value] of raw)
254
+ result[key] = JSON.parse(value);
237
255
  return result;
238
256
  }
257
+ async function loadAllSettingsRaw() {
258
+ const ctx = getRequestContext();
259
+ const cached = ctx && typeof ctx === "object"
260
+ ? _requestAllSettingsCache.get(ctx)
261
+ : undefined;
262
+ if (cached)
263
+ return cached;
264
+ const load = (async () => {
265
+ await ensureTable();
266
+ const client = getDbExec();
267
+ const table = settingsTable();
268
+ const { rows } = await client.execute(`SELECT key, value FROM ${table}`);
269
+ const raw = new Map();
270
+ for (const row of rows)
271
+ raw.set(row.key, row.value);
272
+ // Seed the single-key cache so a `getSetting` after `getAllSettings` in the
273
+ // same request is free rather than another round trip. Only fills keys it
274
+ // does not already hold: a value written earlier in this request is already
275
+ // written through there and must win over this snapshot.
276
+ const perKey = requestSettingsCache();
277
+ if (perKey) {
278
+ for (const [key, value] of raw) {
279
+ if (!perKey.has(key))
280
+ perKey.set(key, value);
281
+ }
282
+ }
283
+ return raw;
284
+ })();
285
+ if (ctx && typeof ctx === "object") {
286
+ // Evicted on failure so one transient error is not memoized as the answer
287
+ // for the rest of the request.
288
+ _requestAllSettingsCache.set(ctx, load.catch((err) => {
289
+ _requestAllSettingsCache.delete(ctx);
290
+ throw err;
291
+ }));
292
+ }
293
+ return load;
294
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Bounded process-level TTL cache for answers that are expensive to re-fetch
3
+ * and safe to serve slightly stale.
4
+ *
5
+ * ## Which cache do I want?
6
+ *
7
+ * This repo has exactly two general caching patterns. Reach for an existing one
8
+ * rather than hand-rolling a third `Map` with timestamps in it:
9
+ *
10
+ * 1. **Request-scoped** — `WeakMap<RequestContext, Map<...>>`, freed with the
11
+ * request. Use when repeated reads within ONE request must collapse but no
12
+ * answer may outlive it. See `settings/store.ts` (`requestSettingsCache`),
13
+ * `org/request-org-cache.ts`, `secrets/storage.ts`.
14
+ * 2. **Process-scoped with a TTL** — this module. Use when every request
15
+ * re-reads the same value and staleness bounded by the TTL is acceptable.
16
+ * Every consumer must also wire an invalidation path for the writes that
17
+ * change the answer; the TTL is a backstop, not the correctness story.
18
+ *
19
+ * Two caches deliberately do NOT use this and should stay as they are, because
20
+ * flattening them into this shape would lose behavior they depend on:
21
+ *
22
+ * - `server/poll.ts`'s collab access cache — different TTL per outcome
23
+ * (allow vs deny), a per-resource invalidation epoch, and in-flight request
24
+ * dedup.
25
+ * - `demo/redact.ts` — a bidirectional memo (forward map plus a produced-value
26
+ * reverse index) with refresh-on-read recency, needed for round-trip
27
+ * stability.
28
+ *
29
+ * The per-request memos elsewhere in this codebase (`event.context` caches,
30
+ * `requestSettingsCache`) collapse repeated reads WITHIN one request. They do
31
+ * nothing for a value every request re-reads identically — a session token's
32
+ * email, an account's org memberships — which on a remote Postgres is a round
33
+ * trip per request per value, forever.
34
+ *
35
+ * ## Only cache successful reads
36
+ *
37
+ * Never `set` a value that stands for "I could not read this". A cached
38
+ * "unreadable" is indistinguishable from a cached "absent" to every later
39
+ * caller, which is exactly how a transient blip turns into a
40
+ * permanent-looking wrong answer. Leave failures on the uncached path so they
41
+ * stay loud and retryable.
42
+ *
43
+ * ## Bounded on purpose
44
+ *
45
+ * Keys here are user-supplied (session tokens, emails), so an unbounded map is
46
+ * a memory leak that grows with traffic. `maxEntries` evicts oldest-written
47
+ * first once the cap is reached.
48
+ */
49
+ export interface TtlCache<V> {
50
+ get(key: string): V | undefined;
51
+ set(key: string, value: V): void;
52
+ delete(key: string): void;
53
+ clear(): void;
54
+ readonly size: number;
55
+ }
56
+ export declare function createTtlCache<V>(options: {
57
+ ttlMs: number;
58
+ maxEntries: number;
59
+ }): TtlCache<V>;
@@ -0,0 +1,43 @@
1
+ export function createTtlCache(options) {
2
+ const { ttlMs, maxEntries } = options;
3
+ if (!(ttlMs > 0)) {
4
+ throw new Error(`createTtlCache: ttlMs must be positive, got ${ttlMs}`);
5
+ }
6
+ if (!(maxEntries > 0)) {
7
+ throw new Error(`createTtlCache: maxEntries must be positive, got ${maxEntries}`);
8
+ }
9
+ const entries = new Map();
10
+ return {
11
+ get(key) {
12
+ const hit = entries.get(key);
13
+ if (!hit)
14
+ return undefined;
15
+ if (hit.expiresAt <= Date.now()) {
16
+ entries.delete(key);
17
+ return undefined;
18
+ }
19
+ return hit.value;
20
+ },
21
+ set(key, value) {
22
+ // Delete before insert so Map insertion order tracks the newest write,
23
+ // which is what the eviction loop below reads as "oldest".
24
+ entries.delete(key);
25
+ entries.set(key, { value, expiresAt: Date.now() + ttlMs });
26
+ while (entries.size > maxEntries) {
27
+ const oldest = entries.keys().next();
28
+ if (oldest.done)
29
+ break;
30
+ entries.delete(oldest.value);
31
+ }
32
+ },
33
+ delete(key) {
34
+ entries.delete(key);
35
+ },
36
+ clear() {
37
+ entries.clear();
38
+ },
39
+ get size() {
40
+ return entries.size;
41
+ },
42
+ };
43
+ }
@@ -12,9 +12,9 @@ declare const _default: import("../../action.js").ActionDefinition<{
12
12
  orgId?: undefined;
13
13
  role?: undefined;
14
14
  } | {
15
- ownerEmail: any;
16
- orgId: any;
17
- visibility: any;
15
+ ownerEmail: string;
16
+ orgId: string;
17
+ visibility: import("../schema.js").Visibility;
18
18
  role: "owner" | import("../schema.js").ShareRole;
19
19
  shares: any;
20
20
  policy: {
@@ -43,7 +43,10 @@ export default defineAction({
43
43
  allowPublic: reg.allowPublic !== false,
44
44
  requireOrgMemberForUserShares: reg.requireOrgMemberForUserShares === true,
45
45
  };
46
- const access = await resolveAccess(args.resourceType, args.resourceId);
46
+ // Only `ownerEmail`/`orgId`/`visibility` are read below, all of which the
47
+ // projected load carries — so the share dialog never pulls a resource's
48
+ // body blob just to render who it is shared with.
49
+ const access = await resolveAccess(args.resourceType, args.resourceId, undefined, { skipResourceBody: true });
47
50
  if (!access)
48
51
  return { ownerEmail: null, visibility: null, shares: [], policy };
49
52
  const db = reg.getDb();
@@ -14,16 +14,22 @@
14
14
  * invalidated wholesale by bumping CONDITION_EVAL_VERSION.
15
15
  */
16
16
  import { createHash } from "node:crypto";
17
+ import { createTtlCache } from "../shared/ttl-cache.js";
17
18
  /**
18
19
  * Bumped whenever the prompt template, model, or hardening logic changes.
19
20
  * Included in the cache key so cached "yes" answers from a previous
20
21
  * (potentially weaker) prompt don't satisfy conditions in the new prompt.
21
22
  */
22
23
  const CONDITION_EVAL_VERSION = "v2";
23
- // LRU cache: hash → { result: boolean, expiresAt: number }
24
- const _cache = new Map();
24
+ // Bounded TTL cache: hash → classifier result. Uses the shared primitive so
25
+ // there is one implementation of "expire and cap an in-memory map" rather than
26
+ // one per call site; see `shared/ttl-cache.ts` for which pattern to use where.
25
27
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
26
28
  const MAX_CACHE_SIZE = 500;
29
+ const _cache = createTtlCache({
30
+ ttlMs: CACHE_TTL_MS,
31
+ maxEntries: MAX_CACHE_SIZE,
32
+ });
27
33
  function cacheKey(condition, payload) {
28
34
  // Salt the cache key with the prompt version + a separate hash of the
29
35
  // payload so two callers can't cross-pollute each other's cache via
@@ -41,26 +47,6 @@ function cacheKey(condition, payload) {
41
47
  const raw = `${CONDITION_EVAL_VERSION}|${condition}|${payloadHash}`;
42
48
  return createHash("sha256").update(raw).digest("hex").slice(0, 32);
43
49
  }
44
- function pruneCache() {
45
- if (_cache.size <= MAX_CACHE_SIZE)
46
- return;
47
- const now = Date.now();
48
- for (const [key, entry] of _cache) {
49
- if (entry.expiresAt < now)
50
- _cache.delete(key);
51
- }
52
- // If still too large, drop oldest entries
53
- if (_cache.size > MAX_CACHE_SIZE) {
54
- const excess = _cache.size - MAX_CACHE_SIZE;
55
- let deleted = 0;
56
- for (const key of _cache.keys()) {
57
- if (deleted >= excess)
58
- break;
59
- _cache.delete(key);
60
- deleted++;
61
- }
62
- }
63
- }
64
50
  /**
65
51
  * Evaluate whether a natural-language condition matches an event payload.
66
52
  * Returns true if the condition is empty/undefined (unconditional trigger).
@@ -70,12 +56,10 @@ export async function evaluateCondition(condition, payload, apiKey) {
70
56
  return true;
71
57
  const key = cacheKey(condition, payload);
72
58
  const cached = _cache.get(key);
73
- if (cached && cached.expiresAt > Date.now()) {
74
- return cached.result;
75
- }
59
+ if (cached !== undefined)
60
+ return cached;
76
61
  const result = await callHaikuClassifier(condition, payload, apiKey);
77
- pruneCache();
78
- _cache.set(key, { result, expiresAt: Date.now() + CACHE_TTL_MS });
62
+ _cache.set(key, result);
79
63
  return result;
80
64
  }
81
65
  async function callHaikuClassifier(condition, payload, apiKey) {
@@ -433,6 +433,49 @@ npx @agent-native/core@latest deploy --preset netlify
433
433
 
434
434
  The workspace build writes static assets under `dist/_workspace_static/` and routes each app to its own Netlify function without forced asset redirects, so files like `/mail/assets/...` are served statically before the server function handles app routes.
435
435
 
436
+ ### Keep-warm and scale-to-zero databases {#netlify-keep-warm}
437
+
438
+ The Netlify build emits a scheduled function that requests `/_agent-native/health` **every minute**. That keeps the server function and its database warm so the next visitor after an idle period doesn't pay a cold start (1–5s on Neon). On provisioned compute this is the right default.
439
+
440
+ **On a metered scale-to-zero database it is the wrong default.** A once-a-minute wake means autosuspend never fires, so the database bills and quota-limits as though it were awake continuously with nobody using the app. On a free-tier Neon this exhausts the compute quota, and the database then rejects **all** reads mid-session — the symptom is a dead app, not a slow one.
441
+
442
+ Three knobs, all opt-in, defaults unchanged:
443
+
444
+ | Variable | Effect |
445
+ | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
446
+ | `AGENT_NATIVE_DISABLE_KEEP_WARM` | Skips the scheduled function entirely. The database is free to autosuspend; the next visitor after an idle period pays its cold start. |
447
+ | `AGENT_NATIVE_KEEP_WARM_SCHEDULE` | Overrides the cadence with a 5-field cron expression, e.g. `*/5 * * * *`. An unparseable value **fails the build** rather than silently reverting to once a minute. |
448
+ | `AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND` | Drops only the background-function warm, keeping the server warm. Warming `server` is one request; warming `-background` starts a fresh container that re-probes the schema. |
449
+
450
+ If you are on a free or otherwise quota-limited database tier, set `AGENT_NATIVE_DISABLE_KEEP_WARM=1` or pick a slower cadence.
451
+
452
+ ### On-demand schema DDL {#skip-ensure-tables}
453
+
454
+ Each store calls `ensureTable()` on its first database touch, which probes
455
+ `information_schema` / `pg_indexes` and issues `CREATE TABLE` / `ADD COLUMN` /
456
+ `CREATE INDEX` only for what is genuinely missing. On a long-lived Node server
457
+ that cost is paid once per boot and is invisible. On serverless, "first touch" is
458
+ **every cold start**, across ~57 stores — so a cold request can pay a few hundred
459
+ serial round trips before it does any real work.
460
+
461
+ Two things reduce it:
462
+
463
+ - The probes are answered from **one batched introspection pass per database**
464
+ (two queries), instead of one query per table, column, and index. This is
465
+ automatic and needs no configuration.
466
+ - `AGENT_NATIVE_SKIP_ENSURE_TABLES=1` skips the probe-and-DDL machinery
467
+ altogether, which is the largest cold-start latency win available.
468
+
469
+ Set `AGENT_NATIVE_SKIP_ENSURE_TABLES=1` when your database schema is already
470
+ migrated and you do not want application boot creating or altering tables — the
471
+ normal case for a production deployment with a real migration step.
472
+
473
+ <Callout tone="risk">
474
+
475
+ **This flag does not and cannot fail closed.** If the schema is in fact missing, the first real query fails loudly rather than being repaired on the fly — which is the point, but it means you own the migration story. Do not set it on a deployment that relies on application boot to provision its tables.
476
+
477
+ </Callout>
478
+
436
479
  ## Cloudflare Pages {#cloudflare-pages}
437
480
 
438
481
  ```ts filename="vite.config.ts"
@@ -532,6 +575,11 @@ hosting behavior.
532
575
  | `APP_URL` | Optional canonical public origin. Checked ahead of `BETTER_AUTH_URL` when resolving the A2A JWT audience, the OAuth public origin, self-dispatch webhook targets, and generated resource links. Set it explicitly on preview/staging deploys where the request host is unreliable. |
533
576
  | `AGENT_PROD_CODE_EXECUTION` | Optional production code-execution mode: `off` (default), `sandboxed`, or `trusted`. See [Production Code Execution](#production-code-execution). |
534
577
  | `AGENT_NATIVE_SSR_CACHE` | Deployment-wide SSR shell cache policy: unset/`on` keeps the default hard-cache, `off` sends `no-store`, or a duration such as `30s` / `5m` sets a shorter freshness. See [SSR Caching](#ssr-caching). |
578
+ | `AGENT_NATIVE_DISABLE_KEEP_WARM` | Skip the Netlify keep-warm scheduled function so a scale-to-zero database can autosuspend. See [Keep-warm](#netlify-keep-warm). |
579
+ | `AGENT_NATIVE_KEEP_WARM_SCHEDULE` | Override the keep-warm cadence with a 5-field cron expression (default `* * * * *`). An unparseable value fails the build. See [Keep-warm](#netlify-keep-warm). |
580
+ | `AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND` | Drop only the background-function warm and keep the server warm. See [Keep-warm](#netlify-keep-warm). |
581
+ | `AGENT_NATIVE_DISABLE_INPROCESS_SWEEPS` | Turn off the in-process backstop sweep timers (automation redispatch, agent-teams reconciliation, sandbox-execution recovery). On serverless these run per warm container, so their query rate scales with instance count. Set it **only** where a durable scheduler already drives the same recovery, or queued work will not be retried. |
582
+ | `AGENT_NATIVE_SKIP_ENSURE_TABLES` | Skip on-demand schema DDL entirely. Removes the schema-probe round trips every cold start pays before doing any work. Requires that your schema is already migrated — see [On-demand schema DDL](#skip-ensure-tables). |
535
583
  | `AGENT_NATIVE_BUILDER_RELAY_SECRET` | Dedicated 32+ char shared HMAC secret for preview-safe Builder authorization relay. Set the same value on the approved corporate callback deployment and preview deployments only when authorization must land in an isolated preview database. |
536
584
  | `AGENT_NATIVE_BUILDER_RELAY_TARGET_ORIGINS` | Corporate callback-only, comma-separated allowlist of exact trusted preview origins (for example, `https://0123456789abcdef01234567--content.netlify.app`). Set it only on the corporate callback deployment. Netlify origins must use an immutable, deploy-specific 24-hex permalink; mutable `deploy-preview-*` aliases, wildcards, and domain suffixes are rejected. |
537
585
 
@@ -40,6 +40,8 @@ deployment-specific values in the environment or scoped secret store.
40
40
  | `WORKSPACE_SECRETS_ENCRYPTION_KEY` | Stable encryption key for a workspace-shared secrets vault. | The same stable key in every app that reads the vault. |
41
41
  | `WORKSPACE_SECRETS_ENCRYPTION_KEY_PREVIOUS` | Previous workspace key during rotation. | The old key only; remove it after migration. |
42
42
  | `ANTHROPIC_API_KEY` | Optional deploy-level fallback for the embedded production agent. | An API key; prefer scoped credentials for multi-tenant apps. |
43
+ | `AGENT_ENGINE` | Explicitly selects the agent engine instead of auto-detecting from provider keys. | A registered engine name such as `anthropic`, `ai-sdk:openai`, `builder`, or `codex-cli`. |
44
+ | `AGENT_ENGINE_PREFER_BYO_KEY` | Prefers a provider key you set over the managed Builder gateway during auto-detection. | `true` or `1`; Builder still applies as the fallback when no provider key is set. |
43
45
  | `AUTH_MODE` | Selects the local authentication behavior used by CLI and agent flows. | Unset or `local`; this is not a browser login bypass. |
44
46
  | `AUTH_DISABLED` | Disables auth for local or preview-only work. | `true` or `1`; never use on a public production app. |
45
47
  | `AUTH_MAGIC_LINK` | Controls the password-first versus magic-link sign-in path. | `0` keeps password-first sign-in; otherwise a configured email transport can enable magic links. |
@@ -47,6 +49,11 @@ deployment-specific values in the environment or scoped secret store.
47
49
  | `ACCESS_TOKEN` / `ACCESS_TOKENS` | Static bearer fallback for MCP and Connect clients. | One token or a delimited token list; these are not browser auth. |
48
50
  | `AGENT_PROD_CODE_EXECUTION` | Production code-execution policy. | `off`, `sandboxed`, or `trusted`. See [Production Code Execution](/docs/deployment#production-code-execution). |
49
51
  | `AGENT_NATIVE_SSR_CACHE` | Deployment-wide public SSR shell cache policy. | Unset/`on`, `off`, or a duration such as `30s` or `5m`. See [SSR Caching](/docs/deployment#ssr-caching). |
52
+ | `AGENT_NATIVE_DISABLE_KEEP_WARM` | Lets a scale-to-zero database autosuspend instead of being woken every minute. | `1`; see [Keep-warm](/docs/deployment#netlify-keep-warm). |
53
+ | `AGENT_NATIVE_KEEP_WARM_SCHEDULE` | Keep-warm cadence override. | A 5-field cron expression such as `*/5 * * * *`; an unparseable value fails the build. |
54
+ | `AGENT_NATIVE_DISABLE_KEEP_WARM_BACKGROUND` | Drops only the background-function warm and keeps the server warm. | `1`; see [Keep-warm](/docs/deployment#netlify-keep-warm). |
55
+ | `AGENT_NATIVE_DISABLE_INPROCESS_SWEEPS` | Turns off the in-process backstop sweep timers where a durable scheduler drives the same work. | `1`; queued work is not retried without another driver. |
56
+ | `AGENT_NATIVE_SKIP_ENSURE_TABLES` | Skips on-demand schema DDL for an already-migrated database. | `1`; see [On-demand schema DDL](/docs/deployment#skip-ensure-tables). You own migrations when this is set. |
50
57
  | `COOKIE_DOMAIN` | Shares auth cookies across same-database subdomains when explicitly configured. | A cookie domain. |
51
58
  | `CORS_ALLOWED_ORIGINS` | Cross-origin request allowlist. | A comma-separated list of exact origins. |
52
59
  | `WEBHOOK_BASE_URL` | Optional public base URL for self-callback and webhook targets. | An absolute public `http(s)` origin. |
@@ -90,8 +97,12 @@ specific Google API integrations; they are not required for browser sign-in.
90
97
  These are deploy-level fallbacks for single-tenant or local use. Hosted,
91
98
  multi-user apps should prefer the scoped credential and connection store.
92
99
 
100
+ Every provider key below selects its engine on its own: dropping one in is
101
+ enough, and `AGENT_ENGINE` is only needed to pick between several.
102
+
93
103
  | Variable | What it controls | Supported values or format |
94
104
  | ------------------------------ | -------------------------------------------------------- | ------------------------------------------------------- |
105
+ | `ANTHROPIC_API_KEY` | Claude model fallback for the built-in AI engine. | An Anthropic API key. |
95
106
  | `OPENAI_API_KEY` | OpenAI model, realtime voice, or transcription fallback. | An OpenAI API key. |
96
107
  | `OPENAI_BASE_URL` | OpenAI-compatible endpoint used with `OPENAI_API_KEY`. | An absolute `http(s)` URL without embedded credentials. |
97
108
  | `GOOGLE_GENERATIVE_AI_API_KEY` | Gemini model fallback for the built-in AI engine. | A Google AI API key. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.145.0",
3
+ "version": "0.145.2",
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": {