@agent-native/core 0.101.2 → 0.101.4
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 +1 -1
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
- package/corpus/core/src/client/sse-event-processor.ts +36 -1
- package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
- package/corpus/core/src/credentials/index.ts +41 -8
- package/corpus/core/src/deploy/build.ts +78 -25
- package/corpus/core/src/extensions/fetch-tool.ts +33 -3
- package/corpus/core/src/notifications/channels.ts +20 -6
- package/corpus/core/src/secrets/crypto.ts +26 -10
- package/corpus/core/src/secrets/storage.ts +16 -3
- package/corpus/core/src/server/agent-chat-plugin.ts +60 -7
- package/corpus/core/src/server/derived-secret.ts +5 -1
- package/corpus/templates/analytics/changelog/2026-07-14-long-running-cross-app-analyses-now-continue-reliably-in-the.md +6 -0
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +8 -4
- package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
- package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +44 -81
- package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
- package/corpus/templates/clips/app/i18n/ar-SA.ts +0 -8
- package/corpus/templates/clips/app/i18n/de-DE.ts +0 -9
- package/corpus/templates/clips/app/i18n/en-US.ts +0 -8
- package/corpus/templates/clips/app/i18n/es-ES.ts +0 -9
- package/corpus/templates/clips/app/i18n/fr-FR.ts +0 -9
- package/corpus/templates/clips/app/i18n/hi-IN.ts +0 -8
- package/corpus/templates/clips/app/i18n/ja-JP.ts +0 -9
- package/corpus/templates/clips/app/i18n/ko-KR.ts +0 -9
- package/corpus/templates/clips/app/i18n/pt-BR.ts +0 -9
- package/corpus/templates/clips/app/i18n/zh-CN.ts +0 -7
- package/corpus/templates/clips/app/i18n/zh-TW.ts +0 -7
- package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-14-share-dialogs-show-separate-links-for-people-and-agents.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
- package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
- package/corpus/templates/dispatch/changelog/2026-07-14-dispatch-now-distinguishes-connected-agents-from-locally-mou.md +6 -0
- package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
- package/corpus/templates/slides/actions/import-docx.ts +3 -9
- package/corpus/templates/slides/actions/import-file.ts +11 -14
- package/corpus/templates/slides/actions/import-pptx.ts +3 -9
- package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
- package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
- package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
- package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
- package/dist/client/RunStuckBanner.d.ts.map +1 -1
- package/dist/client/RunStuckBanner.js +9 -5
- package/dist/client/RunStuckBanner.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +33 -1
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts +4 -4
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +104 -19
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/credentials/index.d.ts +12 -7
- package/dist/credentials/index.d.ts.map +1 -1
- package/dist/credentials/index.js +33 -8
- package/dist/credentials/index.js.map +1 -1
- package/dist/deploy/build.d.ts +15 -14
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +64 -23
- package/dist/deploy/build.js.map +1 -1
- package/dist/extensions/fetch-tool.d.ts +13 -1
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +9 -2
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/notifications/channels.js +20 -4
- package/dist/notifications/channels.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/crypto.d.ts +15 -7
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +23 -10
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/secrets/storage.d.ts.map +1 -1
- package/dist/secrets/storage.js +16 -3
- package/dist/secrets/storage.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +20 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +38 -4
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/derived-secret.d.ts +1 -1
- package/dist/server/derived-secret.d.ts.map +1 -1
- package/dist/server/derived-secret.js.map +1 -1
- package/package.json +2 -2
- package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +0 -68
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.101.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7d72df1: Keep chat responsive while replaying dense agent event bursts, and surface safe cancellation when a live background run has gone quiet.
|
|
8
|
+
- 7d72df1: Bundle one complete Yjs runtime into serverless deploy output so SSR starts without missing named exports.
|
|
9
|
+
|
|
10
|
+
## 0.101.3
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 7cfb087: Keep centered chat composer skeletons aligned with the real composer while the chat UI loads.
|
|
15
|
+
- 7cfb087: Document PR Visual Recap's cost drivers and cheaper model options (`claude-haiku-4-5`, `gpt-5.6-luna`/`gpt-5.6-terra`, `openai-compatible` providers), and note that the Claude backend now defaults to `claude-sonnet-5` when `VISUAL_RECAP_MODEL` is unset. Adds a matching cost/model-choice section to the Visual Plans template doc.
|
|
16
|
+
- 7cfb087: Fail closed when first-party scaffolds cannot be downloaded from the CLI's immutable core version tags instead of mixing newer templates with an older runtime.
|
|
17
|
+
- 7cfb087: Resolve `${keys.NAME}` in the agent fetch tool and notification webhooks through the request-scope cascade so Dispatch-vault keys scoped to the workspace/org resolve like they already do for extensions and automations.
|
|
18
|
+
- 7cfb087: Resolve provider credentials from encrypted Dispatch vault secrets, keep hosted A2A agent discovery distinct from mounted workspace app inventory, and run long Analytics A2A tasks on the durable background worker.
|
|
19
|
+
- 7cfb087: Expose the embedded OG font resolver so template social images keep their text visible in serverless runtimes.
|
|
20
|
+
- 7cfb087: Stop agent turns from looping across alternating read-only docs and source search tools.
|
|
21
|
+
- 7cfb087: Publish PR Visual Recap helpers as a dependency-light CLI package so recap workflows no longer install the full Agent-Native framework dependency graph.
|
|
22
|
+
- 7cfb087: Derive workspace-shared secret encryption material from A2A_SECRET on hosted workspace deploys so vault keys decrypt across sibling apps, and stop serving stale shared ciphertext after a value is updated without shared key material.
|
|
23
|
+
- Updated dependencies [7cfb087]
|
|
24
|
+
- Updated dependencies [7cfb087]
|
|
25
|
+
- Updated dependencies [7cfb087]
|
|
26
|
+
- @agent-native/recap-cli@0.4.0
|
|
27
|
+
|
|
3
28
|
## 0.101.2
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.101.
|
|
3
|
+
"version": "0.101.4",
|
|
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": {
|
|
@@ -323,10 +323,17 @@ export function RunStuckBanner({
|
|
|
323
323
|
};
|
|
324
324
|
|
|
325
325
|
const handleRetry = async () => {
|
|
326
|
-
// Defense in depth:
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
if (
|
|
326
|
+
// Defense in depth: Retry is hidden while a background worker is still
|
|
327
|
+
// heartbeating or work is explicitly in flight (see render below). Guard
|
|
328
|
+
// the handler too so this path cannot abort a run the server says is alive.
|
|
329
|
+
if (
|
|
330
|
+
!state.runId ||
|
|
331
|
+
busy.type !== "none" ||
|
|
332
|
+
backgroundWorkerStillAlive ||
|
|
333
|
+
inFlightWork
|
|
334
|
+
) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
330
337
|
const runId = state.runId;
|
|
331
338
|
setBusy({ type: "retry", runId });
|
|
332
339
|
trackEvent("agent_chat_stuck_retry", {
|
|
@@ -380,7 +387,7 @@ export function RunStuckBanner({
|
|
|
380
387
|
</span>
|
|
381
388
|
</div>
|
|
382
389
|
<div className="flex flex-wrap items-center gap-2">
|
|
383
|
-
{
|
|
390
|
+
{stillWorking ? null : (
|
|
384
391
|
<button
|
|
385
392
|
type="button"
|
|
386
393
|
onClick={handleRetry}
|
|
@@ -112,6 +112,20 @@ const INTERRUPTED_TOOL_RESULT =
|
|
|
112
112
|
"Interrupted before this tool returned a result.";
|
|
113
113
|
const INTERRUPTED_ACTIVITY_RESULT = "Stopped before this action started.";
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Maximum number of assistant-ui repository updates we deliver in one browser
|
|
117
|
+
* event-loop turn. Durable-run replay can put hundreds of SSE frames into the
|
|
118
|
+
* stream queue before the client attaches; draining all of them through
|
|
119
|
+
* assistant-ui without a macrotask boundary synchronously nests React external
|
|
120
|
+
* store notifications until React throws "Maximum update depth exceeded."
|
|
121
|
+
*
|
|
122
|
+
* A timer scheduled on the first result resets the count when the stream is
|
|
123
|
+
* naturally idle between network chunks. We only await it when results are
|
|
124
|
+
* arriving densely enough to hit this bound, so normal live token streaming
|
|
125
|
+
* keeps its existing latency while replay bursts yield cooperatively.
|
|
126
|
+
*/
|
|
127
|
+
const SSE_RENDER_UPDATES_PER_EVENT_LOOP_TURN = 20;
|
|
128
|
+
|
|
115
129
|
export function settleInterruptedToolCalls(
|
|
116
130
|
content: ContentPart[],
|
|
117
131
|
result = INTERRUPTED_TOOL_RESULT,
|
|
@@ -1609,6 +1623,24 @@ export async function* readSSEStream(
|
|
|
1609
1623
|
completedToolsAfterLastAssistantText: new Set(),
|
|
1610
1624
|
streamProgressDispatched: false,
|
|
1611
1625
|
};
|
|
1626
|
+
let renderUpdatesThisTurn = 0;
|
|
1627
|
+
let nextEventLoopTurn: Promise<void> | null = null;
|
|
1628
|
+
|
|
1629
|
+
const paceRenderUpdate = async (): Promise<void> => {
|
|
1630
|
+
renderUpdatesThisTurn += 1;
|
|
1631
|
+
if (!nextEventLoopTurn) {
|
|
1632
|
+
nextEventLoopTurn = new Promise<void>((resolve) => {
|
|
1633
|
+
setTimeout(() => {
|
|
1634
|
+
renderUpdatesThisTurn = 0;
|
|
1635
|
+
nextEventLoopTurn = null;
|
|
1636
|
+
resolve();
|
|
1637
|
+
}, 0);
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
if (renderUpdatesThisTurn >= SSE_RENDER_UPDATES_PER_EVENT_LOOP_TURN) {
|
|
1641
|
+
await nextEventLoopTurn;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1612
1644
|
|
|
1613
1645
|
const withStreamMetadata = (r: ChatModelRunResult): ChatModelRunResult => {
|
|
1614
1646
|
if (!runId && activityTrail.length === 0) return r;
|
|
@@ -1726,7 +1758,10 @@ export async function* readSSEStream(
|
|
|
1726
1758
|
processEventState,
|
|
1727
1759
|
);
|
|
1728
1760
|
|
|
1729
|
-
if (result)
|
|
1761
|
+
if (result) {
|
|
1762
|
+
await paceRenderUpdate();
|
|
1763
|
+
yield withStreamMetadata(result);
|
|
1764
|
+
}
|
|
1730
1765
|
if (
|
|
1731
1766
|
hasStalledPreparingAction(
|
|
1732
1767
|
preparingActionState,
|
|
@@ -63,10 +63,10 @@ export interface UseRunStuckDetectionOptions {
|
|
|
63
63
|
*/
|
|
64
64
|
backgroundStuckThresholdMs?: number;
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
67
|
-
* fresh process heartbeats.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
66
|
+
* Legacy upper bound for a claimed durable background worker that is still
|
|
67
|
+
* sending fresh process heartbeats. Informational quiet-run UI is never
|
|
68
|
+
* delayed past `backgroundStuckThresholdMs`; this option can only request an
|
|
69
|
+
* earlier notice for live workers. Default 13 minutes.
|
|
70
70
|
*/
|
|
71
71
|
liveBackgroundStuckThresholdMs?: number;
|
|
72
72
|
/** Poll interval. Default 5_000ms. */
|
|
@@ -127,6 +127,137 @@ export function useRunStuckDetection({
|
|
|
127
127
|
const base = apiUrl ?? agentNativePath("/_agent-native/agent-chat");
|
|
128
128
|
let cancelled = false;
|
|
129
129
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
130
|
+
let snapshotTransitionTimer: ReturnType<typeof setTimeout> | null = null;
|
|
131
|
+
let snapshotVersion = 0;
|
|
132
|
+
|
|
133
|
+
type RunHealthSnapshot = {
|
|
134
|
+
active: boolean;
|
|
135
|
+
runId: string | null;
|
|
136
|
+
status: string | null;
|
|
137
|
+
lastProgressAt: number | null;
|
|
138
|
+
stuckSinceMs: number | null;
|
|
139
|
+
heartbeatAt: number | null;
|
|
140
|
+
heartbeatSinceMs: number | null;
|
|
141
|
+
dispatchMode: string | null;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const effectiveThresholdFor = (
|
|
145
|
+
dispatchMode: string | null,
|
|
146
|
+
heartbeatSinceMs: number | null,
|
|
147
|
+
) => {
|
|
148
|
+
const liveBackgroundWorker =
|
|
149
|
+
dispatchMode === "background-processing" &&
|
|
150
|
+
heartbeatSinceMs != null &&
|
|
151
|
+
heartbeatSinceMs >= 0 &&
|
|
152
|
+
heartbeatSinceMs < FRESH_BACKGROUND_HEARTBEAT_MS;
|
|
153
|
+
const serverContinued =
|
|
154
|
+
dispatchMode === "foreground-self-chain" ||
|
|
155
|
+
dispatchMode?.startsWith("background") === true;
|
|
156
|
+
return liveBackgroundWorker
|
|
157
|
+
? Math.min(backgroundStuckThresholdMs, liveBackgroundStuckThresholdMs)
|
|
158
|
+
: serverContinued
|
|
159
|
+
? backgroundStuckThresholdMs
|
|
160
|
+
: stuckThresholdMs;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const scheduleSnapshotTransition = (
|
|
164
|
+
snapshot: RunHealthSnapshot,
|
|
165
|
+
observedAtMs: number,
|
|
166
|
+
version: number,
|
|
167
|
+
) => {
|
|
168
|
+
if (snapshotTransitionTimer) clearTimeout(snapshotTransitionTimer);
|
|
169
|
+
snapshotTransitionTimer = null;
|
|
170
|
+
if (
|
|
171
|
+
cancelled ||
|
|
172
|
+
version !== snapshotVersion ||
|
|
173
|
+
!snapshot.active ||
|
|
174
|
+
snapshot.status !== "running" ||
|
|
175
|
+
snapshot.runId == null
|
|
176
|
+
) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const currentElapsedMs = Math.max(0, Date.now() - observedAtMs);
|
|
181
|
+
const currentHeartbeatSinceMs =
|
|
182
|
+
snapshot.heartbeatSinceMs == null
|
|
183
|
+
? null
|
|
184
|
+
: snapshot.heartbeatSinceMs + currentElapsedMs;
|
|
185
|
+
const currentStuckSinceMs =
|
|
186
|
+
snapshot.stuckSinceMs == null
|
|
187
|
+
? null
|
|
188
|
+
: snapshot.stuckSinceMs + currentElapsedMs;
|
|
189
|
+
const effectiveThresholdMs = effectiveThresholdFor(
|
|
190
|
+
snapshot.dispatchMode,
|
|
191
|
+
currentHeartbeatSinceMs,
|
|
192
|
+
);
|
|
193
|
+
const currentlyStuck = Boolean(
|
|
194
|
+
currentStuckSinceMs != null &&
|
|
195
|
+
currentStuckSinceMs > effectiveThresholdMs,
|
|
196
|
+
);
|
|
197
|
+
const transitionDelaysMs: number[] = [];
|
|
198
|
+
if (
|
|
199
|
+
currentHeartbeatSinceMs != null &&
|
|
200
|
+
currentHeartbeatSinceMs >= 0 &&
|
|
201
|
+
currentHeartbeatSinceMs < FRESH_BACKGROUND_HEARTBEAT_MS
|
|
202
|
+
) {
|
|
203
|
+
transitionDelaysMs.push(
|
|
204
|
+
FRESH_BACKGROUND_HEARTBEAT_MS - currentHeartbeatSinceMs + 1,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (currentStuckSinceMs != null && !currentlyStuck) {
|
|
208
|
+
transitionDelaysMs.push(
|
|
209
|
+
Math.max(1, effectiveThresholdMs - currentStuckSinceMs + 1),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (transitionDelaysMs.length === 0) return;
|
|
213
|
+
|
|
214
|
+
// Both ages originate from the server clock. Advance those snapshots by
|
|
215
|
+
// only a local elapsed duration, which remains safe when client/server
|
|
216
|
+
// wall clocks differ. Wake at the earlier semantic boundary, then
|
|
217
|
+
// reschedule if the other boundary is still ahead.
|
|
218
|
+
const delayMs = Math.max(1, Math.min(...transitionDelaysMs));
|
|
219
|
+
snapshotTransitionTimer = setTimeout(() => {
|
|
220
|
+
snapshotTransitionTimer = null;
|
|
221
|
+
if (cancelled || version !== snapshotVersion) return;
|
|
222
|
+
const elapsedSinceObservationMs = Math.max(
|
|
223
|
+
0,
|
|
224
|
+
Date.now() - observedAtMs,
|
|
225
|
+
);
|
|
226
|
+
const nextHeartbeatSinceMs =
|
|
227
|
+
snapshot.heartbeatSinceMs == null
|
|
228
|
+
? null
|
|
229
|
+
: snapshot.heartbeatSinceMs + elapsedSinceObservationMs;
|
|
230
|
+
const nextStuckSinceMs =
|
|
231
|
+
snapshot.stuckSinceMs == null
|
|
232
|
+
? null
|
|
233
|
+
: snapshot.stuckSinceMs + elapsedSinceObservationMs;
|
|
234
|
+
const nextEffectiveThresholdMs = effectiveThresholdFor(
|
|
235
|
+
snapshot.dispatchMode,
|
|
236
|
+
nextHeartbeatSinceMs,
|
|
237
|
+
);
|
|
238
|
+
const nextIsStuck = Boolean(
|
|
239
|
+
nextStuckSinceMs != null &&
|
|
240
|
+
nextStuckSinceMs > nextEffectiveThresholdMs,
|
|
241
|
+
);
|
|
242
|
+
setState((current) => {
|
|
243
|
+
if (
|
|
244
|
+
version !== snapshotVersion ||
|
|
245
|
+
current.runId !== snapshot.runId ||
|
|
246
|
+
current.lastProgressAt !== snapshot.lastProgressAt ||
|
|
247
|
+
current.heartbeatAt !== snapshot.heartbeatAt
|
|
248
|
+
) {
|
|
249
|
+
return current;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
...current,
|
|
253
|
+
isStuck: nextIsStuck,
|
|
254
|
+
stuckSinceMs: nextStuckSinceMs,
|
|
255
|
+
heartbeatSinceMs: nextHeartbeatSinceMs,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
scheduleSnapshotTransition(snapshot, observedAtMs, version);
|
|
259
|
+
}, delayMs);
|
|
260
|
+
};
|
|
130
261
|
|
|
131
262
|
const poll = async () => {
|
|
132
263
|
if (cancelled) return;
|
|
@@ -153,34 +284,38 @@ export function useRunStuckDetection({
|
|
|
153
284
|
heartbeatAt != null ? nowMs - heartbeatAt : null;
|
|
154
285
|
const dispatchMode =
|
|
155
286
|
typeof data.dispatchMode === "string" ? data.dispatchMode : null;
|
|
156
|
-
// Server-continued runs get the wider threshold: the server's own
|
|
157
|
-
// recovery (150s no-progress backstop + chained continuations) must
|
|
158
|
-
// get its chance before the user sees a "stuck" affordance.
|
|
159
|
-
const serverContinued =
|
|
160
|
-
dispatchMode === "foreground-self-chain" ||
|
|
161
|
-
dispatchMode?.startsWith("background") === true;
|
|
162
287
|
// A claimed durable worker with a fresh heartbeat can legitimately
|
|
163
|
-
// be waiting on a bounded long-running tool/sub-agent call.
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
const
|
|
169
|
-
dispatchMode
|
|
170
|
-
heartbeatSinceMs
|
|
171
|
-
|
|
172
|
-
heartbeatSinceMs < FRESH_BACKGROUND_HEARTBEAT_MS;
|
|
173
|
-
const effectiveThresholdMs = liveBackgroundWorker
|
|
174
|
-
? liveBackgroundStuckThresholdMs
|
|
175
|
-
: serverContinued
|
|
176
|
-
? backgroundStuckThresholdMs
|
|
177
|
-
: stuckThresholdMs;
|
|
288
|
+
// be waiting on a bounded long-running tool/sub-agent call. Still
|
|
289
|
+
// surface informational status at the normal background threshold;
|
|
290
|
+
// RunStuckBanner uses the fresh heartbeat to withhold Retry while
|
|
291
|
+
// keeping an explicit Cancel available. The legacy live-worker
|
|
292
|
+
// threshold may make that notice earlier, but never later.
|
|
293
|
+
const effectiveThresholdMs = effectiveThresholdFor(
|
|
294
|
+
dispatchMode,
|
|
295
|
+
heartbeatSinceMs,
|
|
296
|
+
);
|
|
178
297
|
const isStuck = Boolean(
|
|
179
298
|
data.active &&
|
|
180
299
|
data.status === "running" &&
|
|
181
300
|
stuckSinceMs != null &&
|
|
182
301
|
stuckSinceMs > effectiveThresholdMs,
|
|
183
302
|
);
|
|
303
|
+
const observedAtMs = Date.now();
|
|
304
|
+
const version = ++snapshotVersion;
|
|
305
|
+
scheduleSnapshotTransition(
|
|
306
|
+
{
|
|
307
|
+
active: data.active,
|
|
308
|
+
runId: data.runId ?? null,
|
|
309
|
+
status: data.status ?? null,
|
|
310
|
+
lastProgressAt,
|
|
311
|
+
stuckSinceMs,
|
|
312
|
+
heartbeatAt,
|
|
313
|
+
heartbeatSinceMs,
|
|
314
|
+
dispatchMode,
|
|
315
|
+
},
|
|
316
|
+
observedAtMs,
|
|
317
|
+
version,
|
|
318
|
+
);
|
|
184
319
|
setState({
|
|
185
320
|
isStuck,
|
|
186
321
|
runId: data.runId ?? null,
|
|
@@ -217,7 +352,9 @@ export function useRunStuckDetection({
|
|
|
217
352
|
|
|
218
353
|
return () => {
|
|
219
354
|
cancelled = true;
|
|
355
|
+
snapshotVersion += 1;
|
|
220
356
|
if (timer) clearTimeout(timer);
|
|
357
|
+
if (snapshotTransitionTimer) clearTimeout(snapshotTransitionTimer);
|
|
221
358
|
};
|
|
222
359
|
}, [
|
|
223
360
|
threadId,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
decryptSecretValue,
|
|
4
4
|
isEncryptedSecretValue,
|
|
5
5
|
} from "../secrets/crypto.js";
|
|
6
|
+
import { readAppSecret, type SecretRef } from "../secrets/storage.js";
|
|
6
7
|
import { getSetting, putSetting, deleteSetting } from "../settings/store.js";
|
|
7
8
|
|
|
8
9
|
const SETTING_PREFIX = "credential:";
|
|
@@ -41,6 +42,20 @@ async function readCredentialSetting(
|
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
async function readScopedAppSecret(
|
|
46
|
+
key: string,
|
|
47
|
+
scope: SecretRef["scope"],
|
|
48
|
+
scopeId: string,
|
|
49
|
+
): Promise<string | undefined> {
|
|
50
|
+
try {
|
|
51
|
+
return (await readAppSecret({ key, scope, scopeId }))?.value;
|
|
52
|
+
} catch {
|
|
53
|
+
// Older databases may not have app_secrets yet. Keep the legacy
|
|
54
|
+
// credential store available while the table bootstraps.
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
/**
|
|
45
60
|
* Resolve a credential from one explicit legacy SQL credential scope.
|
|
46
61
|
*
|
|
@@ -61,16 +76,21 @@ export async function resolveCredentialForScope(
|
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
/**
|
|
64
|
-
* Resolve a credential
|
|
65
|
-
*
|
|
79
|
+
* Resolve a credential across the encrypted app_secrets store and the legacy
|
|
80
|
+
* settings-backed credential store. User overrides win, followed by the
|
|
81
|
+
* active org/workspace shared value.
|
|
66
82
|
*
|
|
67
83
|
* SECURITY: NEVER reads from process.env. Env vars are global to the
|
|
68
|
-
* deployment and would leak across users in a multi-tenant app.
|
|
69
|
-
*
|
|
84
|
+
* deployment and would leak across users in a multi-tenant app.
|
|
85
|
+
*
|
|
86
|
+
* Read order:
|
|
87
|
+
* 1. user-scoped app_secrets
|
|
88
|
+
* 2. user-scoped legacy settings credential
|
|
89
|
+
* 3. org-scoped app_secrets
|
|
90
|
+
* 4. legacy workspace-scoped app_secrets for the org
|
|
91
|
+
* 5. org-scoped legacy settings credential
|
|
70
92
|
*
|
|
71
|
-
*
|
|
72
|
-
* 1. u:<email>:credential:<KEY> — per-user override
|
|
73
|
-
* 2. o:<orgId>:credential:<KEY> — per-org shared credential (if orgId given)
|
|
93
|
+
* Without an active org, step 3 is replaced by the solo workspace scope.
|
|
74
94
|
*/
|
|
75
95
|
export async function resolveCredential(
|
|
76
96
|
key: string,
|
|
@@ -78,6 +98,9 @@ export async function resolveCredential(
|
|
|
78
98
|
): Promise<string | undefined> {
|
|
79
99
|
if (!ctx?.userEmail) return undefined;
|
|
80
100
|
|
|
101
|
+
const userSecret = await readScopedAppSecret(key, "user", ctx.userEmail);
|
|
102
|
+
if (userSecret) return userSecret;
|
|
103
|
+
|
|
81
104
|
const userSetting = await resolveCredentialForScope(key, {
|
|
82
105
|
...ctx,
|
|
83
106
|
scope: "user",
|
|
@@ -85,10 +108,20 @@ export async function resolveCredential(
|
|
|
85
108
|
if (userSetting) return userSetting;
|
|
86
109
|
|
|
87
110
|
if (ctx.orgId) {
|
|
111
|
+
const orgSecret = await readScopedAppSecret(key, "org", ctx.orgId);
|
|
112
|
+
if (orgSecret) return orgSecret;
|
|
113
|
+
|
|
114
|
+
const workspaceSecret = await readScopedAppSecret(
|
|
115
|
+
key,
|
|
116
|
+
"workspace",
|
|
117
|
+
ctx.orgId,
|
|
118
|
+
);
|
|
119
|
+
if (workspaceSecret) return workspaceSecret;
|
|
120
|
+
|
|
88
121
|
return resolveCredentialForScope(key, { ...ctx, scope: "org" });
|
|
89
122
|
}
|
|
90
123
|
|
|
91
|
-
return
|
|
124
|
+
return readScopedAppSecret(key, "workspace", `solo:${ctx.userEmail}`);
|
|
92
125
|
}
|
|
93
126
|
|
|
94
127
|
/**
|
|
@@ -2535,17 +2535,18 @@ function walkServerJavaScriptFiles(
|
|
|
2535
2535
|
}
|
|
2536
2536
|
|
|
2537
2537
|
/**
|
|
2538
|
-
* Nitro
|
|
2539
|
-
*
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2538
|
+
* Nitro receives the React Router SSR build as prebuilt chunks, so its normal
|
|
2539
|
+
* dependency resolver cannot reliably fold the preserved bare `yjs` imports
|
|
2540
|
+
* into the same module instance used by core's server collaboration code.
|
|
2541
|
+
* Keep Yjs external through Nitro, bundle its complete public ESM surface once,
|
|
2542
|
+
* then point every emitted server chunk at that one portable runtime module.
|
|
2542
2543
|
*/
|
|
2543
|
-
export function
|
|
2544
|
+
export function bundleYjsRuntimeForServerlessOutput(
|
|
2544
2545
|
serverDir: string,
|
|
2546
|
+
projectCwd: string,
|
|
2545
2547
|
): string[] {
|
|
2546
2548
|
const bareImports: string[] = [];
|
|
2547
2549
|
const unsupportedSubpathImports: string[] = [];
|
|
2548
|
-
const bundledYjsPath = path.join(serverDir, "_libs", "yjs.mjs");
|
|
2549
2550
|
|
|
2550
2551
|
walkServerJavaScriptFiles(serverDir, (filePath) => {
|
|
2551
2552
|
const source = fs.readFileSync(filePath, "utf-8");
|
|
@@ -2563,11 +2564,22 @@ export function rewriteBareYjsImportsForServerlessOutput(
|
|
|
2563
2564
|
);
|
|
2564
2565
|
}
|
|
2565
2566
|
if (bareImports.length === 0) return [];
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2567
|
+
|
|
2568
|
+
const bundledYjsPath = path.join(serverDir, "_libs", "yjs-runtime.mjs");
|
|
2569
|
+
fs.mkdirSync(path.dirname(bundledYjsPath), { recursive: true });
|
|
2570
|
+
execFileSync(
|
|
2571
|
+
findEsbuild(),
|
|
2572
|
+
[
|
|
2573
|
+
resolveNitroBundledYjsEntry(),
|
|
2574
|
+
"--bundle",
|
|
2575
|
+
"--format=esm",
|
|
2576
|
+
"--platform=node",
|
|
2577
|
+
"--target=node22",
|
|
2578
|
+
"--minify",
|
|
2579
|
+
`--outfile=${bundledYjsPath}`,
|
|
2580
|
+
],
|
|
2581
|
+
{ cwd: projectCwd, stdio: "pipe" },
|
|
2582
|
+
);
|
|
2571
2583
|
|
|
2572
2584
|
for (const filePath of bareImports) {
|
|
2573
2585
|
const bundledImport = path
|
|
@@ -2702,6 +2714,26 @@ export function assertSingleTemplateNetlifyBuildOutput(
|
|
|
2702
2714
|
);
|
|
2703
2715
|
}
|
|
2704
2716
|
|
|
2717
|
+
// Nitro's `_libs/yjs.mjs` is a private tree-shaken chunk, not a package
|
|
2718
|
+
// facade. Repointing a prebuilt SSR chunk at it can request public exports
|
|
2719
|
+
// (notably `Text`) that the private chunk did not retain. The controlled
|
|
2720
|
+
// serverless pass must instead target the complete `yjs-runtime.mjs` bundle.
|
|
2721
|
+
const privateYjsImports: string[] = [];
|
|
2722
|
+
walkServerJavaScriptFiles(serverDir, (filePath) => {
|
|
2723
|
+
if (
|
|
2724
|
+
/\b(?:from\s*|import\s*\(\s*|import\s*)(["'])[^"']*_libs\/yjs\.mjs\1/.test(
|
|
2725
|
+
fs.readFileSync(filePath, "utf-8"),
|
|
2726
|
+
)
|
|
2727
|
+
) {
|
|
2728
|
+
privateYjsImports.push(path.relative(projectCwd, filePath));
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
if (privateYjsImports.length > 0) {
|
|
2732
|
+
failures.push(
|
|
2733
|
+
`Netlify server bundle imports Nitro's internal tree-shaken _libs/yjs.mjs: ${privateYjsImports.join(", ")}`,
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2705
2737
|
if (isDurableBackgroundDeployEnabled()) {
|
|
2706
2738
|
const backgroundDir = path.join(
|
|
2707
2739
|
internalDir,
|
|
@@ -3153,18 +3185,27 @@ const BROWSER_ONLY_SERVER_LIBS = [
|
|
|
3153
3185
|
];
|
|
3154
3186
|
|
|
3155
3187
|
/**
|
|
3156
|
-
* Dependencies
|
|
3157
|
-
*
|
|
3158
|
-
*
|
|
3159
|
-
* `yjs` is a direct core dependency, but it is deliberately externalized from
|
|
3160
|
-
* the intermediate Vite SSR graph so that Vite and Nitro do not create two
|
|
3161
|
-
* incompatible Yjs constructors. On file-traced serverless presets, leaving it
|
|
3162
|
-
* external at Nitro's final build can emit `import "yjs"` into a function
|
|
3163
|
-
* chunk without placing the package in that function's `node_modules`. Bundle
|
|
3164
|
-
* it in Nitro's final output so every template receives the one portable copy.
|
|
3188
|
+
* Dependencies Nitro itself must bundle outside the controlled serverless
|
|
3189
|
+
* output pass. Netlify, Vercel, and Lambda keep Yjs external through Nitro;
|
|
3190
|
+
* `bundleYjsRuntimeForServerlessOutput` then creates their one portable copy.
|
|
3165
3191
|
*/
|
|
3166
3192
|
export const NITRO_SERVER_RUNTIME_BUNDLED_DEPS = ["yjs"] as const;
|
|
3167
3193
|
|
|
3194
|
+
/**
|
|
3195
|
+
* Locate the core-owned ESM entry used by the controlled serverless bundling
|
|
3196
|
+
* pass. Resolving from this module keeps the build independent of whether a
|
|
3197
|
+
* template exposes core's transitive Yjs dependency at its own package root.
|
|
3198
|
+
*/
|
|
3199
|
+
export function resolveNitroBundledYjsEntry(): string {
|
|
3200
|
+
const requireFromCore = createRequire(import.meta.url);
|
|
3201
|
+
const packageDir = path.dirname(requireFromCore.resolve("yjs/package.json"));
|
|
3202
|
+
const entry = path.join(packageDir, "dist", "yjs.mjs");
|
|
3203
|
+
if (!fs.existsSync(entry)) {
|
|
3204
|
+
throw new Error(`[build] Could not resolve the Yjs ESM entry at ${entry}`);
|
|
3205
|
+
}
|
|
3206
|
+
return entry;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3168
3209
|
/**
|
|
3169
3210
|
* Edge runtimes have no node_modules, while Node/serverless outputs only need
|
|
3170
3211
|
* the small set above bundled to keep their package manifests traceable.
|
|
@@ -3175,7 +3216,11 @@ export function nitroNoExternalsForPreset(
|
|
|
3175
3216
|
return targetPreset.startsWith("cloudflare") ||
|
|
3176
3217
|
targetPreset.startsWith("deno")
|
|
3177
3218
|
? true
|
|
3178
|
-
:
|
|
3219
|
+
: targetPreset === "netlify" ||
|
|
3220
|
+
targetPreset === "vercel" ||
|
|
3221
|
+
targetPreset === "aws-lambda"
|
|
3222
|
+
? []
|
|
3223
|
+
: NITRO_SERVER_RUNTIME_BUNDLED_DEPS;
|
|
3179
3224
|
}
|
|
3180
3225
|
|
|
3181
3226
|
/**
|
|
@@ -3327,6 +3372,14 @@ export default bundle;
|
|
|
3327
3372
|
// (ReferenceError: window is not defined → every request 502s). Mirrors the
|
|
3328
3373
|
// Vite `ssrStubPlugin`, which only covers the `build/server` step.
|
|
3329
3374
|
rollupConfig: {
|
|
3375
|
+
// Nitro treats the intermediate React Router SSR files as prebuilt
|
|
3376
|
+
// chunks, while core's server collaboration files participate in the
|
|
3377
|
+
// final Rolldown graph. Externalize Yjs consistently on serverless so
|
|
3378
|
+
// both graphs retain their public import shapes; the controlled
|
|
3379
|
+
// post-build pass below bundles and rewrites them to one module.
|
|
3380
|
+
...(preset === "netlify" || preset === "vercel" || preset === "aws-lambda"
|
|
3381
|
+
? { external: ["yjs"] }
|
|
3382
|
+
: {}),
|
|
3330
3383
|
plugins: [createBrowserOnlyServerStubPlugin()],
|
|
3331
3384
|
},
|
|
3332
3385
|
...(providedPluginsNitroPlugin
|
|
@@ -3334,9 +3387,9 @@ export default bundle;
|
|
|
3334
3387
|
: {}),
|
|
3335
3388
|
routeRules: mcpEmbedStaticAssetRouteRules(appBasePath),
|
|
3336
3389
|
// Edge presets (cloudflare, deno) bundle all deps because node_modules are
|
|
3337
|
-
// unavailable at runtime. Node
|
|
3338
|
-
//
|
|
3339
|
-
//
|
|
3390
|
+
// unavailable at runtime. Ordinary Node presets bundle Yjs through Nitro.
|
|
3391
|
+
// Controlled serverless presets externalize it above, then emit one full
|
|
3392
|
+
// runtime module after Nitro has preserved every consumer's public imports.
|
|
3340
3393
|
noExternals: nitroNoExternalsForPreset(preset),
|
|
3341
3394
|
} as any);
|
|
3342
3395
|
|
|
@@ -3354,7 +3407,7 @@ export default bundle;
|
|
|
3354
3407
|
copyInstalledResvgPackages(nitro.options.output.serverDir);
|
|
3355
3408
|
copyInstalledFfmpegStaticPackage(nitro.options.output.serverDir);
|
|
3356
3409
|
sanitizeServerlessFunctionPackageManifest(nitro.options.output.serverDir);
|
|
3357
|
-
|
|
3410
|
+
bundleYjsRuntimeForServerlessOutput(nitro.options.output.serverDir, cwd);
|
|
3358
3411
|
}
|
|
3359
3412
|
|
|
3360
3413
|
// Durable background agent runs (default-OFF / opt-in; enable with a truthy
|