bopodev-api 0.1.25 → 0.1.26

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.
@@ -1,15 +1,20 @@
1
1
  import { mkdir } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
3
- import { and, eq, inArray, sql } from "drizzle-orm";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { and, desc, eq, inArray, sql } from "drizzle-orm";
4
4
  import { nanoid } from "nanoid";
5
5
  import { resolveAdapter } from "bopodev-agent-sdk";
6
6
  import type { AgentState, HeartbeatContext } from "bopodev-agent-sdk";
7
7
  import {
8
+ type AgentFinalRunOutput,
8
9
  ControlPlaneHeadersJsonSchema,
9
10
  ControlPlaneRequestHeadersSchema,
10
11
  ControlPlaneRuntimeEnvSchema,
11
12
  ExecutionOutcomeSchema,
12
- type ExecutionOutcome
13
+ type ExecutionOutcome,
14
+ type RunArtifact,
15
+ type RunCompletionReason,
16
+ type RunCompletionReport,
17
+ type RunCostSummary
13
18
  } from "bopodev-contracts";
14
19
  import type { BopoDb } from "bopodev-db";
15
20
  import {
@@ -30,7 +35,12 @@ import {
30
35
  import { appendAuditEvent, appendCost } from "bopodev-db";
31
36
  import { parseRuntimeConfigFromAgentRow } from "../lib/agent-config";
32
37
  import { bootstrapRepositoryWorkspace, ensureIsolatedGitWorktree, GitRuntimeError } from "../lib/git-runtime";
33
- import { isInsidePath, normalizeCompanyWorkspacePath, resolveProjectWorkspacePath } from "../lib/instance-paths";
38
+ import {
39
+ isInsidePath,
40
+ normalizeCompanyWorkspacePath,
41
+ resolveCompanyWorkspaceRootPath,
42
+ resolveProjectWorkspacePath
43
+ } from "../lib/instance-paths";
34
44
  import { assertRuntimeCwdForCompany, getProjectWorkspaceContextMap, hasText, resolveAgentFallbackWorkspace } from "../lib/workspace-policy";
35
45
  import type { RealtimeHub } from "../realtime/hub";
36
46
  import { createHeartbeatRunsRealtimeEvent } from "../realtime/heartbeat-runs";
@@ -73,6 +83,39 @@ type HeartbeatWakeContext = {
73
83
 
74
84
  const AGENT_COMMENT_EMOJI_REGEX = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
75
85
 
86
+ type RunDigestSignal = {
87
+ sequence: number;
88
+ kind: "system" | "assistant" | "thinking" | "tool_call" | "tool_result" | "result" | "stderr";
89
+ label: string | null;
90
+ text: string | null;
91
+ payload: string | null;
92
+ signalLevel: "high" | "medium" | "low" | "noise";
93
+ groupKey: string | null;
94
+ source: "stdout" | "stderr" | "trace_fallback";
95
+ };
96
+
97
+ type RunDigest = {
98
+ status: "completed" | "failed" | "skipped";
99
+ headline: string;
100
+ summary: string;
101
+ successes: string[];
102
+ failures: string[];
103
+ blockers: string[];
104
+ nextAction: string;
105
+ evidence: {
106
+ transcriptSignalCount: number;
107
+ outcomeActionCount: number;
108
+ outcomeBlockerCount: number;
109
+ failureType: string | null;
110
+ };
111
+ };
112
+
113
+ type RunTerminalPresentation = {
114
+ internalStatus: "completed" | "failed" | "skipped";
115
+ publicStatus: "completed" | "failed";
116
+ completionReason: RunCompletionReason;
117
+ };
118
+
76
119
  export async function claimIssuesForAgent(
77
120
  db: BopoDb,
78
121
  companyId: string,
@@ -334,18 +377,81 @@ export async function runHeartbeatForAgent(
334
377
  if (blockedProjectBudgetChecks.length > 0) {
335
378
  const blockedProjectIds = blockedProjectBudgetChecks.map((entry) => entry.projectId);
336
379
  const message = `Heartbeat skipped due to project budget hard-stop: ${blockedProjectIds.join(",")}.`;
380
+ const runDigest = buildRunDigest({
381
+ status: "skipped",
382
+ executionSummary: message,
383
+ outcome: null,
384
+ trace: null,
385
+ signals: []
386
+ });
387
+ const runReport = buildRunCompletionReport({
388
+ companyId,
389
+ agentName: agent.name,
390
+ providerType: agent.providerType as HeartbeatProviderType,
391
+ issueIds: [],
392
+ executionSummary: message,
393
+ outcome: null,
394
+ trace: null,
395
+ digest: runDigest,
396
+ terminal: resolveRunTerminalPresentation({
397
+ internalStatus: "skipped",
398
+ executionSummary: message,
399
+ outcome: null,
400
+ trace: null
401
+ }),
402
+ cost: buildRunCostSummary({
403
+ tokenInput: 0,
404
+ tokenOutput: 0,
405
+ usdCost: null,
406
+ usdCostStatus: "unknown",
407
+ pricingSource: null,
408
+ source: "none"
409
+ })
410
+ });
411
+ const runListMessage = buildRunListMessageFromReport(runReport);
337
412
  await db.insert(heartbeatRuns).values({
338
413
  id: runId,
339
414
  companyId,
340
415
  agentId,
341
416
  status: "skipped",
342
- message
417
+ finishedAt: new Date(),
418
+ message: runListMessage
343
419
  });
344
420
  publishHeartbeatRunStatus(options?.realtimeHub, {
345
421
  companyId,
346
422
  runId,
347
423
  status: "skipped",
348
- message
424
+ message: runListMessage,
425
+ finishedAt: new Date()
426
+ });
427
+ await appendAuditEvent(db, {
428
+ companyId,
429
+ actorType: "system",
430
+ eventType: "heartbeat.failed",
431
+ entityType: "heartbeat_run",
432
+ entityId: runId,
433
+ correlationId: options?.requestId ?? runId,
434
+ payload: {
435
+ agentId,
436
+ issueIds: [],
437
+ result: runReport.resultSummary,
438
+ message: runListMessage,
439
+ errorType: runReport.completionReason,
440
+ errorMessage: message,
441
+ report: runReport,
442
+ outcome: null,
443
+ usage: {
444
+ tokenInput: 0,
445
+ tokenOutput: 0,
446
+ usdCostStatus: "unknown",
447
+ source: "none"
448
+ },
449
+ trace: null,
450
+ diagnostics: {
451
+ requestId: options?.requestId,
452
+ trigger: runTrigger
453
+ }
454
+ }
349
455
  });
350
456
  for (const blockedProject of blockedProjectBudgetChecks) {
351
457
  const approvalId = await ensureProjectBudgetOverrideApprovalRequest(db, {
@@ -386,45 +492,156 @@ export async function runHeartbeatForAgent(
386
492
  if (!claimed) {
387
493
  const skippedRunId = nanoid(14);
388
494
  const skippedAt = new Date();
495
+ const overlapMessage = "Heartbeat skipped: another run is already in progress for this agent.";
496
+ const runDigest = buildRunDigest({
497
+ status: "skipped",
498
+ executionSummary: overlapMessage,
499
+ outcome: null,
500
+ trace: null,
501
+ signals: []
502
+ });
503
+ const runReport = buildRunCompletionReport({
504
+ companyId,
505
+ agentName: agent.name,
506
+ providerType: agent.providerType as HeartbeatProviderType,
507
+ issueIds: [],
508
+ executionSummary: overlapMessage,
509
+ outcome: null,
510
+ trace: null,
511
+ digest: runDigest,
512
+ terminal: resolveRunTerminalPresentation({
513
+ internalStatus: "skipped",
514
+ executionSummary: overlapMessage,
515
+ outcome: null,
516
+ trace: null
517
+ }),
518
+ cost: buildRunCostSummary({
519
+ tokenInput: 0,
520
+ tokenOutput: 0,
521
+ usdCost: null,
522
+ usdCostStatus: "unknown",
523
+ pricingSource: null,
524
+ source: "none"
525
+ })
526
+ });
527
+ const runListMessage = buildRunListMessageFromReport(runReport);
389
528
  await db.insert(heartbeatRuns).values({
390
529
  id: skippedRunId,
391
530
  companyId,
392
531
  agentId,
393
532
  status: "skipped",
394
533
  finishedAt: skippedAt,
395
- message: "Heartbeat skipped: another run is already in progress for this agent."
534
+ message: runListMessage
396
535
  });
397
536
  publishHeartbeatRunStatus(options?.realtimeHub, {
398
537
  companyId,
399
538
  runId: skippedRunId,
400
539
  status: "skipped",
401
- message: "Heartbeat skipped: another run is already in progress for this agent.",
540
+ message: runListMessage,
402
541
  finishedAt: skippedAt
403
542
  });
404
543
  await appendAuditEvent(db, {
405
544
  companyId,
406
545
  actorType: "system",
407
- eventType: "heartbeat.skipped_overlap",
546
+ eventType: "heartbeat.failed",
408
547
  entityType: "heartbeat_run",
409
548
  entityId: skippedRunId,
410
549
  correlationId: options?.requestId ?? skippedRunId,
411
- payload: { agentId, requestId: options?.requestId, trigger: runTrigger }
550
+ payload: {
551
+ agentId,
552
+ issueIds: [],
553
+ result: runReport.resultSummary,
554
+ message: runListMessage,
555
+ errorType: runReport.completionReason,
556
+ errorMessage: overlapMessage,
557
+ report: runReport,
558
+ outcome: null,
559
+ usage: {
560
+ tokenInput: 0,
561
+ tokenOutput: 0,
562
+ usdCostStatus: "unknown",
563
+ source: "none"
564
+ },
565
+ trace: null,
566
+ diagnostics: { requestId: options?.requestId, trigger: runTrigger }
567
+ }
412
568
  });
413
569
  return skippedRunId;
414
570
  }
415
571
  } else {
572
+ const budgetMessage = "Heartbeat skipped due to budget hard-stop.";
573
+ const runDigest = buildRunDigest({
574
+ status: "skipped",
575
+ executionSummary: budgetMessage,
576
+ outcome: null,
577
+ trace: null,
578
+ signals: []
579
+ });
580
+ const runReport = buildRunCompletionReport({
581
+ companyId,
582
+ agentName: agent.name,
583
+ providerType: agent.providerType as HeartbeatProviderType,
584
+ issueIds: [],
585
+ executionSummary: budgetMessage,
586
+ outcome: null,
587
+ trace: null,
588
+ digest: runDigest,
589
+ terminal: resolveRunTerminalPresentation({
590
+ internalStatus: "skipped",
591
+ executionSummary: budgetMessage,
592
+ outcome: null,
593
+ trace: null
594
+ }),
595
+ cost: buildRunCostSummary({
596
+ tokenInput: 0,
597
+ tokenOutput: 0,
598
+ usdCost: null,
599
+ usdCostStatus: "unknown",
600
+ pricingSource: null,
601
+ source: "none"
602
+ })
603
+ });
604
+ const runListMessage = buildRunListMessageFromReport(runReport);
416
605
  await db.insert(heartbeatRuns).values({
417
606
  id: runId,
418
607
  companyId,
419
608
  agentId,
420
609
  status: "skipped",
421
- message: "Heartbeat skipped due to budget hard-stop."
610
+ finishedAt: new Date(),
611
+ message: runListMessage
422
612
  });
423
613
  publishHeartbeatRunStatus(options?.realtimeHub, {
424
614
  companyId,
425
615
  runId,
426
616
  status: "skipped",
427
- message: "Heartbeat skipped due to budget hard-stop."
617
+ message: runListMessage,
618
+ finishedAt: new Date()
619
+ });
620
+ await appendAuditEvent(db, {
621
+ companyId,
622
+ actorType: "system",
623
+ eventType: "heartbeat.failed",
624
+ entityType: "heartbeat_run",
625
+ entityId: runId,
626
+ correlationId: options?.requestId ?? runId,
627
+ payload: {
628
+ agentId,
629
+ issueIds: [],
630
+ result: runReport.resultSummary,
631
+ message: runListMessage,
632
+ errorType: runReport.completionReason,
633
+ errorMessage: budgetMessage,
634
+ report: runReport,
635
+ outcome: null,
636
+ usage: {
637
+ tokenInput: 0,
638
+ tokenOutput: 0,
639
+ usdCostStatus: "unknown",
640
+ source: "none"
641
+ },
642
+ trace: null,
643
+ diagnostics: { requestId: options?.requestId, trigger: runTrigger }
644
+ }
428
645
  });
429
646
  }
430
647
 
@@ -518,6 +735,13 @@ export async function runHeartbeatForAgent(
518
735
  let runtimeLaunchSummary: ReturnType<typeof summarizeRuntimeLaunch> | null = null;
519
736
  let primaryIssueId: string | null = null;
520
737
  let primaryProjectId: string | null = null;
738
+ let providerUsageLimitDisposition:
739
+ | {
740
+ message: string;
741
+ notifyBoard: boolean;
742
+ pauseAgent: boolean;
743
+ }
744
+ | null = null;
521
745
  let transcriptSequence = 0;
522
746
  let transcriptWriteQueue = Promise.resolve();
523
747
  let transcriptLiveCount = 0;
@@ -526,6 +750,7 @@ export async function runHeartbeatForAgent(
526
750
  let transcriptPersistFailureReported = false;
527
751
  let pluginFailureSummary: string[] = [];
528
752
  const seenResultMessages = new Set<string>();
753
+ const runDigestSignals: RunDigestSignal[] = [];
529
754
 
530
755
  const enqueueTranscriptEvent = (event: {
531
756
  kind: string;
@@ -553,6 +778,21 @@ export async function runHeartbeatForAgent(
553
778
  if (signalLevel === "high") {
554
779
  transcriptLiveHighSignalCount += 1;
555
780
  }
781
+ if (isUsefulTranscriptSignal(signalLevel)) {
782
+ runDigestSignals.push({
783
+ sequence,
784
+ kind: normalizeTranscriptKind(event.kind),
785
+ label: event.label ?? null,
786
+ text: event.text ?? null,
787
+ payload: event.payload ?? null,
788
+ signalLevel,
789
+ groupKey: groupKey ?? null,
790
+ source
791
+ });
792
+ if (runDigestSignals.length > 200) {
793
+ runDigestSignals.splice(0, runDigestSignals.length - 200);
794
+ }
795
+ }
556
796
  transcriptWriteQueue = transcriptWriteQueue
557
797
  .then(async () => {
558
798
  await appendHeartbeatRunMessages(db, {
@@ -943,7 +1183,20 @@ export async function runHeartbeatForAgent(
943
1183
  runtime: workspaceResolution.runtime,
944
1184
  externalAbortSignal: activeRunAbort.signal
945
1185
  });
946
- executionSummary = execution.summary;
1186
+ const usageLimitHint = execution.dispositionHint?.kind === "provider_usage_limited" ? execution.dispositionHint : null;
1187
+ if (usageLimitHint) {
1188
+ providerUsageLimitDisposition = {
1189
+ message: usageLimitHint.message,
1190
+ notifyBoard: usageLimitHint.notifyBoard,
1191
+ pauseAgent: usageLimitHint.pauseAgent
1192
+ };
1193
+ }
1194
+ executionSummary =
1195
+ usageLimitHint?.message && usageLimitHint.message.trim().length > 0 ? usageLimitHint.message.trim() : execution.summary;
1196
+ executionSummary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(executionSummary));
1197
+ const persistedExecutionStatus: "ok" | "failed" | "skipped" = usageLimitHint ? "skipped" : execution.status;
1198
+ const persistedRunStatus: "completed" | "failed" | "skipped" =
1199
+ persistedExecutionStatus === "ok" ? "completed" : persistedExecutionStatus;
947
1200
  const normalizedUsage = execution.usage ?? {
948
1201
  inputTokens: Math.max(0, execution.tokenInput),
949
1202
  cachedInputTokens: 0,
@@ -972,7 +1225,6 @@ export async function runHeartbeatForAgent(
972
1225
  if (afterAdapterHook.failures.length > 0) {
973
1226
  pluginFailureSummary = [...pluginFailureSummary, ...afterAdapterHook.failures];
974
1227
  }
975
- emitCanonicalResultEvent(executionSummary, "completed");
976
1228
  executionTrace = execution.trace ?? null;
977
1229
  const runtimeModelId = resolveRuntimeModelId({
978
1230
  runtimeModel: persistedRuntime.runtimeModel,
@@ -983,6 +1235,7 @@ export async function runHeartbeatForAgent(
983
1235
  const costDecision = await appendFinishedRunCostEntry({
984
1236
  db,
985
1237
  companyId,
1238
+ runId,
986
1239
  providerType: agent.providerType,
987
1240
  runtimeModelId: effectivePricingModelId ?? runtimeModelId,
988
1241
  pricingProviderType: effectivePricingProviderType,
@@ -994,7 +1247,7 @@ export async function runHeartbeatForAgent(
994
1247
  issueId: primaryIssueId,
995
1248
  projectId: primaryProjectId,
996
1249
  agentId,
997
- status: execution.status
1250
+ status: persistedExecutionStatus
998
1251
  });
999
1252
  const executionUsdCost = costDecision.usdCost;
1000
1253
  await appendProjectBudgetUsage(db, {
@@ -1007,8 +1260,8 @@ export async function runHeartbeatForAgent(
1007
1260
  companyId,
1008
1261
  agentId,
1009
1262
  runId,
1010
- status: execution.status,
1011
- summary: execution.summary,
1263
+ status: persistedExecutionStatus === "ok" ? "ok" : "failed",
1264
+ summary: executionSummary,
1012
1265
  outcomeKind: executionOutcome?.kind ?? null,
1013
1266
  mission: context.company.mission ?? null,
1014
1267
  goalContext: {
@@ -1030,7 +1283,7 @@ export async function runHeartbeatForAgent(
1030
1283
  candidateFacts: persistedMemory.candidateFacts
1031
1284
  }
1032
1285
  });
1033
- if (execution.status === "ok") {
1286
+ if (execution.status === "ok" && !usageLimitHint) {
1034
1287
  for (const fact of persistedMemory.candidateFacts) {
1035
1288
  const targetFile = await appendDurableFact({
1036
1289
  companyId,
@@ -1054,7 +1307,7 @@ export async function runHeartbeatForAgent(
1054
1307
  }
1055
1308
  }
1056
1309
  const missionAlignment = computeMissionAlignmentSignal({
1057
- summary: execution.summary,
1310
+ summary: executionSummary,
1058
1311
  mission: context.company.mission ?? null,
1059
1312
  companyGoals: context.goalContext?.companyGoals ?? [],
1060
1313
  projectGoals: context.goalContext?.projectGoals ?? []
@@ -1079,7 +1332,7 @@ export async function runHeartbeatForAgent(
1079
1332
  executionUsdCost > 0 ||
1080
1333
  effectiveTokenInput > 0 ||
1081
1334
  effectiveTokenOutput > 0 ||
1082
- execution.status !== "skipped"
1335
+ persistedExecutionStatus !== "skipped"
1083
1336
  ) {
1084
1337
  await db
1085
1338
  .update(agents)
@@ -1157,8 +1410,8 @@ export async function runHeartbeatForAgent(
1157
1410
  runId,
1158
1411
  requestId: options?.requestId,
1159
1412
  providerType: agent.providerType,
1160
- status: execution.status,
1161
- summary: execution.summary
1413
+ status: persistedExecutionStatus,
1414
+ summary: executionSummary
1162
1415
  },
1163
1416
  failClosed: false
1164
1417
  });
@@ -1166,29 +1419,74 @@ export async function runHeartbeatForAgent(
1166
1419
  pluginFailureSummary = [...pluginFailureSummary, ...beforePersistHook.failures];
1167
1420
  }
1168
1421
 
1422
+ const runDigest = buildRunDigest({
1423
+ status: persistedRunStatus,
1424
+ executionSummary,
1425
+ outcome: executionOutcome,
1426
+ trace: executionTrace,
1427
+ signals: runDigestSignals
1428
+ });
1429
+ const terminalPresentation = resolveRunTerminalPresentation({
1430
+ internalStatus: persistedRunStatus,
1431
+ executionSummary,
1432
+ outcome: executionOutcome,
1433
+ trace: executionTrace
1434
+ });
1435
+ const runCost = buildRunCostSummary({
1436
+ tokenInput: effectiveTokenInput,
1437
+ tokenOutput: effectiveTokenOutput,
1438
+ usdCost: costDecision.usdCostStatus === "unknown" ? null : executionUsdCost,
1439
+ usdCostStatus: costDecision.usdCostStatus,
1440
+ pricingSource: costDecision.pricingSource ?? null,
1441
+ source: readTraceString(execution.trace, "usageSource") ?? "unknown"
1442
+ });
1443
+ const runReport = buildRunCompletionReport({
1444
+ companyId,
1445
+ agentName: agent.name,
1446
+ providerType: agent.providerType as HeartbeatProviderType,
1447
+ issueIds,
1448
+ executionSummary,
1449
+ outcome: executionOutcome,
1450
+ finalRunOutput: execution.finalRunOutput ?? null,
1451
+ trace: executionTrace,
1452
+ digest: runDigest,
1453
+ terminal: terminalPresentation,
1454
+ cost: runCost,
1455
+ runtimeCwd: workspaceResolution.runtime.cwd
1456
+ });
1457
+ emitCanonicalResultEvent(runReport.resultSummary, runReport.finalStatus);
1458
+ const runListMessage = buildRunListMessageFromReport(runReport);
1169
1459
  await db
1170
1460
  .update(heartbeatRuns)
1171
1461
  .set({
1172
- status: execution.status === "failed" ? "failed" : "completed",
1462
+ status: persistedRunStatus,
1173
1463
  finishedAt: new Date(),
1174
- message: execution.summary
1464
+ message: runListMessage
1175
1465
  })
1176
1466
  .where(eq(heartbeatRuns.id, runId));
1177
1467
  publishHeartbeatRunStatus(options?.realtimeHub, {
1178
1468
  companyId,
1179
1469
  runId,
1180
- status: execution.status === "failed" ? "failed" : "completed",
1181
- message: execution.summary,
1470
+ status: persistedRunStatus,
1471
+ message: runListMessage,
1182
1472
  finishedAt: new Date()
1183
1473
  });
1474
+ await appendAuditEvent(db, {
1475
+ companyId,
1476
+ actorType: "system",
1477
+ eventType: "heartbeat.run_digest",
1478
+ entityType: "heartbeat_run",
1479
+ entityId: runId,
1480
+ correlationId: options?.requestId ?? runId,
1481
+ payload: runDigest
1482
+ });
1184
1483
  try {
1185
1484
  await appendRunSummaryComments(db, {
1186
1485
  companyId,
1187
1486
  issueIds,
1188
1487
  agentId,
1189
1488
  runId,
1190
- status: execution.status === "failed" ? "failed" : "completed",
1191
- executionSummary: execution.summary
1489
+ report: runReport
1192
1490
  });
1193
1491
  } catch (commentError) {
1194
1492
  await appendAuditEvent(db, {
@@ -1209,6 +1507,7 @@ export async function runHeartbeatForAgent(
1209
1507
  const fallbackMessages = normalizeTraceTranscript(executionTrace);
1210
1508
  const fallbackHighSignalCount = fallbackMessages.filter((message) => message.signalLevel === "high").length;
1211
1509
  const shouldAppendFallback =
1510
+ !providerUsageLimitDisposition &&
1212
1511
  fallbackMessages.length > 0 &&
1213
1512
  (transcriptLiveCount === 0 ||
1214
1513
  transcriptLiveUsefulCount < 2 ||
@@ -1253,6 +1552,24 @@ export async function runHeartbeatForAgent(
1253
1552
  source: "trace_fallback",
1254
1553
  createdAt
1255
1554
  }));
1555
+ for (const row of rows) {
1556
+ if (!isUsefulTranscriptSignal(row.signalLevel)) {
1557
+ continue;
1558
+ }
1559
+ runDigestSignals.push({
1560
+ sequence: row.sequence,
1561
+ kind: row.kind,
1562
+ label: row.label,
1563
+ text: row.text,
1564
+ payload: row.payloadJson,
1565
+ signalLevel: row.signalLevel,
1566
+ groupKey: row.groupKey,
1567
+ source: "trace_fallback"
1568
+ });
1569
+ }
1570
+ if (runDigestSignals.length > 200) {
1571
+ runDigestSignals.splice(0, runDigestSignals.length - 200);
1572
+ }
1256
1573
  await appendHeartbeatRunMessages(db, {
1257
1574
  companyId,
1258
1575
  runId,
@@ -1287,8 +1604,8 @@ export async function runHeartbeatForAgent(
1287
1604
  runId,
1288
1605
  requestId: options?.requestId,
1289
1606
  providerType: agent.providerType,
1290
- status: execution.status,
1291
- summary: execution.summary,
1607
+ status: persistedExecutionStatus,
1608
+ summary: executionSummary,
1292
1609
  trace: executionTrace,
1293
1610
  outcome: executionOutcome
1294
1611
  },
@@ -1298,6 +1615,48 @@ export async function runHeartbeatForAgent(
1298
1615
  pluginFailureSummary = [...pluginFailureSummary, ...afterPersistHook.failures];
1299
1616
  }
1300
1617
 
1618
+ if (providerUsageLimitDisposition) {
1619
+ await appendAuditEvent(db, {
1620
+ companyId,
1621
+ actorType: "system",
1622
+ eventType: "heartbeat.provider_usage_limited",
1623
+ entityType: "heartbeat_run",
1624
+ entityId: runId,
1625
+ correlationId: options?.requestId ?? runId,
1626
+ payload: {
1627
+ agentId,
1628
+ providerType: agent.providerType,
1629
+ issueIds,
1630
+ message: providerUsageLimitDisposition.message
1631
+ }
1632
+ });
1633
+ const pauseResult = providerUsageLimitDisposition.pauseAgent
1634
+ ? await pauseAgentForProviderUsageLimit(db, {
1635
+ companyId,
1636
+ agentId,
1637
+ requestId: options?.requestId ?? runId,
1638
+ runId,
1639
+ providerType: agent.providerType,
1640
+ message: providerUsageLimitDisposition.message
1641
+ })
1642
+ : { paused: false };
1643
+ if (providerUsageLimitDisposition.notifyBoard) {
1644
+ await appendProviderUsageLimitBoardComments(db, {
1645
+ companyId,
1646
+ issueIds,
1647
+ agentId,
1648
+ runId,
1649
+ providerType: agent.providerType,
1650
+ message: providerUsageLimitDisposition.message,
1651
+ paused: pauseResult.paused
1652
+ });
1653
+ if (options?.realtimeHub) {
1654
+ await publishAttentionSnapshot(db, options.realtimeHub, companyId);
1655
+ }
1656
+ }
1657
+ await publishOfficeOccupantForAgent(db, options?.realtimeHub, companyId, agentId);
1658
+ }
1659
+
1301
1660
  await appendAuditEvent(db, {
1302
1661
  companyId,
1303
1662
  actorType: "system",
@@ -1307,14 +1666,17 @@ export async function runHeartbeatForAgent(
1307
1666
  correlationId: options?.requestId ?? runId,
1308
1667
  payload: {
1309
1668
  agentId,
1310
- result: execution.summary,
1311
- message: execution.summary,
1669
+ status: persistedRunStatus,
1670
+ result: runReport.resultSummary,
1671
+ message: runListMessage,
1672
+ report: runReport,
1312
1673
  outcome: executionOutcome,
1313
1674
  issueIds,
1314
1675
  usage: {
1315
1676
  tokenInput: effectiveTokenInput,
1316
1677
  tokenOutput: effectiveTokenOutput,
1317
1678
  usdCost: executionUsdCost,
1679
+ usdCostStatus: costDecision.usdCostStatus,
1318
1680
  source: readTraceString(execution.trace, "usageSource") ?? "unknown"
1319
1681
  },
1320
1682
  trace: execution.trace ?? null,
@@ -1408,6 +1770,7 @@ export async function runHeartbeatForAgent(
1408
1770
  const failureCostDecision = await appendFinishedRunCostEntry({
1409
1771
  db,
1410
1772
  companyId,
1773
+ runId,
1411
1774
  providerType: agent.providerType,
1412
1775
  runtimeModelId,
1413
1776
  pricingProviderType: agent.providerType,
@@ -1423,29 +1786,76 @@ export async function runHeartbeatForAgent(
1423
1786
  companyId,
1424
1787
  projectCostsUsd: buildProjectBudgetCostAllocations(executionWorkItemsForBudget, failureCostDecision.usdCost)
1425
1788
  });
1789
+ const runDigest = buildRunDigest({
1790
+ status: "failed",
1791
+ executionSummary,
1792
+ outcome: executionOutcome,
1793
+ trace: executionTrace,
1794
+ signals: runDigestSignals
1795
+ });
1796
+ const runCost = buildRunCostSummary({
1797
+ tokenInput: 0,
1798
+ tokenOutput: 0,
1799
+ usdCost: failureCostDecision.usdCostStatus === "unknown" ? null : failureCostDecision.usdCost,
1800
+ usdCostStatus: failureCostDecision.usdCostStatus,
1801
+ pricingSource: failureCostDecision.pricingSource ?? null,
1802
+ source: readTraceString(executionTrace, "usageSource") ?? "unknown"
1803
+ });
1804
+ const runReport = buildRunCompletionReport({
1805
+ companyId,
1806
+ agentName: agent.name,
1807
+ providerType: agent.providerType as HeartbeatProviderType,
1808
+ issueIds,
1809
+ executionSummary,
1810
+ outcome: executionOutcome,
1811
+ finalRunOutput: null,
1812
+ trace: executionTrace,
1813
+ digest: runDigest,
1814
+ terminal: resolveRunTerminalPresentation({
1815
+ internalStatus: "failed",
1816
+ executionSummary,
1817
+ outcome: executionOutcome,
1818
+ trace: executionTrace,
1819
+ errorType: classified.type
1820
+ }),
1821
+ cost: runCost,
1822
+ runtimeCwd: runtimeLaunchSummary?.cwd ?? persistedRuntime.runtimeCwd ?? null,
1823
+ errorType: classified.type,
1824
+ errorMessage: classified.message
1825
+ });
1826
+ const runListMessage = buildRunListMessageFromReport(runReport);
1426
1827
  await db
1427
1828
  .update(heartbeatRuns)
1428
1829
  .set({
1429
1830
  status: "failed",
1430
1831
  finishedAt: new Date(),
1431
- message: executionSummary
1832
+ message: runListMessage
1432
1833
  })
1433
1834
  .where(eq(heartbeatRuns.id, runId));
1434
1835
  publishHeartbeatRunStatus(options?.realtimeHub, {
1435
1836
  companyId,
1436
1837
  runId,
1437
1838
  status: "failed",
1438
- message: executionSummary,
1839
+ message: runListMessage,
1439
1840
  finishedAt: new Date()
1440
1841
  });
1842
+ emitCanonicalResultEvent(runReport.resultSummary, runReport.finalStatus);
1843
+ await appendAuditEvent(db, {
1844
+ companyId,
1845
+ actorType: "system",
1846
+ eventType: "heartbeat.run_digest",
1847
+ entityType: "heartbeat_run",
1848
+ entityId: runId,
1849
+ correlationId: options?.requestId ?? runId,
1850
+ payload: runDigest
1851
+ });
1441
1852
  try {
1442
1853
  await appendRunSummaryComments(db, {
1443
1854
  companyId,
1444
1855
  issueIds,
1445
1856
  agentId,
1446
1857
  runId,
1447
- status: "failed",
1448
- executionSummary
1858
+ report: runReport
1449
1859
  });
1450
1860
  } catch (commentError) {
1451
1861
  await appendAuditEvent(db, {
@@ -1472,12 +1882,17 @@ export async function runHeartbeatForAgent(
1472
1882
  payload: {
1473
1883
  agentId,
1474
1884
  issueIds,
1475
- result: executionSummary,
1476
- message: executionSummary,
1885
+ result: runReport.resultSummary,
1886
+ message: runListMessage,
1477
1887
  errorType: classified.type,
1478
1888
  errorMessage: classified.message,
1889
+ report: runReport,
1479
1890
  outcome: executionOutcome,
1480
1891
  usage: {
1892
+ tokenInput: 0,
1893
+ tokenOutput: 0,
1894
+ usdCost: failureCostDecision.usdCost,
1895
+ usdCostStatus: failureCostDecision.usdCostStatus,
1481
1896
  source: readTraceString(executionTrace, "usageSource") ?? "unknown"
1482
1897
  },
1483
1898
  trace: executionTrace,
@@ -2290,16 +2705,6 @@ function sanitizeAgentSummaryCommentBody(body: string) {
2290
2705
  return sanitized.length > 0 ? sanitized : "Run update.";
2291
2706
  }
2292
2707
 
2293
- function buildRunSummaryCommentBody(input: { status: "completed" | "failed"; executionSummary: string }) {
2294
- const summary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
2295
- if (input.status === "failed") {
2296
- return summary.toLowerCase().startsWith("couldn't")
2297
- ? summary
2298
- : `Couldn't complete this run: ${summary.charAt(0).toLowerCase()}${summary.slice(1)}`;
2299
- }
2300
- return summary;
2301
- }
2302
-
2303
2708
  function extractNaturalRunUpdate(executionSummary: string) {
2304
2709
  const normalized = executionSummary.trim();
2305
2710
  const jsonSummary = extractSummaryFromJsonLikeText(normalized);
@@ -2323,6 +2728,676 @@ function extractNaturalRunUpdate(executionSummary: string) {
2323
2728
  return /[.!?]$/.test(bounded) ? bounded : `${bounded}.`;
2324
2729
  }
2325
2730
 
2731
+ function buildRunDigest(input: {
2732
+ status: "completed" | "failed" | "skipped";
2733
+ executionSummary: string;
2734
+ outcome: ExecutionOutcome | null;
2735
+ trace: unknown;
2736
+ signals: RunDigestSignal[];
2737
+ }): RunDigest {
2738
+ const summary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
2739
+ const successes: string[] = [];
2740
+ const failures: string[] = [];
2741
+ const blockers: string[] = [];
2742
+ if (input.outcome) {
2743
+ for (const action of input.outcome.actions) {
2744
+ const detail = summarizeRunDigestPoint(action.detail);
2745
+ if (!detail) {
2746
+ continue;
2747
+ }
2748
+ if (action.status === "ok") {
2749
+ successes.push(detail);
2750
+ } else if (action.status === "error") {
2751
+ failures.push(detail);
2752
+ }
2753
+ }
2754
+ for (const blocker of input.outcome.blockers) {
2755
+ const detail = summarizeRunDigestPoint(blocker.message);
2756
+ if (detail) {
2757
+ blockers.push(detail);
2758
+ }
2759
+ }
2760
+ }
2761
+ for (const signal of input.signals) {
2762
+ if (signal.signalLevel !== "high" && signal.signalLevel !== "medium") {
2763
+ continue;
2764
+ }
2765
+ const signalText = summarizeRunDigestPoint(signal.text ?? signal.payload ?? "");
2766
+ if (!signalText) {
2767
+ continue;
2768
+ }
2769
+ if (signal.kind === "tool_result" || signal.kind === "stderr") {
2770
+ if (looksLikeRunFailureSignal(signalText)) {
2771
+ failures.push(signalText);
2772
+ } else if (signal.kind === "tool_result") {
2773
+ successes.push(signalText);
2774
+ }
2775
+ continue;
2776
+ }
2777
+ if (signal.kind === "result" && !looksLikeRunFailureSignal(signalText)) {
2778
+ successes.push(signalText);
2779
+ }
2780
+ }
2781
+ if (input.status === "completed" && successes.length === 0) {
2782
+ successes.push(summary);
2783
+ }
2784
+ if (input.status === "failed" && failures.length === 0) {
2785
+ failures.push(summary);
2786
+ }
2787
+ if (input.status === "failed" && blockers.length === 0) {
2788
+ const traceFailureType = summarizeRunDigestPoint(readTraceString(input.trace, "failureType") ?? "");
2789
+ if (traceFailureType) {
2790
+ blockers.push(`failure type: ${traceFailureType}`);
2791
+ }
2792
+ }
2793
+ const uniqueSuccesses = dedupeRunDigestPoints(successes, 3);
2794
+ const uniqueFailures = dedupeRunDigestPoints(failures, 3);
2795
+ const uniqueBlockers = dedupeRunDigestPoints(blockers, 2);
2796
+ const headline =
2797
+ input.status === "completed"
2798
+ ? `Run completed: ${summary}`
2799
+ : input.status === "failed"
2800
+ ? `Run failed: ${summary}`
2801
+ : `Run skipped: ${summary}`;
2802
+ const nextAction = resolveRunDigestNextAction({
2803
+ status: input.status,
2804
+ blockers: uniqueBlockers,
2805
+ failures: uniqueFailures
2806
+ });
2807
+ return {
2808
+ status: input.status,
2809
+ headline,
2810
+ summary,
2811
+ successes: uniqueSuccesses,
2812
+ failures: uniqueFailures,
2813
+ blockers: uniqueBlockers,
2814
+ nextAction,
2815
+ evidence: {
2816
+ transcriptSignalCount: input.signals.length,
2817
+ outcomeActionCount: input.outcome?.actions.length ?? 0,
2818
+ outcomeBlockerCount: input.outcome?.blockers.length ?? 0,
2819
+ failureType: readTraceString(input.trace, "failureType")
2820
+ }
2821
+ };
2822
+ }
2823
+
2824
+ function summarizeRunDigestPoint(value: string | null | undefined) {
2825
+ if (!value) {
2826
+ return "";
2827
+ }
2828
+ const normalized = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(value));
2829
+ if (!normalized || normalized.toLowerCase() === "run update.") {
2830
+ return "";
2831
+ }
2832
+ const bounded = normalized.length > 180 ? `${normalized.slice(0, 177).trimEnd()}...` : normalized;
2833
+ return bounded;
2834
+ }
2835
+
2836
+ function dedupeRunDigestPoints(values: string[], limit: number) {
2837
+ const seen = new Set<string>();
2838
+ const deduped: string[] = [];
2839
+ for (const value of values) {
2840
+ const key = value.toLowerCase().replace(/\s+/g, " ").trim();
2841
+ if (!key || seen.has(key)) {
2842
+ continue;
2843
+ }
2844
+ seen.add(key);
2845
+ deduped.push(value);
2846
+ if (deduped.length >= limit) {
2847
+ break;
2848
+ }
2849
+ }
2850
+ return deduped;
2851
+ }
2852
+
2853
+ function looksLikeRunFailureSignal(value: string) {
2854
+ const normalized = value.toLowerCase();
2855
+ return /(failed|error|exception|timed out|timeout|unauthorized|not supported|unsupported|no capacity|rate limit|429|500|blocked|unable to)/.test(
2856
+ normalized
2857
+ );
2858
+ }
2859
+
2860
+ function resolveRunDigestNextAction(input: { status: "completed" | "failed" | "skipped"; blockers: string[]; failures: string[] }) {
2861
+ if (input.status === "completed") {
2862
+ return "Review outputs and move the issue to the next workflow state.";
2863
+ }
2864
+ const combined = [...input.blockers, ...input.failures].join(" ").toLowerCase();
2865
+ if (combined.includes("auth") || combined.includes("unauthorized") || combined.includes("login")) {
2866
+ return "Fix credentials/authentication, then rerun.";
2867
+ }
2868
+ if (combined.includes("model") && (combined.includes("not supported") || combined.includes("unavailable"))) {
2869
+ return "Select a supported model and rerun.";
2870
+ }
2871
+ if (combined.includes("usage limit") || combined.includes("rate limit") || combined.includes("no capacity")) {
2872
+ return "Retry after provider quota/capacity recovers.";
2873
+ }
2874
+ return "Fix listed failures/blockers and rerun.";
2875
+ }
2876
+
2877
+ function resolveRunTerminalPresentation(input: {
2878
+ internalStatus: "completed" | "failed" | "skipped";
2879
+ executionSummary: string;
2880
+ outcome: ExecutionOutcome | null;
2881
+ trace: unknown;
2882
+ errorType?: string | null;
2883
+ }) : RunTerminalPresentation {
2884
+ if (isNoAssignedWorkOutcomeForReport(input.outcome)) {
2885
+ return {
2886
+ internalStatus: input.internalStatus,
2887
+ publicStatus: "completed",
2888
+ completionReason: "no_assigned_work"
2889
+ };
2890
+ }
2891
+ if (input.internalStatus === "completed") {
2892
+ return {
2893
+ internalStatus: input.internalStatus,
2894
+ publicStatus: "completed",
2895
+ completionReason: "task_completed"
2896
+ };
2897
+ }
2898
+ const completionReason = inferRunCompletionReason(input);
2899
+ return {
2900
+ internalStatus: input.internalStatus,
2901
+ publicStatus: "failed",
2902
+ completionReason
2903
+ };
2904
+ }
2905
+
2906
+ function inferRunCompletionReason(input: {
2907
+ internalStatus: "completed" | "failed" | "skipped";
2908
+ executionSummary: string;
2909
+ outcome: ExecutionOutcome | null;
2910
+ trace: unknown;
2911
+ errorType?: string | null;
2912
+ }): RunCompletionReason {
2913
+ const texts = [
2914
+ input.executionSummary,
2915
+ readTraceString(input.trace, "failureType") ?? "",
2916
+ readTraceString(input.trace, "stderrPreview") ?? "",
2917
+ input.errorType ?? "",
2918
+ ...(input.outcome?.blockers ?? []).flatMap((blocker) => [blocker.code, blocker.message]),
2919
+ ...(input.outcome?.actions ?? []).flatMap((action) => [action.type, action.detail ?? ""])
2920
+ ];
2921
+ const combined = texts.join("\n").toLowerCase();
2922
+ if (
2923
+ combined.includes("insufficient_quota") ||
2924
+ combined.includes("billing_hard_limit_reached") ||
2925
+ combined.includes("out of funds") ||
2926
+ combined.includes("payment required")
2927
+ ) {
2928
+ return "provider_out_of_funds";
2929
+ }
2930
+ if (
2931
+ combined.includes("usage limit") ||
2932
+ combined.includes("rate limit") ||
2933
+ combined.includes("429") ||
2934
+ combined.includes("quota")
2935
+ ) {
2936
+ return combined.includes("quota") ? "provider_quota_exhausted" : "provider_rate_limited";
2937
+ }
2938
+ if (combined.includes("budget hard-stop")) {
2939
+ return "budget_hard_stop";
2940
+ }
2941
+ if (combined.includes("already in progress") || combined.includes("skipped_overlap")) {
2942
+ return "overlap_in_progress";
2943
+ }
2944
+ if (combined.includes("unauthorized") || combined.includes("auth") || combined.includes("api key")) {
2945
+ return "auth_error";
2946
+ }
2947
+ if (combined.includes("contract") || combined.includes("missing_structured_output")) {
2948
+ return "contract_invalid";
2949
+ }
2950
+ if (combined.includes("watchdog_timeout") || combined.includes("runtime_timeout") || combined.includes("timed out")) {
2951
+ return "timeout";
2952
+ }
2953
+ if (combined.includes("cancelled")) {
2954
+ return "cancelled";
2955
+ }
2956
+ if (combined.includes("enoent") || combined.includes("runtime_missing")) {
2957
+ return "runtime_missing";
2958
+ }
2959
+ if (
2960
+ combined.includes("provider unavailable") ||
2961
+ combined.includes("no capacity") ||
2962
+ combined.includes("unavailable") ||
2963
+ combined.includes("http_error")
2964
+ ) {
2965
+ return "provider_unavailable";
2966
+ }
2967
+ if (input.outcome?.kind === "blocked") {
2968
+ return "blocked";
2969
+ }
2970
+ return "runtime_error";
2971
+ }
2972
+
2973
+ function isNoAssignedWorkOutcomeForReport(outcome: ExecutionOutcome | null) {
2974
+ if (!outcome) {
2975
+ return false;
2976
+ }
2977
+ if (outcome.kind !== "skipped") {
2978
+ return false;
2979
+ }
2980
+ if (outcome.issueIdsTouched.length === 0) {
2981
+ return true;
2982
+ }
2983
+ return outcome.actions.some((action) => action.type === "heartbeat.skip");
2984
+ }
2985
+
2986
+ function buildRunCostSummary(input: {
2987
+ tokenInput: number;
2988
+ tokenOutput: number;
2989
+ usdCost: number | null;
2990
+ usdCostStatus: "exact" | "estimated" | "unknown";
2991
+ pricingSource: string | null;
2992
+ source: string | null;
2993
+ }): RunCostSummary {
2994
+ return {
2995
+ tokenInput: Math.max(0, input.tokenInput),
2996
+ tokenOutput: Math.max(0, input.tokenOutput),
2997
+ usdCost: input.usdCostStatus === "unknown" ? null : Math.max(0, input.usdCost ?? 0),
2998
+ usdCostStatus: input.usdCostStatus,
2999
+ pricingSource: input.pricingSource ?? null,
3000
+ source: input.source ?? null
3001
+ };
3002
+ }
3003
+
3004
+ function buildRunArtifacts(input: {
3005
+ outcome: ExecutionOutcome | null;
3006
+ finalRunOutput?: AgentFinalRunOutput | null;
3007
+ runtimeCwd?: string | null;
3008
+ workspaceRootPath?: string | null;
3009
+ companyId?: string;
3010
+ }): RunArtifact[] {
3011
+ const sourceArtifacts =
3012
+ input.finalRunOutput?.artifacts && input.finalRunOutput.artifacts.length > 0
3013
+ ? input.finalRunOutput.artifacts
3014
+ : input.outcome?.artifacts ?? [];
3015
+ if (sourceArtifacts.length === 0) {
3016
+ return [];
3017
+ }
3018
+ const runtimeCwd = input.runtimeCwd?.trim() ? input.runtimeCwd.trim() : null;
3019
+ const workspaceRootPath = input.workspaceRootPath?.trim() ? input.workspaceRootPath.trim() : null;
3020
+ const companyId = input.companyId?.trim() ? input.companyId.trim() : null;
3021
+ return sourceArtifacts.map((artifact) => {
3022
+ const originalPath = artifact.path.trim();
3023
+ const artifactIsAbsolute = isAbsolute(originalPath);
3024
+ const absolutePath = artifactIsAbsolute ? resolve(originalPath) : runtimeCwd ? resolve(runtimeCwd, originalPath) : null;
3025
+ let relativePathValue: string | null = null;
3026
+ if (absolutePath && workspaceRootPath && isInsidePath(workspaceRootPath, absolutePath)) {
3027
+ relativePathValue = toNormalizedWorkspaceRelativePath(relative(workspaceRootPath, absolutePath));
3028
+ } else if (!artifactIsAbsolute) {
3029
+ relativePathValue = toNormalizedWorkspaceRelativePath(originalPath);
3030
+ } else if (runtimeCwd) {
3031
+ const candidate = toNormalizedWorkspaceRelativePath(relative(runtimeCwd, absolutePath ?? originalPath));
3032
+ relativePathValue = candidate && !candidate.startsWith("../") ? candidate : null;
3033
+ }
3034
+ if (companyId) {
3035
+ const normalizedRelative = normalizeAgentOperatingArtifactRelativePath(relativePathValue, companyId);
3036
+ if (normalizedRelative) {
3037
+ relativePathValue = normalizedRelative;
3038
+ } else {
3039
+ const normalizedOriginal = toNormalizedWorkspaceRelativePath(originalPath);
3040
+ const normalizedFromOriginal = normalizeAgentOperatingArtifactRelativePath(normalizedOriginal, companyId);
3041
+ if (normalizedFromOriginal) {
3042
+ relativePathValue = normalizedFromOriginal;
3043
+ }
3044
+ }
3045
+ }
3046
+ const location = relativePathValue ?? absolutePath ?? originalPath;
3047
+ return {
3048
+ path: originalPath,
3049
+ kind: artifact.kind,
3050
+ label: describeArtifact(artifact.kind, location),
3051
+ relativePath: relativePathValue,
3052
+ absolutePath
3053
+ };
3054
+ });
3055
+ }
3056
+
3057
+ function toNormalizedWorkspaceRelativePath(inputPath: string | null | undefined) {
3058
+ const trimmed = inputPath?.trim();
3059
+ if (!trimmed) {
3060
+ return null;
3061
+ }
3062
+ const unixSeparated = trimmed.replace(/\\/g, "/");
3063
+ const parts: string[] = [];
3064
+ for (const part of unixSeparated.split("/")) {
3065
+ if (!part || part === ".") {
3066
+ continue;
3067
+ }
3068
+ if (part === "..") {
3069
+ if (parts.length > 0 && parts[parts.length - 1] !== "..") {
3070
+ parts.pop();
3071
+ } else {
3072
+ parts.push(part);
3073
+ }
3074
+ continue;
3075
+ }
3076
+ parts.push(part);
3077
+ }
3078
+ const normalized = parts.join("/");
3079
+ return normalized || null;
3080
+ }
3081
+
3082
+ function normalizeAgentOperatingArtifactRelativePath(pathValue: string | null, companyId: string) {
3083
+ const normalized = toNormalizedWorkspaceRelativePath(pathValue);
3084
+ if (!normalized) {
3085
+ return null;
3086
+ }
3087
+ const workspaceScopedMatch = normalized.match(/(?:^|\/)(workspace\/[^/]+\/agents\/[^/]+\/operating(?:\/.*)?)$/);
3088
+ if (workspaceScopedMatch) {
3089
+ const scopedPath = toNormalizedWorkspaceRelativePath(workspaceScopedMatch[1]);
3090
+ if (!scopedPath) {
3091
+ return null;
3092
+ }
3093
+ const parsed = scopedPath.match(/^workspace\/([^/]+)\/agents\/([^/]+)\/operating(\/.*)?$/);
3094
+ if (!parsed) {
3095
+ return null;
3096
+ }
3097
+ const embeddedCompanyId = parsed[1]?.trim() || companyId;
3098
+ const agentId = parsed[2];
3099
+ const suffix = parsed[3] ?? "";
3100
+ const effectiveCompanyId = embeddedCompanyId;
3101
+ return `workspace/${effectiveCompanyId}/agents/${agentId}/operating${suffix}`;
3102
+ }
3103
+ const directMatch = normalized.match(/^agents\/([^/]+)\/operating(\/.*)?$/);
3104
+ if (directMatch) {
3105
+ const [, agentId, suffix = ""] = directMatch;
3106
+ return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
3107
+ }
3108
+ const issueScopedMatch = normalized.match(
3109
+ /^(?:[^/]+\/)?projects\/[^/]+\/issues\/[^/]+\/agents\/([^/]+)\/operating(\/.*)?$/
3110
+ );
3111
+ if (issueScopedMatch) {
3112
+ const [, agentId, suffix = ""] = issueScopedMatch;
3113
+ return `workspace/${companyId}/agents/${agentId}/operating${suffix}`;
3114
+ }
3115
+ return null;
3116
+ }
3117
+
3118
+ function describeArtifact(kind: string, location: string) {
3119
+ const normalizedKind = kind.toLowerCase();
3120
+ if (normalizedKind.includes("folder") || normalizedKind.includes("directory") || normalizedKind === "website") {
3121
+ return `Created ${normalizedKind.replace(/_/g, " ")} at ${location}`;
3122
+ }
3123
+ if (normalizedKind.includes("file")) {
3124
+ return `Updated file ${location}`;
3125
+ }
3126
+ return `Produced ${normalizedKind.replace(/_/g, " ")} at ${location}`;
3127
+ }
3128
+
3129
+ function buildRunCompletionReport(input: {
3130
+ companyId?: string;
3131
+ agentName: string;
3132
+ providerType: HeartbeatProviderType;
3133
+ issueIds: string[];
3134
+ executionSummary: string;
3135
+ outcome: ExecutionOutcome | null;
3136
+ finalRunOutput?: AgentFinalRunOutput | null;
3137
+ trace: unknown;
3138
+ digest: RunDigest;
3139
+ terminal: RunTerminalPresentation;
3140
+ cost: RunCostSummary;
3141
+ runtimeCwd?: string | null;
3142
+ errorType?: string | null;
3143
+ errorMessage?: string | null;
3144
+ }): RunCompletionReport {
3145
+ const workspaceRootPath = input.companyId ? resolveCompanyWorkspaceRootPath(input.companyId) : null;
3146
+ const artifacts = buildRunArtifacts({
3147
+ outcome: input.outcome,
3148
+ finalRunOutput: input.finalRunOutput,
3149
+ runtimeCwd: input.runtimeCwd,
3150
+ workspaceRootPath,
3151
+ companyId: input.companyId
3152
+ });
3153
+ const fallbackSummary = sanitizeAgentSummaryCommentBody(extractNaturalRunUpdate(input.executionSummary));
3154
+ const employeeComment =
3155
+ input.finalRunOutput?.employee_comment?.trim() || buildLegacyEmployeeComment(fallbackSummary);
3156
+ const results = input.finalRunOutput
3157
+ ? input.finalRunOutput.results.filter((value): value is string => Boolean(value))
3158
+ : input.terminal.publicStatus === "completed"
3159
+ ? dedupeRunDigestPoints(
3160
+ [
3161
+ input.digest.successes[0],
3162
+ artifacts[0]?.label,
3163
+ input.terminal.completionReason === "no_assigned_work" ? "No assigned work was available for this run." : null
3164
+ ].filter((value): value is string => Boolean(value)),
3165
+ 4
3166
+ )
3167
+ : [];
3168
+ const errors =
3169
+ input.finalRunOutput?.errors.filter((value): value is string => Boolean(value)) ??
3170
+ dedupeRunDigestPoints([...input.digest.blockers, ...input.digest.failures].filter((value): value is string => Boolean(value)), 4);
3171
+ const summary = firstMeaningfulReportLine(employeeComment) || results[0] || fallbackSummary;
3172
+ const resultSummary =
3173
+ results[0] ??
3174
+ (input.terminal.publicStatus === "completed"
3175
+ ? artifacts[0]?.label ??
3176
+ (input.terminal.completionReason === "no_assigned_work" ? "No assigned work was available for this run." : summary)
3177
+ : input.finalRunOutput
3178
+ ? summary
3179
+ : "No valid final run output was produced.");
3180
+ const statusHeadline =
3181
+ input.terminal.publicStatus === "completed"
3182
+ ? `Completed: ${summary}`
3183
+ : `Failed: ${summary}`;
3184
+ const blockers = dedupeRunDigestPoints(errors, 4);
3185
+ const artifactPaths = artifacts
3186
+ .map((artifact) => artifact.relativePath ?? artifact.absolutePath ?? artifact.path)
3187
+ .filter((value): value is string => Boolean(value));
3188
+ const managerReport = {
3189
+ agentName: input.agentName,
3190
+ providerType: input.providerType,
3191
+ whatWasDone: results[0] ?? (input.terminal.publicStatus === "completed" ? input.digest.successes[0] ?? summary : summary),
3192
+ resultSummary,
3193
+ artifactPaths,
3194
+ blockers,
3195
+ nextAction: input.digest.nextAction,
3196
+ costLine: formatRunCostLine(input.cost)
3197
+ };
3198
+ const fallbackOutcome: ExecutionOutcome = input.outcome ?? {
3199
+ kind:
3200
+ input.terminal.completionReason === "no_assigned_work"
3201
+ ? "skipped"
3202
+ : input.terminal.publicStatus === "completed"
3203
+ ? "completed"
3204
+ : "failed",
3205
+ issueIdsTouched: input.issueIds,
3206
+ artifacts: artifacts.map((artifact) => ({ path: artifact.path, kind: artifact.kind })),
3207
+ actions:
3208
+ results.length > 0
3209
+ ? results.slice(0, 4).map((result) => ({
3210
+ type: input.terminal.publicStatus === "completed" ? "run.completed" : "run.failed",
3211
+ status: input.terminal.publicStatus === "completed" ? "ok" : "error",
3212
+ detail: result
3213
+ }))
3214
+ : [
3215
+ {
3216
+ type: input.terminal.publicStatus === "completed" ? "run.completed" : "run.failed",
3217
+ status: input.terminal.publicStatus === "completed" ? "ok" : "error",
3218
+ detail: managerReport.whatWasDone
3219
+ }
3220
+ ],
3221
+ blockers: blockers.map((message) => ({
3222
+ code: input.terminal.completionReason,
3223
+ message,
3224
+ retryable: input.terminal.publicStatus !== "completed"
3225
+ })),
3226
+ nextSuggestedState: input.terminal.publicStatus === "completed" ? "in_review" : "blocked"
3227
+ };
3228
+ return {
3229
+ finalStatus: input.terminal.publicStatus,
3230
+ completionReason: input.terminal.completionReason,
3231
+ statusHeadline,
3232
+ summary,
3233
+ employeeComment,
3234
+ results,
3235
+ errors,
3236
+ resultStatus: artifacts.length > 0 ? "reported" : "none_reported",
3237
+ resultSummary,
3238
+ issueIds: input.issueIds,
3239
+ artifacts,
3240
+ blockers,
3241
+ nextAction: input.digest.nextAction,
3242
+ cost: input.cost,
3243
+ managerReport,
3244
+ outcome: input.outcome ?? fallbackOutcome,
3245
+ debug: {
3246
+ persistedRunStatus: input.terminal.internalStatus,
3247
+ failureType: readTraceString(input.trace, "failureType"),
3248
+ errorType: input.errorType ?? null,
3249
+ errorMessage: input.errorMessage ?? null
3250
+ }
3251
+ };
3252
+ }
3253
+
3254
+ function firstMeaningfulReportLine(value: string) {
3255
+ for (const rawLine of value.split(/\r?\n/)) {
3256
+ const line = rawLine.replace(/^[#>*\-\s`]+/, "").trim();
3257
+ if (line) {
3258
+ return line;
3259
+ }
3260
+ }
3261
+ return "";
3262
+ }
3263
+
3264
+ function buildLegacyEmployeeComment(summary: string) {
3265
+ return summary;
3266
+ }
3267
+
3268
+ function formatRunCostLine(cost: RunCostSummary) {
3269
+ const tokens = `${cost.tokenInput} input / ${cost.tokenOutput} output tokens`;
3270
+ if (cost.usdCostStatus === "unknown" || cost.usdCost === null || cost.usdCost === undefined) {
3271
+ return `${tokens}; dollar cost unknown`;
3272
+ }
3273
+ const qualifier = cost.usdCostStatus === "estimated" ? "estimated" : "exact";
3274
+ return `${tokens}; ${qualifier} cost $${cost.usdCost.toFixed(6)}`;
3275
+ }
3276
+
3277
+ function buildHumanRunUpdateCommentFromReport(
3278
+ report: RunCompletionReport,
3279
+ options: { runId: string; companyId: string }
3280
+ ) {
3281
+ const lines = [
3282
+ report.employeeComment.trim(),
3283
+ "",
3284
+ `- Status: ${report.finalStatus}`,
3285
+ `- Agent: ${report.managerReport.agentName}`,
3286
+ `- Provider: ${report.managerReport.providerType}`,
3287
+ ""
3288
+ ];
3289
+ if (report.results.length > 0) {
3290
+ lines.push("### Results", "");
3291
+ for (const result of report.results) {
3292
+ lines.push(`- ${result}`);
3293
+ }
3294
+ lines.push("");
3295
+ }
3296
+ lines.push("### Result", "", `- What was done: ${report.managerReport.whatWasDone}`, `- Summary: ${report.managerReport.resultSummary}`);
3297
+ if (report.artifacts.length > 0) {
3298
+ for (const [artifactIndex, artifact] of report.artifacts.entries()) {
3299
+ lines.push(`- Artifact: ${formatRunArtifactMarkdownLink(artifact, { ...options, artifactIndex })}`);
3300
+ }
3301
+ }
3302
+ lines.push("");
3303
+ lines.push("### Cost", "");
3304
+ lines.push(`- Input tokens: \`${report.cost.tokenInput}\``);
3305
+ lines.push(`- Output tokens: \`${report.cost.tokenOutput}\``);
3306
+ lines.push(`- Dollar cost: ${formatRunCostForHumanReport(report.cost)}`);
3307
+ if (report.errors.length > 0) {
3308
+ lines.push("");
3309
+ lines.push("### Errors", "");
3310
+ for (const error of report.errors) {
3311
+ lines.push(`- ${error}`);
3312
+ }
3313
+ }
3314
+ return lines.join("\n");
3315
+ }
3316
+
3317
+ function formatRunArtifactMarkdownLink(
3318
+ artifact: RunArtifact,
3319
+ options: { runId: string; companyId: string; artifactIndex: number }
3320
+ ) {
3321
+ const label = resolveRunArtifactDisplayPath(artifact);
3322
+ const href = buildRunArtifactLinkHref(options);
3323
+ if (!label) {
3324
+ return "`artifact`";
3325
+ }
3326
+ if (!href) {
3327
+ return `\`${label}\``;
3328
+ }
3329
+ return `[${label}](${href})`;
3330
+ }
3331
+
3332
+ function resolveRunArtifactDisplayPath(artifact: RunArtifact) {
3333
+ const relative = toNormalizedWorkspaceRelativePath(artifact.relativePath);
3334
+ if (relative && !relative.startsWith("../")) {
3335
+ return relative;
3336
+ }
3337
+ const pathValue = toNormalizedWorkspaceRelativePath(artifact.path);
3338
+ if (pathValue && !pathValue.startsWith("../") && !isAbsolute(artifact.path)) {
3339
+ return pathValue;
3340
+ }
3341
+ return null;
3342
+ }
3343
+
3344
+ function buildRunArtifactLinkHref(options: { runId: string; companyId: string; artifactIndex: number }) {
3345
+ const apiBaseUrl = resolveControlPlaneApiBaseUrl().replace(/\/+$/, "");
3346
+ const runId = encodeURIComponent(options.runId);
3347
+ const artifactIndex = encodeURIComponent(String(options.artifactIndex));
3348
+ const companyId = encodeURIComponent(options.companyId);
3349
+ return `${apiBaseUrl}/observability/heartbeats/${runId}/artifacts/${artifactIndex}/download?companyId=${companyId}`;
3350
+ }
3351
+
3352
+ function formatRunCostForHumanReport(cost: RunCostSummary) {
3353
+ if (cost.usdCostStatus === "unknown" || cost.usdCost === null || cost.usdCost === undefined) {
3354
+ return "unknown";
3355
+ }
3356
+ const qualifier = cost.usdCostStatus === "estimated" ? "estimated " : "exact ";
3357
+ return `${qualifier}\`$${cost.usdCost.toFixed(6)}\``;
3358
+ }
3359
+
3360
+ function buildRunListMessageFromReport(report: RunCompletionReport) {
3361
+ const resultParts =
3362
+ report.finalStatus === "completed"
3363
+ ? report.results.length > 0
3364
+ ? report.results.slice(0, 2)
3365
+ : [report.resultSummary]
3366
+ : [];
3367
+ const parts = [report.statusHeadline, ...resultParts];
3368
+ if (report.artifacts.length > 0) {
3369
+ parts.push(`Artifacts: ${report.managerReport.artifactPaths.join(", ")}`);
3370
+ }
3371
+ if (report.cost.usdCostStatus === "unknown") {
3372
+ parts.push("Cost: unknown");
3373
+ } else if (report.cost.usdCost !== null && report.cost.usdCost !== undefined) {
3374
+ parts.push(`Cost: $${report.cost.usdCost.toFixed(6)}`);
3375
+ }
3376
+ const compact = parts.filter(Boolean).join(" | ");
3377
+ return compact.length > 220 ? `${compact.slice(0, 217).trimEnd()}...` : compact;
3378
+ }
3379
+
3380
+ function isMachineNoiseLine(text: string) {
3381
+ const normalized = text.trim();
3382
+ if (!normalized) {
3383
+ return true;
3384
+ }
3385
+ if (normalized.length > 220) {
3386
+ return true;
3387
+ }
3388
+ const patterns = [
3389
+ /^command:\s*/i,
3390
+ /^\s*[\[{].*[\]}]\s*$/,
3391
+ /\/bin\/(bash|zsh|sh)/i,
3392
+ /(^|\s)(\/Users\/|\/home\/|\/private\/var\/|[A-Za-z]:\\)/,
3393
+ /\b(stderr|stdout|stack trace|exit code|payload_json|tokeninput|tokenoutput|usdcost)\b/i,
3394
+ /(^|\s)at\s+\S+:\d+:\d+/,
3395
+ /```/,
3396
+ /\{[\s\S]*"(summary|tokenInput|tokenOutput|usdCost|trace|error)"[\s\S]*\}/i
3397
+ ];
3398
+ return patterns.some((pattern) => pattern.test(normalized));
3399
+ }
3400
+
2326
3401
  function extractSummaryFromJsonLikeText(input: string) {
2327
3402
  const fencedMatch = input.match(/```(?:json)?\s*([\s\S]*?)```/i);
2328
3403
  const candidate = fencedMatch?.[1]?.trim() ?? input.match(/\{[\s\S]*\}\s*$/)?.[0]?.trim();
@@ -2354,19 +3429,18 @@ async function appendRunSummaryComments(
2354
3429
  issueIds: string[];
2355
3430
  agentId: string;
2356
3431
  runId: string;
2357
- status: "completed" | "failed";
2358
- executionSummary: string;
3432
+ report: RunCompletionReport;
2359
3433
  }
2360
3434
  ) {
2361
3435
  if (input.issueIds.length === 0) {
2362
3436
  return;
2363
3437
  }
2364
- const commentBody = buildRunSummaryCommentBody({
2365
- status: input.status,
2366
- executionSummary: input.executionSummary
3438
+ const commentBody = buildHumanRunUpdateCommentFromReport(input.report, {
3439
+ runId: input.runId,
3440
+ companyId: input.companyId
2367
3441
  });
2368
3442
  for (const issueId of input.issueIds) {
2369
- const [existingRunComment] = await db
3443
+ const existingRunComments = await db
2370
3444
  .select({ id: issueComments.id })
2371
3445
  .from(issueComments)
2372
3446
  .where(
@@ -2378,6 +3452,58 @@ async function appendRunSummaryComments(
2378
3452
  eq(issueComments.authorId, input.agentId)
2379
3453
  )
2380
3454
  )
3455
+ .orderBy(desc(issueComments.createdAt));
3456
+ if (existingRunComments.length > 0) {
3457
+ await db.delete(issueComments).where(
3458
+ and(
3459
+ eq(issueComments.companyId, input.companyId),
3460
+ inArray(
3461
+ issueComments.id,
3462
+ existingRunComments.map((comment) => comment.id)
3463
+ )
3464
+ )
3465
+ );
3466
+ }
3467
+ await addIssueComment(db, {
3468
+ companyId: input.companyId,
3469
+ issueId,
3470
+ authorType: "agent",
3471
+ authorId: input.agentId,
3472
+ runId: input.runId,
3473
+ body: commentBody
3474
+ });
3475
+ }
3476
+ }
3477
+
3478
+ async function appendProviderUsageLimitBoardComments(
3479
+ db: BopoDb,
3480
+ input: {
3481
+ companyId: string;
3482
+ issueIds: string[];
3483
+ agentId: string;
3484
+ runId: string;
3485
+ providerType: string;
3486
+ message: string;
3487
+ paused: boolean;
3488
+ }
3489
+ ) {
3490
+ if (input.issueIds.length === 0) {
3491
+ return;
3492
+ }
3493
+ const commentBody = buildProviderUsageLimitBoardCommentBody(input);
3494
+ for (const issueId of input.issueIds) {
3495
+ const [existingRunComment] = await db
3496
+ .select({ id: issueComments.id })
3497
+ .from(issueComments)
3498
+ .where(
3499
+ and(
3500
+ eq(issueComments.companyId, input.companyId),
3501
+ eq(issueComments.issueId, issueId),
3502
+ eq(issueComments.runId, input.runId),
3503
+ eq(issueComments.authorType, "system"),
3504
+ eq(issueComments.authorId, input.agentId)
3505
+ )
3506
+ )
2381
3507
  .limit(1);
2382
3508
  if (existingRunComment) {
2383
3509
  continue;
@@ -2385,14 +3511,70 @@ async function appendRunSummaryComments(
2385
3511
  await addIssueComment(db, {
2386
3512
  companyId: input.companyId,
2387
3513
  issueId,
2388
- authorType: "agent",
3514
+ authorType: "system",
2389
3515
  authorId: input.agentId,
2390
3516
  runId: input.runId,
3517
+ recipients: [
3518
+ {
3519
+ recipientType: "board",
3520
+ deliveryStatus: "pending"
3521
+ }
3522
+ ],
2391
3523
  body: commentBody
2392
3524
  });
2393
3525
  }
2394
3526
  }
2395
3527
 
3528
+ function buildProviderUsageLimitBoardCommentBody(input: {
3529
+ providerType: string;
3530
+ message: string;
3531
+ paused: boolean;
3532
+ }) {
3533
+ const providerLabel = input.providerType.replace(/[_-]+/g, " ").trim();
3534
+ const normalizedProvider = providerLabel.charAt(0).toUpperCase() + providerLabel.slice(1);
3535
+ const agentStateLine = input.paused ? "Agent paused." : "Agent already paused.";
3536
+ return `${normalizedProvider} usage limit reached.\nRun failed due to provider limits.\n${agentStateLine}\nNext: resume after usage reset or billing/credential fix.`;
3537
+ }
3538
+
3539
+ async function pauseAgentForProviderUsageLimit(
3540
+ db: BopoDb,
3541
+ input: {
3542
+ companyId: string;
3543
+ agentId: string;
3544
+ requestId: string;
3545
+ runId: string;
3546
+ providerType: string;
3547
+ message: string;
3548
+ }
3549
+ ) {
3550
+ const [agentRow] = await db
3551
+ .select({ status: agents.status })
3552
+ .from(agents)
3553
+ .where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.agentId)))
3554
+ .limit(1);
3555
+ if (!agentRow || agentRow.status === "paused" || agentRow.status === "terminated") {
3556
+ return { paused: false as const };
3557
+ }
3558
+ await db
3559
+ .update(agents)
3560
+ .set({ status: "paused", updatedAt: new Date() })
3561
+ .where(and(eq(agents.companyId, input.companyId), eq(agents.id, input.agentId)));
3562
+ await appendAuditEvent(db, {
3563
+ companyId: input.companyId,
3564
+ actorType: "system",
3565
+ eventType: "agent.paused_auto_provider_limit",
3566
+ entityType: "agent",
3567
+ entityId: input.agentId,
3568
+ correlationId: input.requestId,
3569
+ payload: {
3570
+ runId: input.runId,
3571
+ providerType: input.providerType,
3572
+ reason: input.message
3573
+ }
3574
+ });
3575
+ return { paused: true as const };
3576
+ }
3577
+
2396
3578
  function parseAgentState(stateBlob: string | null) {
2397
3579
  if (!stateBlob) {
2398
3580
  return { state: {} as AgentState, parseError: null };
@@ -2735,10 +3917,7 @@ async function resolveRuntimeWorkspaceForWorkItems(
2735
3917
  }
2736
3918
 
2737
3919
  if (projectIssue?.id) {
2738
- const issueScopedWorkspaceCwd = normalizeCompanyWorkspacePath(
2739
- companyId,
2740
- join(selectedWorkspaceCwd, "issues", projectIssue.id)
2741
- );
3920
+ const issueScopedWorkspaceCwd = resolveProjectIssueWorkspaceCwd(companyId, selectedWorkspaceCwd, projectIssue.id);
2742
3921
  await mkdir(issueScopedWorkspaceCwd, { recursive: true });
2743
3922
  selectedWorkspaceCwd = issueScopedWorkspaceCwd;
2744
3923
  }
@@ -2787,6 +3966,10 @@ async function resolveRuntimeWorkspaceForWorkItems(
2787
3966
  };
2788
3967
  }
2789
3968
 
3969
+ function resolveProjectIssueWorkspaceCwd(companyId: string, projectWorkspaceCwd: string, issueId: string) {
3970
+ return normalizeCompanyWorkspacePath(companyId, join(projectWorkspaceCwd, "issues", issueId));
3971
+ }
3972
+
2790
3973
  function resolveGitWorktreeIsolationEnabled() {
2791
3974
  const value = String(process.env.BOPO_ENABLE_GIT_WORKTREE_ISOLATION ?? "")
2792
3975
  .trim()
@@ -3414,6 +4597,7 @@ function resolveRuntimeModelId(input: { runtimeModel?: string; stateBlob?: strin
3414
4597
  async function appendFinishedRunCostEntry(input: {
3415
4598
  db: BopoDb;
3416
4599
  companyId: string;
4600
+ runId?: string | null;
3417
4601
  providerType: string;
3418
4602
  runtimeModelId: string | null;
3419
4603
  pricingProviderType?: string | null;
@@ -3440,25 +4624,22 @@ async function appendFinishedRunCostEntry(input: {
3440
4624
  const shouldPersist = input.status === "ok" || input.status === "failed";
3441
4625
  const runtimeUsdCost = Math.max(0, input.runtimeUsdCost ?? 0);
3442
4626
  const pricedUsdCost = Math.max(0, pricingDecision.usdCost);
3443
- const shouldUseRuntimeUsdCost = pricedUsdCost <= 0 && runtimeUsdCost > 0;
3444
- const baseUsdCost = shouldUseRuntimeUsdCost ? runtimeUsdCost : pricedUsdCost;
3445
- const effectiveUsdCost =
3446
- baseUsdCost > 0
3447
- ? baseUsdCost
3448
- : input.status === "failed" && input.failureType !== "spawn_error"
3449
- ? 0.000001
3450
- : 0;
4627
+ const usdCostStatus: "exact" | "estimated" | "unknown" =
4628
+ runtimeUsdCost > 0 ? "exact" : pricedUsdCost > 0 ? "estimated" : "unknown";
4629
+ const effectiveUsdCost = usdCostStatus === "exact" ? runtimeUsdCost : usdCostStatus === "estimated" ? pricedUsdCost : 0;
3451
4630
  const effectivePricingSource = pricingDecision.pricingSource;
3452
4631
  const shouldPersistWithUsage =
3453
- shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 || effectiveUsdCost > 0);
4632
+ shouldPersist && (input.tokenInput > 0 || input.tokenOutput > 0 || usdCostStatus !== "unknown");
3454
4633
  if (shouldPersistWithUsage) {
3455
4634
  await appendCost(input.db, {
3456
4635
  companyId: input.companyId,
4636
+ runId: input.runId ?? null,
3457
4637
  providerType: input.providerType,
3458
4638
  runtimeModelId: input.runtimeModelId,
3459
4639
  pricingProviderType: pricingDecision.pricingProviderType,
3460
4640
  pricingModelId: pricingDecision.pricingModelId,
3461
4641
  pricingSource: effectivePricingSource,
4642
+ usdCostStatus,
3462
4643
  tokenInput: input.tokenInput,
3463
4644
  tokenOutput: input.tokenOutput,
3464
4645
  usdCost: effectiveUsdCost.toFixed(6),
@@ -3471,7 +4652,8 @@ async function appendFinishedRunCostEntry(input: {
3471
4652
  return {
3472
4653
  ...pricingDecision,
3473
4654
  pricingSource: effectivePricingSource,
3474
- usdCost: effectiveUsdCost
4655
+ usdCost: effectiveUsdCost,
4656
+ usdCostStatus
3475
4657
  };
3476
4658
  }
3477
4659