@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.
Files changed (24) hide show
  1. package/apps/agents-server/src/app/agents/[agentName]/_utils.ts +7 -5
  2. package/apps/agents-server/src/app/agents/[agentName]/api/user-chats/[chatId]/stream/route.ts +4 -86
  3. package/apps/agents-server/src/components/ApplicationErrorPage/ApplicationErrorPage.tsx +118 -12
  4. package/apps/agents-server/src/utils/agentRouting/resolveAgentRouteTarget.ts +27 -62
  5. package/apps/agents-server/src/utils/errorReporting/applicationErrorHandling.ts +45 -0
  6. package/apps/agents-server/src/utils/errorReporting/refreshApplicationDocument.ts +10 -0
  7. package/apps/agents-server/src/utils/importAgent.ts +1 -57
  8. package/apps/agents-server/src/utils/importAgentWithFallback.ts +0 -10
  9. package/apps/agents-server/src/utils/userChat.ts +0 -1
  10. package/esm/index.es.js +18 -11
  11. package/esm/index.es.js.map +1 -1
  12. package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
  13. package/esm/src/version.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/cli/cli-commands/agents-server/startAgentsServer.ts +6 -1
  16. package/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.ts +91 -14
  17. package/src/other/templates/getTemplatesPipelineCollection.ts +920 -617
  18. package/src/version.ts +2 -2
  19. package/src/versions.txt +1 -0
  20. package/umd/index.umd.js +18 -11
  21. package/umd/index.umd.js.map +1 -1
  22. package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.d.ts +22 -0
  23. package/umd/src/version.d.ts +1 -1
  24. 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 { resolveServerAgentContext } from '@/src/utils/resolveServerAgentContext';
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 $provideAgentCollectionForServer();
95
- const { publicUrl } = await $provideServer();
96
- const baseAgentReferenceResolver = await $provideAgentReferenceResolver();
97
- const resolvedAgentContext = await resolveServerAgentContext({
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,
@@ -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 shouldUseFullPolling = await emitLatestSnapshot();
168
+ let hasActiveJobs = await emitLatestSnapshot();
224
169
 
225
170
  while (!isStreamClosed && !request.signal.aborted) {
226
171
  await waitForNextUserChatStreamPoll(
227
- shouldUseFullPolling
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
- shouldUseFullPolling = shouldUseFullPolling
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: 'Try "Try again" so the last navigation runs with fresh cookies, network state, and session data.',
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={() => reset()}>
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 agents = await collection.listAgents();
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
- * Resolves the common local route case with a targeted indexed database lookup.
290
+ * Finds a local agent route match without loading the whole collection when possible.
322
291
  *
323
- * The reference resolver has to initialize the full local/federated lookup map.
324
- * Most page requests already carry a permanent id or exact local name, so checking
325
- * the Agent table first keeps normal navigation from loading every agent profile.
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 resolveLocalAgentRouteTargetFromDatabase(
296
+ async function findLocalAgentRouteMatch(
297
+ collection: Awaited<ReturnType<typeof $provideAgentCollectionForServer>>,
332
298
  reference: string,
333
- localServerUrl: string,
334
- ): Promise<AgentRouteTarget | null> {
335
- const supabase = $provideSupabaseForServer();
336
- const agentTable = await $getTableName('Agent');
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 agent = result.data[0] as LocalAgentRouteRow;
350
- const canonicalAgentId = agent.permanentId || agent.agentName;
305
+ const agents = await collection.listAgents();
306
+ const normalizedReference = normalizeAgentName(reference);
351
307
 
352
- return {
353
- kind: 'local',
354
- canonicalAgentId,
355
- canonicalUrl: `${localServerUrl}${AGENT_PATH_PREFIX}${encodeURIComponent(canonicalAgentId)}`,
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
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reloads the current browser document so stale build assets can be fetched again.
3
+ */
4
+ export function refreshApplicationDocument(): void {
5
+ if (typeof window === 'undefined') {
6
+ return;
7
+ }
8
+
9
+ window.location.reload();
10
+ }