@promptbook/cli 0.112.0-110 → 0.112.0-111
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/apps/agents-server/src/app/agents/[agentName]/_utils.ts +7 -5
- package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts +4 -86
- package/apps/agents-server/src/components/ApplicationErrorPage/ApplicationErrorPage.tsx +118 -12
- package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -62
- package/apps/agents-server/src/utils/errorReporting/applicationErrorHandling.ts +45 -0
- package/apps/agents-server/src/utils/errorReporting/refreshApplicationDocument.ts +10 -0
- package/apps/agents-server/src/utils/importAgent.ts +1 -57
- package/apps/agents-server/src/utils/importAgentWithFallback.ts +0 -10
- package/apps/agents-server/src/utils/userChat.ts +0 -1
- package/esm/index.es.js +18 -11
- package/esm/index.es.js.map +1 -1
- package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/cli-commands/agents-server/startAgentsServer.ts +6 -1
- package/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.ts +91 -14
- package/src/other/templates/getTemplatesPipelineCollection.ts +920 -617
- package/src/version.ts +2 -2
- package/src/versions.txt +1 -0
- package/umd/index.umd.js +18 -11
- package/umd/index.umd.js.map +1 -1
- package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
- package/umd/src/version.d.ts +1 -1
- package/apps/agents-server/src/utils/userChat/getUserChatRevision.ts +0 -145
|
@@ -5,7 +5,7 @@ import { $provideAgentReferenceResolver } from '@/src/utils/agentReferenceResolv
|
|
|
5
5
|
import {
|
|
6
6
|
parseBookScopedAgentIdentifier,
|
|
7
7
|
} from '@/src/utils/agentReferenceResolver/bookScopedAgentReferences';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveCachedServerAgentContext } from '@/src/utils/cachedServerAgentRuntime';
|
|
9
9
|
import { cache } from 'react';
|
|
10
10
|
import type { AgentsServerDatabase } from '../../../database/schema';
|
|
11
11
|
import { $provideSupabaseForServer } from '../../../database/$provideSupabaseForServer';
|
|
@@ -91,10 +91,12 @@ export async function enforceCanonicalLocalAgentId(
|
|
|
91
91
|
* @returns Resolved agent profile with visibility.
|
|
92
92
|
*/
|
|
93
93
|
const getCachedAgentProfile = cache(async (agentName: string) => {
|
|
94
|
-
const collection = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
const [collection, { publicUrl }, baseAgentReferenceResolver] = await Promise.all([
|
|
95
|
+
$provideAgentCollectionForServer(),
|
|
96
|
+
$provideServer(),
|
|
97
|
+
$provideAgentReferenceResolver(),
|
|
98
|
+
]);
|
|
99
|
+
const resolvedAgentContext = await resolveCachedServerAgentContext({
|
|
98
100
|
collection,
|
|
99
101
|
agentIdentifier: agentName,
|
|
100
102
|
localServerUrl: publicUrl.href,
|
package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { CHAT_STREAM_KEEP_ALIVE_INTERVAL_MS } from '@/src/constants/streaming';
|
|
2
2
|
import { isPrivateModeEnabledFromRequest } from '@/src/utils/privateMode';
|
|
3
|
-
import {
|
|
4
|
-
createUserChatDetailPayload,
|
|
5
|
-
getUserChat,
|
|
6
|
-
getUserChatRevision,
|
|
7
|
-
isFrozenUserChatSource,
|
|
8
|
-
} from '@/src/utils/userChat';
|
|
3
|
+
import { createUserChatDetailPayload, getUserChat, isFrozenUserChatSource } from '@/src/utils/userChat';
|
|
9
4
|
import type { ChatMessage } from '@promptbook-local/types';
|
|
10
5
|
import { NextResponse } from 'next/server';
|
|
11
6
|
import { resolveUserChatScope } from '../../resolveUserChatScope';
|
|
@@ -37,11 +32,6 @@ type UserChatStreamFrame =
|
|
|
37
32
|
type: 'keepalive';
|
|
38
33
|
};
|
|
39
34
|
|
|
40
|
-
/**
|
|
41
|
-
* Lightweight revision shape returned by `getUserChatRevision`.
|
|
42
|
-
*/
|
|
43
|
-
type UserChatRevision = Awaited<ReturnType<typeof getUserChatRevision>>;
|
|
44
|
-
|
|
45
35
|
/**
|
|
46
36
|
* Streams canonical chat snapshots for one scoped user chat so multiple viewers can observe the same background turn.
|
|
47
37
|
*/
|
|
@@ -83,7 +73,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
83
73
|
async start(controller) {
|
|
84
74
|
let isStreamClosed = false;
|
|
85
75
|
let lastSnapshotSignature: string | null = null;
|
|
86
|
-
let lastRevisionSignature: string | null = null;
|
|
87
76
|
let keepAliveInterval: ReturnType<typeof setInterval> | null = null;
|
|
88
77
|
|
|
89
78
|
/**
|
|
@@ -149,24 +138,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
149
138
|
|
|
150
139
|
const payload = await createUserChatDetailPayload(currentChat);
|
|
151
140
|
const nextSignature = createUserChatDetailSignature(payload);
|
|
152
|
-
lastRevisionSignature = createUserChatRevisionSignature({
|
|
153
|
-
id: payload.chat.id,
|
|
154
|
-
updatedAt: payload.chat.updatedAt,
|
|
155
|
-
draftMessage: payload.draftMessage,
|
|
156
|
-
source: payload.chat.source,
|
|
157
|
-
activeJobs: payload.activeJobs.map((job) => ({
|
|
158
|
-
id: job.id,
|
|
159
|
-
status: job.status,
|
|
160
|
-
cancelRequestedAt: job.cancelRequestedAt,
|
|
161
|
-
})),
|
|
162
|
-
activeTimeouts: payload.activeTimeouts.map((timeout) => ({
|
|
163
|
-
id: timeout.id,
|
|
164
|
-
status: timeout.status,
|
|
165
|
-
message: timeout.message,
|
|
166
|
-
dueAt: timeout.dueAt,
|
|
167
|
-
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
168
|
-
})),
|
|
169
|
-
});
|
|
170
141
|
|
|
171
142
|
if (nextSignature !== lastSnapshotSignature) {
|
|
172
143
|
lastSnapshotSignature = nextSignature;
|
|
@@ -178,32 +149,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
178
149
|
return !isFrozenUserChatSource(payload.chat.source) && payload.activeJobs.length > 0;
|
|
179
150
|
};
|
|
180
151
|
|
|
181
|
-
/**
|
|
182
|
-
* Emits a full snapshot only when an idle chat revision changed.
|
|
183
|
-
*
|
|
184
|
-
* @private route helper
|
|
185
|
-
*/
|
|
186
|
-
const emitLatestSnapshotWhenRevisionChanged = async (): Promise<boolean> => {
|
|
187
|
-
const currentRevision = await getUserChatRevision({
|
|
188
|
-
userId: scopeResult.scope.userId,
|
|
189
|
-
viewerIsAdmin: scopeResult.scope.viewerIsAdmin,
|
|
190
|
-
agentPermanentId: scopeResult.scope.agentPermanentId,
|
|
191
|
-
chatId,
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
if (!currentRevision) {
|
|
195
|
-
closeStream();
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const nextRevisionSignature = createUserChatRevisionSignature(currentRevision);
|
|
200
|
-
if (nextRevisionSignature === lastRevisionSignature) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return emitLatestSnapshot();
|
|
205
|
-
};
|
|
206
|
-
|
|
207
152
|
/**
|
|
208
153
|
* Tracks client disconnects so the polling loop can exit promptly.
|
|
209
154
|
*
|
|
@@ -220,11 +165,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
220
165
|
keepAliveInterval.unref?.();
|
|
221
166
|
|
|
222
167
|
try {
|
|
223
|
-
let
|
|
168
|
+
let hasActiveJobs = await emitLatestSnapshot();
|
|
224
169
|
|
|
225
170
|
while (!isStreamClosed && !request.signal.aborted) {
|
|
226
171
|
await waitForNextUserChatStreamPoll(
|
|
227
|
-
|
|
172
|
+
hasActiveJobs
|
|
228
173
|
? ACTIVE_USER_CHAT_STREAM_POLL_INTERVAL_MS
|
|
229
174
|
: IDLE_USER_CHAT_STREAM_POLL_INTERVAL_MS,
|
|
230
175
|
request.signal,
|
|
@@ -234,9 +179,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
234
179
|
break;
|
|
235
180
|
}
|
|
236
181
|
|
|
237
|
-
|
|
238
|
-
? await emitLatestSnapshot()
|
|
239
|
-
: await emitLatestSnapshotWhenRevisionChanged();
|
|
182
|
+
hasActiveJobs = await emitLatestSnapshot();
|
|
240
183
|
}
|
|
241
184
|
} catch (error) {
|
|
242
185
|
if (!isStreamClosed && !request.signal.aborted) {
|
|
@@ -267,30 +210,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ agen
|
|
|
267
210
|
});
|
|
268
211
|
}
|
|
269
212
|
|
|
270
|
-
/**
|
|
271
|
-
* Builds a compact signature for idle stream revision checks.
|
|
272
|
-
*/
|
|
273
|
-
function createUserChatRevisionSignature(revision: NonNullable<UserChatRevision>): string {
|
|
274
|
-
return JSON.stringify({
|
|
275
|
-
id: revision.id,
|
|
276
|
-
updatedAt: revision.updatedAt,
|
|
277
|
-
draftMessage: revision.draftMessage || '',
|
|
278
|
-
source: revision.source,
|
|
279
|
-
activeJobs: revision.activeJobs.map((job) => ({
|
|
280
|
-
id: job.id,
|
|
281
|
-
status: job.status,
|
|
282
|
-
cancelRequestedAt: job.cancelRequestedAt,
|
|
283
|
-
})),
|
|
284
|
-
activeTimeouts: revision.activeTimeouts.map((timeout) => ({
|
|
285
|
-
id: timeout.id,
|
|
286
|
-
status: timeout.status,
|
|
287
|
-
message: timeout.message || '',
|
|
288
|
-
dueAt: timeout.dueAt,
|
|
289
|
-
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
290
|
-
})),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
213
|
/**
|
|
295
214
|
* Builds a stable signature for the user-visible parts of a canonical chat snapshot.
|
|
296
215
|
*/
|
|
@@ -308,7 +227,6 @@ function createUserChatDetailSignature(payload: Awaited<ReturnType<typeof create
|
|
|
308
227
|
activeTimeouts: payload.activeTimeouts.map((timeout) => ({
|
|
309
228
|
id: timeout.id,
|
|
310
229
|
status: timeout.status,
|
|
311
|
-
message: timeout.message || '',
|
|
312
230
|
dueAt: timeout.dueAt,
|
|
313
231
|
cancelRequestedAt: timeout.cancelRequestedAt,
|
|
314
232
|
})),
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
createApplicationErrorHeadline,
|
|
15
15
|
createApplicationErrorReportPayload,
|
|
16
16
|
describeApplicationError,
|
|
17
|
+
isApplicationErrorRecoverableByHardRefresh,
|
|
17
18
|
resolveApplicationErrorVariant,
|
|
18
19
|
} from '../../utils/errorReporting/applicationErrorHandling';
|
|
20
|
+
import { refreshApplicationDocument } from '../../utils/errorReporting/refreshApplicationDocument';
|
|
19
21
|
import { ErrorPageButtonAction, ErrorPageLinkAction } from '../ErrorPage/ErrorPage';
|
|
20
22
|
import { InternalServerErrorPage } from '../InternalServerErrorPage/InternalServerErrorPage';
|
|
21
23
|
|
|
@@ -27,7 +29,7 @@ import { InternalServerErrorPage } from '../InternalServerErrorPage/InternalServ
|
|
|
27
29
|
const troubleshootingSteps = [
|
|
28
30
|
{
|
|
29
31
|
title: 'Refresh the route',
|
|
30
|
-
detail: '
|
|
32
|
+
detail: 'Use the primary action so the page can retry with fresh cookies, network state, and build assets.',
|
|
31
33
|
},
|
|
32
34
|
{
|
|
33
35
|
title: 'Share the digest',
|
|
@@ -46,6 +48,20 @@ const troubleshootingSteps = [
|
|
|
46
48
|
*/
|
|
47
49
|
const REPORT_ACTION_FEEDBACK_DURATION_MS = 2500;
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Delay before one recoverable stale-asset error triggers an automatic full-document refresh.
|
|
53
|
+
*
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
const APPLICATION_ERROR_AUTO_REFRESH_DELAY_MS = 1500;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Prefix used for session-scoped stale-asset refresh guards.
|
|
60
|
+
*
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
const APPLICATION_ERROR_AUTO_REFRESH_STORAGE_KEY_PREFIX = 'promptbook.application-error-hard-refresh';
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Writes plain text to the user clipboard.
|
|
51
67
|
*
|
|
@@ -82,17 +98,25 @@ function downloadMarkdownReport(reportMarkdown: string, filename: string): void
|
|
|
82
98
|
URL.revokeObjectURL(reportUrl);
|
|
83
99
|
}
|
|
84
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Builds the session-storage key used to ensure one stale-asset error only auto-refreshes once per URL and digest.
|
|
103
|
+
*
|
|
104
|
+
* @param pageUrl - Browser URL where the error happened.
|
|
105
|
+
* @param digest - Deterministic digest rendered for the error.
|
|
106
|
+
* @returns Stable session-storage key for the current recoverable error.
|
|
107
|
+
*
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
function createApplicationErrorAutoRefreshStorageKey(pageUrl: string, digest: string): string {
|
|
111
|
+
return `${APPLICATION_ERROR_AUTO_REFRESH_STORAGE_KEY_PREFIX}:${pageUrl}:${digest}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
/**
|
|
86
115
|
* Props accepted by shared action controls used across error variants.
|
|
87
116
|
*
|
|
88
117
|
* @private
|
|
89
118
|
*/
|
|
90
119
|
type ApplicationErrorActionsProps = {
|
|
91
|
-
/**
|
|
92
|
-
* Callback that retries the failed route transition.
|
|
93
|
-
*/
|
|
94
|
-
reset: () => void;
|
|
95
|
-
|
|
96
120
|
/**
|
|
97
121
|
* Digest value displayed for operator correlation.
|
|
98
122
|
*/
|
|
@@ -107,6 +131,16 @@ type ApplicationErrorActionsProps = {
|
|
|
107
131
|
* Default filename used by the markdown save action.
|
|
108
132
|
*/
|
|
109
133
|
reportFilename: string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Primary action label rendered on the left-most action button.
|
|
137
|
+
*/
|
|
138
|
+
primaryActionLabel: string;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Callback used by the left-most primary action button.
|
|
142
|
+
*/
|
|
143
|
+
onPrimaryAction: () => void;
|
|
110
144
|
};
|
|
111
145
|
|
|
112
146
|
/**
|
|
@@ -117,10 +151,11 @@ type ApplicationErrorActionsProps = {
|
|
|
117
151
|
* @private
|
|
118
152
|
*/
|
|
119
153
|
function ApplicationErrorActions({
|
|
120
|
-
reset,
|
|
121
154
|
digest,
|
|
122
155
|
reportMarkdown,
|
|
123
156
|
reportFilename,
|
|
157
|
+
primaryActionLabel,
|
|
158
|
+
onPrimaryAction,
|
|
124
159
|
}: ApplicationErrorActionsProps) {
|
|
125
160
|
const [reportFeedback, setReportFeedback] = useState<string | null>(null);
|
|
126
161
|
const reportFeedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -179,9 +214,7 @@ function ApplicationErrorActions({
|
|
|
179
214
|
return (
|
|
180
215
|
<div className="flex flex-col items-center gap-3">
|
|
181
216
|
<div className="flex flex-wrap justify-center gap-3">
|
|
182
|
-
<ErrorPageButtonAction onClick={
|
|
183
|
-
Try again
|
|
184
|
-
</ErrorPageButtonAction>
|
|
217
|
+
<ErrorPageButtonAction onClick={onPrimaryAction}>{primaryActionLabel}</ErrorPageButtonAction>
|
|
185
218
|
<ErrorPageLinkAction href="/" tone="secondary">
|
|
186
219
|
Go to homepage
|
|
187
220
|
</ErrorPageLinkAction>
|
|
@@ -245,10 +278,25 @@ type ApplicationErrorViewProps = {
|
|
|
245
278
|
*/
|
|
246
279
|
size?: 'default' | 'wide';
|
|
247
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Primary action label rendered on the left-most action button.
|
|
283
|
+
*/
|
|
284
|
+
primaryActionLabel: string;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Callback used by the left-most primary action button.
|
|
288
|
+
*/
|
|
289
|
+
onPrimaryAction: () => void;
|
|
290
|
+
|
|
248
291
|
/**
|
|
249
292
|
* Optional supplementary content rendered beneath shared actions.
|
|
250
293
|
*/
|
|
251
294
|
supplementaryContent?: ReactNode;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Optional recovery note rendered between the actions and supplementary content.
|
|
298
|
+
*/
|
|
299
|
+
recoveryNotice?: ReactNode;
|
|
252
300
|
};
|
|
253
301
|
|
|
254
302
|
/**
|
|
@@ -264,18 +312,22 @@ function ApplicationErrorView({
|
|
|
264
312
|
digest,
|
|
265
313
|
reportMarkdown,
|
|
266
314
|
reportFilename,
|
|
267
|
-
reset,
|
|
268
315
|
size = 'default',
|
|
316
|
+
primaryActionLabel,
|
|
317
|
+
onPrimaryAction,
|
|
318
|
+
recoveryNotice,
|
|
269
319
|
supplementaryContent,
|
|
270
320
|
}: ApplicationErrorViewProps) {
|
|
271
321
|
return (
|
|
272
322
|
<InternalServerErrorPage headline={headline} description={description} size={size}>
|
|
273
323
|
<ApplicationErrorActions
|
|
274
|
-
reset={reset}
|
|
275
324
|
digest={digest}
|
|
276
325
|
reportMarkdown={reportMarkdown}
|
|
277
326
|
reportFilename={reportFilename}
|
|
327
|
+
primaryActionLabel={primaryActionLabel}
|
|
328
|
+
onPrimaryAction={onPrimaryAction}
|
|
278
329
|
/>
|
|
330
|
+
{recoveryNotice}
|
|
279
331
|
{supplementaryContent}
|
|
280
332
|
</InternalServerErrorPage>
|
|
281
333
|
);
|
|
@@ -374,9 +426,11 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
374
426
|
);
|
|
375
427
|
const digest = createApplicationErrorDigest(error);
|
|
376
428
|
const serverName = process.env.NEXT_PUBLIC_SERVER_NAME ?? DEFAULT_APPLICATION_ERROR_SERVER_NAME;
|
|
429
|
+
const isRecoverableByHardRefresh = isApplicationErrorRecoverableByHardRefresh(error);
|
|
377
430
|
const headline = createApplicationErrorHeadline(serverName);
|
|
378
431
|
const description = describeApplicationError(error, serverName);
|
|
379
432
|
const [pageUrl, setPageUrl] = useState<string | undefined>(undefined);
|
|
433
|
+
const [hasAutomaticRefreshAlreadyBeenAttempted, setHasAutomaticRefreshAlreadyBeenAttempted] = useState(false);
|
|
380
434
|
const reportPayload = useMemo(
|
|
381
435
|
() => createApplicationErrorReportPayload(error, digest, serverName, variant, pageUrl),
|
|
382
436
|
[digest, error, pageUrl, serverName, variant],
|
|
@@ -392,6 +446,35 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
392
446
|
setPageUrl(window.location.href);
|
|
393
447
|
}, []);
|
|
394
448
|
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
if (!pageUrl || !isRecoverableByHardRefresh) {
|
|
451
|
+
setHasAutomaticRefreshAlreadyBeenAttempted(false);
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const storageKey = createApplicationErrorAutoRefreshStorageKey(pageUrl, digest);
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const hasStoredRefreshAttempt = window.sessionStorage.getItem(storageKey) === 'done';
|
|
459
|
+
if (hasStoredRefreshAttempt) {
|
|
460
|
+
setHasAutomaticRefreshAlreadyBeenAttempted(true);
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
window.sessionStorage.setItem(storageKey, 'done');
|
|
465
|
+
} catch {
|
|
466
|
+
// Some browser privacy modes may block session storage; the one-shot reload remains best-effort.
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const timeout = window.setTimeout(() => {
|
|
470
|
+
refreshApplicationDocument();
|
|
471
|
+
}, APPLICATION_ERROR_AUTO_REFRESH_DELAY_MS);
|
|
472
|
+
|
|
473
|
+
return () => {
|
|
474
|
+
window.clearTimeout(timeout);
|
|
475
|
+
};
|
|
476
|
+
}, [digest, isRecoverableByHardRefresh, pageUrl]);
|
|
477
|
+
|
|
395
478
|
useEffect(() => {
|
|
396
479
|
if (!pageUrl) {
|
|
397
480
|
return;
|
|
@@ -408,6 +491,23 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
408
491
|
});
|
|
409
492
|
}, [error, pageUrl, reportPayload]);
|
|
410
493
|
|
|
494
|
+
const primaryActionLabel = isRecoverableByHardRefresh ? 'Refresh now' : 'Try again';
|
|
495
|
+
const handlePrimaryAction = () => {
|
|
496
|
+
if (isRecoverableByHardRefresh) {
|
|
497
|
+
refreshApplicationDocument();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
reset();
|
|
502
|
+
};
|
|
503
|
+
const recoveryNotice = isRecoverableByHardRefresh ? (
|
|
504
|
+
<p className="mt-6 text-center text-sm text-gray-600">
|
|
505
|
+
{hasAutomaticRefreshAlreadyBeenAttempted
|
|
506
|
+
? 'Automatic refresh already ran once. If the newest assets still do not load, use "Refresh now" or share the digest with your administrator.'
|
|
507
|
+
: 'Refreshing this page automatically once so the latest application files can be loaded.'}
|
|
508
|
+
</p>
|
|
509
|
+
) : undefined;
|
|
510
|
+
|
|
411
511
|
if (variant === 'simple') {
|
|
412
512
|
return (
|
|
413
513
|
<SimpleApplicationErrorView
|
|
@@ -417,6 +517,9 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
417
517
|
reportMarkdown={reportMarkdown}
|
|
418
518
|
reportFilename={reportFilename}
|
|
419
519
|
reset={reset}
|
|
520
|
+
recoveryNotice={recoveryNotice}
|
|
521
|
+
primaryActionLabel={primaryActionLabel}
|
|
522
|
+
onPrimaryAction={handlePrimaryAction}
|
|
420
523
|
/>
|
|
421
524
|
);
|
|
422
525
|
}
|
|
@@ -429,6 +532,9 @@ export function ApplicationErrorPage({ error, reset }: ApplicationErrorPageProps
|
|
|
429
532
|
reportMarkdown={reportMarkdown}
|
|
430
533
|
reportFilename={reportFilename}
|
|
431
534
|
reset={reset}
|
|
535
|
+
recoveryNotice={recoveryNotice}
|
|
536
|
+
primaryActionLabel={primaryActionLabel}
|
|
537
|
+
onPrimaryAction={handlePrimaryAction}
|
|
432
538
|
/>
|
|
433
539
|
);
|
|
434
540
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { $provideServer } from '../../tools/$provideServer';
|
|
2
2
|
import { $provideAgentCollectionForServer } from '../../tools/$provideAgentCollectionForServer';
|
|
3
|
-
import { $getTableName } from '../../database/$getTableName';
|
|
4
|
-
import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
|
|
5
3
|
import { $provideAgentReferenceResolver } from '../agentReferenceResolver/$provideAgentReferenceResolver';
|
|
6
4
|
import { consumeAgentReferenceResolutionIssues } from '../agentReferenceResolver/AgentReferenceResolutionIssue';
|
|
7
5
|
import { parseBookScopedAgentIdentifier } from '../agentReferenceResolver/bookScopedAgentReferences';
|
|
8
|
-
import { buildAgentNameOrIdFilter } from '../agentIdentifier';
|
|
9
6
|
import { resolvePseudoAgentDescriptor } from '../pseudoAgents';
|
|
10
7
|
import { normalizeAgentName } from '../../../../../src/_packages/core.index';
|
|
11
8
|
import type { PseudoAgentKind } from '../../../../../src/book-2.0/agent-source/pseudoAgentReferences';
|
|
12
|
-
import type { AgentsServerDatabase } from '../../database/schema';
|
|
13
9
|
import { cache } from 'react';
|
|
14
10
|
|
|
15
11
|
/**
|
|
@@ -54,14 +50,6 @@ type PseudoAgentRouteTarget = {
|
|
|
54
50
|
canonicalUrl: string;
|
|
55
51
|
};
|
|
56
52
|
|
|
57
|
-
/**
|
|
58
|
-
* Minimal database row needed to resolve one local route target.
|
|
59
|
-
*/
|
|
60
|
-
type LocalAgentRouteRow = Pick<
|
|
61
|
-
AgentsServerDatabase['public']['Tables']['Agent']['Row'],
|
|
62
|
-
'agentName' | 'permanentId'
|
|
63
|
-
>;
|
|
64
|
-
|
|
65
53
|
/**
|
|
66
54
|
* Target returned for an incoming `/agents/:agentId` route value.
|
|
67
55
|
*/
|
|
@@ -114,11 +102,6 @@ async function resolveAgentRouteTargetUncached(
|
|
|
114
102
|
};
|
|
115
103
|
}
|
|
116
104
|
|
|
117
|
-
const directLocalRouteTarget = await resolveLocalAgentRouteTargetFromDatabase(normalizedReference, localServerUrl);
|
|
118
|
-
if (directLocalRouteTarget) {
|
|
119
|
-
return directLocalRouteTarget;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
105
|
const resolver = await $provideAgentReferenceResolver({ forceRefresh: options?.forceRefresh });
|
|
123
106
|
let resolvedUrlValue: string;
|
|
124
107
|
|
|
@@ -288,21 +271,7 @@ async function resolveLocalAgentRouteTarget(
|
|
|
288
271
|
localServerUrl: string,
|
|
289
272
|
): Promise<AgentRouteTarget | null> {
|
|
290
273
|
const collection = await $provideAgentCollectionForServer();
|
|
291
|
-
const
|
|
292
|
-
const normalizedReference = normalizeAgentName(reference);
|
|
293
|
-
|
|
294
|
-
const agentMatch = agents.find((agent: { agentName: string; permanentId?: string }) => {
|
|
295
|
-
if (agent.agentName === reference || agent.permanentId === reference) {
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const normalizedAgentName = normalizeAgentName(agent.agentName);
|
|
300
|
-
if (normalizedAgentName === normalizedReference) {
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return false;
|
|
305
|
-
});
|
|
274
|
+
const agentMatch = await findLocalAgentRouteMatch(collection, reference);
|
|
306
275
|
|
|
307
276
|
if (!agentMatch) {
|
|
308
277
|
return null;
|
|
@@ -318,40 +287,36 @@ async function resolveLocalAgentRouteTarget(
|
|
|
318
287
|
}
|
|
319
288
|
|
|
320
289
|
/**
|
|
321
|
-
*
|
|
290
|
+
* Finds a local agent route match without loading the whole collection when possible.
|
|
322
291
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* @param reference - Normalized route reference.
|
|
328
|
-
* @param localServerUrl - Normalized URL of the current Agents Server instance.
|
|
329
|
-
* @returns Local route target or `null` when the reference is not a local agent.
|
|
292
|
+
* @param collection - Agents collection backing local routes.
|
|
293
|
+
* @param reference - Normalized route/reference text.
|
|
294
|
+
* @returns Matching local agent profile or `null`.
|
|
330
295
|
*/
|
|
331
|
-
async function
|
|
296
|
+
async function findLocalAgentRouteMatch(
|
|
297
|
+
collection: Awaited<ReturnType<typeof $provideAgentCollectionForServer>>,
|
|
332
298
|
reference: string,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const result = await supabase
|
|
338
|
-
.from(agentTable)
|
|
339
|
-
.select('agentName, permanentId')
|
|
340
|
-
.or(buildAgentNameOrIdFilter(reference))
|
|
341
|
-
.is('deletedAt', null)
|
|
342
|
-
.order('createdAt', { ascending: true })
|
|
343
|
-
.limit(1);
|
|
344
|
-
|
|
345
|
-
if (result.error || !result.data || result.data.length === 0) {
|
|
346
|
-
return null;
|
|
299
|
+
): Promise<{ agentName: string; permanentId?: string | null } | null> {
|
|
300
|
+
const directMatch = await collection.findAgentBasicInformation(reference);
|
|
301
|
+
if (directMatch) {
|
|
302
|
+
return directMatch;
|
|
347
303
|
}
|
|
348
304
|
|
|
349
|
-
const
|
|
350
|
-
const
|
|
305
|
+
const agents = await collection.listAgents();
|
|
306
|
+
const normalizedReference = normalizeAgentName(reference);
|
|
351
307
|
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
308
|
+
return (
|
|
309
|
+
agents.find((agent: { agentName: string; permanentId?: string | null }) => {
|
|
310
|
+
if (agent.agentName === reference || agent.permanentId === reference) {
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const normalizedAgentName = normalizeAgentName(agent.agentName);
|
|
315
|
+
if (normalizedAgentName === normalizedReference) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return false;
|
|
320
|
+
}) || null
|
|
321
|
+
);
|
|
357
322
|
}
|
|
@@ -80,6 +80,21 @@ const EDGE_DASH_PATTERN = /^-+|-+$/g;
|
|
|
80
80
|
*/
|
|
81
81
|
const CODE_FENCE_PATTERN = /```/g;
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Regex matching webpack/Next.js chunk-load failures caused by stale or missing build assets.
|
|
85
|
+
*/
|
|
86
|
+
const CHUNK_LOAD_ERROR_MESSAGE_PATTERN = /loading (css )?chunk .* failed/i;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Regex matching browsers that fail while fetching one dynamically imported module.
|
|
90
|
+
*/
|
|
91
|
+
const DYNAMIC_IMPORT_FETCH_ERROR_MESSAGE_PATTERN = /failed to fetch dynamically imported module/i;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Regex matching Next.js-managed build asset URLs.
|
|
95
|
+
*/
|
|
96
|
+
const NEXT_BUILD_ASSET_PATH_PATTERN = /\/_next\/static\/(?:chunks|css)\//i;
|
|
97
|
+
|
|
83
98
|
/**
|
|
84
99
|
* Shape sent from the app error boundary to server-side Sentry forwarding.
|
|
85
100
|
*/
|
|
@@ -162,6 +177,32 @@ export function createApplicationErrorDigest(error: ApplicationBoundaryError | n
|
|
|
162
177
|
return (hash >>> 0).toString(DECIMAL_BASE).padStart(DIGEST_LENGTH, '0');
|
|
163
178
|
}
|
|
164
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Detects client-side build-asset errors that are commonly fixed by reloading the document once.
|
|
182
|
+
*
|
|
183
|
+
* @param error - Captured boundary exception.
|
|
184
|
+
* @returns True when the current document should be hard-refreshed instead of only retrying React navigation.
|
|
185
|
+
*/
|
|
186
|
+
export function isApplicationErrorRecoverableByHardRefresh(error: ApplicationBoundaryError | null): boolean {
|
|
187
|
+
const errorName = error?.name?.trim() || '';
|
|
188
|
+
const errorMessage = error?.message?.trim() || '';
|
|
189
|
+
const errorStack = error?.stack?.trim() || '';
|
|
190
|
+
const errorDetails = `${errorName}\n${errorMessage}\n${errorStack}`;
|
|
191
|
+
|
|
192
|
+
if (errorName === 'ChunkLoadError') {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (CHUNK_LOAD_ERROR_MESSAGE_PATTERN.test(errorDetails)) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
DYNAMIC_IMPORT_FETCH_ERROR_MESSAGE_PATTERN.test(errorDetails) &&
|
|
202
|
+
NEXT_BUILD_ASSET_PATH_PATTERN.test(errorDetails)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
165
206
|
/**
|
|
166
207
|
* Formats descriptive text shown beneath the application error headline.
|
|
167
208
|
*
|
|
@@ -170,6 +211,10 @@ export function createApplicationErrorDigest(error: ApplicationBoundaryError | n
|
|
|
170
211
|
* @returns Friendly summary of what happened.
|
|
171
212
|
*/
|
|
172
213
|
export function describeApplicationError(error: ApplicationBoundaryError | null, serverName: string): string {
|
|
214
|
+
if (isApplicationErrorRecoverableByHardRefresh(error)) {
|
|
215
|
+
return `The browser could not load the latest application files for ${serverName}. Refreshing the page usually resolves this after a deployment or stale cached shell.`;
|
|
216
|
+
}
|
|
217
|
+
|
|
173
218
|
if (error?.message) {
|
|
174
219
|
return `${error.message.trim()} - the server for ${serverName} logged this failure.`;
|
|
175
220
|
}
|