@promptbook/cli 0.114.0-12 → 0.114.0-13

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 (80) hide show
  1. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +7 -4
  2. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentGoalChatPlannedMessages.tsx +23 -8
  3. package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +14 -3
  4. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +2 -20
  5. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +2 -1
  6. package/apps/agents-server/src/app/agents/[agentName]/projectReferenceActions.ts +61 -0
  7. package/apps/agents-server/src/components/AgentProjects/resolveLatestCompletedAgentMessageKey.ts +32 -0
  8. package/apps/agents-server/src/components/AgentProjects/useAgentProjectMarkdownReferences.ts +124 -2
  9. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +3 -0
  10. package/apps/agents-server/src/languages/translations/czech.yaml +3 -0
  11. package/apps/agents-server/src/languages/translations/english.yaml +3 -0
  12. package/apps/agents-server/src/tools/agentGoalChatTimeoutToolFunctions.ts +24 -2
  13. package/apps/agents-server/src/tools/agentGoalChatTimeoutTools.ts +5 -4
  14. package/apps/agents-server/src/utils/agentGoalChat/agentGoalChatPlannedMessageActions.ts +38 -9
  15. package/apps/agents-server/src/utils/agentGoalChat/createAgentGoalChatNoteContent.ts +15 -6
  16. package/apps/agents-server/src/utils/agentProjects/listAgentProjectNames.ts +35 -0
  17. package/apps/agents-server/src/utils/agentProjects/listAgentProjects.ts +6 -23
  18. package/apps/agents-server/src/utils/agentProjects/resolveAgentChatProjectReferences.ts +25 -0
  19. package/apps/agents-server/src/utils/chatMessageChips/createAnsweredMessageChipToolCalls.ts +37 -0
  20. package/apps/agents-server/src/utils/chatMessageChips/createPlannedMessageChipToolCalls.ts +39 -0
  21. package/apps/agents-server/src/utils/chatMessageChips/createTouchedProjectChipToolCalls.ts +75 -0
  22. package/apps/agents-server/src/utils/localChatRunner/applyLocalAgentPlannedMessageCommands.ts +212 -0
  23. package/apps/agents-server/src/utils/localChatRunner/prepareLocalAgentPlannedMessagesSidecar.ts +66 -0
  24. package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +110 -22
  25. package/apps/agents-server/src/utils/userChat/persistUserChatJobTerminalState.ts +5 -0
  26. package/apps/agents-server/src/utils/userChat/userChatJobRunReport.ts +30 -0
  27. package/apps/agents-server/src/utils/userChatTimeout/createTimeoutWakeUpMessage.ts +9 -1
  28. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore/repeatFiredUserChatTimeout.ts +74 -0
  29. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore.ts +1 -0
  30. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutWorker.ts +63 -54
  31. package/esm/index.es.js +639 -80
  32. package/esm/index.es.js.map +1 -1
  33. package/esm/scripts/run-agent-messages/messages/buildAgentGoalChatPromptSection.d.ts +22 -4
  34. package/esm/scripts/run-agent-messages/messages/buildAgentMessagePrompt.d.ts +3 -3
  35. package/esm/scripts/run-agent-messages/messages/loadAgentPlannedMessagesSidecar.d.ts +10 -0
  36. package/esm/scripts/run-agent-messages/messages/resolveTouchedAgentProjects.d.ts +15 -0
  37. package/esm/src/book-3.0/AgentMessageRunReport.d.ts +28 -0
  38. package/esm/src/book-3.0/AgentPlannedMessagesSidecar.d.ts +94 -0
  39. package/esm/src/book-3.0/AgentPlannedMessagesSidecar.test.d.ts +1 -0
  40. package/esm/src/book-3.0/createAgentMessageSidecarBaseName.d.ts +13 -0
  41. package/esm/src/book-components/Chat/utils/agentProjectToolCall.d.ts +46 -0
  42. package/esm/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +24 -0
  43. package/esm/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.d.ts +59 -0
  44. package/esm/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.d.ts +19 -0
  45. package/esm/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.test.d.ts +1 -0
  46. package/esm/src/version.d.ts +1 -1
  47. package/package.json +1 -1
  48. package/src/book-3.0/AgentMessageRunReport.ts +76 -0
  49. package/src/book-3.0/AgentPlannedMessagesSidecar.ts +229 -0
  50. package/src/book-3.0/AgentTeamConversationWorkspace.ts +3 -5
  51. package/src/book-3.0/createAgentMessageSidecarBaseName.ts +20 -0
  52. package/src/book-components/Chat/Chat/Chat.module.css +6 -1
  53. package/src/book-components/Chat/Chat/renderToolCallDetails.tsx +1 -12
  54. package/src/book-components/Chat/utils/agentProjectToolCall.ts +74 -0
  55. package/src/book-components/Chat/utils/getToolCallChipletInfo.ts +28 -0
  56. package/src/book-components/Chat/utils/timeoutToolCallPresentation.ts +23 -3
  57. package/src/other/templates/getTemplatesPipelineCollection.ts +784 -738
  58. package/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.ts +105 -0
  59. package/src/utils/agent-message-runtime/resolveAgentMessageRuntimeActivity.ts +6 -54
  60. package/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.ts +147 -0
  61. package/src/version.ts +2 -2
  62. package/src/versions.txt +1 -0
  63. package/umd/index.umd.js +639 -80
  64. package/umd/index.umd.js.map +1 -1
  65. package/umd/scripts/run-agent-messages/messages/buildAgentGoalChatPromptSection.d.ts +22 -4
  66. package/umd/scripts/run-agent-messages/messages/buildAgentMessagePrompt.d.ts +3 -3
  67. package/umd/scripts/run-agent-messages/messages/loadAgentPlannedMessagesSidecar.d.ts +10 -0
  68. package/umd/scripts/run-agent-messages/messages/resolveTouchedAgentProjects.d.ts +15 -0
  69. package/umd/src/book-3.0/AgentMessageRunReport.d.ts +28 -0
  70. package/umd/src/book-3.0/AgentPlannedMessagesSidecar.d.ts +94 -0
  71. package/umd/src/book-3.0/AgentPlannedMessagesSidecar.test.d.ts +1 -0
  72. package/umd/src/book-3.0/createAgentMessageSidecarBaseName.d.ts +13 -0
  73. package/umd/src/book-components/Chat/utils/agentProjectToolCall.d.ts +46 -0
  74. package/umd/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +24 -0
  75. package/umd/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.d.ts +59 -0
  76. package/umd/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.d.ts +19 -0
  77. package/umd/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.test.d.ts +1 -0
  78. package/umd/src/version.d.ts +1 -1
  79. package/esm/apps/agents-server/src/utils/agentGoalChat/agentGoalChatConstants.d.ts +0 -12
  80. package/umd/apps/agents-server/src/utils/agentGoalChat/agentGoalChatConstants.d.ts +0 -12
@@ -0,0 +1,66 @@
1
+ import { mkdir, writeFile } from 'fs/promises';
2
+ import { dirname, join } from 'path';
3
+ import {
4
+ createAgentPlannedMessagesSidecarContent,
5
+ createAgentPlannedMessagesSidecarPath,
6
+ type AgentPlannedMessageSnapshot,
7
+ type AgentPlannedMessagesSidecar,
8
+ } from '../../../../../src/book-3.0/AgentPlannedMessagesSidecar';
9
+ import { AGENT_GOAL_CHAT_PLANNED_MESSAGE_ACTIONS } from '../agentGoalChat/agentGoalChatPlannedMessageActions';
10
+ import type { UserChatJobRecord } from '../userChat/UserChatJobRecord';
11
+ import type { LocalAgentFolder } from './ensureLocalAgentFolder';
12
+ import type { LocalUserChatJobMetadata } from './LocalUserChatJobMetadata';
13
+
14
+ /**
15
+ * Writes the planned-message sidecar a coding harness uses to see and change its future wake-ups.
16
+ *
17
+ * The sidecar is the only planning channel of a locally executed turn: the harness never has to reach
18
+ * the Agents Server over the network, and a wake-up exists only when it was actually requested here.
19
+ * Planned messages repeat on their own, so an empty `commands` array keeps the current plan intact.
20
+ *
21
+ * @param options - Answered job, its agent folder, and the local runner metadata of the queued message.
22
+ */
23
+ export async function prepareLocalAgentPlannedMessagesSidecar(options: {
24
+ readonly job: Pick<UserChatJobRecord, 'agentPermanentId'>;
25
+ readonly agentFolder: LocalAgentFolder;
26
+ readonly metadata: LocalUserChatJobMetadata;
27
+ }): Promise<void> {
28
+ const sidecarPath = join(
29
+ options.agentFolder.directoryPath,
30
+ createAgentPlannedMessagesSidecarPath(options.metadata.fileName),
31
+ );
32
+ const plannedMessages = await AGENT_GOAL_CHAT_PLANNED_MESSAGE_ACTIONS.list({
33
+ agentPermanentId: options.job.agentPermanentId,
34
+ });
35
+ const sidecar: AgentPlannedMessagesSidecar = {
36
+ version: 1,
37
+ agentPermanentId: options.job.agentPermanentId,
38
+ currentPlannedMessages: plannedMessages.items.map(createLocalAgentPlannedMessageSnapshot),
39
+ commands: [],
40
+ };
41
+
42
+ await mkdir(dirname(sidecarPath), { recursive: true });
43
+ await writeFile(sidecarPath, createAgentPlannedMessagesSidecarContent(sidecar), 'utf-8');
44
+ }
45
+
46
+ /**
47
+ * Reduces one stored planned message to the fields a coding harness needs.
48
+ *
49
+ * @param plannedMessage - Planned message returned by the shared planned-message actions.
50
+ * @returns Snapshot written into the sidecar.
51
+ *
52
+ * @private function of `prepareLocalAgentPlannedMessagesSidecar`
53
+ */
54
+ function createLocalAgentPlannedMessageSnapshot(plannedMessage: {
55
+ readonly timeoutId: string;
56
+ readonly dueAt: string;
57
+ readonly message: string | null;
58
+ readonly intervalMs: number | null;
59
+ }): AgentPlannedMessageSnapshot {
60
+ return {
61
+ timeoutId: plannedMessage.timeoutId,
62
+ dueAt: plannedMessage.dueAt,
63
+ message: plannedMessage.message,
64
+ intervalMs: plannedMessage.intervalMs,
65
+ };
66
+ }
@@ -1,11 +1,14 @@
1
1
  import type { Json } from '@/src/database/schema';
2
+ import type { ToolCall } from '@promptbook-local/types';
2
3
  import { mkdir, readFile, rm, writeFile } from 'fs/promises';
3
4
  import { join } from 'path';
4
5
  import { parseAgentSource } from '../../../../../src/book-2.0/agent-source/parseAgentSource';
5
6
  import {
6
7
  buildAgentMessageRunReportPath,
7
8
  parseAgentMessageRunReport,
9
+ type AgentMessageRunReport,
8
10
  } from '../../../../../src/book-3.0/AgentMessageRunReport';
11
+ import { createAnsweredMessageChipToolCalls } from '../chatMessageChips/createAnsweredMessageChipToolCalls';
9
12
  import { createUserChatJobFailureDetails } from '../userChat/createUserChatJobFailureDetails';
10
13
  import { claimNextQueuedUserChatJob } from '../userChat/claimNextQueuedUserChatJob';
11
14
  import { finalizeUserChatJob } from '../userChat/finalizeUserChatJob';
@@ -14,6 +17,7 @@ import { getUserChatJobById } from '../userChat/getUserChatJobById';
14
17
  import { persistUserChatJobTerminalState } from '../userChat/persistUserChatJobTerminalState';
15
18
  import { provideUserChatJobTable } from '../userChat/provideUserChatJobTable';
16
19
  import { updateUserChatAssistantMessage } from '../userChat/updateUserChatAssistantMessage';
20
+ import { resolveUserChatJobExecutionTiming } from '../userChat/userChatJobRunReport';
17
21
  import type { UserChatJobRecord } from '../userChat/UserChatJobRecord';
18
22
  import type { UserChatJobRow } from '../userChat/UserChatJobRow';
19
23
  import { createUserChatRunnerThreadMessages } from '../userChat/userChatMessageLifecycle';
@@ -32,6 +36,12 @@ import {
32
36
  parseLocalChatFinishedMessageBook,
33
37
  } from './localChatMessageBook';
34
38
  import { LOCAL_USER_CHAT_JOB_ANSWER_TIMEOUT_MS, LOCAL_USER_CHAT_JOB_PROVIDER } from './localChatRunnerConstants';
39
+ import {
40
+ applyLocalAgentPlannedMessageCommands,
41
+ removeLocalAgentPlannedMessagesSidecar,
42
+ type AppliedAgentPlannedMessageCommand,
43
+ } from './applyLocalAgentPlannedMessageCommands';
44
+ import { prepareLocalAgentPlannedMessagesSidecar } from './prepareLocalAgentPlannedMessagesSidecar';
35
45
  import {
36
46
  ensureLocalAgentFolder,
37
47
  loadLocalAgentSourceSnapshot,
@@ -191,6 +201,7 @@ async function enqueueLocalUserChatJobWithinTerminalCapture(
191
201
  metadata,
192
202
  primaryAgentName: agentSourceSnapshot.agentName,
193
203
  });
204
+ await prepareLocalAgentPlannedMessagesSidecarIfPossible({ job, agentFolder, metadata });
194
205
  await mkdir(join(agentFolder.directoryPath, 'messages', 'queued'), { recursive: true });
195
206
  await writeFile(queuedMessagePath, createLocalChatQueuedMessageBook({ messages: threadMessages }), 'utf-8');
196
207
 
@@ -228,6 +239,22 @@ async function prepareLocalTeamConversationWorkspaceIfPossible(
228
239
  }
229
240
  }
230
241
 
242
+ /**
243
+ * Prepares the planned-message sidecar without preventing an answer when planned messages cannot be read.
244
+ */
245
+ async function prepareLocalAgentPlannedMessagesSidecarIfPossible(
246
+ options: Parameters<typeof prepareLocalAgentPlannedMessagesSidecar>[0],
247
+ ): Promise<void> {
248
+ try {
249
+ await prepareLocalAgentPlannedMessagesSidecar(options);
250
+ } catch (error) {
251
+ console.warn('[local-chat-runner] planned_messages_sidecar_prepare_failed', {
252
+ agentPermanentId: options.job.agentPermanentId,
253
+ error,
254
+ });
255
+ }
256
+ }
257
+
231
258
  /**
232
259
  * Synchronizes one already queued local job with message-folder output.
233
260
  */
@@ -245,7 +272,15 @@ async function synchronizeLocalUserChatJob(
245
272
  });
246
273
 
247
274
  if (content) {
248
- await persistLocalUserChatJobRunReportIfPresent(job, agentDirectoryPath, metadata);
275
+ const runReport = await persistLocalUserChatJobRunReportIfPresent(job, agentDirectoryPath, metadata);
276
+ const executionTiming = resolveUserChatJobExecutionTiming(runReport);
277
+ // Note: Planned messages are applied before the answer becomes terminal, so a wake-up the
278
+ // agent announced in its answer is already stored when the answer becomes visible.
279
+ const appliedPlannedMessageCommands = await applyLocalAgentPlannedMessageCommandsIfPossible({
280
+ job,
281
+ agentDirectoryPath,
282
+ metadata,
283
+ });
249
284
  const teamConversations = await parseFinishedLocalTeamConversations({
250
285
  agentDirectoryPath,
251
286
  metadata,
@@ -255,13 +290,20 @@ async function synchronizeLocalUserChatJob(
255
290
  job,
256
291
  conversations: teamConversations,
257
292
  });
293
+ const chipToolCalls = await createAnsweredMessageChipToolCallsIfPossible({
294
+ job,
295
+ appliedPlannedMessageCommands,
296
+ touchedProjectNames: runReport?.touchedProjectNames || [],
297
+ });
298
+ const toolCalls = [...teamToolCalls, ...chipToolCalls];
258
299
  await persistUserChatJobTerminalState({
259
300
  job,
260
301
  status: 'COMPLETED',
261
302
  provider: LOCAL_USER_CHAT_JOB_PROVIDER,
262
303
  content,
263
- toolCalls: teamToolCalls.length > 0 ? teamToolCalls : undefined,
264
- generationDurationMs: resolveLocalUserChatJobDurationMs(metadata.queuedAt),
304
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
305
+ executedAt: executionTiming?.executedAt,
306
+ generationDurationMs: executionTiming?.generationDurationMs,
265
307
  });
266
308
  return { didMutate: true, outcome: 'completed' };
267
309
  }
@@ -278,26 +320,26 @@ async function synchronizeLocalUserChatJob(
278
320
  return { didMutate: false, outcome: 'waiting' };
279
321
  }
280
322
 
323
+ await removeLocalAgentPlannedMessagesSidecar(agentDirectoryPath, metadata);
281
324
  await persistUserChatJobTerminalState({
282
325
  job,
283
326
  status: 'FAILED',
284
327
  provider: LOCAL_USER_CHAT_JOB_PROVIDER,
285
328
  failureReason,
286
329
  failureDetails: createLocalUserChatJobFailureDetails(job, failureReason, 'localRunnerFailedFile'),
287
- generationDurationMs: resolveLocalUserChatJobDurationMs(metadata.queuedAt),
288
330
  });
289
331
  return { didMutate: true, outcome: 'failed' };
290
332
  }
291
333
 
292
334
  if (isLocalUserChatJobTimedOut(metadata) && job.status !== 'FAILED') {
293
335
  const failureReason = 'Local agent runner did not finish the queued message within 30 minutes.';
336
+ await removeLocalAgentPlannedMessagesSidecar(agentDirectoryPath, metadata);
294
337
  await persistUserChatJobTerminalState({
295
338
  job,
296
339
  status: 'FAILED',
297
340
  provider: LOCAL_USER_CHAT_JOB_PROVIDER,
298
341
  failureReason,
299
342
  failureDetails: createLocalUserChatJobFailureDetails(job, failureReason, 'localRunnerTimeout'),
300
- generationDurationMs: resolveLocalUserChatJobDurationMs(metadata.queuedAt),
301
343
  });
302
344
  return { didMutate: true, outcome: 'failed' };
303
345
  }
@@ -315,28 +357,34 @@ async function synchronizeLocalUserChatJob(
315
357
  *
316
358
  * The report is best-effort telemetry shown in the admin task details, so any read or
317
359
  * persistence failure only logs a warning instead of failing the finished chat answer.
360
+ *
361
+ * @returns The report of the answered message, or `null` when the runner wrote none.
318
362
  */
319
363
  async function persistLocalUserChatJobRunReportIfPresent(
320
364
  job: UserChatJobRecord,
321
365
  agentDirectoryPath: string,
322
366
  metadata: LocalUserChatJobMetadata,
323
- ): Promise<void> {
367
+ ): Promise<AgentMessageRunReport | null> {
324
368
  try {
325
369
  const reportFileContent = await readOptionalTextFile(
326
370
  join(agentDirectoryPath, buildAgentMessageRunReportPath(metadata.finishedPath)),
327
371
  );
328
372
  const report = reportFileContent === null ? null : parseAgentMessageRunReport(reportFileContent);
329
373
  if (!report) {
330
- return;
374
+ return null;
331
375
  }
332
376
 
333
377
  await persistUserChatJobRunReport(job, report);
378
+
379
+ return report;
334
380
  } catch (error) {
335
381
  console.warn('[local-chat-runner] run_report_sync_failed', {
336
382
  chatId: job.chatId,
337
383
  jobId: job.id,
338
384
  error,
339
385
  });
386
+
387
+ return null;
340
388
  }
341
389
  }
342
390
 
@@ -451,9 +499,61 @@ async function removeQueuedLocalMessageIfPresent(metadata: LocalUserChatJobMetad
451
499
  return;
452
500
  }
453
501
 
454
- await rm(join(resolveLocalAgentRootPath(), metadata.agentDirectoryName, metadata.queuedPath), {
455
- force: true,
456
- }).catch(() => undefined);
502
+ const agentDirectoryPath = join(resolveLocalAgentRootPath(), metadata.agentDirectoryName);
503
+
504
+ await rm(join(agentDirectoryPath, metadata.queuedPath), { force: true }).catch(() => undefined);
505
+ await removeLocalAgentPlannedMessagesSidecar(agentDirectoryPath, metadata);
506
+ }
507
+
508
+ /**
509
+ * Applies planned-message commands without preventing an already answered message from completing.
510
+ *
511
+ * @returns The applied commands, or an empty list when the sidecar could not be applied.
512
+ */
513
+ async function applyLocalAgentPlannedMessageCommandsIfPossible(
514
+ options: Parameters<typeof applyLocalAgentPlannedMessageCommands>[0],
515
+ ): Promise<ReadonlyArray<AppliedAgentPlannedMessageCommand>> {
516
+ try {
517
+ return await applyLocalAgentPlannedMessageCommands(options);
518
+ } catch (error) {
519
+ console.warn('[local-chat-runner] planned_messages_sidecar_apply_failed', {
520
+ chatId: options.job.chatId,
521
+ jobId: options.job.id,
522
+ error,
523
+ });
524
+
525
+ return [];
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Builds the chips of one answered message without preventing it from completing.
531
+ *
532
+ * The chips only enrich the answer, so a failure to resolve them never hides an answer the
533
+ * agent already produced.
534
+ *
535
+ * @returns Tool calls rendered as chips below the answer, or an empty list when they failed.
536
+ */
537
+ async function createAnsweredMessageChipToolCallsIfPossible(options: {
538
+ readonly job: UserChatJobRecord;
539
+ readonly appliedPlannedMessageCommands: ReadonlyArray<AppliedAgentPlannedMessageCommand>;
540
+ readonly touchedProjectNames: ReadonlyArray<string>;
541
+ }): Promise<ReadonlyArray<ToolCall>> {
542
+ try {
543
+ return await createAnsweredMessageChipToolCalls({
544
+ agentPermanentId: options.job.agentPermanentId,
545
+ appliedPlannedMessageCommands: options.appliedPlannedMessageCommands,
546
+ touchedProjectNames: options.touchedProjectNames,
547
+ });
548
+ } catch (error) {
549
+ console.warn('[local-chat-runner] message_chips_build_failed', {
550
+ chatId: options.job.chatId,
551
+ jobId: options.job.id,
552
+ error,
553
+ });
554
+
555
+ return [];
556
+ }
457
557
  }
458
558
 
459
559
  /**
@@ -479,18 +579,6 @@ function isLocalUserChatJobTimedOut(metadata: LocalUserChatJobMetadata): boolean
479
579
  return Number.isFinite(queuedAtMs) && Date.now() - queuedAtMs >= LOCAL_USER_CHAT_JOB_ANSWER_TIMEOUT_MS;
480
580
  }
481
581
 
482
- /**
483
- * Resolves elapsed time since the local message was queued.
484
- */
485
- function resolveLocalUserChatJobDurationMs(queuedAt: string): number | undefined {
486
- const queuedAtMs = Date.parse(queuedAt);
487
- if (!Number.isFinite(queuedAtMs)) {
488
- return undefined;
489
- }
490
-
491
- return Math.max(0, Date.now() - queuedAtMs);
492
- }
493
-
494
582
  /**
495
583
  * Returns true when one filesystem error indicates a missing path.
496
584
  */
@@ -28,6 +28,10 @@ export async function persistUserChatJobTerminalState(options: {
28
28
  provider?: string | null;
29
29
  failureReason?: string | null;
30
30
  failureDetails?: string | null;
31
+ /**
32
+ * Actual time at which an out-of-process runner finished this assistant message.
33
+ */
34
+ executedAt?: NonNullable<ChatMessage['createdAt']>;
31
35
  generationDurationMs?: number;
32
36
  }): Promise<void> {
33
37
  let updatedChat: Awaited<ReturnType<typeof updateUserChatAssistantMessage>> | null = null;
@@ -47,6 +51,7 @@ export async function persistUserChatJobTerminalState(options: {
47
51
  ongoingToolCalls: undefined,
48
52
  toolCalls: options.toolCalls ?? message.toolCalls,
49
53
  completedToolCalls: options.toolCalls ?? message.completedToolCalls,
54
+ createdAt: options.executedAt ?? message.createdAt,
50
55
  generationDurationMs: options.generationDurationMs ?? message.generationDurationMs,
51
56
  progressCard: undefined,
52
57
  availableTools: options.availableTools ?? message.availableTools,
@@ -1,4 +1,5 @@
1
1
  import type { Json } from '@/src/database/schema';
2
+ import type { ChatMessage } from '@promptbook-local/types';
2
3
  import {
3
4
  normalizeAgentMessageRunReport,
4
5
  type AgentMessageRunReport,
@@ -42,6 +43,35 @@ export function withUserChatJobRunReport(
42
43
  };
43
44
  }
44
45
 
46
+ /**
47
+ * Resolves display-ready execution timing from one runner-produced message report.
48
+ *
49
+ * The runner's timestamps describe only active harness work, unlike durable job timestamps which also include time
50
+ * waiting for the out-of-process runner to pick up a queued message.
51
+ *
52
+ * @returns The completed message timestamp and actual execution duration, or `null` for older reports without timing.
53
+ */
54
+ export function resolveUserChatJobExecutionTiming(report: AgentMessageRunReport | null): {
55
+ readonly executedAt: NonNullable<ChatMessage['createdAt']>;
56
+ readonly generationDurationMs: number;
57
+ } | null {
58
+ const executionTiming = report?.executionTiming;
59
+ if (!executionTiming) {
60
+ return null;
61
+ }
62
+
63
+ const startedAtMilliseconds = Date.parse(executionTiming.startedAt);
64
+ const finishedAtMilliseconds = Date.parse(executionTiming.finishedAt);
65
+ if (!Number.isFinite(startedAtMilliseconds) || !Number.isFinite(finishedAtMilliseconds)) {
66
+ return null;
67
+ }
68
+
69
+ return {
70
+ executedAt: executionTiming.finishedAt as NonNullable<ChatMessage['createdAt']>,
71
+ generationDurationMs: Math.max(0, finishedAtMilliseconds - startedAtMilliseconds),
72
+ };
73
+ }
74
+
45
75
  /**
46
76
  * Persists one runner-produced run report into the job parameters so task details can show it.
47
77
  */
@@ -1,20 +1,28 @@
1
1
  import { spaceTrim } from 'spacetrim';
2
+ import { formatTimeoutDurationHuman } from '../../../../../src/book-components/Chat/utils/timeoutToolCallPresentation';
2
3
 
3
4
  /**
4
5
  * Builds the synthetic user-like message injected when a timeout elapses.
5
6
  *
7
+ * A repeating planned message stays armed after every firing, so its wake-up says so: the agent
8
+ * only has to act on the message itself, not to plan the same wake-up again.
9
+ *
6
10
  * @private internal utility of userChatTimeout
7
11
  */
8
12
  export function createTimeoutWakeUpMessage(options: {
9
13
  readonly timeoutId: string;
10
14
  readonly durationMs: number;
15
+ readonly intervalMs?: number | null;
11
16
  readonly message?: string | null;
12
17
  }): string {
13
18
  const message = options.message?.trim() || '';
19
+ const headline = options.intervalMs
20
+ ? `⏱️ Planned message elapsed, repeating every ${formatTimeoutDurationHuman(options.intervalMs)}.`
21
+ : `⏱️ Timeout elapsed after ${options.durationMs}ms.`;
14
22
 
15
23
  return spaceTrim(
16
24
  (block) => `
17
- ⏱️ Timeout elapsed after ${options.durationMs}ms.
25
+ ${headline}
18
26
  timeoutId: ${options.timeoutId}
19
27
  ${message ? block(message) : ''}
20
28
  `,
@@ -0,0 +1,74 @@
1
+ import { markTaskTerminalLogFinished } from '../../taskTerminal/taskTerminalLog';
2
+ import type { UserChatTimeoutRecord, UserChatTimeoutRow } from '../UserChatTimeoutRecord';
3
+ import { getUserChatTimeoutById } from './getUserChatTimeoutById';
4
+ import { isMissingUserChatTimeoutRelationError } from './isMissingUserChatTimeoutRelationError';
5
+ import { mapUserChatTimeoutRow } from './mapUserChatTimeoutRow';
6
+ import { normalizeRecurrenceIntervalMs } from './normalizeRecurrenceIntervalMs';
7
+ import { provideUserChatTimeoutTable } from './provideUserChatTimeoutTable';
8
+
9
+ /**
10
+ * Re-arms one fired repeating timeout for its next interval.
11
+ *
12
+ * A repeating timeout keeps its own row and identity across every firing, exactly like one
13
+ * `setInterval` handle: the agent that planned it can still cancel it by the very same `timeoutId`
14
+ * it saw before, and no firing can leave a duplicated schedule behind.
15
+ *
16
+ * @param timeoutId - Identifier of the timeout that has just fired.
17
+ * @returns The re-armed timeout, or `null` when the row must be completed instead because it does not
18
+ * repeat or its repetitions were cancelled while it was firing.
19
+ *
20
+ * @private function of userChatTimeoutStore
21
+ */
22
+ export async function repeatFiredUserChatTimeout(timeoutId: string): Promise<UserChatTimeoutRecord | null> {
23
+ const existingTimeout = await getUserChatTimeoutById(timeoutId);
24
+
25
+ if (!existingTimeout) {
26
+ return null;
27
+ }
28
+
29
+ const recurrenceIntervalMs = normalizeRecurrenceIntervalMs(existingTimeout.recurrenceIntervalMs);
30
+
31
+ if (recurrenceIntervalMs === null || existingTimeout.cancelRequestedAt) {
32
+ return null;
33
+ }
34
+
35
+ const nowIso = new Date().toISOString();
36
+ const nextDueAtIso = new Date(Date.now() + recurrenceIntervalMs).toISOString();
37
+ const userChatTimeoutTable = await provideUserChatTimeoutTable();
38
+ const { data, error } = await userChatTimeoutTable
39
+ .update({
40
+ status: 'QUEUED',
41
+ updatedAt: nowIso,
42
+ queuedAt: nowIso,
43
+ dueAt: nextDueAtIso,
44
+ startedAt: null,
45
+ completedAt: null,
46
+ leaseExpiresAt: null,
47
+ pausedAt: null,
48
+ failureReason: null,
49
+ runCount: existingTimeout.runCount + 1,
50
+ lastFiredAt: nowIso,
51
+ })
52
+ .eq('id', timeoutId)
53
+ .eq('status', existingTimeout.status)
54
+ .eq('runCount', existingTimeout.runCount)
55
+ .select('*')
56
+ .maybeSingle();
57
+
58
+ if (error) {
59
+ if (isMissingUserChatTimeoutRelationError(error)) {
60
+ return null;
61
+ }
62
+
63
+ throw new Error(`Failed to repeat user chat timeout "${timeoutId}": ${error.message}`);
64
+ }
65
+
66
+ // Note: One firing of a repeating timeout is one finished task, even though its row stays armed
67
+ markTaskTerminalLogFinished(timeoutId, { isSuccessful: true });
68
+
69
+ if (!data) {
70
+ return getUserChatTimeoutById(timeoutId);
71
+ }
72
+
73
+ return mapUserChatTimeoutRow(data as UserChatTimeoutRow);
74
+ }
@@ -13,6 +13,7 @@ export { markUserChatTimeoutCancelled } from './userChatTimeoutStore/markUserCha
13
13
  export { markUserChatTimeoutCompleted } from './userChatTimeoutStore/markUserChatTimeoutCompleted';
14
14
  export { markUserChatTimeoutFailed } from './userChatTimeoutStore/markUserChatTimeoutFailed';
15
15
  export { recoverExpiredRunningUserChatTimeouts } from './userChatTimeoutStore/recoverExpiredRunningUserChatTimeouts';
16
+ export { repeatFiredUserChatTimeout } from './userChatTimeoutStore/repeatFiredUserChatTimeout';
16
17
  export { retryUserChatTimeout } from './userChatTimeoutStore/retryUserChatTimeout';
17
18
  export { updateAgentScopedUserChatTimeout } from './userChatTimeoutStore/updateAgentScopedUserChatTimeout';
18
19
  export { cancelUserChatTimeout } from './userChatTimeoutStore/cancelUserChatTimeout';