@codemation/host 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/{AppConfigFactory-CqKWXqOm.js → AppConfigFactory-BPp02HMv.js} +82 -5
  3. package/dist/{AppConfigFactory-CqKWXqOm.js.map → AppConfigFactory-BPp02HMv.js.map} +1 -1
  4. package/dist/{AppConfigFactory-CK28UPK0.d.ts → AppConfigFactory-Dq7ttwQ_.d.ts} +6001 -144
  5. package/dist/{AppContainerFactory-CcSGFNLW.js → AppContainerFactory-D9je1sSV.js} +2604 -155
  6. package/dist/AppContainerFactory-D9je1sSV.js.map +1 -0
  7. package/dist/{CodemationAppContext-KqDoeHqN.d.ts → CodemationAppContext-P7P-xZhQ.d.ts} +2 -2
  8. package/dist/{CodemationAuthoring.types-BP6Inucu.d.ts → CodemationAuthoring.types-OMYu7vKP.d.ts} +3 -3
  9. package/dist/{CodemationConfigNormalizer-DIAE0VHw.d.ts → CodemationConfigNormalizer-BCtBrJDe.d.ts} +2 -2
  10. package/dist/{CodemationConsumerConfigLoader-nj9kTmfJ.d.ts → CodemationConsumerConfigLoader-evvw4b_a.d.ts} +2 -2
  11. package/dist/CodemationPluginListMerger-PSTtEQjC.d.ts +674 -0
  12. package/dist/{CredentialServices-BFQD_VN1.d.ts → CredentialServices-0Hk8RFY1.d.ts} +3 -3
  13. package/dist/{CredentialServices-BPKUF8Xs.js → CredentialServices-BNBMFOPt.js} +6 -1
  14. package/dist/CredentialServices-BNBMFOPt.js.map +1 -0
  15. package/dist/{PublicFrontendBootstrapFactory-DTA1iDo0.d.ts → PublicFrontendBootstrapFactory-D0_ds7nS.d.ts} +2 -2
  16. package/dist/authoring.d.ts +3 -3
  17. package/dist/consumer.d.ts +4 -4
  18. package/dist/credentials.d.ts +3 -3
  19. package/dist/credentials.js +1 -1
  20. package/dist/devServerSidecar.d.ts +1 -1
  21. package/dist/{index-Dd6BrWyH.d.ts → index-CeS2saCe.d.ts} +105 -2
  22. package/dist/index.d.ts +12 -11
  23. package/dist/index.js +5 -5
  24. package/dist/nextServer.d.ts +8 -58
  25. package/dist/nextServer.js +6 -100
  26. package/dist/{persistenceServer-DKbFDxoS.js → persistenceServer-CA0_q0D7.js} +2 -2
  27. package/dist/{persistenceServer-DKbFDxoS.js.map → persistenceServer-CA0_q0D7.js.map} +1 -1
  28. package/dist/{persistenceServer-Y-u7lV7f.d.ts → persistenceServer-CJeu1STC.d.ts} +2 -2
  29. package/dist/persistenceServer.d.ts +5 -5
  30. package/dist/persistenceServer.js +2 -2
  31. package/dist/{server-CFpgKuVE.js → server-C_ZIEOTY.js} +4 -4
  32. package/dist/{server-CFpgKuVE.js.map → server-C_ZIEOTY.js.map} +1 -1
  33. package/dist/{server-axppTMgo.d.ts → server-Clvg5x1w.d.ts} +11 -5
  34. package/dist/server.d.ts +8 -8
  35. package/dist/server.js +5 -5
  36. package/package.json +6 -5
  37. package/prisma/migrations/20260414120000_telemetry_foundation/migration.sql +112 -0
  38. package/prisma/migrations/20260414153000_telemetry_retention_metrics_refactor/migration.sql +239 -0
  39. package/prisma/migrations.sqlite/20260414120000_telemetry_foundation/migration.sql +103 -0
  40. package/prisma/migrations.sqlite/20260414153000_telemetry_retention_metrics_refactor/migration.sql +540 -0
  41. package/prisma/schema.postgresql.prisma +100 -1
  42. package/prisma/schema.sqlite.prisma +100 -1
  43. package/scripts/generate-prisma-clients.mjs +89 -1
  44. package/src/application/contracts/TelemetryDashboardContracts.ts +113 -0
  45. package/src/application/contracts/TelemetryRunTraceContracts.ts +13 -0
  46. package/src/application/cost/FrameworkCostCatalogEntries.ts +126 -0
  47. package/src/application/queries/GetTelemetryDashboardDimensionsQuery.ts +11 -0
  48. package/src/application/queries/GetTelemetryDashboardDimensionsQueryHandler.ts +20 -0
  49. package/src/application/queries/GetTelemetryDashboardRunsQuery.ts +11 -0
  50. package/src/application/queries/GetTelemetryDashboardRunsQueryHandler.ts +20 -0
  51. package/src/application/queries/GetTelemetryDashboardSummaryQuery.ts +11 -0
  52. package/src/application/queries/GetTelemetryDashboardSummaryQueryHandler.ts +29 -0
  53. package/src/application/queries/GetTelemetryDashboardTimeseriesQuery.ts +11 -0
  54. package/src/application/queries/GetTelemetryDashboardTimeseriesQueryHandler.ts +36 -0
  55. package/src/application/queries/GetTelemetryRunTraceQuery.ts +8 -0
  56. package/src/application/queries/GetTelemetryRunTraceQueryHandler.ts +20 -0
  57. package/src/application/queries/WorkflowQueryHandlers.ts +5 -0
  58. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +71 -26
  59. package/src/application/telemetry/CompositeTelemetryExporter.ts +13 -0
  60. package/src/application/telemetry/LazyExecutionTelemetryFactory.ts +21 -0
  61. package/src/application/telemetry/NoOpTelemetryExporter.ts +7 -0
  62. package/src/application/telemetry/OtelExecutionTelemetry.types.ts +41 -0
  63. package/src/application/telemetry/OtelExecutionTelemetryFactory.ts +56 -0
  64. package/src/application/telemetry/OtelIdentityFactory.ts +41 -0
  65. package/src/application/telemetry/RunEventBusTelemetryReporter.ts +188 -0
  66. package/src/application/telemetry/StoredExecutionTelemetry.ts +56 -0
  67. package/src/application/telemetry/StoredNodeExecutionTelemetry.ts +35 -0
  68. package/src/application/telemetry/StoredTelemetrySpanScope.ts +188 -0
  69. package/src/application/telemetry/TelemetryEnricherChain.ts +85 -0
  70. package/src/application/telemetry/TelemetryPrivacyPolicy.ts +19 -0
  71. package/src/application/telemetry/TelemetryQueryService.ts +815 -0
  72. package/src/application/telemetry/TelemetryRetentionTimestampFactory.ts +40 -0
  73. package/src/applicationTokens.ts +18 -0
  74. package/src/bootstrap/AppContainerFactory.ts +124 -1
  75. package/src/bootstrap/AppContainerLifecycle.ts +8 -0
  76. package/src/bootstrap/runtime/FrontendRuntime.ts +8 -0
  77. package/src/bootstrap/runtime/WorkerRuntime.ts +8 -0
  78. package/src/domain/runs/WorkflowRunRepository.ts +3 -1
  79. package/src/domain/telemetry/TelemetryContracts.ts +197 -0
  80. package/src/infrastructure/persistence/InMemoryRunTraceContextRepository.ts +56 -0
  81. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +56 -0
  82. package/src/infrastructure/persistence/InMemoryTelemetryMetricPointStore.ts +97 -0
  83. package/src/infrastructure/persistence/InMemoryTelemetrySpanStore.ts +113 -0
  84. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +4 -2
  85. package/src/infrastructure/persistence/PrismaRunTraceContextRepository.ts +92 -0
  86. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +125 -0
  87. package/src/infrastructure/persistence/PrismaTelemetryMetricPointStore.ts +134 -0
  88. package/src/infrastructure/persistence/PrismaTelemetrySpanStore.ts +166 -0
  89. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +20 -7
  90. package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -4
  91. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -0
  92. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +6488 -102
  93. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -4
  94. package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
  95. package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +100 -0
  96. package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -4
  97. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -0
  98. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +6476 -98
  99. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -4
  100. package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
  101. package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +100 -0
  102. package/src/presentation/http/ApiPaths.ts +22 -0
  103. package/src/presentation/http/hono/registrars/TelemetryHonoApiRouteRegistrar.ts +19 -0
  104. package/src/presentation/http/routeHandlers/TelemetryDashboardRequestError.ts +1 -0
  105. package/src/presentation/http/routeHandlers/TelemetryHttpRouteHandler.ts +181 -0
  106. package/dist/AppContainerFactory-CcSGFNLW.js.map +0 -1
  107. package/dist/CodemationPluginListMerger-QvUa2SIt.d.ts +0 -357
  108. package/dist/CredentialServices-BPKUF8Xs.js.map +0 -1
  109. package/dist/nextServer.js.map +0 -1
@@ -0,0 +1,134 @@
1
+ import { inject, injectable, type TelemetryAttributes } from "@codemation/core";
2
+ import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
3
+ import type {
4
+ TelemetryMetricPointListQuery,
5
+ TelemetryMetricPointRecord,
6
+ TelemetryMetricPointStore,
7
+ TelemetryMetricPointWrite,
8
+ } from "../../domain/telemetry/TelemetryContracts";
9
+ import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
10
+
11
+ @injectable()
12
+ export class PrismaTelemetryMetricPointStore implements TelemetryMetricPointStore {
13
+ constructor(
14
+ @inject(PrismaDatabaseClientToken)
15
+ private readonly prisma: PrismaDatabaseClient,
16
+ @inject(OtelIdentityFactory)
17
+ private readonly otelIdentityFactory: OtelIdentityFactory,
18
+ ) {}
19
+
20
+ async save(record: TelemetryMetricPointWrite): Promise<TelemetryMetricPointRecord> {
21
+ const metricPointId = this.otelIdentityFactory.createArtifactId();
22
+ await this.prisma.telemetryMetricPoint.create({
23
+ data: {
24
+ metricPointId,
25
+ traceId: record.traceId ?? null,
26
+ spanId: record.spanId ?? null,
27
+ runId: record.runId ?? null,
28
+ workflowId: record.workflowId,
29
+ nodeId: record.nodeId ?? null,
30
+ activationId: record.activationId ?? null,
31
+ metricName: record.name,
32
+ value: record.value,
33
+ unit: record.unit ?? null,
34
+ observedAt: record.observedAt,
35
+ workflowFolder: record.workflowFolder ?? null,
36
+ nodeType: record.nodeType ?? null,
37
+ nodeRole: record.nodeRole ?? null,
38
+ modelName: record.modelName ?? null,
39
+ dimensionsJson: record.attributes ? JSON.stringify(record.attributes) : null,
40
+ retentionExpiresAt: record.retentionExpiresAt ?? null,
41
+ },
42
+ });
43
+ return {
44
+ metricPointId,
45
+ traceId: record.traceId,
46
+ spanId: record.spanId,
47
+ runId: record.runId,
48
+ workflowId: record.workflowId,
49
+ nodeId: record.nodeId,
50
+ activationId: record.activationId,
51
+ metricName: record.name,
52
+ value: record.value,
53
+ unit: record.unit,
54
+ observedAt: record.observedAt,
55
+ workflowFolder: record.workflowFolder,
56
+ nodeType: record.nodeType,
57
+ nodeRole: record.nodeRole,
58
+ modelName: record.modelName,
59
+ dimensions: record.attributes,
60
+ retentionExpiresAt: record.retentionExpiresAt,
61
+ };
62
+ }
63
+
64
+ async list(args: TelemetryMetricPointListQuery = {}): Promise<ReadonlyArray<TelemetryMetricPointRecord>> {
65
+ const rows = await this.prisma.telemetryMetricPoint.findMany({
66
+ where: {
67
+ ...(args.traceId ? { traceId: args.traceId } : {}),
68
+ ...(args.runId ? { runId: args.runId } : {}),
69
+ ...(args.runIds && args.runIds.length > 0 ? { runId: { in: [...args.runIds] } } : {}),
70
+ ...(args.workflowId ? { workflowId: args.workflowId } : {}),
71
+ ...(args.workflowIds && args.workflowIds.length > 0 ? { workflowId: { in: [...args.workflowIds] } } : {}),
72
+ ...(args.nodeId ? { nodeId: args.nodeId } : {}),
73
+ ...(args.metricNames && args.metricNames.length > 0 ? { metricName: { in: [...args.metricNames] } } : {}),
74
+ ...(args.modelNames && args.modelNames.length > 0 ? { modelName: { in: [...args.modelNames] } } : {}),
75
+ ...(args.observedAtGte ? { observedAt: { gte: args.observedAtGte } } : {}),
76
+ ...(args.observedAtLte ? { observedAt: { lte: args.observedAtLte } } : {}),
77
+ },
78
+ orderBy: [{ observedAt: "asc" }, { metricPointId: "asc" }],
79
+ ...(args.limit ? { take: args.limit } : {}),
80
+ });
81
+ return rows.map((row) => ({
82
+ metricPointId: row.metricPointId,
83
+ traceId: row.traceId ?? undefined,
84
+ spanId: row.spanId ?? undefined,
85
+ runId: row.runId ?? undefined,
86
+ workflowId: row.workflowId,
87
+ nodeId: row.nodeId ?? undefined,
88
+ activationId: row.activationId ?? undefined,
89
+ metricName: row.metricName,
90
+ value: row.value,
91
+ unit: row.unit ?? undefined,
92
+ observedAt: row.observedAt,
93
+ workflowFolder: row.workflowFolder ?? undefined,
94
+ nodeType: row.nodeType ?? undefined,
95
+ nodeRole: row.nodeRole ?? undefined,
96
+ modelName: row.modelName ?? undefined,
97
+ dimensions: this.parseJson<TelemetryAttributes>(row.dimensionsJson),
98
+ retentionExpiresAt: row.retentionExpiresAt ?? undefined,
99
+ }));
100
+ }
101
+
102
+ async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
103
+ const rows = await this.prisma.telemetryMetricPoint.findMany({
104
+ where: {
105
+ retentionExpiresAt: {
106
+ lte: args.nowIso,
107
+ },
108
+ },
109
+ select: {
110
+ metricPointId: true,
111
+ },
112
+ orderBy: [{ retentionExpiresAt: "asc" }, { metricPointId: "asc" }],
113
+ ...(args.limit ? { take: args.limit } : {}),
114
+ });
115
+ if (rows.length === 0) {
116
+ return 0;
117
+ }
118
+ const result = await this.prisma.telemetryMetricPoint.deleteMany({
119
+ where: {
120
+ metricPointId: {
121
+ in: rows.map((row) => row.metricPointId),
122
+ },
123
+ },
124
+ });
125
+ return result.count;
126
+ }
127
+
128
+ private parseJson<T>(value: string | null): T | undefined {
129
+ if (!value) {
130
+ return undefined;
131
+ }
132
+ return JSON.parse(value) as T;
133
+ }
134
+ }
@@ -0,0 +1,166 @@
1
+ import { inject, injectable, type TelemetryAttributes, type TelemetrySpanEventRecord } from "@codemation/core";
2
+ import type {
3
+ TelemetrySpanListQuery,
4
+ TelemetrySpanRecord,
5
+ TelemetrySpanStore,
6
+ TelemetrySpanUpsert,
7
+ } from "../../domain/telemetry/TelemetryContracts";
8
+ import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
9
+
10
+ @injectable()
11
+ export class PrismaTelemetrySpanStore implements TelemetrySpanStore {
12
+ constructor(
13
+ @inject(PrismaDatabaseClientToken)
14
+ private readonly prisma: PrismaDatabaseClient,
15
+ ) {}
16
+
17
+ async upsert(record: TelemetrySpanUpsert): Promise<void> {
18
+ const telemetrySpanId = this.createTelemetrySpanId(record.traceId, record.spanId);
19
+ const existing = await this.prisma.telemetrySpan.findUnique({
20
+ where: { telemetrySpanId },
21
+ });
22
+ const attributes = this.mergeAttributes(
23
+ this.parseJson<TelemetryAttributes>(existing?.attributesJson ?? null),
24
+ record.attributes,
25
+ );
26
+ const events = this.mergeEvents(
27
+ this.parseJson<ReadonlyArray<TelemetrySpanEventRecord>>(existing?.eventsJson ?? null),
28
+ record.events,
29
+ );
30
+ const data = {
31
+ traceId: record.traceId,
32
+ spanId: record.spanId,
33
+ parentSpanId: record.parentSpanId ?? existing?.parentSpanId ?? null,
34
+ runId: record.runId,
35
+ workflowId: record.workflowId,
36
+ nodeId: record.nodeId ?? existing?.nodeId ?? null,
37
+ activationId: record.activationId ?? existing?.activationId ?? null,
38
+ connectionInvocationId: record.connectionInvocationId ?? existing?.connectionInvocationId ?? null,
39
+ name: record.name ?? existing?.name ?? "codemation.span",
40
+ kind: record.kind ?? existing?.kind ?? "internal",
41
+ status: record.status ?? existing?.status ?? null,
42
+ statusMessage: record.statusMessage ?? existing?.statusMessage ?? null,
43
+ startTime: record.startTime ?? existing?.startTime ?? null,
44
+ endTime: record.endTime ?? existing?.endTime ?? null,
45
+ workflowFolder: record.workflowFolder ?? existing?.workflowFolder ?? null,
46
+ nodeType: record.nodeType ?? existing?.nodeType ?? null,
47
+ nodeRole: record.nodeRole ?? existing?.nodeRole ?? null,
48
+ modelName: record.modelName ?? existing?.modelName ?? null,
49
+ attributesJson: attributes ? JSON.stringify(attributes) : null,
50
+ eventsJson: events.length > 0 ? JSON.stringify(events) : null,
51
+ retentionExpiresAt: record.retentionExpiresAt ?? existing?.retentionExpiresAt ?? null,
52
+ updatedAt: new Date().toISOString(),
53
+ };
54
+ await this.prisma.telemetrySpan.upsert({
55
+ where: { telemetrySpanId },
56
+ create: {
57
+ telemetrySpanId,
58
+ ...data,
59
+ },
60
+ update: data,
61
+ });
62
+ }
63
+
64
+ async list(args: TelemetrySpanListQuery = {}): Promise<ReadonlyArray<TelemetrySpanRecord>> {
65
+ const rows = await this.prisma.telemetrySpan.findMany({
66
+ where: {
67
+ ...(args.traceId ? { traceId: args.traceId } : {}),
68
+ ...(args.runId ? { runId: args.runId } : {}),
69
+ ...(args.runIds && args.runIds.length > 0 ? { runId: { in: [...args.runIds] } } : {}),
70
+ ...(args.workflowId ? { workflowId: args.workflowId } : {}),
71
+ ...(args.workflowIds && args.workflowIds.length > 0 ? { workflowId: { in: [...args.workflowIds] } } : {}),
72
+ ...(args.statuses && args.statuses.length > 0 ? { status: { in: [...args.statuses] } } : {}),
73
+ ...(args.names && args.names.length > 0 ? { name: { in: [...args.names] } } : {}),
74
+ ...(args.modelNames && args.modelNames.length > 0 ? { modelName: { in: [...args.modelNames] } } : {}),
75
+ ...(args.startTimeGte ? { startTime: { gte: args.startTimeGte } } : {}),
76
+ ...(args.endTimeLte ? { endTime: { lte: args.endTimeLte } } : {}),
77
+ },
78
+ orderBy: [{ startTime: "asc" }, { spanId: "asc" }],
79
+ ...(args.limit ? { take: args.limit } : {}),
80
+ });
81
+ return rows.map((row) => ({
82
+ traceId: row.traceId,
83
+ spanId: row.spanId,
84
+ parentSpanId: row.parentSpanId ?? undefined,
85
+ runId: row.runId,
86
+ workflowId: row.workflowId,
87
+ nodeId: row.nodeId ?? undefined,
88
+ activationId: row.activationId ?? undefined,
89
+ connectionInvocationId: row.connectionInvocationId ?? undefined,
90
+ name: row.name,
91
+ kind: row.kind as TelemetrySpanRecord["kind"],
92
+ status: row.status ? (row.status as TelemetrySpanRecord["status"]) : undefined,
93
+ statusMessage: row.statusMessage ?? undefined,
94
+ startTime: row.startTime ?? undefined,
95
+ endTime: row.endTime ?? undefined,
96
+ workflowFolder: row.workflowFolder ?? undefined,
97
+ nodeType: row.nodeType ?? undefined,
98
+ nodeRole: row.nodeRole ?? undefined,
99
+ modelName: row.modelName ?? undefined,
100
+ attributes: this.parseJson<TelemetryAttributes>(row.attributesJson),
101
+ events: this.parseJson<ReadonlyArray<TelemetrySpanEventRecord>>(row.eventsJson) ?? [],
102
+ retentionExpiresAt: row.retentionExpiresAt ?? undefined,
103
+ }));
104
+ }
105
+
106
+ async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetrySpanRecord>> {
107
+ return await this.list({ traceId });
108
+ }
109
+
110
+ async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
111
+ const rows = await this.prisma.telemetrySpan.findMany({
112
+ where: {
113
+ retentionExpiresAt: {
114
+ lte: args.nowIso,
115
+ },
116
+ },
117
+ select: {
118
+ telemetrySpanId: true,
119
+ },
120
+ orderBy: [{ retentionExpiresAt: "asc" }, { telemetrySpanId: "asc" }],
121
+ ...(args.limit ? { take: args.limit } : {}),
122
+ });
123
+ if (rows.length === 0) {
124
+ return 0;
125
+ }
126
+ const result = await this.prisma.telemetrySpan.deleteMany({
127
+ where: {
128
+ telemetrySpanId: {
129
+ in: rows.map((row) => row.telemetrySpanId),
130
+ },
131
+ },
132
+ });
133
+ return result.count;
134
+ }
135
+
136
+ private createTelemetrySpanId(traceId: string, spanId: string): string {
137
+ return `${traceId}:${spanId}`;
138
+ }
139
+
140
+ private parseJson<T>(value: string | null): T | undefined {
141
+ if (!value) {
142
+ return undefined;
143
+ }
144
+ return JSON.parse(value) as T;
145
+ }
146
+
147
+ private mergeAttributes(
148
+ existing: TelemetryAttributes | undefined,
149
+ update: TelemetryAttributes | undefined,
150
+ ): TelemetryAttributes | undefined {
151
+ if (!existing && !update) {
152
+ return undefined;
153
+ }
154
+ return {
155
+ ...(existing ?? {}),
156
+ ...(update ?? {}),
157
+ };
158
+ }
159
+
160
+ private mergeEvents(
161
+ existing: ReadonlyArray<TelemetrySpanEventRecord> | undefined,
162
+ update: ReadonlyArray<TelemetrySpanEventRecord> | undefined,
163
+ ): Array<TelemetrySpanEventRecord> {
164
+ return [...(existing ?? []), ...(update ?? [])];
165
+ }
166
+ }
@@ -402,21 +402,34 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
402
402
  }
403
403
 
404
404
  async listRunsOlderThan(
405
- args: Readonly<{ beforeIso: string; limit?: number }>,
405
+ args: Readonly<{ nowIso: string; defaultRetentionSeconds: number; limit?: number }>,
406
406
  ): Promise<ReadonlyArray<RunPruneCandidate>> {
407
407
  const limit = args.limit ?? 100;
408
408
  const rows = await this.prisma.run.findMany({
409
409
  where: {
410
410
  status: { in: ["completed", "failed"] },
411
- OR: [
412
- { AND: [{ finishedAt: { not: null } }, { finishedAt: { lt: args.beforeIso } }] },
413
- { AND: [{ finishedAt: null }, { updatedAt: { lt: args.beforeIso } }] },
414
- ],
411
+ },
412
+ select: {
413
+ runId: true,
414
+ workflowId: true,
415
+ startedAt: true,
416
+ finishedAt: true,
417
+ updatedAt: true,
418
+ policySnapshotJson: true,
415
419
  },
416
420
  orderBy: { updatedAt: "asc" },
417
- take: limit,
421
+ take: limit * 4,
418
422
  });
419
- return rows.map((r) => this.rowToPruneCandidate(r));
423
+ return rows
424
+ .filter((row) => {
425
+ const finishedAt = row.finishedAt ?? row.updatedAt;
426
+ const policySnapshot = this.parseJson<PersistedRunState["policySnapshot"]>(row.policySnapshotJson);
427
+ const retentionSeconds = policySnapshot?.retentionSeconds ?? args.defaultRetentionSeconds;
428
+ const cutoffIso = new Date(new Date(args.nowIso).getTime() - retentionSeconds * 1000).toISOString();
429
+ return finishedAt < cutoffIso;
430
+ })
431
+ .slice(0, limit)
432
+ .map((row) => this.rowToPruneCandidate(row));
420
433
  }
421
434
 
422
435
  private instancesToDomain(