@promptbook/cli 0.112.0-91 → 0.112.0-93
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/layout.tsx +11 -5
- package/apps/agents-server/src/constants/themeMode.ts +27 -1
- package/apps/agents-server/src/database/$provideClientSql.ts +1 -1
- package/apps/agents-server/src/database/metadataDefaults.ts +10 -0
- package/apps/agents-server/src/database/migrations/2026-05-2600-default-theme.sql +16 -0
- package/apps/agents-server/src/utils/externalChatRunner/createExternalAgentRepositoryFiles.ts +2 -2
- package/apps/agents-server/src/utils/getAdminChatTasksResponse/getAdminChatTasks.ts +532 -1
- package/apps/agents-server/src/utils/userThemeModeSettings.ts +10 -2
- package/esm/index.es.js +65 -65
- package/esm/index.es.js.map +1 -1
- package/esm/scripts/run-agent-messages/main/agentIgnorePatterns.d.ts +2 -2
- package/esm/scripts/run-agent-messages/main/createCoderRunOptionsForAgent.d.ts +1 -1
- package/esm/scripts/run-agent-messages/main/runPersistentAgentWatch.d.ts +1 -1
- package/esm/scripts/run-agent-messages/main/tickAgentMessages.d.ts +1 -1
- package/esm/scripts/run-agent-messages/main/validateAgentRunOptions.d.ts +1 -1
- package/esm/scripts/run-agent-messages/ui/agentRunUiConstants.d.ts +1 -1
- package/esm/scripts/run-agent-messages/ui/buildAgentRunUiFrame.d.ts +1 -1
- package/esm/scripts/run-agent-messages/ui/initializeAgentRunUi.d.ts +1 -1
- package/esm/scripts/run-agent-messages/ui/loadAgentRunUiMetadata.d.ts +2 -2
- package/{umd/src/cli/cli-commands/agent → esm/src/cli/cli-commands/agent-folder}/agentProjectPaths.d.ts +16 -16
- package/{umd/src/cli/cli-commands/agent → esm/src/cli/cli-commands/agent-folder}/agentRunCliOptions.d.ts +5 -5
- package/{umd/src/cli/cli-commands/agent → esm/src/cli/cli-commands/agent-folder}/init.d.ts +1 -1
- package/{umd/src/cli/cli-commands/agent → esm/src/cli/cli-commands/agent-folder}/initializeAgentProjectConfiguration.d.ts +2 -2
- package/esm/src/cli/cli-commands/{agent → agent-folder}/initializeAgentRunnerCommand.d.ts +4 -4
- package/esm/src/cli/cli-commands/{agent → agent-folder}/printAgentInitializationSummary.d.ts +1 -1
- package/{umd/src/cli/cli-commands/agent → esm/src/cli/cli-commands/agent-folder}/run.d.ts +1 -1
- package/{umd/src/cli/cli-commands/agent → esm/src/cli/cli-commands/agent-folder}/runMultiple.d.ts +1 -1
- package/esm/src/cli/cli-commands/{agent → agent-folder}/tick.d.ts +1 -1
- package/esm/src/cli/cli-commands/{agent.d.ts → agent-folder.d.ts} +3 -3
- package/esm/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/cli-commands/{agent → agent-folder}/agentProjectPaths.ts +18 -18
- package/src/cli/cli-commands/{agent → agent-folder}/agentRunCliOptions.ts +7 -7
- package/src/cli/cli-commands/{agent → agent-folder}/init.ts +2 -2
- package/src/cli/cli-commands/{agent → agent-folder}/initializeAgentProjectConfiguration.ts +3 -3
- package/src/cli/cli-commands/{agent → agent-folder}/initializeAgentRunnerCommand.ts +6 -6
- package/src/cli/cli-commands/{agent → agent-folder}/printAgentInitializationSummary.ts +2 -2
- package/src/cli/cli-commands/{agent → agent-folder}/run.ts +2 -2
- package/src/cli/cli-commands/{agent → agent-folder}/runMultiple.ts +2 -2
- package/src/cli/cli-commands/{agent → agent-folder}/tick.ts +2 -2
- package/src/cli/cli-commands/{agent.ts → agent-folder.ts} +16 -16
- package/src/cli/promptbookCli.ts +2 -2
- package/src/other/templates/getTemplatesPipelineCollection.ts +865 -755
- package/src/version.ts +2 -2
- package/src/versions.txt +2 -1
- package/umd/index.umd.js +65 -65
- package/umd/index.umd.js.map +1 -1
- package/umd/scripts/run-agent-messages/main/agentIgnorePatterns.d.ts +2 -2
- package/umd/scripts/run-agent-messages/main/createCoderRunOptionsForAgent.d.ts +1 -1
- package/umd/scripts/run-agent-messages/main/runPersistentAgentWatch.d.ts +1 -1
- package/umd/scripts/run-agent-messages/main/tickAgentMessages.d.ts +1 -1
- package/umd/scripts/run-agent-messages/main/validateAgentRunOptions.d.ts +1 -1
- package/umd/scripts/run-agent-messages/ui/agentRunUiConstants.d.ts +1 -1
- package/umd/scripts/run-agent-messages/ui/buildAgentRunUiFrame.d.ts +1 -1
- package/umd/scripts/run-agent-messages/ui/initializeAgentRunUi.d.ts +1 -1
- package/umd/scripts/run-agent-messages/ui/loadAgentRunUiMetadata.d.ts +2 -2
- package/{esm/src/cli/cli-commands/agent → umd/src/cli/cli-commands/agent-folder}/agentProjectPaths.d.ts +16 -16
- package/{esm/src/cli/cli-commands/agent → umd/src/cli/cli-commands/agent-folder}/agentRunCliOptions.d.ts +5 -5
- package/{esm/src/cli/cli-commands/agent → umd/src/cli/cli-commands/agent-folder}/init.d.ts +1 -1
- package/{esm/src/cli/cli-commands/agent → umd/src/cli/cli-commands/agent-folder}/initializeAgentProjectConfiguration.d.ts +2 -2
- package/umd/src/cli/cli-commands/{agent → agent-folder}/initializeAgentRunnerCommand.d.ts +4 -4
- package/umd/src/cli/cli-commands/{agent → agent-folder}/printAgentInitializationSummary.d.ts +1 -1
- package/{esm/src/cli/cli-commands/agent → umd/src/cli/cli-commands/agent-folder}/run.d.ts +1 -1
- package/umd/src/cli/cli-commands/agent-folder/run.test.d.ts +1 -0
- package/{esm/src/cli/cli-commands/agent → umd/src/cli/cli-commands/agent-folder}/runMultiple.d.ts +1 -1
- package/umd/src/cli/cli-commands/{agent → agent-folder}/tick.d.ts +1 -1
- package/umd/src/cli/cli-commands/{agent.d.ts → agent-folder.d.ts} +3 -3
- package/umd/src/cli/other/vpsInstall.test.d.ts +1 -0
- package/umd/src/version.d.ts +1 -1
- /package/esm/src/cli/cli-commands/{agent → agent-folder}/run.test.d.ts +0 -0
- /package/{umd/src/cli/cli-commands/agent/run.test.d.ts → esm/src/cli/other/vpsInstall.test.d.ts} +0 -0
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { $getTableName } from '@/src/database/$getTableName';
|
|
2
2
|
import { $provideClientSql } from '@/src/database/$provideClientSql';
|
|
3
|
+
import { $provideSupabaseForServer } from '@/src/database/$provideSupabaseForServer';
|
|
4
|
+
import { isAgentsServerSqliteMode } from '@/src/database/agentsServerDatabaseMode';
|
|
5
|
+
import type { AgentsServerDatabase } from '@/src/database/schema';
|
|
3
6
|
import type { AdminChatTaskCounters, AdminChatTaskRecord, AdminChatTaskView } from '../chatTasksAdmin';
|
|
4
|
-
import
|
|
7
|
+
import { provideUserChatJobTable } from '../userChat/provideUserChatJobTable';
|
|
5
8
|
import type { UserChatJobStatus } from '../userChat/UserChatJobRecord';
|
|
9
|
+
import { provideUserChatTimeoutTable } from '../userChatTimeout/userChatTimeoutStore/provideUserChatTimeoutTable';
|
|
10
|
+
import type { ParsedAdminChatTaskQuery } from './parseAdminChatTaskQuery';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Milliseconds in one hour.
|
|
14
|
+
*
|
|
15
|
+
* @private internal constant of `getAdminChatTasksResponse`
|
|
16
|
+
*/
|
|
17
|
+
const HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
|
|
6
18
|
|
|
7
19
|
/**
|
|
8
20
|
* Raw SQL row returned by the paginated admin task query.
|
|
@@ -46,6 +58,71 @@ type AdminChatTaskCountersSqlRow = {
|
|
|
46
58
|
oldestQueuedAgeMs: string | number | null;
|
|
47
59
|
};
|
|
48
60
|
|
|
61
|
+
/**
|
|
62
|
+
* SQLite-backed job row used by the admin task-manager fallback.
|
|
63
|
+
*
|
|
64
|
+
* @private type of `getAdminChatTasksResponse`
|
|
65
|
+
*/
|
|
66
|
+
type AdminChatTaskJobRow = {
|
|
67
|
+
id: string;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
queuedAt: string;
|
|
70
|
+
startedAt: string | null;
|
|
71
|
+
updatedAt: string;
|
|
72
|
+
completedAt: string | null;
|
|
73
|
+
cancelRequestedAt: string | null;
|
|
74
|
+
lastHeartbeatAt: string | null;
|
|
75
|
+
leaseExpiresAt: string | null;
|
|
76
|
+
attemptCount: number;
|
|
77
|
+
failureReason: string | null;
|
|
78
|
+
failureDetails?: string | null;
|
|
79
|
+
userId: number;
|
|
80
|
+
agentPermanentId: string;
|
|
81
|
+
chatId: string;
|
|
82
|
+
status: UserChatJobStatus;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* SQLite-backed timeout row used by the admin task-manager fallback.
|
|
87
|
+
*
|
|
88
|
+
* @private type of `getAdminChatTasksResponse`
|
|
89
|
+
*/
|
|
90
|
+
type AdminChatTaskTimeoutRow = {
|
|
91
|
+
id: string;
|
|
92
|
+
createdAt: string;
|
|
93
|
+
queuedAt: string;
|
|
94
|
+
startedAt: string | null;
|
|
95
|
+
updatedAt: string;
|
|
96
|
+
completedAt: string | null;
|
|
97
|
+
cancelRequestedAt: string | null;
|
|
98
|
+
pausedAt: string | null;
|
|
99
|
+
leaseExpiresAt: string | null;
|
|
100
|
+
recurrenceIntervalMs: number | string | null;
|
|
101
|
+
attemptCount: number;
|
|
102
|
+
failureReason: string | null;
|
|
103
|
+
userId: number;
|
|
104
|
+
agentPermanentId: string;
|
|
105
|
+
chatId: string;
|
|
106
|
+
status: UserChatJobStatus;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Minimal user lookup row needed by the admin task-manager fallback.
|
|
111
|
+
*
|
|
112
|
+
* @private type of `getAdminChatTasksResponse`
|
|
113
|
+
*/
|
|
114
|
+
type AdminChatTaskUserLookupRow = Pick<AgentsServerDatabase['public']['Tables']['User']['Row'], 'id' | 'username'>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Minimal agent lookup row needed by the admin task-manager fallback.
|
|
118
|
+
*
|
|
119
|
+
* @private type of `getAdminChatTasksResponse`
|
|
120
|
+
*/
|
|
121
|
+
type AdminChatTaskAgentLookupRow = Pick<
|
|
122
|
+
AgentsServerDatabase['public']['Tables']['Agent']['Row'],
|
|
123
|
+
'permanentId' | 'agentName'
|
|
124
|
+
>;
|
|
125
|
+
|
|
49
126
|
/**
|
|
50
127
|
* Loaded admin chat-task data before the final response envelope is assembled.
|
|
51
128
|
*
|
|
@@ -63,6 +140,52 @@ export type GetAdminChatTasksData = {
|
|
|
63
140
|
* @private function of `getAdminChatTasksResponse`
|
|
64
141
|
*/
|
|
65
142
|
export async function getAdminChatTasks(query: ParsedAdminChatTaskQuery): Promise<GetAdminChatTasksData> {
|
|
143
|
+
if (isAgentsServerSqliteMode()) {
|
|
144
|
+
return getAdminChatTasksViaSupabaseQuery(query);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return getAdminChatTasksViaClientSql(query);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Loads admin task-manager data through the shared Supabase-shaped adapters used by SQLite mode.
|
|
152
|
+
*
|
|
153
|
+
* @private function of `getAdminChatTasksResponse`
|
|
154
|
+
*/
|
|
155
|
+
async function getAdminChatTasksViaSupabaseQuery(query: ParsedAdminChatTaskQuery): Promise<GetAdminChatTasksData> {
|
|
156
|
+
const [jobRows, timeoutRows] = await Promise.all([loadAdminChatTaskJobRows(), loadAdminChatTaskTimeoutRows()]);
|
|
157
|
+
const allUserIds = [...new Set([...jobRows, ...timeoutRows].map((task) => task.userId))];
|
|
158
|
+
const allAgentPermanentIds = [
|
|
159
|
+
...new Set([...jobRows, ...timeoutRows].map((task) => task.agentPermanentId).filter(Boolean)),
|
|
160
|
+
];
|
|
161
|
+
const [usernamesById, agentNamesByPermanentId] = await Promise.all([
|
|
162
|
+
loadAdminChatTaskUsernames(allUserIds),
|
|
163
|
+
loadAdminChatTaskAgentNames(allAgentPermanentIds),
|
|
164
|
+
]);
|
|
165
|
+
const allTasks = [
|
|
166
|
+
...jobRows.map((row) => mapAdminChatTaskJobRow(row, usernamesById, agentNamesByPermanentId)),
|
|
167
|
+
...timeoutRows.map((row) => mapAdminChatTaskTimeoutRow(row, usernamesById, agentNamesByPermanentId)),
|
|
168
|
+
];
|
|
169
|
+
const nowTimestamp = Date.now();
|
|
170
|
+
const filteredTasks = allTasks
|
|
171
|
+
.filter((task) => matchesAdminChatTaskView(task, query, nowTimestamp))
|
|
172
|
+
.filter((task) => matchesAdminChatTaskSearch(task, query.search))
|
|
173
|
+
.sort((leftTask, rightTask) => compareAdminChatTasks(leftTask, rightTask, query.view));
|
|
174
|
+
const pageOffset = (query.page - 1) * query.pageSize;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
items: filteredTasks.slice(pageOffset, pageOffset + query.pageSize),
|
|
178
|
+
counters: createAdminChatTaskCounters(allTasks, nowTimestamp),
|
|
179
|
+
total: filteredTasks.length,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Loads admin task-manager data through the existing PostgreSQL raw SQL path.
|
|
185
|
+
*
|
|
186
|
+
* @private function of `getAdminChatTasksResponse`
|
|
187
|
+
*/
|
|
188
|
+
async function getAdminChatTasksViaClientSql(query: ParsedAdminChatTaskQuery): Promise<GetAdminChatTasksData> {
|
|
66
189
|
const sql = await $provideClientSql();
|
|
67
190
|
const userChatJobTable = quoteIdentifier(await $getTableName('UserChatJob'));
|
|
68
191
|
const userChatTimeoutTable = quoteIdentifier(await resolvePrefixedTableName('UserChatTimeout'));
|
|
@@ -118,6 +241,414 @@ export async function getAdminChatTasks(query: ParsedAdminChatTaskQuery): Promis
|
|
|
118
241
|
};
|
|
119
242
|
}
|
|
120
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Loads lightweight durable chat-job rows for SQLite mode.
|
|
246
|
+
*
|
|
247
|
+
* @private function of `getAdminChatTasksResponse`
|
|
248
|
+
*/
|
|
249
|
+
async function loadAdminChatTaskJobRows(): Promise<Array<AdminChatTaskJobRow>> {
|
|
250
|
+
const userChatJobTable = await provideUserChatJobTable();
|
|
251
|
+
const { data, error } = await userChatJobTable.select(
|
|
252
|
+
'id,createdAt,queuedAt,startedAt,updatedAt,completedAt,cancelRequestedAt,lastHeartbeatAt,leaseExpiresAt,attemptCount,failureReason,failureDetails,userId,agentPermanentId,chatId,status',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (error) {
|
|
256
|
+
throw new Error(`Failed to list admin user chat jobs: ${error.message}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return (data || []) as unknown as Array<AdminChatTaskJobRow>;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Loads lightweight durable timeout rows for SQLite mode.
|
|
264
|
+
*
|
|
265
|
+
* @private function of `getAdminChatTasksResponse`
|
|
266
|
+
*/
|
|
267
|
+
async function loadAdminChatTaskTimeoutRows(): Promise<Array<AdminChatTaskTimeoutRow>> {
|
|
268
|
+
const userChatTimeoutTable = await provideUserChatTimeoutTable();
|
|
269
|
+
const { data, error } = await userChatTimeoutTable.select(
|
|
270
|
+
'id,createdAt,queuedAt,startedAt,updatedAt,completedAt,cancelRequestedAt,pausedAt,leaseExpiresAt,recurrenceIntervalMs,attemptCount,failureReason,userId,agentPermanentId,chatId,status',
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (error) {
|
|
274
|
+
throw new Error(`Failed to list admin user chat timeouts: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return (data || []) as unknown as Array<AdminChatTaskTimeoutRow>;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Loads usernames keyed by user id for admin task rendering and search.
|
|
282
|
+
*
|
|
283
|
+
* @private function of `getAdminChatTasksResponse`
|
|
284
|
+
*/
|
|
285
|
+
async function loadAdminChatTaskUsernames(userIds: ReadonlyArray<number>): Promise<Map<number, string>> {
|
|
286
|
+
if (userIds.length === 0) {
|
|
287
|
+
return new Map();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const supabase = $provideSupabaseForServer();
|
|
291
|
+
const userTable = await $getTableName('User');
|
|
292
|
+
const { data, error } = await supabase.from(userTable).select('id,username').in('id', [...new Set(userIds)]);
|
|
293
|
+
|
|
294
|
+
if (error) {
|
|
295
|
+
throw new Error(`Failed to load admin task-manager users: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return new Map(
|
|
299
|
+
((data || []) as Array<AdminChatTaskUserLookupRow>).map((userRow) => [userRow.id, userRow.username] as const),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Loads agent names keyed by permanent id for admin task rendering and search.
|
|
305
|
+
*
|
|
306
|
+
* @private function of `getAdminChatTasksResponse`
|
|
307
|
+
*/
|
|
308
|
+
async function loadAdminChatTaskAgentNames(
|
|
309
|
+
agentPermanentIds: ReadonlyArray<string>,
|
|
310
|
+
): Promise<Map<string, string | null>> {
|
|
311
|
+
if (agentPermanentIds.length === 0) {
|
|
312
|
+
return new Map();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const supabase = $provideSupabaseForServer();
|
|
316
|
+
const agentTable = await $getTableName('Agent');
|
|
317
|
+
const { data, error } = await supabase
|
|
318
|
+
.from(agentTable)
|
|
319
|
+
.select('permanentId,agentName')
|
|
320
|
+
.in('permanentId', [...new Set(agentPermanentIds)]);
|
|
321
|
+
|
|
322
|
+
if (error) {
|
|
323
|
+
throw new Error(`Failed to load admin task-manager agents: ${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return new Map(
|
|
327
|
+
((data || []) as Array<AdminChatTaskAgentLookupRow>)
|
|
328
|
+
.filter((agentRow): agentRow is AdminChatTaskAgentLookupRow & { permanentId: string } => Boolean(agentRow.permanentId))
|
|
329
|
+
.map((agentRow) => [agentRow.permanentId, agentRow.agentName] as const),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Maps one SQLite-backed chat job into the public admin task row shape.
|
|
335
|
+
*
|
|
336
|
+
* @private function of `getAdminChatTasksResponse`
|
|
337
|
+
*/
|
|
338
|
+
function mapAdminChatTaskJobRow(
|
|
339
|
+
row: AdminChatTaskJobRow,
|
|
340
|
+
usernamesById: ReadonlyMap<number, string>,
|
|
341
|
+
agentNamesByPermanentId: ReadonlyMap<string, string | null>,
|
|
342
|
+
): AdminChatTaskRecord {
|
|
343
|
+
return {
|
|
344
|
+
id: row.id,
|
|
345
|
+
kind: 'CHAT_COMPLETION',
|
|
346
|
+
status: row.status,
|
|
347
|
+
createdAt: row.createdAt,
|
|
348
|
+
queuedAt: row.queuedAt,
|
|
349
|
+
startedAt: row.startedAt,
|
|
350
|
+
updatedAt: row.updatedAt,
|
|
351
|
+
finishedAt: row.completedAt,
|
|
352
|
+
cancelRequestedAt: row.cancelRequestedAt,
|
|
353
|
+
pausedAt: null,
|
|
354
|
+
lastHeartbeatAt: row.lastHeartbeatAt,
|
|
355
|
+
leaseExpiresAt: row.leaseExpiresAt,
|
|
356
|
+
recurrenceIntervalMs: null,
|
|
357
|
+
priority: null,
|
|
358
|
+
attemptCount: row.attemptCount,
|
|
359
|
+
retryCount: Math.max(0, row.attemptCount - 1),
|
|
360
|
+
lastErrorSummary: row.failureReason,
|
|
361
|
+
lastErrorDetails: row.failureDetails ?? null,
|
|
362
|
+
userId: row.userId,
|
|
363
|
+
username: usernamesById.get(row.userId) ?? null,
|
|
364
|
+
agentPermanentId: row.agentPermanentId,
|
|
365
|
+
agentName: agentNamesByPermanentId.get(row.agentPermanentId) ?? null,
|
|
366
|
+
chatId: row.chatId,
|
|
367
|
+
workerId: null,
|
|
368
|
+
queueName: 'user-chat-jobs',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Maps one SQLite-backed timeout into the public admin task row shape.
|
|
374
|
+
*
|
|
375
|
+
* @private function of `getAdminChatTasksResponse`
|
|
376
|
+
*/
|
|
377
|
+
function mapAdminChatTaskTimeoutRow(
|
|
378
|
+
row: AdminChatTaskTimeoutRow,
|
|
379
|
+
usernamesById: ReadonlyMap<number, string>,
|
|
380
|
+
agentNamesByPermanentId: ReadonlyMap<string, string | null>,
|
|
381
|
+
): AdminChatTaskRecord {
|
|
382
|
+
return {
|
|
383
|
+
id: row.id,
|
|
384
|
+
kind: 'CHAT_TIMEOUT',
|
|
385
|
+
status: row.status,
|
|
386
|
+
createdAt: row.createdAt,
|
|
387
|
+
queuedAt: row.queuedAt,
|
|
388
|
+
startedAt: row.startedAt,
|
|
389
|
+
updatedAt: row.updatedAt,
|
|
390
|
+
finishedAt: row.completedAt,
|
|
391
|
+
cancelRequestedAt: row.cancelRequestedAt,
|
|
392
|
+
pausedAt: row.pausedAt,
|
|
393
|
+
lastHeartbeatAt: null,
|
|
394
|
+
leaseExpiresAt: row.leaseExpiresAt,
|
|
395
|
+
recurrenceIntervalMs: resolveNullableSqlNumber(row.recurrenceIntervalMs),
|
|
396
|
+
priority: null,
|
|
397
|
+
attemptCount: row.attemptCount,
|
|
398
|
+
retryCount: Math.max(0, row.attemptCount - 1),
|
|
399
|
+
lastErrorSummary: row.failureReason,
|
|
400
|
+
lastErrorDetails: null,
|
|
401
|
+
userId: row.userId,
|
|
402
|
+
username: usernamesById.get(row.userId) ?? null,
|
|
403
|
+
agentPermanentId: row.agentPermanentId,
|
|
404
|
+
agentName: agentNamesByPermanentId.get(row.agentPermanentId) ?? null,
|
|
405
|
+
chatId: row.chatId,
|
|
406
|
+
workerId: null,
|
|
407
|
+
queueName: 'user-chat-timeouts',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Returns whether one task belongs in the requested admin task-manager view.
|
|
413
|
+
*
|
|
414
|
+
* @private function of `getAdminChatTasksResponse`
|
|
415
|
+
*/
|
|
416
|
+
function matchesAdminChatTaskView(
|
|
417
|
+
task: AdminChatTaskRecord,
|
|
418
|
+
query: ParsedAdminChatTaskQuery,
|
|
419
|
+
nowTimestamp: number,
|
|
420
|
+
): boolean {
|
|
421
|
+
switch (query.view) {
|
|
422
|
+
case 'running':
|
|
423
|
+
return task.status === 'RUNNING';
|
|
424
|
+
case 'queued':
|
|
425
|
+
return task.status === 'QUEUED';
|
|
426
|
+
case 'failed':
|
|
427
|
+
return (
|
|
428
|
+
task.status === 'FAILED' &&
|
|
429
|
+
isIsoTimestampAtOrAfter(task.finishedAt, nowTimestamp - 24 * HOUR_IN_MILLISECONDS)
|
|
430
|
+
);
|
|
431
|
+
case 'all':
|
|
432
|
+
return isIsoTimestampAtOrAfter(task.updatedAt, nowTimestamp - query.timeWindowHours * HOUR_IN_MILLISECONDS);
|
|
433
|
+
case 'active':
|
|
434
|
+
default:
|
|
435
|
+
return task.status === 'QUEUED' || task.status === 'RUNNING';
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Returns whether one task matches the free-text admin search input.
|
|
441
|
+
*
|
|
442
|
+
* @private function of `getAdminChatTasksResponse`
|
|
443
|
+
*/
|
|
444
|
+
function matchesAdminChatTaskSearch(task: AdminChatTaskRecord, search: string): boolean {
|
|
445
|
+
if (!search) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (
|
|
450
|
+
task.id === search ||
|
|
451
|
+
task.id.startsWith(search) ||
|
|
452
|
+
task.chatId === search ||
|
|
453
|
+
task.chatId.startsWith(search) ||
|
|
454
|
+
task.agentPermanentId === search ||
|
|
455
|
+
task.agentPermanentId.startsWith(search)
|
|
456
|
+
) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const normalizedSearch = search.toLowerCase();
|
|
461
|
+
if ((task.agentName || '').toLowerCase().includes(normalizedSearch)) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
if ((task.username || '').toLowerCase().includes(normalizedSearch)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return /^\d+$/.test(search) && task.userId === Number.parseInt(search, 10);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Calculates the summary counters rendered above the admin task table.
|
|
473
|
+
*
|
|
474
|
+
* @private function of `getAdminChatTasksResponse`
|
|
475
|
+
*/
|
|
476
|
+
function createAdminChatTaskCounters(
|
|
477
|
+
tasks: ReadonlyArray<AdminChatTaskRecord>,
|
|
478
|
+
nowTimestamp: number,
|
|
479
|
+
): AdminChatTaskCounters {
|
|
480
|
+
const queuedTimestamps = tasks
|
|
481
|
+
.filter((task) => task.status === 'QUEUED')
|
|
482
|
+
.map((task) => parseIsoTimestamp(task.queuedAt))
|
|
483
|
+
.filter((timestamp): timestamp is number => timestamp !== null);
|
|
484
|
+
const oldestQueuedTimestamp =
|
|
485
|
+
queuedTimestamps.length > 0 ? Math.min(...queuedTimestamps) : null;
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
runningCount: tasks.filter((task) => task.status === 'RUNNING').length,
|
|
489
|
+
queuedCount: tasks.filter((task) => task.status === 'QUEUED').length,
|
|
490
|
+
failedLast24hCount: tasks.filter(
|
|
491
|
+
(task) =>
|
|
492
|
+
task.status === 'FAILED' &&
|
|
493
|
+
isIsoTimestampAtOrAfter(task.finishedAt, nowTimestamp - 24 * HOUR_IN_MILLISECONDS),
|
|
494
|
+
).length,
|
|
495
|
+
oldestQueuedAgeMs: oldestQueuedTimestamp === null ? null : Math.max(0, nowTimestamp - oldestQueuedTimestamp),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Compares two tasks using the same ordering semantics as the PostgreSQL dashboard query.
|
|
501
|
+
*
|
|
502
|
+
* @private function of `getAdminChatTasksResponse`
|
|
503
|
+
*/
|
|
504
|
+
function compareAdminChatTasks(
|
|
505
|
+
leftTask: AdminChatTaskRecord,
|
|
506
|
+
rightTask: AdminChatTaskRecord,
|
|
507
|
+
view: AdminChatTaskView,
|
|
508
|
+
): number {
|
|
509
|
+
switch (view) {
|
|
510
|
+
case 'running':
|
|
511
|
+
return (
|
|
512
|
+
compareNullableIsoTimestampsDescending(leftTask.startedAt, rightTask.startedAt) ||
|
|
513
|
+
compareIsoTimestampsDescending(leftTask.createdAt, rightTask.createdAt) ||
|
|
514
|
+
compareStringsDescending(leftTask.id, rightTask.id)
|
|
515
|
+
);
|
|
516
|
+
case 'queued':
|
|
517
|
+
return (
|
|
518
|
+
compareIsoTimestampsDescending(leftTask.createdAt, rightTask.createdAt) ||
|
|
519
|
+
compareStringsDescending(leftTask.id, rightTask.id)
|
|
520
|
+
);
|
|
521
|
+
case 'failed':
|
|
522
|
+
return (
|
|
523
|
+
compareNullableIsoTimestampsDescending(leftTask.finishedAt, rightTask.finishedAt) ||
|
|
524
|
+
compareIsoTimestampsDescending(leftTask.updatedAt, rightTask.updatedAt) ||
|
|
525
|
+
compareStringsDescending(leftTask.id, rightTask.id)
|
|
526
|
+
);
|
|
527
|
+
case 'all':
|
|
528
|
+
return (
|
|
529
|
+
compareIsoTimestampsDescending(leftTask.updatedAt, rightTask.updatedAt) ||
|
|
530
|
+
compareIsoTimestampsDescending(leftTask.createdAt, rightTask.createdAt) ||
|
|
531
|
+
compareStringsDescending(leftTask.id, rightTask.id)
|
|
532
|
+
);
|
|
533
|
+
case 'active':
|
|
534
|
+
default:
|
|
535
|
+
return (
|
|
536
|
+
compareNumbersAscending(
|
|
537
|
+
resolveAdminChatTaskActiveStatusRank(leftTask.status),
|
|
538
|
+
resolveAdminChatTaskActiveStatusRank(rightTask.status),
|
|
539
|
+
) ||
|
|
540
|
+
compareNullableIsoTimestampsDescending(
|
|
541
|
+
leftTask.status === 'RUNNING' ? leftTask.startedAt : null,
|
|
542
|
+
rightTask.status === 'RUNNING' ? rightTask.startedAt : null,
|
|
543
|
+
) ||
|
|
544
|
+
compareNullableIsoTimestampsDescending(
|
|
545
|
+
leftTask.status === 'QUEUED' ? leftTask.createdAt : null,
|
|
546
|
+
rightTask.status === 'QUEUED' ? rightTask.createdAt : null,
|
|
547
|
+
) ||
|
|
548
|
+
compareIsoTimestampsDescending(leftTask.updatedAt, rightTask.updatedAt) ||
|
|
549
|
+
compareStringsDescending(leftTask.id, rightTask.id)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Resolves the status sort bucket used by the `Active` dashboard view.
|
|
556
|
+
*
|
|
557
|
+
* @private function of `getAdminChatTasksResponse`
|
|
558
|
+
*/
|
|
559
|
+
function resolveAdminChatTaskActiveStatusRank(status: UserChatJobStatus): number {
|
|
560
|
+
switch (status) {
|
|
561
|
+
case 'RUNNING':
|
|
562
|
+
return 0;
|
|
563
|
+
case 'QUEUED':
|
|
564
|
+
return 1;
|
|
565
|
+
default:
|
|
566
|
+
return 2;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Returns whether one ISO timestamp is at or after the given cutoff.
|
|
572
|
+
*
|
|
573
|
+
* @private function of `getAdminChatTasksResponse`
|
|
574
|
+
*/
|
|
575
|
+
function isIsoTimestampAtOrAfter(timestampIso: string | null, cutoffTimestamp: number): boolean {
|
|
576
|
+
const timestamp = parseIsoTimestamp(timestampIso);
|
|
577
|
+
return timestamp !== null && timestamp >= cutoffTimestamp;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Parses one ISO timestamp into milliseconds since epoch.
|
|
582
|
+
*
|
|
583
|
+
* @private function of `getAdminChatTasksResponse`
|
|
584
|
+
*/
|
|
585
|
+
function parseIsoTimestamp(timestampIso: string | null): number | null {
|
|
586
|
+
if (!timestampIso) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const timestamp = Date.parse(timestampIso);
|
|
591
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Sorts timestamps descending while keeping `null` values last.
|
|
596
|
+
*
|
|
597
|
+
* @private function of `getAdminChatTasksResponse`
|
|
598
|
+
*/
|
|
599
|
+
function compareNullableIsoTimestampsDescending(leftTimestampIso: string | null, rightTimestampIso: string | null): number {
|
|
600
|
+
const leftTimestamp = parseIsoTimestamp(leftTimestampIso);
|
|
601
|
+
const rightTimestamp = parseIsoTimestamp(rightTimestampIso);
|
|
602
|
+
|
|
603
|
+
if (leftTimestamp === rightTimestamp) {
|
|
604
|
+
return 0;
|
|
605
|
+
}
|
|
606
|
+
if (leftTimestamp === null) {
|
|
607
|
+
return 1;
|
|
608
|
+
}
|
|
609
|
+
if (rightTimestamp === null) {
|
|
610
|
+
return -1;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return compareNumbersDescending(leftTimestamp, rightTimestamp);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Sorts required timestamps descending.
|
|
618
|
+
*
|
|
619
|
+
* @private function of `getAdminChatTasksResponse`
|
|
620
|
+
*/
|
|
621
|
+
function compareIsoTimestampsDescending(leftTimestampIso: string, rightTimestampIso: string): number {
|
|
622
|
+
return compareNullableIsoTimestampsDescending(leftTimestampIso, rightTimestampIso);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Sorts numbers ascending.
|
|
627
|
+
*
|
|
628
|
+
* @private function of `getAdminChatTasksResponse`
|
|
629
|
+
*/
|
|
630
|
+
function compareNumbersAscending(leftNumber: number, rightNumber: number): number {
|
|
631
|
+
return leftNumber - rightNumber;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Sorts numbers descending.
|
|
636
|
+
*
|
|
637
|
+
* @private function of `getAdminChatTasksResponse`
|
|
638
|
+
*/
|
|
639
|
+
function compareNumbersDescending(leftNumber: number, rightNumber: number): number {
|
|
640
|
+
return rightNumber - leftNumber;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Sorts strings descending.
|
|
645
|
+
*
|
|
646
|
+
* @private function of `getAdminChatTasksResponse`
|
|
647
|
+
*/
|
|
648
|
+
function compareStringsDescending(leftString: string, rightString: string): number {
|
|
649
|
+
return leftString === rightString ? 0 : leftString < rightString ? 1 : -1;
|
|
650
|
+
}
|
|
651
|
+
|
|
121
652
|
/**
|
|
122
653
|
* Creates the `WHERE` clause and parameter list for the paginated admin task query.
|
|
123
654
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { getMetadata } from '../database/getMetadata';
|
|
1
2
|
import { getUserDataValue, upsertUserDataValue } from './userData';
|
|
2
|
-
import {
|
|
3
|
+
import { DEFAULT_THEME_METADATA_KEY, resolveThemeMode, type ThemeMode } from '../constants/themeMode';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Stored theme preference payload persisted in `UserData`.
|
|
@@ -26,6 +27,13 @@ const USER_THEME_MODE_SETTINGS_VERSION = 1 as const;
|
|
|
26
27
|
*/
|
|
27
28
|
const USER_THEME_MODE_SETTINGS_USER_DATA_KEY = 'settings:theme-mode';
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the server-level default theme mode from metadata.
|
|
32
|
+
*/
|
|
33
|
+
async function getDefaultThemeModeForServer(): Promise<ThemeMode> {
|
|
34
|
+
return resolveThemeMode(await getMetadata(DEFAULT_THEME_METADATA_KEY));
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
/**
|
|
30
38
|
* Validates and normalizes one raw `UserData.value` theme payload.
|
|
31
39
|
*/
|
|
@@ -68,7 +76,7 @@ export async function getUserThemeModeSettingsSnapshotForUser(userId: number): P
|
|
|
68
76
|
const storedSettings = await getUserThemeModeSettingsForUser(userId);
|
|
69
77
|
|
|
70
78
|
return {
|
|
71
|
-
themeMode: storedSettings?.themeMode ||
|
|
79
|
+
themeMode: storedSettings?.themeMode || (await getDefaultThemeModeForServer()),
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
82
|
|