@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,56 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
3
+ import type { RunTraceContextRepository, TelemetryTraceContext } from "../../domain/telemetry/TelemetryContracts";
4
+
5
+ @injectable()
6
+ export class InMemoryRunTraceContextRepository implements RunTraceContextRepository {
7
+ private readonly rows = new Map<string, TelemetryTraceContext>();
8
+
9
+ constructor(@inject(OtelIdentityFactory) private readonly otelIdentityFactory: OtelIdentityFactory) {}
10
+
11
+ async load(runId: string): Promise<TelemetryTraceContext | undefined> {
12
+ return this.rows.get(decodeURIComponent(runId));
13
+ }
14
+
15
+ async getOrCreate(
16
+ args: Readonly<{ runId: string; workflowId: string; serviceName?: string }>,
17
+ ): Promise<TelemetryTraceContext> {
18
+ const key = decodeURIComponent(args.runId);
19
+ const existing = this.rows.get(key);
20
+ if (existing) {
21
+ return existing;
22
+ }
23
+ const created: TelemetryTraceContext = {
24
+ runId: key,
25
+ workflowId: decodeURIComponent(args.workflowId),
26
+ traceId: this.otelIdentityFactory.createTraceId(key),
27
+ rootSpanId: this.otelIdentityFactory.createRootSpanId(key),
28
+ serviceName: args.serviceName,
29
+ createdAt: new Date().toISOString(),
30
+ };
31
+ this.rows.set(key, created);
32
+ return created;
33
+ }
34
+
35
+ async upsertExpiry(args: Readonly<{ runId: string; expiresAt?: string }>): Promise<void> {
36
+ const key = decodeURIComponent(args.runId);
37
+ const existing = this.rows.get(key);
38
+ if (!existing) {
39
+ return;
40
+ }
41
+ this.rows.set(key, {
42
+ ...existing,
43
+ expiresAt: this.resolveLaterExpiry(existing.expiresAt, args.expiresAt),
44
+ });
45
+ }
46
+
47
+ private resolveLaterExpiry(current: string | undefined, candidate: string | undefined): string | undefined {
48
+ if (!current) {
49
+ return candidate;
50
+ }
51
+ if (!candidate) {
52
+ return current;
53
+ }
54
+ return current >= candidate ? current : candidate;
55
+ }
56
+ }
@@ -0,0 +1,56 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
3
+ import type {
4
+ TelemetryArtifactRecord,
5
+ TelemetryArtifactStore,
6
+ TelemetryArtifactWrite,
7
+ } from "../../domain/telemetry/TelemetryContracts";
8
+
9
+ @injectable()
10
+ export class InMemoryTelemetryArtifactStore implements TelemetryArtifactStore {
11
+ private readonly rows = new Map<string, TelemetryArtifactRecord>();
12
+
13
+ constructor(@inject(OtelIdentityFactory) private readonly otelIdentityFactory: OtelIdentityFactory) {}
14
+
15
+ async save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord> {
16
+ const created: TelemetryArtifactRecord = {
17
+ artifactId: this.otelIdentityFactory.createArtifactId(),
18
+ traceId: record.traceId,
19
+ spanId: record.spanId,
20
+ runId: record.runId,
21
+ workflowId: record.workflowId,
22
+ nodeId: record.nodeId,
23
+ activationId: record.activationId,
24
+ kind: record.kind,
25
+ contentType: record.contentType,
26
+ previewText: record.previewText,
27
+ previewJson: record.previewJson,
28
+ payloadText: record.payloadText,
29
+ payloadJson: record.payloadJson,
30
+ bytes: record.bytes,
31
+ truncated: record.truncated,
32
+ createdAt: new Date().toISOString(),
33
+ expiresAt: record.expiresAt?.toISOString(),
34
+ retentionExpiresAt: record.retentionExpiresAt,
35
+ };
36
+ this.rows.set(created.artifactId, created);
37
+ return created;
38
+ }
39
+
40
+ async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetryArtifactRecord>> {
41
+ return [...this.rows.values()]
42
+ .filter((row) => row.traceId === traceId)
43
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt));
44
+ }
45
+
46
+ async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
47
+ const candidates = [...this.rows.entries()]
48
+ .filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
49
+ .sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
50
+ .slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
51
+ for (const [key] of candidates) {
52
+ this.rows.delete(key);
53
+ }
54
+ return candidates.length;
55
+ }
56
+ }
@@ -0,0 +1,97 @@
1
+ import { inject, injectable } 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
+
10
+ @injectable()
11
+ export class InMemoryTelemetryMetricPointStore implements TelemetryMetricPointStore {
12
+ private readonly rows = new Map<string, TelemetryMetricPointRecord>();
13
+
14
+ constructor(@inject(OtelIdentityFactory) private readonly otelIdentityFactory: OtelIdentityFactory) {}
15
+
16
+ async save(record: TelemetryMetricPointWrite): Promise<TelemetryMetricPointRecord> {
17
+ const created: TelemetryMetricPointRecord = {
18
+ metricPointId: this.otelIdentityFactory.createArtifactId(),
19
+ traceId: record.traceId,
20
+ spanId: record.spanId,
21
+ runId: record.runId,
22
+ workflowId: record.workflowId,
23
+ nodeId: record.nodeId,
24
+ activationId: record.activationId,
25
+ metricName: record.name,
26
+ value: record.value,
27
+ unit: record.unit,
28
+ observedAt: record.observedAt,
29
+ workflowFolder: record.workflowFolder,
30
+ nodeType: record.nodeType,
31
+ nodeRole: record.nodeRole,
32
+ modelName: record.modelName,
33
+ dimensions: record.attributes,
34
+ retentionExpiresAt: record.retentionExpiresAt,
35
+ };
36
+ this.rows.set(created.metricPointId, created);
37
+ return created;
38
+ }
39
+
40
+ async list(args: TelemetryMetricPointListQuery = {}): Promise<ReadonlyArray<TelemetryMetricPointRecord>> {
41
+ return [...this.rows.values()]
42
+ .filter((row) => this.matches(row, args))
43
+ .sort((left, right) => {
44
+ const observedCompare = left.observedAt.localeCompare(right.observedAt);
45
+ if (observedCompare !== 0) {
46
+ return observedCompare;
47
+ }
48
+ return left.metricPointId.localeCompare(right.metricPointId);
49
+ })
50
+ .slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
51
+ }
52
+
53
+ async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
54
+ const candidates = [...this.rows.entries()]
55
+ .filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
56
+ .sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
57
+ .slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
58
+ for (const [key] of candidates) {
59
+ this.rows.delete(key);
60
+ }
61
+ return candidates.length;
62
+ }
63
+
64
+ private matches(row: TelemetryMetricPointRecord, args: TelemetryMetricPointListQuery): boolean {
65
+ if (args.traceId && row.traceId !== args.traceId) {
66
+ return false;
67
+ }
68
+ if (args.runId && row.runId !== args.runId) {
69
+ return false;
70
+ }
71
+ if (args.runIds && args.runIds.length > 0 && (!row.runId || !args.runIds.includes(row.runId))) {
72
+ return false;
73
+ }
74
+ if (args.workflowId && row.workflowId !== args.workflowId) {
75
+ return false;
76
+ }
77
+ if (args.workflowIds && args.workflowIds.length > 0 && !args.workflowIds.includes(row.workflowId)) {
78
+ return false;
79
+ }
80
+ if (args.nodeId && row.nodeId !== args.nodeId) {
81
+ return false;
82
+ }
83
+ if (args.metricNames && args.metricNames.length > 0 && !args.metricNames.includes(row.metricName)) {
84
+ return false;
85
+ }
86
+ if (args.modelNames && args.modelNames.length > 0 && (!row.modelName || !args.modelNames.includes(row.modelName))) {
87
+ return false;
88
+ }
89
+ if (args.observedAtGte && row.observedAt < args.observedAtGte) {
90
+ return false;
91
+ }
92
+ if (args.observedAtLte && row.observedAt > args.observedAtLte) {
93
+ return false;
94
+ }
95
+ return true;
96
+ }
97
+ }
@@ -0,0 +1,113 @@
1
+ import { injectable } from "@codemation/core";
2
+ import type {
3
+ TelemetrySpanListQuery,
4
+ TelemetrySpanRecord,
5
+ TelemetrySpanStore,
6
+ TelemetrySpanUpsert,
7
+ } from "../../domain/telemetry/TelemetryContracts";
8
+
9
+ @injectable()
10
+ export class InMemoryTelemetrySpanStore implements TelemetrySpanStore {
11
+ private readonly rows = new Map<string, TelemetrySpanRecord>();
12
+
13
+ async upsert(record: TelemetrySpanUpsert): Promise<void> {
14
+ const key = this.createKey(record.traceId, record.spanId);
15
+ const existing = this.rows.get(key);
16
+ this.rows.set(key, this.merge(existing, record));
17
+ }
18
+
19
+ async list(args: TelemetrySpanListQuery = {}): Promise<ReadonlyArray<TelemetrySpanRecord>> {
20
+ return [...this.rows.values()]
21
+ .filter((row) => this.matches(row, args))
22
+ .sort((left, right) => {
23
+ const startCompare = (left.startTime ?? "").localeCompare(right.startTime ?? "");
24
+ if (startCompare !== 0) {
25
+ return startCompare;
26
+ }
27
+ return left.spanId.localeCompare(right.spanId);
28
+ })
29
+ .slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
30
+ }
31
+
32
+ async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetrySpanRecord>> {
33
+ return await this.list({ traceId });
34
+ }
35
+
36
+ private createKey(traceId: string, spanId: string): string {
37
+ return `${traceId}:${spanId}`;
38
+ }
39
+
40
+ private merge(existing: TelemetrySpanRecord | undefined, update: TelemetrySpanUpsert): TelemetrySpanRecord {
41
+ return {
42
+ traceId: update.traceId,
43
+ spanId: update.spanId,
44
+ parentSpanId: update.parentSpanId ?? existing?.parentSpanId,
45
+ runId: update.runId,
46
+ workflowId: update.workflowId,
47
+ nodeId: update.nodeId ?? existing?.nodeId,
48
+ activationId: update.activationId ?? existing?.activationId,
49
+ connectionInvocationId: update.connectionInvocationId ?? existing?.connectionInvocationId,
50
+ name: update.name ?? existing?.name ?? "codemation.span",
51
+ kind: update.kind ?? existing?.kind ?? "internal",
52
+ status: update.status ?? existing?.status,
53
+ statusMessage: update.statusMessage ?? existing?.statusMessage,
54
+ startTime: update.startTime ?? existing?.startTime,
55
+ endTime: update.endTime ?? existing?.endTime,
56
+ workflowFolder: update.workflowFolder ?? existing?.workflowFolder,
57
+ nodeType: update.nodeType ?? existing?.nodeType,
58
+ nodeRole: update.nodeRole ?? existing?.nodeRole,
59
+ modelName: update.modelName ?? existing?.modelName,
60
+ attributes: {
61
+ ...(existing?.attributes ?? {}),
62
+ ...(update.attributes ?? {}),
63
+ },
64
+ events: [...(existing?.events ?? []), ...(update.events ?? [])],
65
+ retentionExpiresAt: update.retentionExpiresAt ?? existing?.retentionExpiresAt,
66
+ };
67
+ }
68
+
69
+ private matches(row: TelemetrySpanRecord, args: TelemetrySpanListQuery): boolean {
70
+ if (args.traceId && row.traceId !== args.traceId) {
71
+ return false;
72
+ }
73
+ if (args.runId && row.runId !== args.runId) {
74
+ return false;
75
+ }
76
+ if (args.runIds && args.runIds.length > 0 && !args.runIds.includes(row.runId)) {
77
+ return false;
78
+ }
79
+ if (args.workflowId && row.workflowId !== args.workflowId) {
80
+ return false;
81
+ }
82
+ if (args.workflowIds && args.workflowIds.length > 0 && !args.workflowIds.includes(row.workflowId)) {
83
+ return false;
84
+ }
85
+ if (args.statuses && args.statuses.length > 0 && (!row.status || !args.statuses.includes(row.status))) {
86
+ return false;
87
+ }
88
+ if (args.names && args.names.length > 0 && !args.names.includes(row.name)) {
89
+ return false;
90
+ }
91
+ if (args.modelNames && args.modelNames.length > 0 && (!row.modelName || !args.modelNames.includes(row.modelName))) {
92
+ return false;
93
+ }
94
+ if (args.startTimeGte && row.startTime && row.startTime < args.startTimeGte) {
95
+ return false;
96
+ }
97
+ if (args.endTimeLte && row.endTime && row.endTime > args.endTimeLte) {
98
+ return false;
99
+ }
100
+ return true;
101
+ }
102
+
103
+ async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
104
+ const candidates = [...this.rows.entries()]
105
+ .filter(([, row]) => row.retentionExpiresAt !== undefined && row.retentionExpiresAt <= args.nowIso)
106
+ .sort((left, right) => (left[1].retentionExpiresAt ?? "").localeCompare(right[1].retentionExpiresAt ?? ""))
107
+ .slice(0, args.limit ?? Number.MAX_SAFE_INTEGER);
108
+ for (const [key] of candidates) {
109
+ this.rows.delete(key);
110
+ }
111
+ return candidates.length;
112
+ }
113
+ }
@@ -160,14 +160,16 @@ export class InMemoryWorkflowRunRepository implements WorkflowRunRepository, Wor
160
160
  }
161
161
 
162
162
  async listRunsOlderThan(
163
- args: Readonly<{ beforeIso: string; limit?: number }>,
163
+ args: Readonly<{ nowIso: string; defaultRetentionSeconds: number; limit?: number }>,
164
164
  ): Promise<ReadonlyArray<RunPruneCandidate>> {
165
165
  const limit = args.limit ?? 100;
166
166
  const out: RunPruneCandidate[] = [];
167
167
  for (const s of this.runs.values()) {
168
168
  if (s.status !== "completed" && s.status !== "failed") continue;
169
169
  const finishedAt = RunFinishedAtFactory.resolveIso(s);
170
- if (!finishedAt || finishedAt >= args.beforeIso) continue;
170
+ const retentionSeconds = s.policySnapshot?.retentionSeconds ?? args.defaultRetentionSeconds;
171
+ const cutoffIso = new Date(new Date(args.nowIso).getTime() - retentionSeconds * 1000).toISOString();
172
+ if (!finishedAt || finishedAt >= cutoffIso) continue;
171
173
  out.push({
172
174
  runId: s.runId,
173
175
  workflowId: s.workflowId,
@@ -0,0 +1,92 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
3
+ import type { RunTraceContextRepository, TelemetryTraceContext } from "../../domain/telemetry/TelemetryContracts";
4
+ import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
5
+
6
+ @injectable()
7
+ export class PrismaRunTraceContextRepository implements RunTraceContextRepository {
8
+ constructor(
9
+ @inject(PrismaDatabaseClientToken)
10
+ private readonly prisma: PrismaDatabaseClient,
11
+ @inject(OtelIdentityFactory)
12
+ private readonly otelIdentityFactory: OtelIdentityFactory,
13
+ ) {}
14
+
15
+ async load(runId: string): Promise<TelemetryTraceContext | undefined> {
16
+ const row = await this.prisma.runTraceContext.findUnique({
17
+ where: { runId: decodeURIComponent(runId) },
18
+ });
19
+ if (!row) {
20
+ return undefined;
21
+ }
22
+ return {
23
+ runId: row.runId,
24
+ workflowId: row.workflowId,
25
+ traceId: row.traceId,
26
+ rootSpanId: row.rootSpanId,
27
+ serviceName: row.serviceName ?? undefined,
28
+ createdAt: row.createdAt,
29
+ expiresAt: row.expiresAt ?? undefined,
30
+ };
31
+ }
32
+
33
+ async getOrCreate(
34
+ args: Readonly<{ runId: string; workflowId: string; serviceName?: string }>,
35
+ ): Promise<TelemetryTraceContext> {
36
+ const existing = await this.load(args.runId);
37
+ if (existing) {
38
+ return existing;
39
+ }
40
+ const created: TelemetryTraceContext = {
41
+ runId: decodeURIComponent(args.runId),
42
+ workflowId: decodeURIComponent(args.workflowId),
43
+ traceId: this.otelIdentityFactory.createTraceId(args.runId),
44
+ rootSpanId: this.otelIdentityFactory.createRootSpanId(args.runId),
45
+ serviceName: args.serviceName,
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+ await this.prisma.runTraceContext.create({
49
+ data: {
50
+ runId: created.runId,
51
+ workflowId: created.workflowId,
52
+ traceId: created.traceId,
53
+ rootSpanId: created.rootSpanId,
54
+ serviceName: created.serviceName ?? null,
55
+ createdAt: created.createdAt,
56
+ expiresAt: null,
57
+ },
58
+ });
59
+ return created;
60
+ }
61
+
62
+ async upsertExpiry(args: Readonly<{ runId: string; expiresAt?: string }>): Promise<void> {
63
+ if (!args.expiresAt) {
64
+ return;
65
+ }
66
+ const runId = decodeURIComponent(args.runId);
67
+ const existing = await this.prisma.runTraceContext.findUnique({
68
+ where: { runId },
69
+ select: { expiresAt: true },
70
+ });
71
+ if (!existing) {
72
+ return;
73
+ }
74
+ const expiresAt = this.resolveLaterExpiry(existing.expiresAt ?? undefined, args.expiresAt);
75
+ await this.prisma.runTraceContext.update({
76
+ where: { runId },
77
+ data: {
78
+ expiresAt: expiresAt ?? null,
79
+ },
80
+ });
81
+ }
82
+
83
+ private resolveLaterExpiry(current: string | undefined, candidate: string | undefined): string | undefined {
84
+ if (!current) {
85
+ return candidate;
86
+ }
87
+ if (!candidate) {
88
+ return current;
89
+ }
90
+ return current >= candidate ? current : candidate;
91
+ }
92
+ }
@@ -0,0 +1,125 @@
1
+ import { inject, injectable } from "@codemation/core";
2
+ import { OtelIdentityFactory } from "../../application/telemetry/OtelIdentityFactory";
3
+ import type {
4
+ TelemetryArtifactRecord,
5
+ TelemetryArtifactStore,
6
+ TelemetryArtifactWrite,
7
+ } from "../../domain/telemetry/TelemetryContracts";
8
+ import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
9
+
10
+ @injectable()
11
+ export class PrismaTelemetryArtifactStore implements TelemetryArtifactStore {
12
+ constructor(
13
+ @inject(PrismaDatabaseClientToken)
14
+ private readonly prisma: PrismaDatabaseClient,
15
+ @inject(OtelIdentityFactory)
16
+ private readonly otelIdentityFactory: OtelIdentityFactory,
17
+ ) {}
18
+
19
+ async save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord> {
20
+ const artifactId = this.otelIdentityFactory.createArtifactId();
21
+ const createdAt = new Date().toISOString();
22
+ await this.prisma.telemetryArtifact.create({
23
+ data: {
24
+ artifactId,
25
+ traceId: record.traceId,
26
+ spanId: record.spanId,
27
+ runId: record.runId,
28
+ workflowId: record.workflowId,
29
+ nodeId: record.nodeId ?? null,
30
+ activationId: record.activationId ?? null,
31
+ kind: record.kind,
32
+ contentType: record.contentType,
33
+ previewText: record.previewText ?? null,
34
+ previewJson: record.previewJson !== undefined ? JSON.stringify(record.previewJson) : null,
35
+ payloadText: record.payloadText ?? null,
36
+ payloadJson: record.payloadJson !== undefined ? JSON.stringify(record.payloadJson) : null,
37
+ bytes: record.bytes ?? null,
38
+ truncated: record.truncated ?? null,
39
+ createdAt,
40
+ expiresAt: record.expiresAt?.toISOString() ?? null,
41
+ retentionExpiresAt: record.retentionExpiresAt ?? null,
42
+ },
43
+ });
44
+ return {
45
+ artifactId,
46
+ traceId: record.traceId,
47
+ spanId: record.spanId,
48
+ runId: record.runId,
49
+ workflowId: record.workflowId,
50
+ nodeId: record.nodeId,
51
+ activationId: record.activationId,
52
+ kind: record.kind,
53
+ contentType: record.contentType,
54
+ previewText: record.previewText,
55
+ previewJson: record.previewJson,
56
+ payloadText: record.payloadText,
57
+ payloadJson: record.payloadJson,
58
+ bytes: record.bytes,
59
+ truncated: record.truncated,
60
+ createdAt,
61
+ expiresAt: record.expiresAt?.toISOString(),
62
+ retentionExpiresAt: record.retentionExpiresAt,
63
+ };
64
+ }
65
+
66
+ async listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetryArtifactRecord>> {
67
+ const rows = await this.prisma.telemetryArtifact.findMany({
68
+ where: { traceId },
69
+ orderBy: [{ createdAt: "asc" }, { artifactId: "asc" }],
70
+ });
71
+ return rows.map((row) => ({
72
+ artifactId: row.artifactId,
73
+ traceId: row.traceId,
74
+ spanId: row.spanId,
75
+ runId: row.runId,
76
+ workflowId: row.workflowId,
77
+ nodeId: row.nodeId ?? undefined,
78
+ activationId: row.activationId ?? undefined,
79
+ kind: row.kind,
80
+ contentType: row.contentType,
81
+ previewText: row.previewText ?? undefined,
82
+ previewJson: this.parseJson(row.previewJson),
83
+ payloadText: row.payloadText ?? undefined,
84
+ payloadJson: this.parseJson(row.payloadJson),
85
+ bytes: row.bytes ?? undefined,
86
+ truncated: row.truncated ?? undefined,
87
+ createdAt: row.createdAt,
88
+ expiresAt: row.expiresAt ?? undefined,
89
+ retentionExpiresAt: row.retentionExpiresAt ?? undefined,
90
+ }));
91
+ }
92
+
93
+ async pruneExpired(args: Readonly<{ nowIso: string; limit?: number }>): Promise<number> {
94
+ const rows = await this.prisma.telemetryArtifact.findMany({
95
+ where: {
96
+ retentionExpiresAt: {
97
+ lte: args.nowIso,
98
+ },
99
+ },
100
+ select: {
101
+ artifactId: true,
102
+ },
103
+ orderBy: [{ retentionExpiresAt: "asc" }, { artifactId: "asc" }],
104
+ ...(args.limit ? { take: args.limit } : {}),
105
+ });
106
+ if (rows.length === 0) {
107
+ return 0;
108
+ }
109
+ const result = await this.prisma.telemetryArtifact.deleteMany({
110
+ where: {
111
+ artifactId: {
112
+ in: rows.map((row) => row.artifactId),
113
+ },
114
+ },
115
+ });
116
+ return result.count;
117
+ }
118
+
119
+ private parseJson(value: string | null): unknown {
120
+ if (!value) {
121
+ return undefined;
122
+ }
123
+ return JSON.parse(value);
124
+ }
125
+ }