@promptbook/cli 0.112.0-81 → 0.112.0-84

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 (26) hide show
  1. package/apps/agents-server/README.md +7 -1
  2. package/apps/agents-server/src/database/$provideSupabaseForServer.ts +6 -0
  3. package/apps/agents-server/src/database/agentsServerDatabaseMode.ts +34 -0
  4. package/apps/agents-server/src/database/sqlite/$provideLocalSqliteSupabase.ts +1445 -0
  5. package/apps/agents-server/src/database/sqlite/resolveAgentsServerSqliteDatabasePath.ts +13 -0
  6. package/apps/agents-server/src/tools/$provideServer.ts +29 -2
  7. package/apps/agents-server/src/utils/serverRegistry.ts +13 -0
  8. package/apps/agents-server/src/utils/userChat/finalizeUserChatJob.ts +42 -0
  9. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore/claimNextDueUserChatTimeout.ts +63 -0
  10. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore/recoverExpiredRunningUserChatTimeouts.ts +47 -0
  11. package/apps/agents-server/src/utils/validateApiKey.ts +2 -18
  12. package/esm/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +20 -0
  13. package/esm/index.es.js +120 -5
  14. package/esm/index.es.js.map +1 -1
  15. package/esm/src/version.d.ts +1 -1
  16. package/package.json +2 -1
  17. package/src/book-components/Chat/save/pdf/buildChatPdf.ts +3 -26
  18. package/src/cli/cli-commands/agents-server/ensureAgentsServerEnvFile.ts +3 -1
  19. package/src/cli/cli-commands/agents-server/startAgentsServer.ts +148 -3
  20. package/src/other/templates/getTemplatesPipelineCollection.ts +844 -694
  21. package/src/version.ts +2 -2
  22. package/src/versions.txt +2 -0
  23. package/umd/apps/agents-server/src/database/agentsServerDatabaseMode.d.ts +20 -0
  24. package/umd/index.umd.js +120 -5
  25. package/umd/index.umd.js.map +1 -1
  26. package/umd/src/version.d.ts +1 -1
@@ -0,0 +1,13 @@
1
+ import { isAbsolute, join, resolve } from 'path';
2
+ import { AGENTS_SERVER_SQLITE_PATH_ENV_NAME } from '../agentsServerDatabaseMode';
3
+
4
+ /**
5
+ * Resolves the SQLite database path used by the standalone backend.
6
+ */
7
+ export function resolveAgentsServerSqliteDatabasePath(): string {
8
+ const configuredPath = process.env[AGENTS_SERVER_SQLITE_PATH_ENV_NAME]?.trim();
9
+ const fallbackPath = join(process.cwd(), '.promptbook', 'agents-server.sqlite');
10
+ const databasePath = configuredPath || fallbackPath;
11
+
12
+ return isAbsolute(databasePath) ? databasePath : resolve(process.cwd(), databasePath);
13
+ }
@@ -1,6 +1,7 @@
1
1
  import { NEXT_PUBLIC_SITE_URL, SUPABASE_TABLE_PREFIX } from '@/config';
2
2
  import { headers } from 'next/headers';
3
3
  import { cache } from 'react';
4
+ import { isAgentsServerSqliteMode } from '../database/agentsServerDatabaseMode';
4
5
  import { resolveInternalServerOrigin } from '../utils/resolveInternalServerOrigin';
5
6
  import { createServerPublicUrl, listRegisteredServersUsingServiceRole } from '../utils/serverRegistry';
6
7
  import { resolveServerSelection } from '../utils/serverSelection';
@@ -35,9 +36,26 @@ const getCachedProvidedServer = cache(async (): Promise<ProvidedServer> => {
35
36
  const headersList = await headers();
36
37
  const requestHost = headersList.get('host');
37
38
  const xPromptbookServer = headersList.get('x-promptbook-server');
39
+
40
+ if (isAgentsServerSqliteMode()) {
41
+ return {
42
+ id: null,
43
+ publicUrl: resolveFallbackPublicUrl(requestHost),
44
+ tablePrefix: SUPABASE_TABLE_PREFIX,
45
+ };
46
+ }
47
+
48
+ if (isLocalDevelopmentHost(requestHost)) {
49
+ return {
50
+ id: null,
51
+ publicUrl: resolveFallbackPublicUrl(requestHost),
52
+ tablePrefix: SUPABASE_TABLE_PREFIX,
53
+ };
54
+ }
55
+
38
56
  const registeredServers = await listRegisteredServersUsingServiceRole();
39
57
 
40
- if (registeredServers.length === 0 || isLocalDevelopmentHost(requestHost)) {
58
+ if (registeredServers.length === 0) {
41
59
  return {
42
60
  id: null,
43
61
  publicUrl: resolveFallbackPublicUrl(requestHost),
@@ -100,5 +118,14 @@ function isLocalDevelopmentHost(host: string | null): boolean {
100
118
  return false;
101
119
  }
102
120
 
103
- return host.startsWith('127.0.0.1') || host.startsWith('localhost');
121
+ const normalizedHost = host.trim().toLowerCase();
122
+
123
+ return (
124
+ normalizedHost === 'localhost' ||
125
+ normalizedHost.startsWith('localhost:') ||
126
+ normalizedHost === '127.0.0.1' ||
127
+ normalizedHost.startsWith('127.0.0.1:') ||
128
+ normalizedHost === '[::1]' ||
129
+ normalizedHost.startsWith('[::1]:')
130
+ );
104
131
  }
@@ -1,6 +1,7 @@
1
1
  import { createClient, type SupabaseClient } from '@supabase/supabase-js';
2
2
  import { spaceTrim } from 'spacetrim';
3
3
  import { DatabaseError } from '../../../../src/errors/DatabaseError';
4
+ import { isAgentsServerSqliteMode } from '../database/agentsServerDatabaseMode';
4
5
 
5
6
  /**
6
7
  * Supported `_Server.environment` values.
@@ -126,6 +127,10 @@ export async function listRegisteredServers(supabase: Pick<SupabaseClient, 'from
126
127
  export async function listRegisteredServersUsingServiceRole(options?: {
127
128
  readonly forceRefresh?: boolean;
128
129
  }): Promise<Array<ServerRecord>> {
130
+ if (isAgentsServerSqliteMode()) {
131
+ return [];
132
+ }
133
+
129
134
  const shouldReuseCache =
130
135
  !options?.forceRefresh &&
131
136
  cachedServerRegistry !== null &&
@@ -299,6 +304,14 @@ export function isServerEnvironment(value: string): value is ServerEnvironment {
299
304
  * @returns Shared untyped Supabase client.
300
305
  */
301
306
  export function getServerRegistryClient(): SupabaseClient {
307
+ if (isAgentsServerSqliteMode()) {
308
+ throw new DatabaseError(
309
+ spaceTrim(`
310
+ Cannot create a Supabase server-registry client while Agents Server is using SQLite.
311
+ `),
312
+ );
313
+ }
314
+
302
315
  if (cachedServerRegistryClient) {
303
316
  return cachedServerRegistryClient;
304
317
  }
@@ -1,6 +1,10 @@
1
1
  import { $getTableName } from '@/src/database/$getTableName';
2
2
  import { $provideClientSql } from '@/src/database/$provideClientSql';
3
+ import { isAgentsServerSqliteMode } from '@/src/database/agentsServerDatabaseMode';
4
+ import type { UserChatJobRow } from './UserChatJobRow';
3
5
  import type { UserChatJobRecord, UserChatJobStatus } from './UserChatJobRecord';
6
+ import { mapUserChatJobRow } from './mapUserChatJobRow';
7
+ import { provideUserChatJobTable } from './provideUserChatJobTable';
4
8
 
5
9
  /**
6
10
  * Final status values accepted when finishing one durable job.
@@ -17,6 +21,10 @@ export async function finalizeUserChatJob(options: {
17
21
  failureReason?: string | null;
18
22
  failureDetails?: string | null;
19
23
  }): Promise<UserChatJobRecord | null> {
24
+ if (isAgentsServerSqliteMode()) {
25
+ return finalizeUserChatJobViaSupabaseQuery(options);
26
+ }
27
+
20
28
  const nowIso = new Date().toISOString();
21
29
  try {
22
30
  const sql = await $provideClientSql();
@@ -74,6 +82,40 @@ export async function finalizeUserChatJob(options: {
74
82
  }
75
83
  }
76
84
 
85
+ /**
86
+ * Persists the final job state through the shared table adapter when PostgreSQL-only raw SQL is unavailable.
87
+ */
88
+ async function finalizeUserChatJobViaSupabaseQuery(options: {
89
+ jobId: string;
90
+ status: FinalUserChatJobStatus;
91
+ provider?: string | null;
92
+ failureReason?: string | null;
93
+ failureDetails?: string | null;
94
+ }): Promise<UserChatJobRecord | null> {
95
+ const nowIso = new Date().toISOString();
96
+ const userChatJobTable = await provideUserChatJobTable();
97
+ const { data, error } = await userChatJobTable
98
+ .update({
99
+ status: options.status,
100
+ updatedAt: nowIso,
101
+ completedAt: nowIso,
102
+ lastHeartbeatAt: nowIso,
103
+ leaseExpiresAt: nowIso,
104
+ provider: options.provider ?? null,
105
+ failureReason: options.failureReason ?? null,
106
+ failureDetails: options.failureDetails ?? null,
107
+ })
108
+ .eq('id', options.jobId)
109
+ .select('*')
110
+ .maybeSingle();
111
+
112
+ if (error) {
113
+ throw new Error(`Failed to finalize user chat job "${options.jobId}": ${error.message}`);
114
+ }
115
+
116
+ return data ? mapUserChatJobRow(data as UserChatJobRow) : null;
117
+ }
118
+
77
119
  /**
78
120
  * Quotes one trusted internal SQL identifier.
79
121
  *
@@ -1,8 +1,10 @@
1
1
  import { $provideClientSql } from '@/src/database/$provideClientSql';
2
+ import { isAgentsServerSqliteMode } from '@/src/database/agentsServerDatabaseMode';
2
3
  import type { UserChatTimeoutRecord, UserChatTimeoutRow } from '../UserChatTimeoutRecord';
3
4
  import { USER_CHAT_TIMEOUT_LEASE_DURATION_MS } from './USER_CHAT_TIMEOUT_LEASE_DURATION_MS';
4
5
  import { getUserChatTimeoutTableName } from './getUserChatTimeoutTableName';
5
6
  import { mapUserChatTimeoutRow } from './mapUserChatTimeoutRow';
7
+ import { provideUserChatTimeoutTable } from './provideUserChatTimeoutTable';
6
8
  import { quoteIdentifier } from './quoteIdentifier';
7
9
  import { rethrowUnlessMissingUserChatTimeoutRelation } from './rethrowUnlessMissingUserChatTimeoutRelation';
8
10
 
@@ -16,6 +18,10 @@ export async function claimNextDueUserChatTimeout(
16
18
  preferredTimeoutId?: string;
17
19
  } = {},
18
20
  ): Promise<UserChatTimeoutRecord | null> {
21
+ if (isAgentsServerSqliteMode()) {
22
+ return claimNextDueUserChatTimeoutViaSupabaseQuery(options);
23
+ }
24
+
19
25
  const sql = await $provideClientSql();
20
26
  const tableIdentifier = quoteIdentifier(await getUserChatTimeoutTableName());
21
27
  const values: Array<unknown> = [USER_CHAT_TIMEOUT_LEASE_DURATION_MS];
@@ -60,3 +66,60 @@ export async function claimNextDueUserChatTimeout(
60
66
 
61
67
  return claimedRows[0] ? mapUserChatTimeoutRow(claimedRows[0]) : null;
62
68
  }
69
+
70
+ /**
71
+ * Claims a due timeout through the shared table adapter when PostgreSQL-only raw SQL is unavailable.
72
+ */
73
+ async function claimNextDueUserChatTimeoutViaSupabaseQuery(
74
+ options: {
75
+ preferredTimeoutId?: string;
76
+ } = {},
77
+ ): Promise<UserChatTimeoutRecord | null> {
78
+ const userChatTimeoutTable = await provideUserChatTimeoutTable();
79
+ const now = new Date();
80
+ const nowIso = now.toISOString();
81
+ const leaseExpiresAt = new Date(now.getTime() + USER_CHAT_TIMEOUT_LEASE_DURATION_MS).toISOString();
82
+ let candidateQuery = userChatTimeoutTable
83
+ .select('*')
84
+ .eq('status', 'QUEUED')
85
+ .is('cancelRequestedAt', null)
86
+ .is('pausedAt', null)
87
+ .lte('dueAt', nowIso)
88
+ .order('dueAt', { ascending: true })
89
+ .order('createdAt', { ascending: true })
90
+ .limit(1);
91
+
92
+ if (options.preferredTimeoutId) {
93
+ candidateQuery = candidateQuery.eq('id', options.preferredTimeoutId);
94
+ }
95
+
96
+ const { data: candidates, error: candidateError } = await candidateQuery;
97
+ if (candidateError) {
98
+ throw new Error(`Failed to load due user chat timeouts: ${candidateError.message}`);
99
+ }
100
+
101
+ const candidate = (candidates || [])[0] as UserChatTimeoutRow | undefined;
102
+ if (!candidate) {
103
+ return null;
104
+ }
105
+
106
+ const { data, error } = await userChatTimeoutTable
107
+ .update({
108
+ status: 'RUNNING',
109
+ updatedAt: nowIso,
110
+ startedAt: candidate.startedAt || nowIso,
111
+ leaseExpiresAt,
112
+ attemptCount: candidate.attemptCount + 1,
113
+ failureReason: null,
114
+ })
115
+ .eq('id', candidate.id)
116
+ .eq('status', 'QUEUED')
117
+ .select('*')
118
+ .maybeSingle();
119
+
120
+ if (error) {
121
+ throw new Error(`Failed to claim user chat timeout "${candidate.id}": ${error.message}`);
122
+ }
123
+
124
+ return data ? mapUserChatTimeoutRow(data as UserChatTimeoutRow) : null;
125
+ }
@@ -1,5 +1,7 @@
1
1
  import { $provideClientSql } from '@/src/database/$provideClientSql';
2
+ import { isAgentsServerSqliteMode } from '@/src/database/agentsServerDatabaseMode';
2
3
  import { getUserChatTimeoutTableName } from './getUserChatTimeoutTableName';
4
+ import { provideUserChatTimeoutTable } from './provideUserChatTimeoutTable';
3
5
  import { quoteIdentifier } from './quoteIdentifier';
4
6
  import { rethrowUnlessMissingUserChatTimeoutRelation } from './rethrowUnlessMissingUserChatTimeoutRelation';
5
7
 
@@ -9,6 +11,10 @@ import { rethrowUnlessMissingUserChatTimeoutRelation } from './rethrowUnlessMiss
9
11
  * @private function of userChatTimeoutStore
10
12
  */
11
13
  export async function recoverExpiredRunningUserChatTimeouts(): Promise<number> {
14
+ if (isAgentsServerSqliteMode()) {
15
+ return recoverExpiredRunningUserChatTimeoutsViaSupabaseQuery();
16
+ }
17
+
12
18
  const sql = await $provideClientSql();
13
19
  const tableIdentifier = quoteIdentifier(await getUserChatTimeoutTableName());
14
20
  let recoveredRows: Array<{ id: string }>;
@@ -49,3 +55,44 @@ export async function recoverExpiredRunningUserChatTimeouts(): Promise<number> {
49
55
 
50
56
  return recoveredRows.length;
51
57
  }
58
+
59
+ /**
60
+ * Recovers expired timeout leases through the shared table adapter when raw PostgreSQL is unavailable.
61
+ */
62
+ async function recoverExpiredRunningUserChatTimeoutsViaSupabaseQuery(): Promise<number> {
63
+ const userChatTimeoutTable = await provideUserChatTimeoutTable();
64
+ const nowIso = new Date().toISOString();
65
+ const { data, error } = await userChatTimeoutTable
66
+ .select('id, cancelRequestedAt, failureReason')
67
+ .eq('status', 'RUNNING')
68
+ .not('leaseExpiresAt', 'is', null)
69
+ .lt('leaseExpiresAt', nowIso);
70
+
71
+ if (error) {
72
+ throw new Error(`Failed to load expired running user chat timeouts: ${error.message}`);
73
+ }
74
+
75
+ let recoveredCount = 0;
76
+ for (const row of data || []) {
77
+ const timeoutRow = row as { id: string; cancelRequestedAt: string | null; failureReason: string | null };
78
+ const isCancelled = timeoutRow.cancelRequestedAt !== null;
79
+ const updateResult = await userChatTimeoutTable
80
+ .update({
81
+ status: isCancelled ? 'CANCELLED' : 'QUEUED',
82
+ updatedAt: nowIso,
83
+ startedAt: isCancelled ? undefined : null,
84
+ completedAt: isCancelled ? nowIso : null,
85
+ leaseExpiresAt: null,
86
+ failureReason: isCancelled ? timeoutRow.failureReason || 'Timeout was cancelled.' : timeoutRow.failureReason,
87
+ })
88
+ .eq('id', timeoutRow.id);
89
+
90
+ if (updateResult.error) {
91
+ throw new Error(`Failed to recover expired user chat timeout "${timeoutRow.id}": ${updateResult.error.message}`);
92
+ }
93
+
94
+ recoveredCount++;
95
+ }
96
+
97
+ return recoveredCount;
98
+ }
@@ -1,6 +1,6 @@
1
- import { createClient } from '@supabase/supabase-js';
2
1
  import { NextRequest } from 'next/server';
3
2
  import { $getTableName } from '../database/$getTableName';
3
+ import { $provideSupabaseForServer } from '../database/$provideSupabaseForServer';
4
4
 
5
5
  /**
6
6
  * Result of Api key validation.
@@ -56,24 +56,8 @@ export async function validateApiKey(request: NextRequest): Promise<ApiKeyValida
56
56
  };
57
57
  }
58
58
 
59
- const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
60
- const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
61
-
62
- if (!supabaseUrl || !supabaseKey) {
63
- console.error('Supabase configuration missing for API key validation');
64
- return {
65
- isValid: false,
66
- error: 'Server configuration error',
67
- };
68
- }
69
-
70
59
  try {
71
- const supabase = createClient(supabaseUrl, supabaseKey, {
72
- auth: {
73
- persistSession: false,
74
- autoRefreshToken: false,
75
- },
76
- });
60
+ const supabase = $provideSupabaseForServer();
77
61
 
78
62
  const { data, error } = await supabase
79
63
  .from(await $getTableName(`ApiTokens`))
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Environment variable selecting the Agents Server database backend.
3
+ */
4
+ export declare const AGENTS_SERVER_DATABASE_ENV_NAME = "PTBK_AGENTS_SERVER_DATABASE";
5
+ /**
6
+ * Environment variable pointing to the standalone SQLite database file.
7
+ */
8
+ export declare const AGENTS_SERVER_SQLITE_PATH_ENV_NAME = "PTBK_AGENTS_SERVER_SQLITE_PATH";
9
+ /**
10
+ * Supported Agents Server database backends.
11
+ */
12
+ export type AgentsServerDatabaseMode = 'supabase' | 'sqlite';
13
+ /**
14
+ * Resolves the configured Agents Server database backend.
15
+ */
16
+ export declare function resolveAgentsServerDatabaseMode(): AgentsServerDatabaseMode;
17
+ /**
18
+ * Returns whether the Agents Server is using the local SQLite backend.
19
+ */
20
+ export declare function isAgentsServerSqliteMode(): boolean;
package/esm/index.es.js CHANGED
@@ -58,7 +58,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
58
58
  * @generated
59
59
  * @see https://github.com/webgptorg/promptbook
60
60
  */
61
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-81';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-84';
62
62
  /**
63
63
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
64
64
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -2469,9 +2469,11 @@ const AGENTS_SERVER_ENV_DOCUMENTATION_BASE_URL = 'https://github.com/webgptorg/p
2469
2469
  */
2470
2470
  const AGENTS_SERVER_ENV_CREATED_COMMENT = '# Created by `ptbk agents-server init` command';
2471
2471
  /**
2472
- * Variables required for a local Agents Server backed by a Supabase project.
2472
+ * Variables required for a local Agents Server backed by Supabase or standalone SQLite.
2473
2473
  */
2474
2474
  const REQUIRED_AGENTS_SERVER_ENV_VARIABLES = [
2475
+ createAgentsServerEnvVariable('PTBK_AGENTS_SERVER_DATABASE', 'supabase'),
2476
+ createAgentsServerEnvVariable('PTBK_AGENTS_SERVER_SQLITE_PATH', '.promptbook/agents-server.sqlite'),
2475
2477
  createAgentsServerEnvVariable('OPENAI_API_KEY', ''),
2476
2478
  createAgentsServerEnvVariable('POSTGRES_URL', ''),
2477
2479
  createAgentsServerEnvVariable('NEXT_PUBLIC_SUPABASE_URL', ''),
@@ -35830,6 +35832,18 @@ async function withCurrentWorkingDirectory(directoryPath, callback) {
35830
35832
  * @private internal constant of `ptbk agents-server`
35831
35833
  */
35832
35834
  const USER_CHAT_JOB_WORKER_POLL_INTERVAL_MS = 2000;
35835
+ /**
35836
+ * Number of identical worker failures suppressed before logging a repeated summary.
35837
+ *
35838
+ * @private internal constant of `ptbk agents-server`
35839
+ */
35840
+ const USER_CHAT_JOB_WORKER_REPEATED_ERROR_LOG_INTERVAL = 10;
35841
+ /**
35842
+ * Maximum worker response body length shown in foreground diagnostics.
35843
+ *
35844
+ * @private internal constant of `ptbk agents-server`
35845
+ */
35846
+ const USER_CHAT_JOB_WORKER_ERROR_BODY_MAX_LENGTH = 2000;
35833
35847
  /**
35834
35848
  * HTTP status used by an idle internal worker tick with no job to process.
35835
35849
  *
@@ -35854,6 +35868,18 @@ const AGENTS_SERVER_PROJECT_ENV_FILE_NAME = '.env';
35854
35868
  * @private internal constant of `ptbk agents-server`
35855
35869
  */
35856
35870
  const PTBK_AGENTS_SERVER_AGENT_ROOT_ENV = 'PTBK_AGENTS_SERVER_AGENT_ROOT';
35871
+ /**
35872
+ * Public database mode environment name consumed by the Agents Server app.
35873
+ *
35874
+ * @private internal constant of `ptbk agents-server`
35875
+ */
35876
+ const PTBK_AGENTS_SERVER_DATABASE_ENV = 'PTBK_AGENTS_SERVER_DATABASE';
35877
+ /**
35878
+ * Local SQLite file environment name consumed by the Agents Server app.
35879
+ *
35880
+ * @private internal constant of `ptbk agents-server`
35881
+ */
35882
+ const PTBK_AGENTS_SERVER_SQLITE_PATH_ENV = 'PTBK_AGENTS_SERVER_SQLITE_PATH';
35857
35883
  /**
35858
35884
  * Entropy size for the local-only token shared by the CLI pump and the Next app.
35859
35885
  *
@@ -36023,11 +36049,15 @@ function forwardChildOutput(chunk, options) {
36023
36049
  * Creates the subprocess environment for Next and its internal local runner bridge.
36024
36050
  */
36025
36051
  function createAgentsServerChildEnvironment(port, agentRootPath) {
36052
+ const launchWorkingDirectory = process.cwd();
36026
36053
  return {
36027
36054
  ...process.env,
36028
36055
  PORT: String(port),
36029
36056
  NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL || `http://localhost:${port}`,
36030
36057
  [PTBK_AGENTS_SERVER_AGENT_ROOT_ENV]: agentRootPath,
36058
+ [PTBK_AGENTS_SERVER_SQLITE_PATH_ENV]: process.env[PTBK_AGENTS_SERVER_SQLITE_PATH_ENV] ||
36059
+ join(launchWorkingDirectory, '.promptbook', 'agents-server.sqlite'),
36060
+ [PTBK_AGENTS_SERVER_DATABASE_ENV]: process.env[PTBK_AGENTS_SERVER_DATABASE_ENV] || 'supabase',
36031
36061
  // Next loads app-local `.env` values after the CLI has prepared this bridge environment.
36032
36062
  PTBK_AGENTS_SERVER_USER_CHAT_WORKER_TOKEN: process.env.PTBK_AGENTS_SERVER_USER_CHAT_WORKER_TOKEN ||
36033
36063
  randomBytes(LOCAL_USER_CHAT_WORKER_TOKEN_BYTE_LENGTH).toString('hex'),
@@ -36062,10 +36092,16 @@ function startUserChatJobWorkerPump(options) {
36062
36092
  }
36063
36093
  isTickRunning = true;
36064
36094
  triggerUserChatJobWorkerTick(options)
36095
+ .then(() => {
36096
+ clearUserChatJobWorkerError(options.state);
36097
+ })
36065
36098
  .catch((error) => {
36066
36099
  const message = error instanceof Error ? error.message : String(error);
36067
- logRunnerEvent(options.logStreams.runner, `User chat worker tick failed: ${message}`);
36068
- addUiError(options.state, message);
36100
+ reportUserChatJobWorkerError({
36101
+ logStream: options.logStreams.runner,
36102
+ message,
36103
+ state: options.state,
36104
+ });
36069
36105
  })
36070
36106
  .finally(() => {
36071
36107
  isTickRunning = false;
@@ -36087,8 +36123,87 @@ async function triggerUserChatJobWorkerTick(options) {
36087
36123
  body: '{}',
36088
36124
  });
36089
36125
  if (!response.ok && response.status !== HTTP_NO_CONTENT_STATUS_CODE) {
36090
- throw new Error(`Internal user chat worker returned ${response.status} ${response.statusText}.`);
36126
+ const details = await readUserChatJobWorkerErrorDetails(response);
36127
+ throw new Error(createUserChatJobWorkerErrorMessage(response, details));
36128
+ }
36129
+ }
36130
+ /**
36131
+ * Reports worker failures while suppressing identical repeated foreground noise.
36132
+ */
36133
+ function reportUserChatJobWorkerError(options) {
36134
+ const previousError = options.state.lastUserChatJobWorkerError;
36135
+ if ((previousError === null || previousError === void 0 ? void 0 : previousError.message) === options.message) {
36136
+ const repeatCount = previousError.repeatCount + 1;
36137
+ options.state.lastUserChatJobWorkerError = {
36138
+ message: options.message,
36139
+ repeatCount,
36140
+ };
36141
+ if (repeatCount % USER_CHAT_JOB_WORKER_REPEATED_ERROR_LOG_INTERVAL !== 0) {
36142
+ return;
36143
+ }
36144
+ const repeatedMessage = `User chat worker tick is still failing after ${repeatCount} attempts: ${options.message}`;
36145
+ logRunnerEvent(options.logStream, repeatedMessage);
36146
+ addUiError(options.state, repeatedMessage);
36147
+ return;
36148
+ }
36149
+ options.state.lastUserChatJobWorkerError = {
36150
+ message: options.message,
36151
+ repeatCount: 1,
36152
+ };
36153
+ logRunnerEvent(options.logStream, `User chat worker tick failed: ${options.message}`);
36154
+ addUiError(options.state, options.message);
36155
+ }
36156
+ /**
36157
+ * Resets repeated-error suppression after a successful worker tick.
36158
+ */
36159
+ function clearUserChatJobWorkerError(state) {
36160
+ state.lastUserChatJobWorkerError = undefined;
36161
+ }
36162
+ /**
36163
+ * Reads a worker error payload so foreground logs show the route-level reason.
36164
+ */
36165
+ async function readUserChatJobWorkerErrorDetails(response) {
36166
+ const body = await response.text().catch(() => '');
36167
+ const trimmedBody = body.trim();
36168
+ if (!trimmedBody) {
36169
+ return null;
36170
+ }
36171
+ const parsedMessage = parseUserChatJobWorkerErrorMessage(trimmedBody);
36172
+ return truncateUserChatJobWorkerErrorDetails(parsedMessage || trimmedBody);
36173
+ }
36174
+ /**
36175
+ * Extracts a readable error message from the worker route JSON response.
36176
+ */
36177
+ function parseUserChatJobWorkerErrorMessage(body) {
36178
+ try {
36179
+ const parsedBody = JSON.parse(body);
36180
+ const errorMessage = typeof parsedBody.error === 'string' ? parsedBody.error : undefined;
36181
+ const fallbackMessage = typeof parsedBody.message === 'string' ? parsedBody.message : undefined;
36182
+ return errorMessage || fallbackMessage || null;
36183
+ }
36184
+ catch (_a) {
36185
+ return null;
36186
+ }
36187
+ }
36188
+ /**
36189
+ * Builds the foreground worker failure message from HTTP status and route details.
36190
+ */
36191
+ function createUserChatJobWorkerErrorMessage(response, details) {
36192
+ const statusText = response.statusText ? ` ${response.statusText}` : '';
36193
+ const statusMessage = `${response.status}${statusText}`;
36194
+ if (!details) {
36195
+ return `Internal user chat worker returned ${statusMessage}.`;
36196
+ }
36197
+ return `Internal user chat worker returned ${statusMessage}: ${details}`;
36198
+ }
36199
+ /**
36200
+ * Keeps foreground worker diagnostics bounded when a route returns a large payload.
36201
+ */
36202
+ function truncateUserChatJobWorkerErrorDetails(details) {
36203
+ if (details.length <= USER_CHAT_JOB_WORKER_ERROR_BODY_MAX_LENGTH) {
36204
+ return details;
36091
36205
  }
36206
+ return `${details.slice(0, USER_CHAT_JOB_WORKER_ERROR_BODY_MAX_LENGTH)}...`;
36092
36207
  }
36093
36208
  /**
36094
36209
  * Creates file streams for service output persisted below `./logs`.