@agent-native/core 0.70.3 → 0.72.0
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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +51 -0
- package/corpus/core/docs/content/actions.md +23 -0
- package/corpus/core/docs/content/audit-log.md +111 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/action.ts +80 -1
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +357 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +143 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/application-state/store.ts +5 -0
- package/corpus/core/src/audit/actions/get-audit-event.ts +23 -0
- package/corpus/core/src/audit/actions/list-audit-events.ts +65 -0
- package/corpus/core/src/audit/cleanup-job.ts +100 -0
- package/corpus/core/src/audit/config.ts +91 -0
- package/corpus/core/src/audit/index.ts +43 -0
- package/corpus/core/src/audit/record.ts +143 -0
- package/corpus/core/src/audit/redact.ts +109 -0
- package/corpus/core/src/audit/store.ts +244 -0
- package/corpus/core/src/audit/types.ts +125 -0
- package/corpus/core/src/chat-threads/store.ts +10 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/AssistantChat.tsx +6 -0
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- package/corpus/core/src/client/chat/repo-helpers.ts +38 -0
- package/corpus/core/src/db/client.ts +4 -0
- package/corpus/core/src/db/widen-columns.ts +75 -0
- package/corpus/core/src/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +23 -13
- package/corpus/core/src/oauth-tokens/store.ts +6 -0
- package/corpus/core/src/provider-api/custom-registry.ts +8 -0
- package/corpus/core/src/resources/store.ts +10 -0
- package/corpus/core/src/scripts/runner.ts +6 -3
- package/corpus/core/src/server/action-discovery.ts +6 -0
- package/corpus/core/src/server/action-routes.ts +1 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- package/corpus/core/src/server/auth.ts +5 -0
- package/corpus/core/src/server/core-routes-plugin.ts +14 -0
- package/corpus/core/src/server/security-headers.ts +9 -6
- package/corpus/core/src/settings/store.ts +6 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/corpus/core/src/usage/store.ts +6 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
- package/corpus/templates/calendar/app/components/calendar/GoogleConnectBanner.tsx +60 -6
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +6 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +20 -43
- package/corpus/templates/clips/app/components/recorder/recording-toolbar.tsx +5 -1
- package/corpus/templates/clips/app/lib/countdown-audio-cue.ts +3 -51
- package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-public-clips-now-play-inline-in-slack-connect-a-workspac.md +6 -0
- package/corpus/templates/clips/chrome-extension/PERMISSIONS.md +72 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -10
- package/corpus/templates/clips/chrome-extension/src/background.ts +163 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +301 -24
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +245 -67
- package/corpus/templates/clips/chrome-extension/src/overlay.css +77 -43
- package/corpus/templates/clips/chrome-extension/src/overlay.html +7 -1
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +102 -38
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +275 -4
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/chrome-extension/vite.config.ts +5 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +10 -17
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/server/lib/media-permissions.ts +5 -1
- package/corpus/templates/clips/shared/recording-audio.ts +62 -0
- package/corpus/templates/clips/shared/recording-core.ts +94 -0
- package/dist/action.d.ts +31 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +45 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/context-xray/schema.d.ts +1 -1
- package/dist/agent/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/observational-memory/schema.d.ts +1 -1
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +320 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +138 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +5 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/audit/actions/get-audit-event.d.ts +11 -0
- package/dist/audit/actions/get-audit-event.d.ts.map +1 -0
- package/dist/audit/actions/get-audit-event.js +22 -0
- package/dist/audit/actions/get-audit-event.js.map +1 -0
- package/dist/audit/actions/list-audit-events.d.ts +22 -0
- package/dist/audit/actions/list-audit-events.d.ts.map +1 -0
- package/dist/audit/actions/list-audit-events.js +61 -0
- package/dist/audit/actions/list-audit-events.js.map +1 -0
- package/dist/audit/cleanup-job.d.ts +12 -0
- package/dist/audit/cleanup-job.d.ts.map +1 -0
- package/dist/audit/cleanup-job.js +93 -0
- package/dist/audit/cleanup-job.js.map +1 -0
- package/dist/audit/config.d.ts +30 -0
- package/dist/audit/config.d.ts.map +1 -0
- package/dist/audit/config.js +65 -0
- package/dist/audit/config.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +6 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/record.d.ts +24 -0
- package/dist/audit/record.d.ts.map +1 -0
- package/dist/audit/record.js +106 -0
- package/dist/audit/record.js.map +1 -0
- package/dist/audit/redact.d.ts +28 -0
- package/dist/audit/redact.d.ts.map +1 -0
- package/dist/audit/redact.js +109 -0
- package/dist/audit/redact.js.map +1 -0
- package/dist/audit/store.d.ts +14 -0
- package/dist/audit/store.d.ts.map +1 -0
- package/dist/audit/store.js +219 -0
- package/dist/audit/store.js.map +1 -0
- package/dist/audit/types.d.ts +114 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +15 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +10 -0
- package/dist/chat-threads/store.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +6 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/chat/repo-helpers.d.ts +16 -0
- package/dist/client/chat/repo-helpers.d.ts.map +1 -1
- package/dist/client/chat/repo-helpers.js +40 -0
- package/dist/client/chat/repo-helpers.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +3 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/widen-columns.d.ts +39 -0
- package/dist/db/widen-columns.d.ts.map +1 -0
- package/dist/db/widen-columns.js +73 -0
- package/dist/db/widen-columns.js.map +1 -0
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +24 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +6 -0
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/provider-api/custom-registry.d.ts.map +1 -1
- package/dist/provider-api/custom-registry.js +8 -0
- package/dist/provider-api/custom-registry.js.map +1 -1
- package/dist/resources/store.d.ts.map +1 -1
- package/dist/resources/store.js +9 -0
- package/dist/resources/store.js.map +1 -1
- package/dist/scripts/runner.js +4 -3
- package/dist/scripts/runner.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +6 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +1 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +13 -0
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/security-headers.d.ts +8 -5
- package/dist/server/security-headers.d.ts.map +1 -1
- package/dist/server/security-headers.js +9 -6
- package/dist/server/security-headers.js.map +1 -1
- package/dist/settings/store.d.ts.map +1 -1
- package/dist/settings/store.js +6 -0
- package/dist/settings/store.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
- package/dist/usage/store.d.ts.map +1 -1
- package/dist/usage/store.js +5 -0
- package/dist/usage/store.js.map +1 -1
- package/docs/content/actions.md +23 -0
- package/docs/content/audit-log.md +111 -0
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +2 -1
- package/src/templates/workspace-core/.agents/skills/audit-log/SKILL.md +93 -0
|
@@ -64,9 +64,42 @@ export const DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS = 40_000;
|
|
|
64
64
|
* env value just guarantees the cutoff is a hard error instead of a graceful
|
|
65
65
|
* hand-off. Any resolved value above this is clamped down when hosted. Local
|
|
66
66
|
* dev (non-hosted) is left alone so long-running local turns aren't chunked.
|
|
67
|
+
*
|
|
68
|
+
* IMPORTANT: this clamp is for the INTERACTIVE / foreground path and must NOT
|
|
69
|
+
* be raised. The foreground POST still rides a synchronous serverless function
|
|
70
|
+
* (~60-65s wall), so 40s remains correct there. The only sanctioned exception
|
|
71
|
+
* is the opt-in `backgroundFunction` mode (see
|
|
72
|
+
* `BACKGROUND_SOFT_TIMEOUT_CEILING_MS`), which runs inside a Netlify background
|
|
73
|
+
* function (no ~60s wall, 15-min budget) and therefore can safely outlast 40s.
|
|
67
74
|
*/
|
|
68
75
|
export const HOSTED_SOFT_TIMEOUT_CEILING_MS = 40_000;
|
|
69
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Hard ceiling for the soft timeout when a run executes inside a Netlify
|
|
79
|
+
* background function (any deployed function whose name ends in `-background`).
|
|
80
|
+
* Background functions return 202 immediately and run detached for up to 15
|
|
81
|
+
* minutes, so the ~60s synchronous function wall that 40s defends against does
|
|
82
|
+
* NOT apply. 13 minutes leaves ~2 min of headroom under Netlify's 15-min hard
|
|
83
|
+
* kill to abort, persist the partial turn, write the terminal event, and (for
|
|
84
|
+
* the rare >13-min turn) self-fire another background continuation.
|
|
85
|
+
*
|
|
86
|
+
* This ceiling is used ONLY when a caller explicitly opts in with
|
|
87
|
+
* `backgroundFunction: true`. It does not change the foreground/interactive
|
|
88
|
+
* ceiling and does not fire unless the durable-background path dispatched the
|
|
89
|
+
* run into a background function. Per the design doc Guardrail, the 40s
|
|
90
|
+
* interactive clamp stays correct for every non-background run.
|
|
91
|
+
*/
|
|
92
|
+
export const BACKGROUND_SOFT_TIMEOUT_CEILING_MS = 13 * 60_000; // 780_000
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Default soft-timeout budget for a background-function run when the caller
|
|
96
|
+
* does not pass an explicit `softTimeoutMs`. Same value as the ceiling — we
|
|
97
|
+
* want a background turn to use nearly its whole 15-min budget before handing
|
|
98
|
+
* off to a chained background continuation.
|
|
99
|
+
*/
|
|
100
|
+
export const DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS =
|
|
101
|
+
BACKGROUND_SOFT_TIMEOUT_CEILING_MS;
|
|
102
|
+
|
|
70
103
|
/** Default SQL retention for completed run event logs (24 hours). */
|
|
71
104
|
export const DEFAULT_COMPLETED_RUN_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
72
105
|
|
|
@@ -137,10 +170,28 @@ export interface StartRunOptions {
|
|
|
137
170
|
* share one `turnId` so the durable assistant message can be folded across
|
|
138
171
|
* them instead of dropped per-run. Defaults to the runId (turn == run). */
|
|
139
172
|
turnId?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Opt into the durable-background-function soft-timeout regime for THIS run
|
|
175
|
+
* only. When true, `resolveRunSoftTimeoutMs` lifts the hosted ceiling from
|
|
176
|
+
* 40s to ~13min (`BACKGROUND_SOFT_TIMEOUT_CEILING_MS`) because the run is
|
|
177
|
+
* executing inside a Netlify background function (no ~60s wall). Off by
|
|
178
|
+
* default — the foreground/interactive path never sets this, so its 40s
|
|
179
|
+
* clamp is unchanged. See the design doc + the durable-background dispatch
|
|
180
|
+
* decision in production-agent.ts.
|
|
181
|
+
*/
|
|
182
|
+
backgroundFunction?: boolean;
|
|
140
183
|
}
|
|
141
184
|
|
|
142
185
|
export interface ResolveRunSoftTimeoutOptions {
|
|
143
186
|
useHostedDefault?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Resolve the soft timeout for a run executing inside a Netlify background
|
|
189
|
+
* function. Lifts the hosted clamp to `BACKGROUND_SOFT_TIMEOUT_CEILING_MS`
|
|
190
|
+
* (~13min) for this invocation only and, when no override/env is supplied,
|
|
191
|
+
* defaults to `DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS`. Does NOT change the
|
|
192
|
+
* foreground ceiling. Off by default.
|
|
193
|
+
*/
|
|
194
|
+
backgroundFunction?: boolean;
|
|
144
195
|
}
|
|
145
196
|
|
|
146
197
|
function isHostedRuntime(): boolean {
|
|
@@ -172,15 +223,21 @@ export function resolveRunSoftTimeoutMs(
|
|
|
172
223
|
options?: ResolveRunSoftTimeoutOptions,
|
|
173
224
|
): number {
|
|
174
225
|
const hosted = isHostedRuntime();
|
|
226
|
+
const background = options?.backgroundFunction === true;
|
|
227
|
+
// The interactive/foreground ceiling is 40s — the synchronous serverless
|
|
228
|
+
// function wall. A background-function run (opt-in only) has no ~60s wall, so
|
|
229
|
+
// it is allowed to outlast that and is clamped to the larger 13-min ceiling
|
|
230
|
+
// instead. The 40s clamp for non-background hosted runs is unchanged.
|
|
231
|
+
const ceiling = background
|
|
232
|
+
? BACKGROUND_SOFT_TIMEOUT_CEILING_MS
|
|
233
|
+
: HOSTED_SOFT_TIMEOUT_CEILING_MS;
|
|
175
234
|
// A configured/env soft timeout that exceeds the upstream walls can never
|
|
176
235
|
// actually fire (the gateway/function kills the run first), so clamp it down
|
|
177
236
|
// on hosted runtimes. This is what makes auto_continue reach the client
|
|
178
237
|
// instead of the run dying as builder_gateway_timeout / stale_run. `0` means
|
|
179
238
|
// "disabled" and is never clamped up.
|
|
180
239
|
const clampHosted = (ms: number): number =>
|
|
181
|
-
hosted && ms >
|
|
182
|
-
? HOSTED_SOFT_TIMEOUT_CEILING_MS
|
|
183
|
-
: ms;
|
|
240
|
+
hosted && ms > ceiling ? ceiling : ms;
|
|
184
241
|
|
|
185
242
|
if (typeof overrideMs === "number" && Number.isFinite(overrideMs)) {
|
|
186
243
|
return clampHosted(Math.max(0, overrideMs));
|
|
@@ -190,6 +247,11 @@ export function resolveRunSoftTimeoutMs(
|
|
|
190
247
|
const raw = Number(envValue);
|
|
191
248
|
if (Number.isFinite(raw) && raw >= 0) return clampHosted(raw);
|
|
192
249
|
}
|
|
250
|
+
// A background-function run uses the full background budget by default; the
|
|
251
|
+
// foreground default (40s) is unchanged.
|
|
252
|
+
if (background) {
|
|
253
|
+
return hosted ? DEFAULT_BACKGROUND_RUN_SOFT_TIMEOUT_MS : 0;
|
|
254
|
+
}
|
|
193
255
|
return options?.useHostedDefault && hosted
|
|
194
256
|
? DEFAULT_HOSTED_RUN_SOFT_TIMEOUT_MS
|
|
195
257
|
: 0;
|
|
@@ -343,6 +405,7 @@ export function startRun(
|
|
|
343
405
|
}, 1500);
|
|
344
406
|
const softTimeoutMs = resolveRunSoftTimeoutMs(options?.softTimeoutMs, {
|
|
345
407
|
useHostedDefault: options?.useHostedSoftTimeoutDefault === true,
|
|
408
|
+
backgroundFunction: options?.backgroundFunction === true,
|
|
346
409
|
});
|
|
347
410
|
const softTimeoutTimer =
|
|
348
411
|
softTimeoutMs > 0
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* reliable reconnection after page refreshes.
|
|
5
5
|
*/
|
|
6
6
|
import { getDbExec, intType, isPostgres } from "../db/client.js";
|
|
7
|
+
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
7
8
|
import { captureError } from "../server/capture-error.js";
|
|
8
9
|
import type { AgentChatEvent } from "./types.js";
|
|
9
10
|
|
|
@@ -18,6 +19,21 @@ let _initPromise: Promise<void> | undefined;
|
|
|
18
19
|
*/
|
|
19
20
|
export const RUN_STALE_MS = 15_000;
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Stale window for runs dispatched into a Netlify background function
|
|
24
|
+
* (`dispatch_mode = 'background'`). The design doc flags the 15s reaper vs a
|
|
25
|
+
* background cold-start as the #1 false-failure risk: the foreground POST
|
|
26
|
+
* inserts the `running` row, then `fireInternalDispatch` returns 202 and the
|
|
27
|
+
* background function may take >15s to cold-start and emit its first heartbeat.
|
|
28
|
+
* With the normal 15s window the reaper would falsely kill that freshly-
|
|
29
|
+
* inserted-but-not-yet-heartbeaten row. 90s tolerates a slow background
|
|
30
|
+
* cold-start while still reaping a genuinely dead background worker promptly.
|
|
31
|
+
*
|
|
32
|
+
* Only applied to rows explicitly marked background-dispatched; ordinary
|
|
33
|
+
* foreground runs keep the tight 15s window unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export const BACKGROUND_RUN_STALE_MS = 90_000;
|
|
36
|
+
|
|
21
37
|
export const STALE_RUN_ERROR_EVENT = {
|
|
22
38
|
type: "error",
|
|
23
39
|
error:
|
|
@@ -44,7 +60,8 @@ async function ensureRunTables(): Promise<void> {
|
|
|
44
60
|
last_progress_at ${intType()},
|
|
45
61
|
turn_id TEXT,
|
|
46
62
|
error_code TEXT,
|
|
47
|
-
error_detail TEXT
|
|
63
|
+
error_detail TEXT,
|
|
64
|
+
dispatch_mode TEXT
|
|
48
65
|
)
|
|
49
66
|
`);
|
|
50
67
|
// Backfill heartbeat_at on older deployments.
|
|
@@ -97,7 +114,16 @@ async function ensureRunTables(): Promise<void> {
|
|
|
97
114
|
// error_code / error_detail = terminal failure classification captured
|
|
98
115
|
// at completion so errored/cut-off runs are queryable for
|
|
99
116
|
// pattern analysis (see listErroredRuns).
|
|
100
|
-
|
|
117
|
+
// dispatch_mode marks how a run was started: NULL/"foreground" for the
|
|
118
|
+
// normal synchronous path, "background" for a run dispatched into a
|
|
119
|
+
// Netlify background function. The reaper/claim widen the stale window
|
|
120
|
+
// for background rows so a slow cold-start isn't falsely reaped.
|
|
121
|
+
for (const col of [
|
|
122
|
+
"turn_id",
|
|
123
|
+
"error_code",
|
|
124
|
+
"error_detail",
|
|
125
|
+
"dispatch_mode",
|
|
126
|
+
] as const) {
|
|
101
127
|
try {
|
|
102
128
|
if (isPostgres()) {
|
|
103
129
|
await client.execute(
|
|
@@ -134,6 +160,19 @@ async function ensureRunTables(): Promise<void> {
|
|
|
134
160
|
PRIMARY KEY (thread_id, tool_key)
|
|
135
161
|
)
|
|
136
162
|
`);
|
|
163
|
+
// Widen millisecond-timestamp columns that older deployments created as
|
|
164
|
+
// 32-bit `INTEGER`. `insertRun()` writes `Date.now()` into `started_at`
|
|
165
|
+
// on every turn, so an int4 column makes every agent prompt fail on
|
|
166
|
+
// Postgres with "value … is out of range for type integer". No-op once
|
|
167
|
+
// widened (and on fresh DBs that already use BIGINT). See
|
|
168
|
+
// widenIntColumnsToBigInt.
|
|
169
|
+
await widenIntColumnsToBigInt("agent_runs", [
|
|
170
|
+
"started_at",
|
|
171
|
+
"completed_at",
|
|
172
|
+
"heartbeat_at",
|
|
173
|
+
"last_progress_at",
|
|
174
|
+
]);
|
|
175
|
+
await widenIntColumnsToBigInt("agent_tool_ledger", ["completed_at"]);
|
|
137
176
|
})().catch((err) => {
|
|
138
177
|
// Retry init on the next call after a failed startup.
|
|
139
178
|
_initPromise = undefined;
|
|
@@ -234,16 +273,68 @@ export async function insertRun(
|
|
|
234
273
|
id: string,
|
|
235
274
|
threadId: string,
|
|
236
275
|
turnId?: string,
|
|
276
|
+
options?: { dispatchMode?: "foreground" | "background" },
|
|
237
277
|
): Promise<void> {
|
|
238
278
|
await ensureRunTables();
|
|
239
279
|
const client = getDbExec();
|
|
240
280
|
const now = Date.now();
|
|
241
281
|
await client.execute({
|
|
242
|
-
sql: `INSERT INTO agent_runs (id, thread_id, status, started_at, heartbeat_at, last_progress_at, turn_id) VALUES (?, ?, 'running', ?, ?, ?, ?)`,
|
|
243
|
-
args: [
|
|
282
|
+
sql: `INSERT INTO agent_runs (id, thread_id, status, started_at, heartbeat_at, last_progress_at, turn_id, dispatch_mode) VALUES (?, ?, 'running', ?, ?, ?, ?, ?)`,
|
|
283
|
+
args: [
|
|
284
|
+
id,
|
|
285
|
+
threadId,
|
|
286
|
+
now,
|
|
287
|
+
now,
|
|
288
|
+
now,
|
|
289
|
+
turnId ?? id,
|
|
290
|
+
options?.dispatchMode ?? null,
|
|
291
|
+
],
|
|
244
292
|
});
|
|
245
293
|
}
|
|
246
294
|
|
|
295
|
+
/**
|
|
296
|
+
* SQL fragment that resolves the per-row stale cutoff. Background-dispatched
|
|
297
|
+
* runs (`dispatch_mode` starting with `background`) tolerate a much longer gap
|
|
298
|
+
* without a heartbeat (slow Netlify background-function cold-start) before being
|
|
299
|
+
* reaped; every other run keeps the tight 15s window. The bound `now` is
|
|
300
|
+
* subtracted by the resolved window so the comparison is
|
|
301
|
+
* `COALESCE(heartbeat_at, started_at) < (now - window)`.
|
|
302
|
+
*
|
|
303
|
+
* `dispatch_mode` is one of NULL/"foreground" (normal sync path), "background"
|
|
304
|
+
* (foreground inserted the row for a background dispatch), or
|
|
305
|
+
* "background-processing" (the background worker claimed it). Both background
|
|
306
|
+
* states get the wider window via a LIKE-prefix match.
|
|
307
|
+
*/
|
|
308
|
+
function backgroundAwareStaleCutoffSql(): string {
|
|
309
|
+
return `(? - CASE WHEN dispatch_mode LIKE 'background%' THEN ${BACKGROUND_RUN_STALE_MS} ELSE ${RUN_STALE_MS} END)`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Atomically claim a background-dispatched run for processing. The foreground
|
|
314
|
+
* POST inserts the run row with `dispatch_mode = 'background'`; the FIRST
|
|
315
|
+
* delivery of the background dispatch flips it to `background-processing` and
|
|
316
|
+
* wins the claim. A duplicate Netlify delivery (background functions can be
|
|
317
|
+
* retried) sees `background-processing` and loses, so it no-ops — mirroring
|
|
318
|
+
* `claimAgentTeamRun` returning null. Returns true when this caller won.
|
|
319
|
+
*
|
|
320
|
+
* Idempotent and conditional: the WHERE clause only matches the unclaimed
|
|
321
|
+
* `background` state AND a still-running row, so a reaped/terminal row can't be
|
|
322
|
+
* re-claimed.
|
|
323
|
+
*/
|
|
324
|
+
export async function claimBackgroundRun(runId: string): Promise<boolean> {
|
|
325
|
+
await ensureRunTables();
|
|
326
|
+
const client = getDbExec();
|
|
327
|
+
const { rowsAffected } = await client.execute({
|
|
328
|
+
sql: `UPDATE agent_runs
|
|
329
|
+
SET dispatch_mode = 'background-processing'
|
|
330
|
+
WHERE id = ?
|
|
331
|
+
AND status = 'running'
|
|
332
|
+
AND dispatch_mode = 'background'`,
|
|
333
|
+
args: [runId],
|
|
334
|
+
});
|
|
335
|
+
return (rowsAffected ?? 0) > 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
247
338
|
/**
|
|
248
339
|
* Atomically acquire a run lease for a thread. Succeeds (returns true) only
|
|
249
340
|
* when no other run for the same thread is currently status='running' with a
|
|
@@ -256,18 +347,37 @@ export async function insertRun(
|
|
|
256
347
|
*/
|
|
257
348
|
export async function tryClaimRunSlot(
|
|
258
349
|
threadId: string,
|
|
259
|
-
maxStaleMs
|
|
350
|
+
maxStaleMs?: number,
|
|
260
351
|
): Promise<{ claimed: boolean; activeRunId: string | null }> {
|
|
261
352
|
await ensureRunTables();
|
|
262
353
|
const client = getDbExec();
|
|
263
|
-
const
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
// Default: per-row background-aware window so a live background run (which can
|
|
356
|
+
// legitimately go >15s between heartbeats during a cold-start) isn't seen as
|
|
357
|
+
// "free" and double-claimed by a racing foreground POST. An explicit
|
|
358
|
+
// `maxStaleMs` override keeps a flat window for callers that want one.
|
|
359
|
+
if (typeof maxStaleMs === "number") {
|
|
360
|
+
const heartbeatCutoff = now - maxStaleMs;
|
|
361
|
+
const { rows } = await client.execute({
|
|
362
|
+
sql: `SELECT id FROM agent_runs
|
|
363
|
+
WHERE thread_id = ?
|
|
364
|
+
AND status = 'running'
|
|
365
|
+
AND COALESCE(heartbeat_at, started_at) >= ?
|
|
366
|
+
ORDER BY started_at DESC LIMIT 1`,
|
|
367
|
+
args: [threadId, heartbeatCutoff],
|
|
368
|
+
});
|
|
369
|
+
if (rows.length > 0) {
|
|
370
|
+
return { claimed: false, activeRunId: (rows[0] as { id: string }).id };
|
|
371
|
+
}
|
|
372
|
+
return { claimed: true, activeRunId: null };
|
|
373
|
+
}
|
|
264
374
|
const { rows } = await client.execute({
|
|
265
375
|
sql: `SELECT id FROM agent_runs
|
|
266
376
|
WHERE thread_id = ?
|
|
267
377
|
AND status = 'running'
|
|
268
|
-
AND COALESCE(heartbeat_at, started_at) >=
|
|
378
|
+
AND COALESCE(heartbeat_at, started_at) >= ${backgroundAwareStaleCutoffSql()}
|
|
269
379
|
ORDER BY started_at DESC LIMIT 1`,
|
|
270
|
-
args: [threadId,
|
|
380
|
+
args: [threadId, now],
|
|
271
381
|
});
|
|
272
382
|
if (rows.length > 0) {
|
|
273
383
|
const row = rows[0] as { id: string };
|
|
@@ -335,12 +445,20 @@ export async function bumpRunProgress(runId: string): Promise<void> {
|
|
|
335
445
|
*/
|
|
336
446
|
export async function reapIfStale(
|
|
337
447
|
runId: string,
|
|
338
|
-
maxStaleMs
|
|
448
|
+
maxStaleMs?: number,
|
|
339
449
|
): Promise<boolean> {
|
|
340
450
|
await ensureRunTables();
|
|
341
451
|
const client = getDbExec();
|
|
342
452
|
const completedAt = Date.now();
|
|
343
|
-
|
|
453
|
+
// Background-dispatched runs get the wider stale window so a slow cold-start
|
|
454
|
+
// isn't reaped; foreground runs keep the tight 15s window. An explicit
|
|
455
|
+
// override forces a flat window for callers that want one.
|
|
456
|
+
const staleClause =
|
|
457
|
+
typeof maxStaleMs === "number"
|
|
458
|
+
? `COALESCE(heartbeat_at, started_at) < ?`
|
|
459
|
+
: `COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`;
|
|
460
|
+
const staleArgs =
|
|
461
|
+
typeof maxStaleMs === "number" ? [completedAt - maxStaleMs] : [completedAt];
|
|
344
462
|
const { rowsAffected } = await client.execute({
|
|
345
463
|
sql: `UPDATE agent_runs
|
|
346
464
|
SET status = 'errored',
|
|
@@ -349,13 +467,13 @@ export async function reapIfStale(
|
|
|
349
467
|
error_detail = ?
|
|
350
468
|
WHERE id = ?
|
|
351
469
|
AND status = 'running'
|
|
352
|
-
AND
|
|
470
|
+
AND ${staleClause}`,
|
|
353
471
|
args: [
|
|
354
472
|
completedAt,
|
|
355
473
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
356
474
|
STALE_RUN_ERROR_EVENT.details,
|
|
357
475
|
runId,
|
|
358
|
-
|
|
476
|
+
...staleArgs,
|
|
359
477
|
],
|
|
360
478
|
});
|
|
361
479
|
const reaped = (rowsAffected ?? 0) > 0;
|
|
@@ -675,12 +793,13 @@ export async function getCurrentTurnEventsForThread(
|
|
|
675
793
|
export async function reapAllStaleRuns(): Promise<number> {
|
|
676
794
|
await ensureRunTables();
|
|
677
795
|
const client = getDbExec();
|
|
678
|
-
const
|
|
796
|
+
const now = Date.now();
|
|
797
|
+
// Background-dispatched runs use the wider window; everything else 15s.
|
|
679
798
|
const stale = await client.execute({
|
|
680
799
|
sql: `SELECT id FROM agent_runs
|
|
681
800
|
WHERE status = 'running'
|
|
682
|
-
AND COALESCE(heartbeat_at, started_at) <
|
|
683
|
-
args: [
|
|
801
|
+
AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
|
|
802
|
+
args: [now],
|
|
684
803
|
});
|
|
685
804
|
const completedAt = Date.now();
|
|
686
805
|
const { rowsAffected } = await client.execute({
|
|
@@ -690,12 +809,12 @@ export async function reapAllStaleRuns(): Promise<number> {
|
|
|
690
809
|
error_code = ?,
|
|
691
810
|
error_detail = ?
|
|
692
811
|
WHERE status = 'running'
|
|
693
|
-
AND COALESCE(heartbeat_at, started_at) <
|
|
812
|
+
AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
|
|
694
813
|
args: [
|
|
695
814
|
completedAt,
|
|
696
815
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
697
816
|
STALE_RUN_ERROR_EVENT.details,
|
|
698
|
-
|
|
817
|
+
completedAt,
|
|
699
818
|
],
|
|
700
819
|
});
|
|
701
820
|
for (const row of stale.rows) {
|
|
@@ -730,15 +849,15 @@ export async function cleanupOldRuns(
|
|
|
730
849
|
// SELECT covers BOTH UPDATE conditions so the terminal-event-append loop
|
|
731
850
|
// below catches every row we're about to flip — a 24h-old row with a
|
|
732
851
|
// somehow-fresh heartbeat would slip past a heartbeat-only SELECT.
|
|
733
|
-
const
|
|
852
|
+
const now = Date.now();
|
|
734
853
|
const stale = await client.execute({
|
|
735
854
|
sql: `SELECT id FROM agent_runs
|
|
736
855
|
WHERE status = 'running'
|
|
737
856
|
AND (
|
|
738
|
-
COALESCE(heartbeat_at, started_at) <
|
|
857
|
+
COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}
|
|
739
858
|
OR started_at < ?
|
|
740
859
|
)`,
|
|
741
|
-
args: [
|
|
860
|
+
args: [now, cutoff],
|
|
742
861
|
});
|
|
743
862
|
const completedAt = Date.now();
|
|
744
863
|
await client.execute({
|
|
@@ -755,7 +874,8 @@ export async function cleanupOldRuns(
|
|
|
755
874
|
cutoff,
|
|
756
875
|
],
|
|
757
876
|
});
|
|
758
|
-
// Also expire runs whose heartbeat is stale — producer has died.
|
|
877
|
+
// Also expire runs whose heartbeat is stale — producer has died. Uses the
|
|
878
|
+
// background-aware window so a slow background cold-start isn't reaped early.
|
|
759
879
|
await client.execute({
|
|
760
880
|
sql: `UPDATE agent_runs
|
|
761
881
|
SET status = 'errored',
|
|
@@ -763,12 +883,12 @@ export async function cleanupOldRuns(
|
|
|
763
883
|
error_code = ?,
|
|
764
884
|
error_detail = ?
|
|
765
885
|
WHERE status = 'running'
|
|
766
|
-
AND COALESCE(heartbeat_at, started_at) <
|
|
886
|
+
AND COALESCE(heartbeat_at, started_at) < ${backgroundAwareStaleCutoffSql()}`,
|
|
767
887
|
args: [
|
|
768
888
|
completedAt,
|
|
769
889
|
STALE_RUN_ERROR_EVENT.errorCode,
|
|
770
890
|
STALE_RUN_ERROR_EVENT.details,
|
|
771
|
-
|
|
891
|
+
completedAt,
|
|
772
892
|
],
|
|
773
893
|
});
|
|
774
894
|
for (const row of stale.rows) {
|
|
@@ -129,6 +129,26 @@ export interface AgentChatRequest {
|
|
|
129
129
|
attachments?: AgentChatAttachment[];
|
|
130
130
|
/** Internal retry/continuation requests should not create visible user turns. */
|
|
131
131
|
internalContinuation?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Internal marker set ONLY by the durable-background self-dispatch (see
|
|
134
|
+
* `AGENT_CHAT_BACKGROUND_RUN_FIELD`). Present when the agent-chat handler is
|
|
135
|
+
* re-entered as the background worker: it carries the pre-claimed `runId` and
|
|
136
|
+
* logical `turnId` so the worker runs the loop inline with the background
|
|
137
|
+
* soft-timeout instead of re-claiming the slot or re-dispatching. Untrusted
|
|
138
|
+
* on its own — the `_process-run` route HMAC-verifies the dispatch before
|
|
139
|
+
* invoking the handler. Absent on every normal client request.
|
|
140
|
+
*/
|
|
141
|
+
__backgroundRun?: {
|
|
142
|
+
runId: string;
|
|
143
|
+
turnId?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Number of server-driven background→background continuations already
|
|
146
|
+
* chained into this logical turn (0 on the first chunk). The worker
|
|
147
|
+
* increments this when it re-fires `_process-run` at a soft-timeout
|
|
148
|
+
* boundary and refuses to chain past `MAX_BACKGROUND_RUN_CONTINUATIONS`.
|
|
149
|
+
*/
|
|
150
|
+
continuationCount?: number;
|
|
151
|
+
};
|
|
132
152
|
/**
|
|
133
153
|
* Stable identity for the logical assistant turn this request belongs to.
|
|
134
154
|
* The client sends the SAME turnId for the initial POST and every
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isPostgres,
|
|
5
5
|
intType,
|
|
6
6
|
} from "../db/client.js";
|
|
7
|
+
import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
|
|
7
8
|
import { emitAppStateChange, emitAppStateDelete } from "./emitter.js";
|
|
8
9
|
import type { StoreWriteOptions } from "../settings/store.js";
|
|
9
10
|
|
|
@@ -30,6 +31,10 @@ async function ensureTable(): Promise<void> {
|
|
|
30
31
|
PRIMARY KEY (session_id, key)
|
|
31
32
|
)
|
|
32
33
|
`);
|
|
34
|
+
// Older deployments created `updated_at` as 32-bit `INTEGER`; on Postgres
|
|
35
|
+
// the `Date.now()` written by appStatePut() on every turn overflows int4.
|
|
36
|
+
// Widen it in place (no-op once done / on fresh BIGINT databases).
|
|
37
|
+
await widenIntColumnsToBigInt("application_state", ["updated_at"]);
|
|
33
38
|
// Indexes for the two hot poll paths:
|
|
34
39
|
// - `SELECT … WHERE updated_at > ?` (watermark scan, every poll cycle)
|
|
35
40
|
// - `SELECT … WHERE key = ? … ORDER BY updated_at ASC` (marker lookups)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { getAuditEventById } from "../store.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetch a single audit event by id, including its redacted input payload.
|
|
7
|
+
* Scoped to the caller's identity — returns null if they can't access it.
|
|
8
|
+
*/
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Get one audit-log event by id, with its full redacted input payload. Returns null if you don't have access to it.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
id: z.string().describe("The audit event id."),
|
|
14
|
+
}),
|
|
15
|
+
http: { method: "GET" },
|
|
16
|
+
run: async (args, ctx) => {
|
|
17
|
+
const event = await getAuditEventById(args.id, {
|
|
18
|
+
userEmail: ctx?.userEmail,
|
|
19
|
+
orgId: ctx?.orgId ?? null,
|
|
20
|
+
});
|
|
21
|
+
return { event };
|
|
22
|
+
},
|
|
23
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineAction } from "../../action.js";
|
|
3
|
+
import { queryAuditEvents } from "../store.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List audit-log events the current user can see — their own actions plus the
|
|
7
|
+
* agent's actions on their behalf, scoped in SQL to the caller's identity and
|
|
8
|
+
* org. Read-only; never exposes other tenants' rows.
|
|
9
|
+
*/
|
|
10
|
+
export default defineAction({
|
|
11
|
+
description:
|
|
12
|
+
"List audit-log events (who changed what, when, and whether it was you or the agent) for resources you can access. Supports filtering by target resource, actor (agent vs human), status, agent thread/turn, and time. Use this to answer 'what did the agent change', 'who edited this record', or 'show recent changes'.",
|
|
13
|
+
schema: z.object({
|
|
14
|
+
targetType: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Filter to one resource type, e.g. 'recording'."),
|
|
18
|
+
targetId: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("Filter to one resource id (pair with targetType)."),
|
|
22
|
+
actorKind: z
|
|
23
|
+
.enum(["agent", "human", "system"])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Filter to changes made by the agent, a human, or the system."),
|
|
26
|
+
actorEmail: z.string().optional().describe("Filter to one actor's email."),
|
|
27
|
+
status: z
|
|
28
|
+
.enum(["success", "error", "denied"])
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Filter by outcome."),
|
|
31
|
+
threadId: z.string().optional().describe("Filter to one agent thread."),
|
|
32
|
+
turnId: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Filter to one agent turn (a single agent response)."),
|
|
36
|
+
action: z.string().optional().describe("Filter to one action name."),
|
|
37
|
+
sinceMs: z
|
|
38
|
+
.number()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Only events at or after this Unix epoch (ms)."),
|
|
41
|
+
limit: z
|
|
42
|
+
.number()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Max events to return (default 100, max 500)."),
|
|
45
|
+
}),
|
|
46
|
+
http: { method: "GET" },
|
|
47
|
+
run: async (args, ctx) => {
|
|
48
|
+
const events = await queryAuditEvents(
|
|
49
|
+
{ userEmail: ctx?.userEmail, orgId: ctx?.orgId ?? null },
|
|
50
|
+
{
|
|
51
|
+
...(args.targetType ? { targetType: args.targetType } : {}),
|
|
52
|
+
...(args.targetId ? { targetId: args.targetId } : {}),
|
|
53
|
+
...(args.actorKind ? { actorKind: args.actorKind } : {}),
|
|
54
|
+
...(args.actorEmail ? { actorEmail: args.actorEmail } : {}),
|
|
55
|
+
...(args.status ? { status: args.status } : {}),
|
|
56
|
+
...(args.threadId ? { threadId: args.threadId } : {}),
|
|
57
|
+
...(args.turnId ? { turnId: args.turnId } : {}),
|
|
58
|
+
...(args.action ? { action: args.action } : {}),
|
|
59
|
+
...(typeof args.sinceMs === "number" ? { sinceMs: args.sinceMs } : {}),
|
|
60
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
return { events, count: events.length };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit-log retention job.
|
|
3
|
+
*
|
|
4
|
+
* Periodically purges `agent_audit_log` rows older than the configured horizon
|
|
5
|
+
* so the log doesn't grow unbounded. Retention is configurable via
|
|
6
|
+
* `AGENT_NATIVE_AUDIT_RETENTION_DAYS` (default: 365 days — audit trails are kept
|
|
7
|
+
* far longer than sampled traces). Setting it to `0` disables the purge (keep
|
|
8
|
+
* forever). Mirrors observability/cleanup-job.ts.
|
|
9
|
+
*
|
|
10
|
+
* Runs once on startup after a short delay, then on a 24-hour interval. Timers
|
|
11
|
+
* are unref'd so they never keep the process alive on their own.
|
|
12
|
+
*/
|
|
13
|
+
import { deleteOldAuditEvents } from "./store.js";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RETENTION_DAYS = 365;
|
|
16
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
const STARTUP_DELAY_MS = 5 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
let _cleanupTimer: NodeJS.Timeout | null = null;
|
|
20
|
+
let _intervalTimer: NodeJS.Timeout | null = null;
|
|
21
|
+
|
|
22
|
+
function resolveRetentionDays(): number {
|
|
23
|
+
const raw = process.env.AGENT_NATIVE_AUDIT_RETENTION_DAYS;
|
|
24
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
25
|
+
return DEFAULT_RETENTION_DAYS;
|
|
26
|
+
}
|
|
27
|
+
const parsed = Number.parseInt(raw, 10);
|
|
28
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_RETENTION_DAYS;
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run the audit cleanup once. Returns the deleted row count, or null when
|
|
34
|
+
* retention is disabled (`AGENT_NATIVE_AUDIT_RETENTION_DAYS=0`).
|
|
35
|
+
*/
|
|
36
|
+
export async function runAuditCleanupOnce(): Promise<number | null> {
|
|
37
|
+
const days = resolveRetentionDays();
|
|
38
|
+
if (days === 0) return null;
|
|
39
|
+
const cutoff = Date.now() - days * ONE_DAY_MS;
|
|
40
|
+
return deleteOldAuditEvents(cutoff);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Start the recurring audit-cleanup job. Idempotent — calling more than once is
|
|
45
|
+
* a no-op while a previous schedule is active. Returns a stop function.
|
|
46
|
+
*/
|
|
47
|
+
export function startAuditCleanupJob(): () => void {
|
|
48
|
+
if (_cleanupTimer || _intervalTimer) return stopAuditCleanupJob;
|
|
49
|
+
const days = resolveRetentionDays();
|
|
50
|
+
if (days === 0) {
|
|
51
|
+
if (process.env.DEBUG)
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.log(
|
|
54
|
+
"[audit] Audit cleanup disabled (AGENT_NATIVE_AUDIT_RETENTION_DAYS=0)",
|
|
55
|
+
);
|
|
56
|
+
return () => {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tick = () => {
|
|
60
|
+
runAuditCleanupOnce()
|
|
61
|
+
.then((deleted) => {
|
|
62
|
+
if (deleted == null) return;
|
|
63
|
+
if (process.env.DEBUG) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log(
|
|
66
|
+
`[audit] Audit cleanup purged ${deleted} rows (retention=${days}d)`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.catch((err) => {
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.error("[audit] Audit cleanup failed:", err?.message ?? err);
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
_cleanupTimer = setTimeout(() => {
|
|
77
|
+
_cleanupTimer = null;
|
|
78
|
+
tick();
|
|
79
|
+
_intervalTimer = setInterval(tick, ONE_DAY_MS);
|
|
80
|
+
if (typeof _intervalTimer.unref === "function") _intervalTimer.unref();
|
|
81
|
+
}, STARTUP_DELAY_MS);
|
|
82
|
+
if (typeof _cleanupTimer.unref === "function") _cleanupTimer.unref();
|
|
83
|
+
|
|
84
|
+
if (process.env.DEBUG)
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.log(`[audit] Audit cleanup scheduled (retention=${days}d, daily)`);
|
|
87
|
+
|
|
88
|
+
return stopAuditCleanupJob;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function stopAuditCleanupJob(): void {
|
|
92
|
+
if (_cleanupTimer) {
|
|
93
|
+
clearTimeout(_cleanupTimer);
|
|
94
|
+
_cleanupTimer = null;
|
|
95
|
+
}
|
|
96
|
+
if (_intervalTimer) {
|
|
97
|
+
clearInterval(_intervalTimer);
|
|
98
|
+
_intervalTimer = null;
|
|
99
|
+
}
|
|
100
|
+
}
|