@promptbook/cli 0.112.0 → 0.113.0-0

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 (89) hide show
  1. package/README.md +45 -29
  2. package/agents/default/developer.book +2 -1
  3. package/apps/agents-server/next.config.ts +21 -0
  4. package/apps/agents-server/src/app/admin/limits/LimitsClient.tsx +1 -1
  5. package/apps/agents-server/src/app/admin/update/UpdateClient.tsx +52 -33
  6. package/apps/agents-server/src/app/api/agent-folders/[folderId]/visibility/route.ts +19 -7
  7. package/apps/agents-server/src/app/api/agents/[agentName]/route.ts +8 -18
  8. package/apps/agents-server/src/app/api/internal/agent-runner-limits/route.ts +1 -1
  9. package/apps/agents-server/src/app/api/v1/agents/[agentId]/route.ts +13 -8
  10. package/apps/agents-server/src/components/AgentProfile/AgentCapabilityChips.tsx +39 -5
  11. package/apps/agents-server/src/components/Homepage/AgentCard.tsx +7 -1
  12. package/apps/agents-server/src/components/Homepage/AgentsList.tsx +3 -0
  13. package/apps/agents-server/src/components/Homepage/AgentsListHeader.tsx +41 -0
  14. package/apps/agents-server/src/components/Homepage/hiddenFolders.ts +104 -0
  15. package/apps/agents-server/src/components/Homepage/useAgentsListQueryState.ts +35 -0
  16. package/apps/agents-server/src/components/Homepage/useAgentsListState.ts +24 -4
  17. package/apps/agents-server/src/constants/serverLimits.ts +19 -0
  18. package/apps/agents-server/src/database/seedCoreAgents.ts +0 -2
  19. package/apps/agents-server/src/database/seedDefaultAgents.ts +0 -2
  20. package/apps/agents-server/src/tools/agent_progress.ts +4 -10
  21. package/apps/agents-server/src/utils/agentVisibility.ts +25 -62
  22. package/apps/agents-server/src/utils/createAgentWithDefaultVisibility.ts +3 -1
  23. package/apps/agents-server/src/utils/externalChatRunner/processExternalUserChatJob.ts +9 -17
  24. package/apps/agents-server/src/utils/localChatRunner/LocalUserChatJobMetadata.ts +15 -5
  25. package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +17 -26
  26. package/apps/agents-server/src/utils/serverLimits.ts +2 -0
  27. package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerCoreAgents.ts +2 -2
  28. package/apps/agents-server/src/utils/serverManagement/createManagedServer/seedServerDefaultAgents.ts +1 -1
  29. package/apps/agents-server/src/utils/userChat/createUserChatHarnessProgressCard.ts +0 -6
  30. package/apps/agents-server/src/utils/userChat/createUserChatRunnerProgressCard.ts +0 -6
  31. package/apps/agents-server/src/utils/userChat/userChatMessageLifecycle.ts +69 -0
  32. package/apps/agents-server/src/utils/userChat/userChatProgressCard.ts +0 -6
  33. package/apps/agents-server/src/utils/vpsSelfUpdate.ts +138 -24
  34. package/esm/apps/agents-server/src/constants/serverLimits.d.ts +17 -0
  35. package/esm/index.es.js +581 -93
  36. package/esm/index.es.js.map +1 -1
  37. package/esm/scripts/run-agent-messages/main/runMultipleAgentMessages.d.ts +1 -0
  38. package/esm/scripts/run-agent-messages/main/tickAgentMessages.d.ts +1 -0
  39. package/esm/src/_packages/core.index.d.ts +22 -0
  40. package/esm/src/_packages/types.index.d.ts +2 -0
  41. package/esm/src/book-2.0/agent-source/AgentBasicInformation.d.ts +2 -0
  42. package/esm/src/book-2.0/agent-source/agentSourceVisibility.d.ts +97 -0
  43. package/esm/src/book-2.0/agent-source/agentSourceVisibility.test.d.ts +1 -0
  44. package/esm/src/book-components/BookEditor/BookEditorMonacoTokenization.d.ts +1 -0
  45. package/esm/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +6 -0
  46. package/esm/src/collection/agent-collection/CreateAgentInput.d.ts +2 -1
  47. package/esm/src/collection/agent-collection/constructors/agent-collection-in-supabase/prepareAgentSourceForPersistence.d.ts +21 -1
  48. package/esm/src/commitments/META_VISIBILITY/META_VISIBILITY.d.ts +27 -0
  49. package/esm/src/commitments/index.d.ts +2 -1
  50. package/esm/src/version.d.ts +1 -1
  51. package/package.json +1 -1
  52. package/src/_packages/core.index.ts +22 -0
  53. package/src/_packages/types.index.ts +2 -0
  54. package/src/book-2.0/agent-source/AgentBasicInformation.ts +2 -0
  55. package/src/book-2.0/agent-source/agentSourceVisibility.ts +214 -0
  56. package/src/book-2.0/agent-source/parseAgentSource/applyMetaCommitment.ts +17 -0
  57. package/src/book-2.0/book-language-documentation/createStandaloneBookLanguageMarkdown.ts +1 -0
  58. package/src/book-components/BookEditor/BookEditorMonacoTokenization.ts +11 -0
  59. package/src/book-components/BookEditor/useBookEditorMonacoLanguage.ts +7 -3
  60. package/src/cli/cli-commands/agents-server/buildAgentsServer.ts +144 -16
  61. package/src/cli/cli-commands/agents-server/startAgentsServer.ts +22 -2
  62. package/src/cli/cli-commands/coder/getDefaultCoderPackageJsonScripts.ts +1 -1
  63. package/src/cli/cli-commands/coder/run.ts +30 -0
  64. package/src/collection/agent-collection/CreateAgentInput.ts +10 -5
  65. package/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentCollectionInSupabase.ts +7 -3
  66. package/src/collection/agent-collection/constructors/agent-collection-in-supabase/createAgentPersistenceRecords.ts +7 -4
  67. package/src/collection/agent-collection/constructors/agent-collection-in-supabase/prepareAgentSourceForPersistence.ts +44 -8
  68. package/src/commitments/META_VISIBILITY/META_VISIBILITY.ts +78 -0
  69. package/src/commitments/index.ts +2 -0
  70. package/src/other/templates/getTemplatesPipelineCollection.ts +854 -692
  71. package/src/version.ts +2 -2
  72. package/src/versions.txt +1 -0
  73. package/umd/apps/agents-server/src/constants/serverLimits.d.ts +17 -0
  74. package/umd/index.umd.js +581 -93
  75. package/umd/index.umd.js.map +1 -1
  76. package/umd/scripts/run-agent-messages/main/runMultipleAgentMessages.d.ts +1 -0
  77. package/umd/scripts/run-agent-messages/main/tickAgentMessages.d.ts +1 -0
  78. package/umd/src/_packages/core.index.d.ts +22 -0
  79. package/umd/src/_packages/types.index.d.ts +2 -0
  80. package/umd/src/book-2.0/agent-source/AgentBasicInformation.d.ts +2 -0
  81. package/umd/src/book-2.0/agent-source/agentSourceVisibility.d.ts +97 -0
  82. package/umd/src/book-2.0/agent-source/agentSourceVisibility.test.d.ts +1 -0
  83. package/umd/src/book-components/BookEditor/BookEditorMonacoTokenization.d.ts +1 -0
  84. package/umd/src/cli/cli-commands/agents-server/buildAgentsServer.d.ts +6 -0
  85. package/umd/src/collection/agent-collection/CreateAgentInput.d.ts +2 -1
  86. package/umd/src/collection/agent-collection/constructors/agent-collection-in-supabase/prepareAgentSourceForPersistence.d.ts +21 -1
  87. package/umd/src/commitments/META_VISIBILITY/META_VISIBILITY.d.ts +27 -0
  88. package/umd/src/commitments/index.d.ts +2 -1
  89. package/umd/src/version.d.ts +1 -1
@@ -23,10 +23,11 @@ export function createLocalUserChatJobMetadata(options: {
23
23
  agentDirectoryName: string;
24
24
  threadId: string;
25
25
  threadCreatedAt: string;
26
+ jobId: string;
26
27
  queuedAt: string;
27
28
  expectedMessagesBeforeAnswer: number;
28
29
  }): LocalUserChatJobMetadata {
29
- const fileName = createLocalChatMessageFileName(options.threadId, options.threadCreatedAt);
30
+ const fileName = createLocalChatMessageFileName(options.threadId, options.threadCreatedAt, options.jobId);
30
31
 
31
32
  return {
32
33
  version: 1,
@@ -102,15 +103,24 @@ export function withoutLocalUserChatJobMetadata(parameters: UserChatJobParameter
102
103
  /**
103
104
  * Creates the filename used by one local message-thread book.
104
105
  */
105
- export function createLocalChatMessageFileName(threadId: string, threadCreatedAt: string): string {
106
+ export function createLocalChatMessageFileName(threadId: string, threadCreatedAt: string, jobId?: string): string {
106
107
  const createdOnDate = resolveLocalChatThreadCreatedOnDate(threadCreatedAt);
107
- const normalizedThreadId = threadId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
108
+ const normalizedThreadId = threadId
109
+ .trim()
110
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
111
+ .replace(/^-+|-+$/g, '');
112
+ const normalizedJobId = jobId
113
+ ?.trim()
114
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
115
+ .replace(/^-+|-+$/g, '');
108
116
 
109
117
  if (normalizedThreadId.length > 0) {
110
- return `${createdOnDate}-${normalizedThreadId}.book`;
118
+ return `${createdOnDate}-${normalizedThreadId}${normalizedJobId ? `-${normalizedJobId}` : ''}.book`;
111
119
  }
112
120
 
113
- return `${createdOnDate}-${Buffer.from(threadId, 'utf8').toString('hex') || 'thread'}.book`;
121
+ return `${createdOnDate}-${Buffer.from(threadId, 'utf8').toString('hex') || 'thread'}${
122
+ normalizedJobId ? `-${normalizedJobId}` : ''
123
+ }.book`;
114
124
  }
115
125
 
116
126
  /**
@@ -13,7 +13,7 @@ import { createUserChatRunnerProgressCard } from '../userChat/createUserChatRunn
13
13
  import { updateUserChatAssistantMessage } from '../userChat/updateUserChatAssistantMessage';
14
14
  import type { UserChatJobRecord } from '../userChat/UserChatJobRecord';
15
15
  import type { UserChatJobRow } from '../userChat/UserChatJobRow';
16
- import { resolvePromptThreadBeforeUserMessage } from '../userChat/userChatMessageLifecycle';
16
+ import { createUserChatRunnerThreadMessages } from '../userChat/userChatMessageLifecycle';
17
17
  import {
18
18
  createLocalUserChatJobMetadata,
19
19
  getLocalUserChatJobMetadata,
@@ -85,9 +85,11 @@ export async function processLocalUserChatJob(job: UserChatJobRecord): Promise<P
85
85
  /**
86
86
  * Processes the next local user chat job waiting for worker attention.
87
87
  */
88
- export async function processNextLocalUserChatJob(options: {
89
- preferredJobId?: string;
90
- } = {}): Promise<ProcessLocalUserChatJobResult | null> {
88
+ export async function processNextLocalUserChatJob(
89
+ options: {
90
+ preferredJobId?: string;
91
+ } = {},
92
+ ): Promise<ProcessLocalUserChatJobResult | null> {
91
93
  if (options.preferredJobId) {
92
94
  const claimedPreferredJob = await claimNextQueuedUserChatJob({ preferredJobId: options.preferredJobId });
93
95
  if (claimedPreferredJob) {
@@ -142,27 +144,11 @@ async function enqueueLocalUserChatJob(job: UserChatJobRecord): Promise<ProcessL
142
144
  throw new Error(`User message "${job.userMessageId}" was not found in chat "${job.chatId}".`);
143
145
  }
144
146
 
145
- const previousThreadMessages = resolvePromptThreadBeforeUserMessage(chat.messages, job.userMessageId);
146
- if (previousThreadMessages.some((message) => message.sender === 'AGENT' && message.isComplete === false)) {
147
- return { didMutate: false, outcome: 'waiting' };
148
- }
149
-
150
- const agentInitialMessage = previousThreadMessages.length === 0
151
- ? parseAgentSource(agentSourceSnapshot.agentSource).initialMessage
152
- : null;
153
- const initialAgentThreadMessages = agentInitialMessage
154
- ? [{ sender: 'AGENT' as const, content: agentInitialMessage }]
155
- : [];
156
-
157
- const threadMessages = [
158
- ...initialAgentThreadMessages,
159
- ...[...previousThreadMessages, userMessage]
160
- .filter((message) => message.isComplete !== false)
161
- .filter((message) => message.sender === 'USER' || message.sender === 'AGENT'),
162
- ].map((message) => ({
163
- sender: String(message.sender),
164
- content: message.content,
165
- }));
147
+ const threadMessages = createUserChatRunnerThreadMessages({
148
+ messages: chat.messages,
149
+ userMessageId: job.userMessageId,
150
+ resolveInitialAgentMessage: () => parseAgentSource(agentSourceSnapshot.agentSource).initialMessage,
151
+ });
166
152
 
167
153
  const agentFolder = await ensureLocalAgentFolder(agentSourceSnapshot);
168
154
  const queuedAt = new Date().toISOString();
@@ -170,6 +156,7 @@ async function enqueueLocalUserChatJob(job: UserChatJobRecord): Promise<ProcessL
170
156
  agentDirectoryName: agentFolder.directoryName,
171
157
  threadId: chat.id,
172
158
  threadCreatedAt: chat.createdAt,
159
+ jobId: job.id,
173
160
  queuedAt,
174
161
  expectedMessagesBeforeAnswer: threadMessages.length,
175
162
  });
@@ -326,7 +313,11 @@ async function handleLocalUserChatJobProcessingError(
326
313
  const metadata = getLocalUserChatJobMetadata(job);
327
314
  const failureReason = error instanceof Error ? error.message : 'Local agent runner synchronization failed.';
328
315
 
329
- if (job.status === 'QUEUED' || (job.status === 'RUNNING' && !metadata) || (metadata && isLocalUserChatJobTimedOut(metadata))) {
316
+ if (
317
+ job.status === 'QUEUED' ||
318
+ (job.status === 'RUNNING' && !metadata) ||
319
+ (metadata && isLocalUserChatJobTimedOut(metadata))
320
+ ) {
330
321
  await persistUserChatJobTerminalState({
331
322
  job,
332
323
  status: 'FAILED',
@@ -46,6 +46,7 @@ export type SpawnAgentLimits = {
46
46
  */
47
47
  export type LocalAgentRunnerLimits = {
48
48
  readonly maxFailedAttempts: number;
49
+ readonly maxParallelMessages: number;
49
50
  };
50
51
 
51
52
  /**
@@ -270,6 +271,7 @@ export async function getLocalAgentRunnerLimits(): Promise<LocalAgentRunnerLimit
270
271
  const limits = await getServerLimits();
271
272
  return {
272
273
  maxFailedAttempts: limits[SERVER_LIMIT_KEYS.LOCAL_AGENT_RUNNER_MAX_FAILED_ATTEMPTS],
274
+ maxParallelMessages: limits[SERVER_LIMIT_KEYS.LOCAL_AGENT_RUNNER_MAX_PARALLEL_MESSAGES],
273
275
  };
274
276
  }
275
277
 
@@ -35,7 +35,7 @@ export async function seedServerCoreAgents(
35
35
  const createdAt = new Date().toISOString();
36
36
  const { agentInsertRecord, agentHistoryInsertRecord } = createAgentPersistenceRecords(
37
37
  coreAgentBook,
38
- { folderId: coreFolderId, sortOrder: index, visibility: DEFAULT_AGENT_VISIBILITY },
38
+ { folderId: coreFolderId, sortOrder: index },
39
39
  createdAt,
40
40
  );
41
41
 
@@ -158,5 +158,5 @@ async function insertCoreFolder(
158
158
  }),
159
159
  );
160
160
 
161
- return insertResult.rows[0].id;
161
+ return insertResult.rows[0]!.id;
162
162
  }
@@ -29,7 +29,7 @@ export async function seedServerDefaultAgents(
29
29
  const createdAt = new Date().toISOString();
30
30
  const { agentInsertRecord, agentHistoryInsertRecord } = createAgentPersistenceRecords(
31
31
  defaultAgentBook,
32
- { sortOrder: index, visibility: DEFAULT_AGENT_VISIBILITY },
32
+ { sortOrder: index },
33
33
  createdAt,
34
34
  );
35
35
 
@@ -4,11 +4,6 @@ import { TOOL_TITLES } from '../../../../../src/book-components/Chat/utils/getTo
4
4
  import { isVisibleChatToolCall } from '../../../../../src/book-components/Chat/utils/isVisibleChatToolCall';
5
5
  import { resolveToolCallState } from '../../../../../src/book-components/Chat/utils/resolveToolCallState';
6
6
 
7
- /**
8
- * Default title used while a running chat job reports harness action progress.
9
- */
10
- const USER_CHAT_HARNESS_PROGRESS_TITLE = 'Working on your request';
11
-
12
7
  /**
13
8
  * Creates a user-facing progress card from the latest visible harness/tool action.
14
9
  *
@@ -24,7 +19,6 @@ export function createUserChatHarnessProgressCard(
24
19
  }
25
20
 
26
21
  return {
27
- title: USER_CHAT_HARNESS_PROGRESS_TITLE,
28
22
  now: createHarnessProgressNowText(latestVisibleToolCall),
29
23
  items: [],
30
24
  updatedAt: new Date().toISOString() as NonNullable<ChatMessage['progressCard']>['updatedAt'],
@@ -11,11 +11,6 @@ export type UserChatRunnerKind = 'local' | 'external';
11
11
  */
12
12
  export type UserChatRunnerProgressPhase = 'queued_for_runner';
13
13
 
14
- /**
15
- * Default title used while an out-of-process runner is working on a chat answer.
16
- */
17
- const USER_CHAT_RUNNER_PROGRESS_TITLE = 'Working on your request';
18
-
19
14
  /**
20
15
  * User-facing labels for durable chat runners.
21
16
  */
@@ -35,7 +30,6 @@ export function createUserChatRunnerProgressCard(options: {
35
30
  readonly phase: UserChatRunnerProgressPhase;
36
31
  }): NonNullable<ChatMessage['progressCard']> {
37
32
  return {
38
- title: USER_CHAT_RUNNER_PROGRESS_TITLE,
39
33
  now: createUserChatRunnerProgressNowText(options),
40
34
  items: [],
41
35
  updatedAt: new Date().toISOString() as NonNullable<ChatMessage['progressCard']>['updatedAt'],
@@ -4,6 +4,13 @@ import type { ChatMessage } from '../../../../../src/_packages/types.index';
4
4
  */
5
5
  export type UserChatMessageLifecycleState = NonNullable<ChatMessage['lifecycleState']>;
6
6
 
7
+ /**
8
+ * Message shape written into local/external chat-runner `.book` files.
9
+ */
10
+ export type UserChatRunnerThreadMessage = Pick<ChatMessage, 'content'> & {
11
+ sender: 'USER' | 'AGENT';
12
+ };
13
+
7
14
  /**
8
15
  * Creates one persisted user-authored message for a durable chat turn.
9
16
  */
@@ -89,6 +96,36 @@ export function resolvePromptThreadBeforeUserMessage(
89
96
  return messages.slice(0, userMessageIndex);
90
97
  }
91
98
 
99
+ /**
100
+ * Creates the complete chat thread mirrored to a runner before it writes the next answer.
101
+ */
102
+ export function createUserChatRunnerThreadMessages(options: {
103
+ messages: ReadonlyArray<ChatMessage>;
104
+ userMessageId: string;
105
+ resolveInitialAgentMessage: () => string | null | undefined;
106
+ }): Array<UserChatRunnerThreadMessage> {
107
+ const userMessage = options.messages.find((message) => message.id === options.userMessageId);
108
+ if (!userMessage) {
109
+ return [];
110
+ }
111
+
112
+ const previousThreadMessages = resolvePromptThreadBeforeUserMessage(options.messages, options.userMessageId);
113
+
114
+ return [
115
+ ...createInitialUserChatRunnerThreadMessages({
116
+ isFirstUserTurn: previousThreadMessages.length === 0,
117
+ resolveInitialAgentMessage: options.resolveInitialAgentMessage,
118
+ }),
119
+ ...previousThreadMessages,
120
+ userMessage,
121
+ ]
122
+ .filter(isUserChatRunnerThreadMessage)
123
+ .map((message) => ({
124
+ sender: message.sender,
125
+ content: message.content,
126
+ }));
127
+ }
128
+
92
129
  /**
93
130
  * Maps durable job status values to message lifecycle labels rendered by the UI.
94
131
  */
@@ -107,3 +144,35 @@ export function resolveMessageLifecycleStateFromJobStatus(status: string): UserC
107
144
  return 'completed';
108
145
  }
109
146
  }
147
+
148
+ /**
149
+ * Recreates the synthetic first agent message that the UI shows for empty durable chats.
150
+ */
151
+ function createInitialUserChatRunnerThreadMessages(options: {
152
+ isFirstUserTurn: boolean;
153
+ resolveInitialAgentMessage: () => string | null | undefined;
154
+ }): Array<UserChatRunnerThreadMessage> {
155
+ if (!options.isFirstUserTurn) {
156
+ return [];
157
+ }
158
+
159
+ const initialAgentMessage = options.resolveInitialAgentMessage();
160
+ if (!initialAgentMessage || initialAgentMessage.trim().length === 0) {
161
+ return [];
162
+ }
163
+
164
+ return [
165
+ {
166
+ sender: 'AGENT',
167
+ content: initialAgentMessage,
168
+ },
169
+ ];
170
+ }
171
+
172
+ /**
173
+ * Returns true when one persisted message should be visible to out-of-process runners.
174
+ */
175
+ function isUserChatRunnerThreadMessage(message: ChatMessage): message is ChatMessage & UserChatRunnerThreadMessage {
176
+ const isRunnerSender = message.sender === 'USER' || message.sender === 'AGENT';
177
+ return message.isComplete !== false && isRunnerSender;
178
+ }
@@ -75,11 +75,6 @@ export type UserChatProgressContext = {
75
75
  readonly isAgentKitCached?: boolean;
76
76
  };
77
77
 
78
- /**
79
- * Default title used by durable chat progress cards.
80
- */
81
- const USER_CHAT_PROGRESS_TITLE = 'Working on your request';
82
-
83
78
  /**
84
79
  * Ordered progress item identifiers shared across all durable chat phases.
85
80
  */
@@ -165,7 +160,6 @@ export function createUserChatProgressCard(
165
160
  const { now, next } = composeUserChatProgressPhaseText(phase, context);
166
161
 
167
162
  return {
168
- title: USER_CHAT_PROGRESS_TITLE,
169
163
  now,
170
164
  next,
171
165
  items: USER_CHAT_PROGRESS_ITEMS.map((item) => ({
@@ -203,6 +203,10 @@ export type VpsSelfUpdateOverview = {
203
203
  * Author timestamp of the latest remote commit in ISO format.
204
204
  */
205
205
  readonly latestRemoteCommitDate: string | null;
206
+ /**
207
+ * Latest remote commit subject.
208
+ */
209
+ readonly latestRemoteCommitMessage: string | null;
206
210
  /**
207
211
  * Number of commits the deployed checkout is behind the latest remote commit, or `null` when unknown.
208
212
  */
@@ -456,22 +460,24 @@ export async function readVpsSelfUpdateOverview(): Promise<VpsSelfUpdateOverview
456
460
  });
457
461
  }
458
462
 
459
- const [currentCommitSha, currentCommitMessage, currentCommitDate, latestRemoteCommitSha] = await Promise.all([
460
- runGitInRepository(repositoryDirectory, ['rev-parse', 'HEAD']),
461
- runGitInRepository(repositoryDirectory, ['log', '-1', '--format=%s']),
462
- runGitInRepository(repositoryDirectory, ['log', '-1', '--format=%aI']),
463
- readRemoteCommitSha(repositoryDirectory, currentEnvironment.branch, originRepositoryUrl),
463
+ await refreshVpsSelfUpdateRemoteBranch(repositoryDirectory, currentEnvironment.branch, originRepositoryUrl);
464
+
465
+ const [currentCommit, latestRemoteCommitSha] = await Promise.all([
466
+ readCommitMetadataFromRepository(repositoryDirectory, 'HEAD'),
467
+ readLatestRemoteBranchCommitSha(repositoryDirectory, currentEnvironment.branch, originRepositoryUrl),
464
468
  ]);
465
- const latestRemoteCommitDate = latestRemoteCommitSha
466
- ? await readCommitDateFromRepository(repositoryDirectory, latestRemoteCommitSha)
469
+ const latestRemoteCommit = latestRemoteCommitSha
470
+ ? await readCommitMetadataFromRepository(repositoryDirectory, latestRemoteCommitSha)
467
471
  : null;
472
+ const currentCommitSha = currentCommit?.commitSha ?? null;
473
+ const latestRemoteCommitResolvedSha = latestRemoteCommit?.commitSha ?? latestRemoteCommitSha;
468
474
  const commitsBehindCount =
469
- currentCommitSha && latestRemoteCommitSha
470
- ? await countCommitsBetween(repositoryDirectory, currentCommitSha, latestRemoteCommitSha)
475
+ currentCommitSha && latestRemoteCommitResolvedSha
476
+ ? await countCommitsBetween(repositoryDirectory, currentCommitSha, latestRemoteCommitResolvedSha)
471
477
  : null;
472
478
  const pendingCommits =
473
- currentCommitSha && latestRemoteCommitSha
474
- ? await listCommitsBetween(repositoryDirectory, currentCommitSha, latestRemoteCommitSha)
479
+ currentCommitSha && latestRemoteCommitResolvedSha
480
+ ? await listCommitsBetween(repositoryDirectory, currentCommitSha, latestRemoteCommitResolvedSha)
475
481
  : [];
476
482
  const resolvedJob = resolveVpsSelfUpdateJobForOverview(job, {
477
483
  currentEnvironment,
@@ -486,15 +492,16 @@ export async function readVpsSelfUpdateOverview(): Promise<VpsSelfUpdateOverview
486
492
  repositoryDirectory,
487
493
  currentCommitSha,
488
494
  currentCommitShortSha: abbreviateCommitSha(currentCommitSha),
489
- currentCommitMessage,
490
- currentCommitDate,
491
- latestRemoteCommitSha,
492
- latestRemoteCommitShortSha: abbreviateCommitSha(latestRemoteCommitSha),
493
- latestRemoteCommitDate,
495
+ currentCommitMessage: currentCommit?.subject ?? null,
496
+ currentCommitDate: currentCommit?.authoredAt ?? null,
497
+ latestRemoteCommitSha: latestRemoteCommitResolvedSha,
498
+ latestRemoteCommitShortSha: abbreviateCommitSha(latestRemoteCommitResolvedSha),
499
+ latestRemoteCommitDate: latestRemoteCommit?.authoredAt ?? null,
500
+ latestRemoteCommitMessage: latestRemoteCommit?.subject ?? null,
494
501
  commitsBehindCount,
495
502
  pendingCommits,
496
503
  isUpdateAvailable: Boolean(
497
- currentCommitSha && latestRemoteCommitSha && currentCommitSha !== latestRemoteCommitSha,
504
+ currentCommitSha && latestRemoteCommitResolvedSha && currentCommitSha !== latestRemoteCommitResolvedSha,
498
505
  ),
499
506
  originRepositoryUrl,
500
507
  isOriginRepositoryDefault: originRepositoryUrl === VPS_SELF_UPDATE_DEFAULT_ORIGIN_REPOSITORY_URL,
@@ -525,11 +532,34 @@ export type VpsSelfUpdatePendingCommit = {
525
532
  readonly authoredAt: string | null;
526
533
  };
527
534
 
535
+ /**
536
+ * Browser-safe metadata read from one git commit object.
537
+ */
538
+ type VpsSelfUpdateCommitMetadata = {
539
+ /**
540
+ * Full commit hash.
541
+ */
542
+ readonly commitSha: string;
543
+ /**
544
+ * Single-line commit subject.
545
+ */
546
+ readonly subject: string;
547
+ /**
548
+ * Author timestamp in ISO format or `null` when unknown.
549
+ */
550
+ readonly authoredAt: string | null;
551
+ };
552
+
528
553
  /**
529
554
  * Hard ceiling for the pending-commits listing returned in the overview to avoid huge payloads on a long-stale server.
530
555
  */
531
556
  const VPS_SELF_UPDATE_MAX_PENDING_COMMITS = 100;
532
557
 
558
+ /**
559
+ * Number of latest branch commits fetched for the update overview.
560
+ */
561
+ const VPS_SELF_UPDATE_OVERVIEW_FETCH_DEPTH = VPS_SELF_UPDATE_MAX_PENDING_COMMITS + 1;
562
+
533
563
  /**
534
564
  * Browser-safe summary of one commit that the super admin can pick from the custom-target picker.
535
565
  */
@@ -825,6 +855,7 @@ function createUnavailableOverview(context: {
825
855
  latestRemoteCommitSha: null,
826
856
  latestRemoteCommitShortSha: null,
827
857
  latestRemoteCommitDate: null,
858
+ latestRemoteCommitMessage: null,
828
859
  commitsBehindCount: null,
829
860
  pendingCommits: [],
830
861
  isUpdateAvailable: false,
@@ -1171,6 +1202,59 @@ async function runGitInRepository(repositoryDirectory: string, args: ReadonlyArr
1171
1202
  }
1172
1203
  }
1173
1204
 
1205
+ /**
1206
+ * Fetches the tracked remote branch into the local object database before building the browser overview.
1207
+ *
1208
+ * @param repositoryDirectory - Repository checkout path.
1209
+ * @param branch - Target branch.
1210
+ * @param originRepositoryUrl - Configured upstream repository URL.
1211
+ */
1212
+ async function refreshVpsSelfUpdateRemoteBranch(
1213
+ repositoryDirectory: string,
1214
+ branch: string,
1215
+ originRepositoryUrl: string,
1216
+ ): Promise<void> {
1217
+ if (!branch) {
1218
+ return;
1219
+ }
1220
+
1221
+ await runGitInRepository(repositoryDirectory, [
1222
+ 'fetch',
1223
+ '--no-tags',
1224
+ '--prune',
1225
+ `--depth=${VPS_SELF_UPDATE_OVERVIEW_FETCH_DEPTH}`,
1226
+ originRepositoryUrl,
1227
+ `+refs/heads/${branch}:${createVpsSelfUpdateRemoteBranchReference(branch)}`,
1228
+ ]);
1229
+ }
1230
+
1231
+ /**
1232
+ * Reads the latest tracked-branch commit from the local remote-tracking ref, falling back to `ls-remote`.
1233
+ *
1234
+ * @param repositoryDirectory - Repository checkout path.
1235
+ * @param branch - Target branch.
1236
+ * @param originRepositoryUrl - Configured upstream repository URL.
1237
+ * @returns Remote branch commit sha or `null`.
1238
+ */
1239
+ async function readLatestRemoteBranchCommitSha(
1240
+ repositoryDirectory: string,
1241
+ branch: string,
1242
+ originRepositoryUrl: string,
1243
+ ): Promise<string | null> {
1244
+ if (!branch) {
1245
+ return null;
1246
+ }
1247
+
1248
+ const remoteBranchReference = createVpsSelfUpdateRemoteBranchReference(branch);
1249
+ const localCommitSha = await runGitInRepository(repositoryDirectory, [
1250
+ 'rev-parse',
1251
+ '--verify',
1252
+ `${remoteBranchReference}^{commit}`,
1253
+ ]);
1254
+
1255
+ return localCommitSha || readRemoteCommitSha(repositoryDirectory, branch, originRepositoryUrl);
1256
+ }
1257
+
1174
1258
  /**
1175
1259
  * Reads the latest remote branch commit without mutating the local checkout.
1176
1260
  *
@@ -1197,17 +1281,47 @@ async function readRemoteCommitSha(
1197
1281
  }
1198
1282
 
1199
1283
  /**
1200
- * Reads the author timestamp of a known commit from the local repository.
1284
+ * Creates the local remote-tracking reference used for the update overview fetch.
1285
+ *
1286
+ * @param branch - Target branch.
1287
+ * @returns Local remote-tracking reference.
1288
+ */
1289
+ function createVpsSelfUpdateRemoteBranchReference(branch: string): string {
1290
+ return `refs/remotes/origin/${branch}`;
1291
+ }
1292
+
1293
+ /**
1294
+ * Reads hash, subject and author timestamp for one known commit from the local repository.
1201
1295
  *
1202
1296
  * @param repositoryDirectory - Repository checkout path.
1203
- * @param commitSha - Commit hash to look up.
1204
- * @returns ISO timestamp or `null` when the commit is unknown locally.
1297
+ * @param commitReference - Commit hash or git revision reference to look up.
1298
+ * @returns Commit metadata or `null` when the commit cannot be resolved locally.
1205
1299
  */
1206
- async function readCommitDateFromRepository(
1300
+ async function readCommitMetadataFromRepository(
1207
1301
  repositoryDirectory: string,
1208
- commitSha: string,
1209
- ): Promise<string | null> {
1210
- return runGitInRepository(repositoryDirectory, ['log', '-1', '--format=%aI', commitSha]);
1302
+ commitReference: string,
1303
+ ): Promise<VpsSelfUpdateCommitMetadata | null> {
1304
+ const output = await runGitInRepository(repositoryDirectory, [
1305
+ 'log',
1306
+ '-1',
1307
+ `--format=%H${GIT_LOG_FIELD_SEPARATOR}%aI${GIT_LOG_FIELD_SEPARATOR}%s`,
1308
+ commitReference,
1309
+ ]);
1310
+ if (!output) {
1311
+ return null;
1312
+ }
1313
+
1314
+ const fields = output.split(GIT_LOG_FIELD_SEPARATOR);
1315
+ const commitSha = fields[0] ?? '';
1316
+ if (!commitSha) {
1317
+ return null;
1318
+ }
1319
+
1320
+ return {
1321
+ commitSha,
1322
+ authoredAt: fields[1] || null,
1323
+ subject: fields.slice(2).join(GIT_LOG_FIELD_SEPARATOR),
1324
+ };
1211
1325
  }
1212
1326
 
1213
1327
  /**
@@ -28,6 +28,12 @@ export declare const DEFAULT_SPAWN_AGENT_RATE_LIMIT_WINDOW_MS: number;
28
28
  * @private shared Agents Server constant
29
29
  */
30
30
  export declare const DEFAULT_LOCAL_AGENT_RUNNER_MAX_FAILED_ATTEMPTS = 3;
31
+ /**
32
+ * Default maximum number of local runner harness instances allowed to answer queued messages at once.
33
+ *
34
+ * @private shared Agents Server constant
35
+ */
36
+ export declare const DEFAULT_LOCAL_AGENT_RUNNER_MAX_PARALLEL_MESSAGES = 3;
31
37
  /**
32
38
  * Stable keys used by the dedicated server-limits table.
33
39
  *
@@ -42,6 +48,7 @@ export declare const SERVER_LIMIT_KEYS: {
42
48
  readonly SPAWN_AGENT_RATE_LIMIT_MAX: "SPAWN_AGENT_RATE_LIMIT_MAX";
43
49
  readonly SPAWN_AGENT_RATE_LIMIT_WINDOW_MS: "SPAWN_AGENT_RATE_LIMIT_WINDOW_MS";
44
50
  readonly LOCAL_AGENT_RUNNER_MAX_FAILED_ATTEMPTS: "LOCAL_AGENT_RUNNER_MAX_FAILED_ATTEMPTS";
51
+ readonly LOCAL_AGENT_RUNNER_MAX_PARALLEL_MESSAGES: "LOCAL_AGENT_RUNNER_MAX_PARALLEL_MESSAGES";
45
52
  };
46
53
  /**
47
54
  * One supported dedicated server-limit key.
@@ -156,6 +163,16 @@ export declare const SERVER_LIMIT_DEFINITIONS: ({
156
163
  minimumValue: number;
157
164
  step: number;
158
165
  legacyMetadataKeys: never[];
166
+ } | {
167
+ key: "LOCAL_AGENT_RUNNER_MAX_PARALLEL_MESSAGES";
168
+ category: "Local agent runner";
169
+ title: string;
170
+ description: string;
171
+ unit: "count";
172
+ defaultValue: number;
173
+ minimumValue: number;
174
+ step: number;
175
+ legacyMetadataKeys: never[];
159
176
  })[];
160
177
  /**
161
178
  * Fast lookup map used by server-limit validators and UI helpers.