@agent-native/core 0.161.2 → 0.161.6
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/templates/analytics/actions/update-dashboard.ts +8 -0
- package/corpus/templates/clips/actions/save-browser-transcript.ts +20 -4
- package/corpus/templates/clips/app/components/meetings/transcript-bubbles.tsx +167 -35
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +8 -1
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +39 -2
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +3 -0
- package/corpus/templates/slides/actions/list-decks.ts +36 -1
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +29 -17
- package/corpus/templates/slides/app/context/DeckContext.tsx +17 -6
- package/dist/agent/engine/builder-engine.js +30 -15
- package/dist/agent/engine/types.d.ts +19 -0
- package/dist/agent/engine/types.js +3 -0
- package/dist/agent/production-agent.d.ts +56 -1
- package/dist/agent/production-agent.js +130 -3
- package/dist/agent/run-manager.d.ts +15 -4
- package/dist/agent/run-manager.js +26 -0
- package/dist/agent/run-store.d.ts +5 -5
- package/dist/agent/run-store.js +52 -15
- package/dist/agent/thread-data-builder.js +7 -0
- package/dist/agent/types.d.ts +10 -0
- package/dist/cli/code-agent-connector.js +6 -1
- package/dist/client/AssistantChat.d.ts +1 -0
- package/dist/client/AssistantChat.js +10 -1
- package/dist/client/MultiTabAssistantChat.js +33 -1
- package/dist/client/sse-event-processor.js +11 -7
- package/dist/client/use-chat-threads.js +9 -9
- package/dist/db/client.js +10 -2
- package/dist/db/create-get-db.js +42 -0
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/onboarding-html.js +22 -77
- package/dist/server/poll.d.ts +5 -5
- package/dist/server/poll.js +19 -26
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/auth-copy.d.ts +7 -0
- package/dist/shared/auth-copy.js +77 -0
- package/dist/shared/mcp-embed-headers.js +8 -4
- package/dist/vite/client.js +94 -3
- package/package.json +1 -1
package/dist/agent/run-store.js
CHANGED
|
@@ -2357,6 +2357,8 @@ const RUN_OUTCOME_DAY_MS = 86_400_000;
|
|
|
2357
2357
|
* every run id a user pasted into a bug report was gone before anyone looked.
|
|
2358
2358
|
*/
|
|
2359
2359
|
const UNSUCCESSFUL_STATUS_SQL_LIST = `('errored', 'aborted', 'truncated')`;
|
|
2360
|
+
const RUN_OUTCOME_PRUNE_BATCH_LIMIT = 200;
|
|
2361
|
+
const RUN_OUTCOME_PRUNE_LOCK_KEY = "agent-native:run-outcome-prune";
|
|
2360
2362
|
/**
|
|
2361
2363
|
* Fold the terminal outcomes of the rows `cleanupOldRuns` is about to delete
|
|
2362
2364
|
* into `agent_run_outcome_daily`, so success/failure RATES survive pruning even
|
|
@@ -2364,29 +2366,46 @@ const UNSUCCESSFUL_STATUS_SQL_LIST = `('errored', 'aborted', 'truncated')`;
|
|
|
2364
2366
|
* covers exactly the unpruned ones, so a rate over any window is
|
|
2365
2367
|
* `getRunOutcomeCounters()` plus the live rows — no gap, no double count.
|
|
2366
2368
|
*
|
|
2367
|
-
*
|
|
2368
|
-
*
|
|
2369
|
-
* Grouping the returned rows in TypeScript avoids
|
|
2369
|
+
* Postgres callers take a transaction-scoped advisory lease before the claim.
|
|
2370
|
+
* The bounded DELETE ... RETURNING is still the source of truth for which rows
|
|
2371
|
+
* this invocation owns. Grouping the returned rows in TypeScript avoids
|
|
2372
|
+
* dialect-specific date SQL.
|
|
2370
2373
|
* Counter upserts run in the same transaction as the delete; a failed upsert
|
|
2371
2374
|
* rolls back the claim so the source rows remain available for a retry.
|
|
2372
2375
|
*/
|
|
2373
2376
|
async function pruneAndRollUpPrunedRunOutcomes(client, cutoff, erroredCutoff) {
|
|
2374
2377
|
const prune = async (tx) => {
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2378
|
+
if (isPostgres()) {
|
|
2379
|
+
const lockResult = await tx.execute({
|
|
2380
|
+
sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
|
|
2381
|
+
args: [RUN_OUTCOME_PRUNE_LOCK_KEY],
|
|
2382
|
+
});
|
|
2383
|
+
const acquired = lockResult.rows[0]?.acquired;
|
|
2384
|
+
if (acquired !== true && acquired !== "t")
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2383
2387
|
const { rows } = await tx.execute({
|
|
2384
2388
|
sql: `DELETE FROM agent_runs
|
|
2385
|
-
WHERE
|
|
2386
|
-
|
|
2387
|
-
|
|
2389
|
+
WHERE id IN (
|
|
2390
|
+
SELECT id FROM agent_runs
|
|
2391
|
+
WHERE (status = 'completed' AND completed_at < ?)
|
|
2392
|
+
OR (status IN ${UNSUCCESSFUL_STATUS_SQL_LIST} AND completed_at < ?)
|
|
2393
|
+
ORDER BY completed_at ASC, id ASC
|
|
2394
|
+
LIMIT ${RUN_OUTCOME_PRUNE_BATCH_LIMIT}
|
|
2395
|
+
)
|
|
2396
|
+
RETURNING id, status, completed_at, terminal_reason`,
|
|
2388
2397
|
args: [cutoff, erroredCutoff],
|
|
2389
2398
|
});
|
|
2399
|
+
const runIds = rows
|
|
2400
|
+
.map((row) => row.id)
|
|
2401
|
+
.filter((id) => typeof id === "string");
|
|
2402
|
+
if (runIds.length > 0) {
|
|
2403
|
+
const placeholders = runIds.map(() => "?").join(", ");
|
|
2404
|
+
await tx.execute({
|
|
2405
|
+
sql: `DELETE FROM agent_run_events WHERE run_id IN (${placeholders})`,
|
|
2406
|
+
args: runIds,
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2390
2409
|
const groups = new Map();
|
|
2391
2410
|
for (const row of rows) {
|
|
2392
2411
|
const outcome = row;
|
|
@@ -2470,7 +2489,8 @@ export async function getRunOutcomeCounters(options) {
|
|
|
2470
2489
|
* pruned at `olderThanMs`; errored/aborted/truncated runs are kept until
|
|
2471
2490
|
* `erroredOlderThanMs` (a longer window, falling back to `olderThanMs`) so
|
|
2472
2491
|
* their event log survives for cut-off pattern analysis via listErroredRuns. */
|
|
2473
|
-
|
|
2492
|
+
let cleanupOldRunsInFlight;
|
|
2493
|
+
async function cleanupOldRunsInternal(olderThanMs, erroredOlderThanMs) {
|
|
2474
2494
|
await ensureRunTables();
|
|
2475
2495
|
const client = getDbExec();
|
|
2476
2496
|
const cutoff = Date.now() - olderThanMs;
|
|
@@ -2567,6 +2587,23 @@ export async function cleanupOldRuns(olderThanMs, erroredOlderThanMs) {
|
|
|
2567
2587
|
// counting the same source rows.
|
|
2568
2588
|
await pruneAndRollUpPrunedRunOutcomes(client, cutoff, erroredCutoff);
|
|
2569
2589
|
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Run cleanup is scheduled after every completed run, including completions
|
|
2592
|
+
* from several concurrent requests in one isolate. Share one sweep locally;
|
|
2593
|
+
* Postgres additionally serializes the durable prune across isolates.
|
|
2594
|
+
*/
|
|
2595
|
+
export function cleanupOldRuns(olderThanMs, erroredOlderThanMs) {
|
|
2596
|
+
if (cleanupOldRunsInFlight)
|
|
2597
|
+
return cleanupOldRunsInFlight;
|
|
2598
|
+
const current = cleanupOldRunsInternal(olderThanMs, erroredOlderThanMs);
|
|
2599
|
+
let settled;
|
|
2600
|
+
settled = current.finally(() => {
|
|
2601
|
+
if (cleanupOldRunsInFlight === settled)
|
|
2602
|
+
cleanupOldRunsInFlight = undefined;
|
|
2603
|
+
});
|
|
2604
|
+
cleanupOldRunsInFlight = settled;
|
|
2605
|
+
return settled;
|
|
2606
|
+
}
|
|
2570
2607
|
/**
|
|
2571
2608
|
* List recent unsuccessful runs (errored, aborted, and truncated) for cut-off
|
|
2572
2609
|
* pattern analysis. Read-only, bounded, and ordered newest-first. Surfaced via
|
|
@@ -9,6 +9,13 @@ function isInternalContinuationError(event) {
|
|
|
9
9
|
const msg = event.error.toLowerCase();
|
|
10
10
|
if (code === "builder_gateway_error")
|
|
11
11
|
return false;
|
|
12
|
+
// An explicit `recoverable: false` outranks the code and message inference
|
|
13
|
+
// below, matching `isRecoverableContinuationError`. The background
|
|
14
|
+
// no-progress breaker stops a turn while PRESERVING the underlying transient
|
|
15
|
+
// code, so reading the code instead of the flag drops the one error the user
|
|
16
|
+
// was supposed to see out of the persisted turn.
|
|
17
|
+
if (event.recoverable === false)
|
|
18
|
+
return false;
|
|
12
19
|
return (event.recoverable === true ||
|
|
13
20
|
code === "builder_gateway_timeout" ||
|
|
14
21
|
// Carries what `msg.includes("stream ended")` below used to: a
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -175,6 +175,16 @@ export interface AgentChatRequest {
|
|
|
175
175
|
* boundary and refuses to chain past `MAX_BACKGROUND_RUN_CONTINUATIONS`.
|
|
176
176
|
*/
|
|
177
177
|
continuationCount?: number;
|
|
178
|
+
/**
|
|
179
|
+
* Terminal error code the previous chunk failed with, plus how many chunks
|
|
180
|
+
* in a row have now ended on that same code having emitted no assistant
|
|
181
|
+
* text and no tool activity. Carried on the marker because each chunk is a
|
|
182
|
+
* separate invocation with no memory of the last one — without it the
|
|
183
|
+
* no-progress circuit breaker in `shouldChainBackgroundContinuation`
|
|
184
|
+
* cannot see a repeat at all.
|
|
185
|
+
*/
|
|
186
|
+
noProgressErrorCode?: string;
|
|
187
|
+
noProgressCount?: number;
|
|
178
188
|
/**
|
|
179
189
|
* True when the dispatcher expects the self-POST to land in a real
|
|
180
190
|
* Netlify `-background` function rather than the ~60s synchronous function.
|
|
@@ -163,6 +163,9 @@ class RemoteCodeAgentConnector {
|
|
|
163
163
|
ok: false,
|
|
164
164
|
error: err instanceof Error ? err.message : String(err),
|
|
165
165
|
}));
|
|
166
|
+
if (result.ok === false) {
|
|
167
|
+
this.output.write(`Remote command ${command.id} failed: ${typeof result.error === "string" ? result.error : "Unknown connector error."}\n`);
|
|
168
|
+
}
|
|
166
169
|
await this.postCommandResult(command, result);
|
|
167
170
|
}
|
|
168
171
|
}
|
|
@@ -334,7 +337,9 @@ class RemoteCodeAgentConnector {
|
|
|
334
337
|
commandId: command.id,
|
|
335
338
|
deviceId: this.config.deviceId,
|
|
336
339
|
relayUrl: this.relayUrl,
|
|
337
|
-
...(portalWorkspace
|
|
340
|
+
...(portalWorkspace && requestedRunId
|
|
341
|
+
? { remoteRunId: requestedRunId }
|
|
342
|
+
: {}),
|
|
338
343
|
},
|
|
339
344
|
...(portalWorkspace
|
|
340
345
|
? {
|
|
@@ -14,6 +14,7 @@ import { type ContentPart } from "./sse-event-processor.js";
|
|
|
14
14
|
import type { ChatThreadScope, ChatThreadSnapshot } from "./use-chat-threads.js";
|
|
15
15
|
export { AssistantMessageListErrorBoundary, AssistantUiStaleIndexErrorBoundary, assistantUiRecoverableRenderErrorKind, isAssistantUiRecoverableRenderError, isAssistantUiStaleIndexError, } from "./assistant-ui-recovery.js";
|
|
16
16
|
export { displayableUserMessageText } from "./chat/message-components.js";
|
|
17
|
+
export declare function shouldSuppressUnauthenticatedDesktopThreadRestore(surface: AgentChatSurfaceKind, status: number): boolean;
|
|
17
18
|
type AssistantUiMessageResourceShape = {
|
|
18
19
|
id: string;
|
|
19
20
|
content: readonly unknown[];
|
|
@@ -44,6 +44,11 @@ import { useRunStuckDetection } from "./use-run-stuck-detection.js";
|
|
|
44
44
|
import { cn } from "./utils.js";
|
|
45
45
|
export { AssistantMessageListErrorBoundary, AssistantUiStaleIndexErrorBoundary, assistantUiRecoverableRenderErrorKind, isAssistantUiRecoverableRenderError, isAssistantUiStaleIndexError, } from "./assistant-ui-recovery.js";
|
|
46
46
|
export { displayableUserMessageText } from "./chat/message-components.js";
|
|
47
|
+
// Desktop chat mounts beside the parent identity gate, so an unauthenticated
|
|
48
|
+
// relay is an expected empty state until that gate establishes a session.
|
|
49
|
+
export function shouldSuppressUnauthenticatedDesktopThreadRestore(surface, status) {
|
|
50
|
+
return surface === "desktop" && (status === 401 || status === 403);
|
|
51
|
+
}
|
|
47
52
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
48
53
|
export function assistantUiMessageListStructureKey(messages) {
|
|
49
54
|
return JSON.stringify(messages.map((message) => [
|
|
@@ -2654,7 +2659,11 @@ const AssistantChatInner = forwardRef(function AssistantChatInner({ emptyStateTe
|
|
|
2654
2659
|
const res = await fetch(`${apiUrl}/threads/${encodeURIComponent(threadId)}`);
|
|
2655
2660
|
if (!res.ok) {
|
|
2656
2661
|
if (!cancelled) {
|
|
2657
|
-
setThreadRestoreError(res.status
|
|
2662
|
+
setThreadRestoreError(shouldSuppressUnauthenticatedDesktopThreadRestore(agentChatSurface, res.status)
|
|
2663
|
+
? null
|
|
2664
|
+
: res.status === 404
|
|
2665
|
+
? "not-found"
|
|
2666
|
+
: "unavailable");
|
|
2658
2667
|
}
|
|
2659
2668
|
return;
|
|
2660
2669
|
}
|
|
@@ -789,7 +789,11 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
789
789
|
catch { }
|
|
790
790
|
}, [subAgentNames, SUB_AGENT_NAMES_KEY]);
|
|
791
791
|
// Open tabs — persisted to localStorage so they survive refresh.
|
|
792
|
-
|
|
792
|
+
// Per-scope, for the same reason the active thread is: the tab list must
|
|
793
|
+
// follow the resource in view, so one resource's tabs never stay mounted
|
|
794
|
+
// (and rebroadcasting their run state) while another resource is open.
|
|
795
|
+
const scopeKeyPart = scope ? `:scope:${scope.type}:${scope.id}` : "";
|
|
796
|
+
const OPEN_TABS_KEY = `agent-chat-open-tabs${keyPrefix}${scopeKeyPart}`;
|
|
793
797
|
const [openTabIds, setOpenTabIds] = useState(() => {
|
|
794
798
|
if (!restoreActiveThread && activeThreadId) {
|
|
795
799
|
for (const id of [activeThreadId])
|
|
@@ -814,7 +818,35 @@ export function MultiTabAssistantChat({ showTabBar = true, renderHeader, renderO
|
|
|
814
818
|
const openTabIdsRef = useRef(openTabIds);
|
|
815
819
|
openTabIdsRef.current = openTabIds;
|
|
816
820
|
const initializedRef = useRef(false);
|
|
821
|
+
// Rehydrate open tabs when the scope flips. Read the new key before the
|
|
822
|
+
// persistence effect can write the current (now-wrong) tab list under it.
|
|
817
823
|
const openTabsKeyRef = useRef(OPEN_TABS_KEY);
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
if (openTabsKeyRef.current === OPEN_TABS_KEY)
|
|
826
|
+
return;
|
|
827
|
+
openTabsKeyRef.current = OPEN_TABS_KEY;
|
|
828
|
+
initializedRef.current = false;
|
|
829
|
+
if (!restoreActiveThread) {
|
|
830
|
+
setOpenTabIds(activeThreadId ? [activeThreadId] : []);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const saved = localStorage.getItem(OPEN_TABS_KEY);
|
|
835
|
+
if (saved) {
|
|
836
|
+
const parsed = JSON.parse(saved);
|
|
837
|
+
if (Array.isArray(parsed)) {
|
|
838
|
+
for (const id of parsed)
|
|
839
|
+
mountedTabsRef.current.add(id);
|
|
840
|
+
setOpenTabIds(parsed);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// coercion-ok: malformed persisted tab data is an absent tab list.
|
|
847
|
+
}
|
|
848
|
+
setOpenTabIds([]);
|
|
849
|
+
}, [OPEN_TABS_KEY, activeThreadId, restoreActiveThread]);
|
|
818
850
|
useBrowserLayoutEffect(() => {
|
|
819
851
|
const nextScope = scope;
|
|
820
852
|
if (!nextScope)
|
|
@@ -473,6 +473,17 @@ async function readChunkWithProgressTimeout(reader, lastMeaningfulEventAt, noPro
|
|
|
473
473
|
function isAutoRecoverableError(ev, errMsg) {
|
|
474
474
|
const code = String(ev.errorCode ?? "").toLowerCase();
|
|
475
475
|
const msg = errMsg.toLowerCase();
|
|
476
|
+
// An explicit `recoverable: false` outranks EVERY inference below — the code
|
|
477
|
+
// list as well as the message sniff — matching the server's own precedence in
|
|
478
|
+
// `isRecoverableContinuationError`. The repeat guards stop a turn with a
|
|
479
|
+
// message that names the looping tool, so a stop on
|
|
480
|
+
// `list-workspace-connections` matched the "connection" sniff and
|
|
481
|
+
// auto-continued the very loop it was emitted to break; the background
|
|
482
|
+
// no-progress breaker stops one while PRESERVING the underlying transient
|
|
483
|
+
// code (so the failure stays diagnosable), so reading the code instead of the
|
|
484
|
+
// flag re-POSTs the exact chain the server just refused to continue.
|
|
485
|
+
if (ev.recoverable === false)
|
|
486
|
+
return false;
|
|
476
487
|
if (code === "context_length_exceeded" ||
|
|
477
488
|
code === "input_too_long" ||
|
|
478
489
|
code.startsWith("credits-limit") ||
|
|
@@ -548,13 +559,6 @@ function isAutoRecoverableError(ev, errMsg) {
|
|
|
548
559
|
}
|
|
549
560
|
if (ev.recoverable === true)
|
|
550
561
|
return true;
|
|
551
|
-
// An explicit flag outranks the message sniff below, which exists only for
|
|
552
|
-
// events that carry no flag at all. The repeat guards stop a turn with
|
|
553
|
-
// `recoverable: false` and a message that names the looping tool, so a stop
|
|
554
|
-
// on `list-workspace-connections` matched the "connection" sniff and
|
|
555
|
-
// auto-continued the very loop it was emitted to break.
|
|
556
|
-
if (ev.recoverable === false)
|
|
557
|
-
return false;
|
|
558
562
|
if (msg.includes("daily gateway request cap"))
|
|
559
563
|
return false;
|
|
560
564
|
// The engine's structural verdict, checked after every terminal code above so
|
|
@@ -467,9 +467,9 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
467
467
|
// it; the server hasn't seen it yet because there's no POST anymore,
|
|
468
468
|
// the row gets written when the user sends a message.
|
|
469
469
|
// - savedId is set but not on the current page → look it up directly. A
|
|
470
|
-
// found thread stays active. An unavailable lookup keeps the saved id
|
|
471
|
-
//
|
|
472
|
-
//
|
|
470
|
+
// found thread stays active. An unavailable lookup keeps the saved id for
|
|
471
|
+
// list-only readers and explicit routes, while a normal home surface drops
|
|
472
|
+
// a dead local pointer instead of opening with a restore error.
|
|
473
473
|
// - No savedId → synthesize a fresh local id (no POST; server creates the
|
|
474
474
|
// row on first message). The server may contain chats from another
|
|
475
475
|
// branch, preview, or project that shares the same user/database, so
|
|
@@ -511,12 +511,12 @@ export function useChatThreads(apiUrl = agentNativePath("/_agent-native/agent-ch
|
|
|
511
511
|
const restoredIsUnavailable = restoredThread === null && lookupRestored && !restoredOnPage;
|
|
512
512
|
const restoredBelongsElsewhere = Boolean(restoredThread &&
|
|
513
513
|
!threadCanStayVisibleInScope(restoredThread.scope ?? null, scopeRef.current));
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
514
|
+
// A missing saved id is stale local UI state on a normal home surface,
|
|
515
|
+
// not a reason to show an error above a fresh composer. Preserve it for
|
|
516
|
+
// explicit routes and list-only readers, where the caller still owns
|
|
517
|
+
// recovery for a deliberately selected thread.
|
|
518
|
+
const restoredNeedsReplacement = restoredBelongsElsewhere ||
|
|
519
|
+
(restoredIsUnavailable && autoCreate && !routeControlsActiveThread);
|
|
520
520
|
if (restoredNeedsReplacement)
|
|
521
521
|
setActiveThreadId(null);
|
|
522
522
|
const savedId = restoredNeedsReplacement ? null : restoredId;
|
package/dist/db/client.js
CHANGED
|
@@ -1258,7 +1258,12 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1258
1258
|
const { rawSql, args } = sqlAndArgs(sql);
|
|
1259
1259
|
const { timeoutMs } = dbExecQueryBudget(sql);
|
|
1260
1260
|
const pgSql = sqliteToPostgresParams(rawSql);
|
|
1261
|
-
|
|
1261
|
+
// Neon only accepts multiple SQL commands through its simple protocol;
|
|
1262
|
+
// the transaction start has no parameters, so use that overload.
|
|
1263
|
+
const runQuery = () => args.length === 0 && rawSql.includes(";")
|
|
1264
|
+
? client.query(pgSql)
|
|
1265
|
+
: client.query(pgSql, args);
|
|
1266
|
+
const result = await withDbTimeout("query", () => runQuery(), timeoutOverrideMs ?? timeoutMs);
|
|
1262
1267
|
return {
|
|
1263
1268
|
rows: result.rows,
|
|
1264
1269
|
rowsAffected: result.rowCount ?? 0,
|
|
@@ -1404,7 +1409,10 @@ async function createDbExecInternal(config = {}, trackSingletonResources = false
|
|
|
1404
1409
|
},
|
|
1405
1410
|
};
|
|
1406
1411
|
try {
|
|
1407
|
-
|
|
1412
|
+
// Send the transaction start and idle reaper together. Neon
|
|
1413
|
+
// transaction pooling can ignore startup parameters, and a
|
|
1414
|
+
// worker can die between separate BEGIN and SET LOCAL calls.
|
|
1415
|
+
await queryNeonClient(client, "BEGIN; SET LOCAL idle_in_transaction_session_timeout = 30000");
|
|
1408
1416
|
const result = await fn(tx);
|
|
1409
1417
|
await queryNeonClient(client, "COMMIT");
|
|
1410
1418
|
releaseClient();
|
package/dist/db/create-get-db.js
CHANGED
|
@@ -40,6 +40,43 @@ function getNeonServerlessDrizzle() {
|
|
|
40
40
|
export function isSqlRead(sql) {
|
|
41
41
|
return /^\s*(SELECT|WITH\s)/i.test(sql);
|
|
42
42
|
}
|
|
43
|
+
const NEON_IDLE_IN_TRANSACTION_TIMEOUT_SQL = "SET LOCAL idle_in_transaction_session_timeout = 30000";
|
|
44
|
+
function queryText(sql) {
|
|
45
|
+
if (typeof sql === "string")
|
|
46
|
+
return sql;
|
|
47
|
+
if (sql && typeof sql === "object" && "text" in sql) {
|
|
48
|
+
const text = sql.text;
|
|
49
|
+
return typeof text === "string" ? text : "";
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
function isBeginQuery(sql) {
|
|
54
|
+
return /^\s*BEGIN(?:\s|$)/i.test(queryText(sql));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Drizzle sends BEGIN through the client returned by pool.connect(), so a
|
|
58
|
+
* pool startup parameter alone is not enough protection when Neon routes the
|
|
59
|
+
* connection through a transaction pooler. Put the idle timeout in the same
|
|
60
|
+
* simple-protocol message as BEGIN; a worker killed before its next query
|
|
61
|
+
* still leaves a backend that will reap itself.
|
|
62
|
+
*/
|
|
63
|
+
function guardNeonTransactionClient(client) {
|
|
64
|
+
return new Proxy(client, {
|
|
65
|
+
get(target, prop) {
|
|
66
|
+
if (prop !== "query") {
|
|
67
|
+
const value = target[prop];
|
|
68
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
69
|
+
}
|
|
70
|
+
return (...args) => {
|
|
71
|
+
const sql = args[0];
|
|
72
|
+
if (!isBeginQuery(sql))
|
|
73
|
+
return target.query(...args);
|
|
74
|
+
const text = queryText(sql).replace(/;\s*$/, "");
|
|
75
|
+
return target.query(`${text}; ${NEON_IDLE_IN_TRANSACTION_TIMEOUT_SQL}`);
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
43
80
|
/**
|
|
44
81
|
* Wraps a @neondatabase/serverless Pool so every query goes through
|
|
45
82
|
* the same withDbTimeout + retryOnConnectionError resilience that the
|
|
@@ -129,6 +166,11 @@ export function buildResilientNeonPool(pool) {
|
|
|
129
166
|
get(target, prop) {
|
|
130
167
|
if (prop === "query")
|
|
131
168
|
return resilientQuery;
|
|
169
|
+
if (prop === "connect") {
|
|
170
|
+
return (...args) => target
|
|
171
|
+
.connect(...args)
|
|
172
|
+
.then((client) => guardNeonTransactionClient(client));
|
|
173
|
+
}
|
|
132
174
|
const val = target[prop];
|
|
133
175
|
return typeof val === "function" ? val.bind(target) : val;
|
|
134
176
|
},
|
|
@@ -42,22 +42,22 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
44
|
error?: undefined;
|
|
45
|
+
ok?: undefined;
|
|
45
46
|
summary: import("./types.js").TraceSummary;
|
|
46
47
|
spans: import("./types.js").TraceSpan[];
|
|
47
48
|
id?: undefined;
|
|
48
|
-
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
50
|
error?: undefined;
|
|
51
|
+
ok?: undefined;
|
|
51
52
|
summary?: undefined;
|
|
52
53
|
spans?: undefined;
|
|
53
54
|
id: string;
|
|
54
|
-
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
|
+
ok?: undefined;
|
|
56
57
|
summary?: undefined;
|
|
57
58
|
spans?: undefined;
|
|
58
59
|
id?: undefined;
|
|
59
60
|
error: any;
|
|
60
|
-
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
62
|
error?: undefined;
|
|
63
63
|
summary?: undefined;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
error: string;
|
|
52
51
|
ok?: undefined;
|
|
52
|
+
error: string;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
status?: undefined;
|
|
38
37
|
ok?: undefined;
|
|
38
|
+
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
+
ok?: undefined;
|
|
44
45
|
error: string;
|
|
45
46
|
removed?: undefined;
|
|
46
|
-
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — validate an optional candidate value
|
|
54
54
|
* or the current stored value without changing anything.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
+
ok?: undefined;
|
|
57
58
|
error: string;
|
|
58
59
|
note?: undefined;
|
|
59
|
-
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,11 +95,11 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|