@codemation/host 0.2.5 → 0.3.1

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 (123) hide show
  1. package/CHANGELOG.md +45 -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-DL_qZ80U.js} +2665 -264
  6. package/dist/AppContainerFactory-DL_qZ80U.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-axppTMgo.d.ts → server-Clvg5x1w.d.ts} +11 -5
  32. package/dist/{server-CFpgKuVE.js → server-DteBORJX.js} +4 -4
  33. package/dist/{server-CFpgKuVE.js.map → server-DteBORJX.js.map} +1 -1
  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 +183 -64
  75. package/src/bootstrap/AppContainerLifecycle.ts +8 -0
  76. package/src/bootstrap/CodemationContainerRegistrationRegistrar.ts +1 -3
  77. package/src/bootstrap/runtime/FrontendRuntime.ts +8 -0
  78. package/src/bootstrap/runtime/WorkerRuntime.ts +8 -0
  79. package/src/domain/runs/WorkflowRunRepository.ts +3 -1
  80. package/src/domain/telemetry/TelemetryContracts.ts +197 -0
  81. package/src/infrastructure/config/CodemationPluginRegistrar.ts +2 -6
  82. package/src/infrastructure/di/HandlesDomainEventRegistry.ts +5 -7
  83. package/src/infrastructure/persistence/InMemoryRunTraceContextRepository.ts +56 -0
  84. package/src/infrastructure/persistence/InMemoryTelemetryArtifactStore.ts +56 -0
  85. package/src/infrastructure/persistence/InMemoryTelemetryMetricPointStore.ts +97 -0
  86. package/src/infrastructure/persistence/InMemoryTelemetrySpanStore.ts +113 -0
  87. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +4 -2
  88. package/src/infrastructure/persistence/PrismaRunTraceContextRepository.ts +92 -0
  89. package/src/infrastructure/persistence/PrismaTelemetryArtifactStore.ts +125 -0
  90. package/src/infrastructure/persistence/PrismaTelemetryMetricPointStore.ts +134 -0
  91. package/src/infrastructure/persistence/PrismaTelemetrySpanStore.ts +166 -0
  92. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +20 -7
  93. package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -4
  94. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -0
  95. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +6488 -102
  96. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -4
  97. package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
  98. package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +100 -0
  99. package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -4
  100. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -0
  101. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +6476 -98
  102. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -4
  103. package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
  104. package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +100 -0
  105. package/src/presentation/http/ApiPaths.ts +22 -0
  106. package/src/presentation/http/hono/registrars/AuthHonoApiRouteRegistrar.ts +1 -3
  107. package/src/presentation/http/hono/registrars/BinaryHonoApiRouteRegistrar.ts +1 -3
  108. package/src/presentation/http/hono/registrars/BootstrapHonoApiRouteRegistrar.ts +1 -3
  109. package/src/presentation/http/hono/registrars/CredentialHonoApiRouteRegistrar.ts +1 -3
  110. package/src/presentation/http/hono/registrars/DevHonoApiRouteRegistrar.ts +1 -3
  111. package/src/presentation/http/hono/registrars/OAuth2HonoApiRouteRegistrar.ts +1 -3
  112. package/src/presentation/http/hono/registrars/RunHonoApiRouteRegistrar.ts +1 -3
  113. package/src/presentation/http/hono/registrars/TelemetryHonoApiRouteRegistrar.ts +17 -0
  114. package/src/presentation/http/hono/registrars/UserHonoApiRouteRegistrar.ts +1 -3
  115. package/src/presentation/http/hono/registrars/WebhookHonoApiRouteRegistrar.ts +1 -3
  116. package/src/presentation/http/hono/registrars/WhitelabelHonoApiRouteRegistrar.ts +1 -3
  117. package/src/presentation/http/hono/registrars/WorkflowHonoApiRouteRegistrar.ts +1 -3
  118. package/src/presentation/http/routeHandlers/TelemetryDashboardRequestError.ts +1 -0
  119. package/src/presentation/http/routeHandlers/TelemetryHttpRouteHandler.ts +181 -0
  120. package/dist/AppContainerFactory-CcSGFNLW.js.map +0 -1
  121. package/dist/CodemationPluginListMerger-QvUa2SIt.d.ts +0 -357
  122. package/dist/CredentialServices-BPKUF8Xs.js.map +0 -1
  123. package/dist/nextServer.js.map +0 -1
@@ -0,0 +1,197 @@
1
+ import type {
2
+ TelemetryArtifactAttachment,
3
+ TelemetryAttributes,
4
+ TelemetryMetricRecord,
5
+ TelemetrySpanEventRecord,
6
+ } from "@codemation/core";
7
+
8
+ export type TelemetrySpanStatus = "running" | "completed" | "failed";
9
+ export type TelemetrySpanKind = "internal" | "client";
10
+
11
+ export interface TelemetryTraceContext {
12
+ readonly runId: string;
13
+ readonly workflowId: string;
14
+ readonly traceId: string;
15
+ readonly rootSpanId: string;
16
+ readonly serviceName?: string;
17
+ readonly createdAt: string;
18
+ readonly expiresAt?: string;
19
+ }
20
+
21
+ export interface TelemetrySpanRecord {
22
+ readonly traceId: string;
23
+ readonly spanId: string;
24
+ readonly parentSpanId?: string;
25
+ readonly runId: string;
26
+ readonly workflowId: string;
27
+ readonly nodeId?: string;
28
+ readonly activationId?: string;
29
+ readonly connectionInvocationId?: string;
30
+ readonly name: string;
31
+ readonly kind: TelemetrySpanKind;
32
+ readonly status?: TelemetrySpanStatus;
33
+ readonly statusMessage?: string;
34
+ readonly startTime?: string;
35
+ readonly endTime?: string;
36
+ readonly workflowFolder?: string;
37
+ readonly nodeType?: string;
38
+ readonly nodeRole?: string;
39
+ readonly modelName?: string;
40
+ readonly attributes?: TelemetryAttributes;
41
+ readonly events?: ReadonlyArray<TelemetrySpanEventRecord>;
42
+ readonly retentionExpiresAt?: string;
43
+ }
44
+
45
+ export interface TelemetrySpanUpsert {
46
+ readonly traceId: string;
47
+ readonly spanId: string;
48
+ readonly parentSpanId?: string;
49
+ readonly runId: string;
50
+ readonly workflowId: string;
51
+ readonly nodeId?: string;
52
+ readonly activationId?: string;
53
+ readonly connectionInvocationId?: string;
54
+ readonly name?: string;
55
+ readonly kind?: TelemetrySpanKind;
56
+ readonly status?: TelemetrySpanStatus;
57
+ readonly statusMessage?: string;
58
+ readonly startTime?: string;
59
+ readonly endTime?: string;
60
+ readonly workflowFolder?: string;
61
+ readonly nodeType?: string;
62
+ readonly nodeRole?: string;
63
+ readonly modelName?: string;
64
+ readonly attributes?: TelemetryAttributes;
65
+ readonly events?: ReadonlyArray<TelemetrySpanEventRecord>;
66
+ readonly retentionExpiresAt?: string;
67
+ }
68
+
69
+ export interface TelemetryArtifactRecord {
70
+ readonly artifactId: string;
71
+ readonly traceId: string;
72
+ readonly spanId: string;
73
+ readonly runId: string;
74
+ readonly workflowId: string;
75
+ readonly nodeId?: string;
76
+ readonly activationId?: string;
77
+ readonly kind: string;
78
+ readonly contentType: string;
79
+ readonly previewText?: string;
80
+ readonly previewJson?: unknown;
81
+ readonly payloadText?: string;
82
+ readonly payloadJson?: unknown;
83
+ readonly bytes?: number;
84
+ readonly truncated?: boolean;
85
+ readonly createdAt: string;
86
+ readonly expiresAt?: string;
87
+ readonly retentionExpiresAt?: string;
88
+ }
89
+
90
+ export interface TelemetryArtifactWrite extends TelemetryArtifactAttachment {
91
+ readonly traceId: string;
92
+ readonly spanId: string;
93
+ readonly runId: string;
94
+ readonly workflowId: string;
95
+ readonly nodeId?: string;
96
+ readonly activationId?: string;
97
+ readonly retentionExpiresAt?: string;
98
+ }
99
+
100
+ export interface TelemetryMetricPointRecord {
101
+ readonly metricPointId: string;
102
+ readonly traceId?: string;
103
+ readonly spanId?: string;
104
+ readonly runId?: string;
105
+ readonly workflowId: string;
106
+ readonly nodeId?: string;
107
+ readonly activationId?: string;
108
+ readonly metricName: string;
109
+ readonly value: number;
110
+ readonly unit?: string;
111
+ readonly observedAt: string;
112
+ readonly workflowFolder?: string;
113
+ readonly nodeType?: string;
114
+ readonly nodeRole?: string;
115
+ readonly modelName?: string;
116
+ readonly dimensions?: TelemetryAttributes;
117
+ readonly retentionExpiresAt?: string;
118
+ }
119
+
120
+ export interface TelemetryMetricPointWrite extends TelemetryMetricRecord {
121
+ readonly traceId?: string;
122
+ readonly spanId?: string;
123
+ readonly runId?: string;
124
+ readonly workflowId: string;
125
+ readonly nodeId?: string;
126
+ readonly activationId?: string;
127
+ readonly observedAt: string;
128
+ readonly workflowFolder?: string;
129
+ readonly nodeType?: string;
130
+ readonly nodeRole?: string;
131
+ readonly modelName?: string;
132
+ readonly retentionExpiresAt?: string;
133
+ }
134
+
135
+ export interface TelemetrySpanListQuery {
136
+ readonly traceId?: string;
137
+ readonly runId?: string;
138
+ readonly runIds?: ReadonlyArray<string>;
139
+ readonly workflowId?: string;
140
+ readonly workflowIds?: ReadonlyArray<string>;
141
+ readonly statuses?: ReadonlyArray<TelemetrySpanStatus>;
142
+ readonly names?: ReadonlyArray<string>;
143
+ readonly modelNames?: ReadonlyArray<string>;
144
+ readonly startTimeGte?: string;
145
+ readonly endTimeLte?: string;
146
+ readonly limit?: number;
147
+ }
148
+
149
+ export interface TelemetryMetricPointListQuery {
150
+ readonly traceId?: string;
151
+ readonly runId?: string;
152
+ readonly runIds?: ReadonlyArray<string>;
153
+ readonly workflowId?: string;
154
+ readonly workflowIds?: ReadonlyArray<string>;
155
+ readonly nodeId?: string;
156
+ readonly metricNames?: ReadonlyArray<string>;
157
+ readonly modelNames?: ReadonlyArray<string>;
158
+ readonly observedAtGte?: string;
159
+ readonly observedAtLte?: string;
160
+ readonly limit?: number;
161
+ }
162
+
163
+ export interface TelemetryPruneArgs {
164
+ readonly nowIso: string;
165
+ readonly limit?: number;
166
+ }
167
+
168
+ export interface RunTraceContextRepository {
169
+ load(runId: string): Promise<TelemetryTraceContext | undefined>;
170
+ getOrCreate(
171
+ args: Readonly<{ runId: string; workflowId: string; serviceName?: string }>,
172
+ ): Promise<TelemetryTraceContext>;
173
+ upsertExpiry(args: Readonly<{ runId: string; expiresAt?: string }>): Promise<void>;
174
+ }
175
+
176
+ export interface TelemetrySpanStore {
177
+ upsert(record: TelemetrySpanUpsert): Promise<void>;
178
+ list(args?: TelemetrySpanListQuery): Promise<ReadonlyArray<TelemetrySpanRecord>>;
179
+ listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetrySpanRecord>>;
180
+ pruneExpired(args: TelemetryPruneArgs): Promise<number>;
181
+ }
182
+
183
+ export interface TelemetryArtifactStore {
184
+ save(record: TelemetryArtifactWrite): Promise<TelemetryArtifactRecord>;
185
+ listByTraceId(traceId: string): Promise<ReadonlyArray<TelemetryArtifactRecord>>;
186
+ pruneExpired(args: TelemetryPruneArgs): Promise<number>;
187
+ }
188
+
189
+ export interface TelemetryMetricPointStore {
190
+ save(record: TelemetryMetricPointWrite): Promise<TelemetryMetricPointRecord>;
191
+ list(args?: TelemetryMetricPointListQuery): Promise<ReadonlyArray<TelemetryMetricPointRecord>>;
192
+ pruneExpired(args: TelemetryPruneArgs): Promise<number>;
193
+ }
194
+
195
+ export interface TelemetryExporter {
196
+ exportSpans(spans: ReadonlyArray<TelemetrySpanRecord>): Promise<void>;
197
+ }
@@ -27,17 +27,13 @@ export class CodemationPluginRegistrar {
27
27
  loggerFactory: args.loggerFactory,
28
28
  registerCredentialType: (type) => args.registerCredentialType(type),
29
29
  registerNode: (token, implementation) => {
30
- args.container.register(token, {
31
- useClass: (implementation ?? token) as never,
32
- });
30
+ args.container.registerSingleton(token as never, (implementation ?? token) as never);
33
31
  },
34
32
  registerValue: (token, value) => {
35
33
  args.container.registerInstance(token, value);
36
34
  },
37
35
  registerClass: (token, implementation) => {
38
- args.container.register(token, {
39
- useClass: implementation as never,
40
- });
36
+ args.container.registerSingleton(token as never, implementation as never);
41
37
  },
42
38
  registerFactory: (token, factory) => {
43
39
  args.container.register(token, {
@@ -1,4 +1,4 @@
1
- import { injectable, registry } from "@codemation/core";
1
+ import { container as tsyringeContainer, injectable } from "@codemation/core";
2
2
  import type { DomainEvent } from "../../application/bus/DomainEvent";
3
3
  import type { DomainEventHandler } from "../../application/bus/DomainEventHandler";
4
4
  import { ApplicationTokens } from "../../applicationTokens";
@@ -13,12 +13,10 @@ export class HandlesDomainEvent {
13
13
  return (target) => {
14
14
  Reflect.defineMetadata(domainEventHandlerMetadataKey, eventType, target);
15
15
  injectable()(target as never);
16
- registry([
17
- {
18
- token: ApplicationTokens.DomainEventHandler,
19
- useClass: target as unknown as ConcreteType<DomainEventHandler<DomainEvent>>,
20
- },
21
- ])(target as never);
16
+ tsyringeContainer.registerSingleton(
17
+ ApplicationTokens.DomainEventHandler as never,
18
+ target as unknown as ConcreteType<DomainEventHandler<DomainEvent>>,
19
+ );
22
20
  };
23
21
  }
24
22
  }
@@ -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
+ }