@codemation/host 0.1.0 → 0.1.2
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.
- package/CHANGELOG.md +24 -0
- package/dist/{AppConfigFactory-Ciz9YKWx.js → AppConfigFactory-ByT1D8dM.js} +253 -15
- package/dist/{AppConfigFactory-Ciz9YKWx.js.map → AppConfigFactory-ByT1D8dM.js.map} +1 -1
- package/dist/{AppConfigFactory-CiBPHleh.d.ts → AppConfigFactory-_fqSok1J.d.ts} +6565 -177
- package/dist/{AppContainerFactory-DH88oxpg.js → AppContainerFactory-BRU02PTm.js} +838 -145
- package/dist/AppContainerFactory-BRU02PTm.js.map +1 -0
- package/dist/{CodemationConfig-DfK1KLvO.d.ts → CodemationConfig-CNfytKR6.d.ts} +2 -2
- package/dist/{CodemationConfigNormalizer-BKgIOeLm.d.ts → CodemationConfigNormalizer-BuKWVNEq.d.ts} +2 -2
- package/dist/{CodemationConsumerConfigLoader-bdhJsBKt.d.ts → CodemationConsumerConfigLoader-Mv4cywWu.d.ts} +2 -2
- package/dist/{CodemationPluginListMerger-BFZeO0WG.d.ts → CodemationPluginListMerger-BD5mR6gK.d.ts} +11 -5
- package/dist/{CredentialServices-aKIwHEhf.d.ts → CredentialServices-BQsEtctT.d.ts} +8 -7
- package/dist/{CredentialServices-DNb3CZwW.js → CredentialServices-xVxVA9Tq.js} +60 -114
- package/dist/CredentialServices-xVxVA9Tq.js.map +1 -0
- package/dist/{PublicFrontendBootstrapFactory-6ahaU0XM.d.ts → PublicFrontendBootstrapFactory-kTyAJdHI.d.ts} +2 -2
- package/dist/consumer.d.ts +4 -4
- package/dist/credentials.d.ts +3 -3
- package/dist/credentials.js +1 -1
- package/dist/devServerSidecar.d.ts +1 -1
- package/dist/{index-BYbzmUwS.d.ts → index-CX752QE9.d.ts} +68 -4
- package/dist/index.d.ts +10 -10
- package/dist/index.js +5 -5
- package/dist/nextServer.d.ts +20 -15
- package/dist/nextServer.js +35 -58
- package/dist/nextServer.js.map +1 -1
- package/dist/{persistenceServer-DL8yBGDU.d.ts → persistenceServer-CLY4qtMo.d.ts} +2 -2
- package/dist/{persistenceServer-CuAqL_fF.js → persistenceServer-DMvIOGW8.js} +2 -2
- package/dist/{persistenceServer-CuAqL_fF.js.map → persistenceServer-DMvIOGW8.js.map} +1 -1
- package/dist/persistenceServer.d.ts +5 -5
- package/dist/persistenceServer.js +2 -2
- package/dist/{server-EbxQft_X.js → server-ChTCEc6R.js} +4 -4
- package/dist/{server-EbxQft_X.js.map → server-ChTCEc6R.js.map} +1 -1
- package/dist/{server-BceIfIJf.d.ts → server-DwpcwzFb.d.ts} +6 -5
- package/dist/server.d.ts +8 -8
- package/dist/server.js +5 -5
- package/package.json +5 -5
- package/prisma/migrations/20260407140000_run_normalized_persistence/migration.sql +327 -0
- package/prisma/migrations/20260407193000_rename_run_projection_to_run_slot_projection/migration.sql +10 -0
- package/prisma/migrations.sqlite/20260407140000_run_normalized_persistence/migration.sql +326 -0
- package/prisma/migrations.sqlite/20260407193000_rename_run_projection_to_run_slot_projection/migration.sql +38 -0
- package/prisma/schema.postgresql.prisma +100 -1
- package/prisma/schema.sqlite.prisma +101 -1
- package/scripts/integration-database-global-setup.mjs +0 -3
- package/src/application/mapping/WorkflowDefinitionMapper.ts +95 -56
- package/src/application/mapping/WorkflowPolicyUiPresentationFactory.ts +1 -1
- package/src/application/queries/GetWorkflowRunDetailQuery.ts +8 -0
- package/src/application/queries/GetWorkflowRunDetailQueryHandler.ts +24 -0
- package/src/application/queries/WorkflowQueryHandlers.ts +1 -0
- package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +52 -27
- package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +113 -158
- package/src/domain/runs/WorkflowRunRepository.ts +7 -1
- package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +123 -1
- package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +226 -6
- package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +796 -109
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -5
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -1
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +7107 -237
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -5
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
- package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +101 -1
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -5
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -1
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +7104 -242
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -5
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
- package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +101 -1
- package/src/presentation/http/ApiPaths.ts +4 -0
- package/src/presentation/http/hono/registrars/RunHonoApiRouteRegistrar.ts +1 -0
- package/src/presentation/http/routeHandlers/RunHttpRouteHandler.ts +13 -0
- package/dist/AppContainerFactory-DH88oxpg.js.map +0 -1
- package/dist/CredentialServices-DNb3CZwW.js.map +0 -1
|
@@ -1,31 +1,78 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ConnectionInvocationRecord,
|
|
3
|
+
ExecutionInstanceDto,
|
|
4
|
+
NodeInputsByPort,
|
|
5
|
+
NodeExecutionSnapshot,
|
|
2
6
|
NodeId,
|
|
3
7
|
NodeOutputs,
|
|
4
8
|
ParentExecutionRef,
|
|
9
|
+
PendingNodeExecution,
|
|
10
|
+
PersistedRunSchedulingState,
|
|
5
11
|
PersistedRunState,
|
|
6
12
|
RunId,
|
|
7
13
|
RunPruneCandidate,
|
|
14
|
+
RunQueueEntry,
|
|
8
15
|
RunSummary,
|
|
16
|
+
SlotExecutionStateDto,
|
|
17
|
+
WorkflowRunDetailDto,
|
|
9
18
|
WorkflowExecutionRepository,
|
|
10
19
|
WorkflowId,
|
|
11
20
|
} from "@codemation/core";
|
|
12
21
|
import { inject, injectable } from "@codemation/core";
|
|
13
22
|
import type { WorkflowRunRepository } from "../../domain/runs/WorkflowRunRepository";
|
|
23
|
+
import type { Prisma } from "./generated/prisma-postgresql-client/client.js";
|
|
14
24
|
import { PrismaDatabaseClientToken, type PrismaDatabaseClient } from "./PrismaDatabaseClient";
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
type ExecutionInstanceRow = {
|
|
27
|
+
instanceId: string;
|
|
28
|
+
runId: string;
|
|
29
|
+
workflowId: string;
|
|
30
|
+
slotNodeId: string;
|
|
31
|
+
workflowNodeId: string;
|
|
32
|
+
kind: string;
|
|
33
|
+
connectionKind: string | null;
|
|
34
|
+
activationId: string | null;
|
|
35
|
+
batchId: string;
|
|
36
|
+
runIndex: number;
|
|
37
|
+
parentInstanceId: string | null;
|
|
38
|
+
status: string;
|
|
39
|
+
queuedAt: string | null;
|
|
40
|
+
startedAt: string | null;
|
|
41
|
+
finishedAt: string | null;
|
|
42
|
+
updatedAt: string;
|
|
43
|
+
itemCount: number;
|
|
44
|
+
inputJson: string | null;
|
|
45
|
+
outputJson: string | null;
|
|
46
|
+
errorJson: string | null;
|
|
47
|
+
inputItemIndicesJson: string | null;
|
|
48
|
+
outputItemCount: number | null;
|
|
49
|
+
successfulItemCount: number | null;
|
|
50
|
+
failedItemCount: number | null;
|
|
51
|
+
usedPinnedOutput: boolean | null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type RunWorkItemRecord = {
|
|
55
|
+
workItemId: string;
|
|
56
|
+
runId: string;
|
|
57
|
+
workflowId: string;
|
|
58
|
+
status: string;
|
|
59
|
+
targetNodeId: string;
|
|
60
|
+
batchId: string;
|
|
61
|
+
queueName: string | null;
|
|
62
|
+
claimToken: string | null;
|
|
63
|
+
availableAt: string;
|
|
64
|
+
enqueuedAt: string;
|
|
65
|
+
itemsIn: number;
|
|
66
|
+
inputsByPortJson: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type RunSlotProjectionRow = {
|
|
70
|
+
runId: string;
|
|
71
|
+
workflowId: string;
|
|
72
|
+
revision: number;
|
|
73
|
+
updatedAt: string;
|
|
74
|
+
slotStatesJson: string;
|
|
75
|
+
};
|
|
29
76
|
|
|
30
77
|
@injectable()
|
|
31
78
|
export class PrismaWorkflowRunRepository implements WorkflowRunRepository, WorkflowExecutionRepository {
|
|
@@ -44,24 +91,6 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
44
91
|
engineCounters?: PersistedRunState["engineCounters"];
|
|
45
92
|
}): Promise<void> {
|
|
46
93
|
const now = new Date().toISOString();
|
|
47
|
-
const state: PersistedRunState = {
|
|
48
|
-
runId: args.runId,
|
|
49
|
-
workflowId: args.workflowId,
|
|
50
|
-
startedAt: args.startedAt,
|
|
51
|
-
parent: args.parent,
|
|
52
|
-
executionOptions: args.executionOptions,
|
|
53
|
-
control: args.control,
|
|
54
|
-
workflowSnapshot: args.workflowSnapshot,
|
|
55
|
-
mutableState: args.mutableState,
|
|
56
|
-
policySnapshot: args.policySnapshot,
|
|
57
|
-
engineCounters: args.engineCounters,
|
|
58
|
-
status: "running",
|
|
59
|
-
queue: [],
|
|
60
|
-
outputsByNode: {} as Record<NodeId, NodeOutputs>,
|
|
61
|
-
nodeSnapshotsByNodeId: {},
|
|
62
|
-
connectionInvocations: [],
|
|
63
|
-
};
|
|
64
|
-
const stateJson = this.serializeStateBlob(state);
|
|
65
94
|
await this.prisma.run.create({
|
|
66
95
|
data: {
|
|
67
96
|
runId: args.runId,
|
|
@@ -71,7 +100,13 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
71
100
|
parentJson: args.parent ? JSON.stringify(args.parent) : null,
|
|
72
101
|
executionOptionsJson: args.executionOptions ? JSON.stringify(args.executionOptions) : null,
|
|
73
102
|
updatedAt: now,
|
|
74
|
-
|
|
103
|
+
revision: 0,
|
|
104
|
+
outputsByNodeJson: JSON.stringify({}),
|
|
105
|
+
controlJson: args.control ? JSON.stringify(args.control) : null,
|
|
106
|
+
workflowSnapshotJson: args.workflowSnapshot ? JSON.stringify(args.workflowSnapshot) : null,
|
|
107
|
+
policySnapshotJson: args.policySnapshot ? JSON.stringify(args.policySnapshot) : null,
|
|
108
|
+
engineCountersJson: args.engineCounters ? JSON.stringify(args.engineCounters) : null,
|
|
109
|
+
mutableStateJson: args.mutableState ? JSON.stringify(args.mutableState) : null,
|
|
75
110
|
},
|
|
76
111
|
});
|
|
77
112
|
}
|
|
@@ -80,31 +115,273 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
80
115
|
const id = decodeURIComponent(runId) as RunId;
|
|
81
116
|
const row = await this.prisma.run.findUnique({ where: { runId: id } });
|
|
82
117
|
if (!row) return undefined;
|
|
83
|
-
|
|
118
|
+
|
|
119
|
+
const [schedulingState, instances] = await Promise.all([
|
|
120
|
+
this.loadSchedulingState(id),
|
|
121
|
+
this.prisma.executionInstance.findMany({
|
|
122
|
+
where: { runId: id },
|
|
123
|
+
orderBy: [{ slotNodeId: "asc" }, { runIndex: "asc" }],
|
|
124
|
+
}),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const { nodeSnapshotsByNodeId, connectionInvocations } = this.instancesToDomain(
|
|
128
|
+
row.runId as RunId,
|
|
129
|
+
row.workflowId as WorkflowId,
|
|
130
|
+
instances as ExecutionInstanceRow[],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const parent = this.parseJson<ParentExecutionRef>(row.parentJson);
|
|
134
|
+
const persistedOutputsByNode = this.parseJson<Record<NodeId, NodeOutputs>>(row.outputsByNodeJson) ?? {};
|
|
135
|
+
const merged: PersistedRunState = {
|
|
136
|
+
runId: row.runId as RunId,
|
|
137
|
+
workflowId: row.workflowId as WorkflowId,
|
|
138
|
+
startedAt: row.startedAt,
|
|
139
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
140
|
+
revision: row.revision,
|
|
141
|
+
parent,
|
|
142
|
+
executionOptions: this.parseJson(row.executionOptionsJson),
|
|
143
|
+
control: this.parseJson(row.controlJson),
|
|
144
|
+
workflowSnapshot: this.parseJson(row.workflowSnapshotJson),
|
|
145
|
+
mutableState: this.parseJson(row.mutableStateJson),
|
|
146
|
+
policySnapshot: this.parseJson(row.policySnapshotJson),
|
|
147
|
+
engineCounters: this.parseJson(row.engineCountersJson),
|
|
148
|
+
status: row.status as PersistedRunState["status"],
|
|
149
|
+
pending: schedulingState?.pending,
|
|
150
|
+
queue: schedulingState?.queue ?? [],
|
|
151
|
+
outputsByNode: this.mergePersistedOutputsByNode({
|
|
152
|
+
persistedOutputsByNode,
|
|
153
|
+
nodeSnapshotsByNodeId,
|
|
154
|
+
}),
|
|
155
|
+
nodeSnapshotsByNodeId,
|
|
156
|
+
connectionInvocations: connectionInvocations.length > 0 ? connectionInvocations : undefined,
|
|
157
|
+
};
|
|
158
|
+
return this.applyParentToSnapshots(merged);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async loadSchedulingState(runId: RunId): Promise<PersistedRunSchedulingState | undefined> {
|
|
162
|
+
const id = decodeURIComponent(runId) as RunId;
|
|
163
|
+
const row = await this.prisma.run.findUnique({
|
|
164
|
+
where: { runId: id },
|
|
165
|
+
select: { runId: true },
|
|
166
|
+
});
|
|
167
|
+
if (!row) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const workItems = (await this.prisma.runWorkItem.findMany({
|
|
171
|
+
where: { runId: id },
|
|
172
|
+
orderBy: [{ status: "desc" }, { enqueuedAt: "asc" }, { workItemId: "asc" }],
|
|
173
|
+
})) as RunWorkItemRecord[];
|
|
174
|
+
const queue: RunQueueEntry[] = [];
|
|
175
|
+
let pending: PendingNodeExecution | undefined;
|
|
176
|
+
for (const workItem of workItems) {
|
|
177
|
+
if (workItem.status === "claimed") {
|
|
178
|
+
pending = this.toPendingNodeExecution(workItem);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (workItem.status === "queued") {
|
|
182
|
+
queue.push(this.toQueueEntry(workItem));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { pending, queue };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async loadRunDetail(runId: string): Promise<WorkflowRunDetailDto | undefined> {
|
|
189
|
+
const id = decodeURIComponent(runId) as RunId;
|
|
190
|
+
const [row, projection, instances] = await Promise.all([
|
|
191
|
+
this.prisma.run.findUnique({ where: { runId: id } }),
|
|
192
|
+
this.prisma.runSlotProjection.findUnique({ where: { runId: id } }),
|
|
193
|
+
this.prisma.executionInstance.findMany({
|
|
194
|
+
where: { runId: id },
|
|
195
|
+
orderBy: [{ updatedAt: "asc" }, { runIndex: "asc" }],
|
|
196
|
+
}),
|
|
197
|
+
]);
|
|
198
|
+
if (!row) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
const slotStates = this.toSlotStateDtos(projection as RunSlotProjectionRow | null);
|
|
202
|
+
const executionInstances = (instances as ExecutionInstanceRow[]).map((instance) =>
|
|
203
|
+
this.toExecutionInstanceDto(instance),
|
|
204
|
+
);
|
|
205
|
+
return {
|
|
206
|
+
runId: row.runId as RunId,
|
|
207
|
+
workflowId: row.workflowId as WorkflowId,
|
|
208
|
+
startedAt: row.startedAt,
|
|
209
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
210
|
+
status: row.status as WorkflowRunDetailDto["status"],
|
|
211
|
+
workflowSnapshot: this.parseJson(row.workflowSnapshotJson),
|
|
212
|
+
mutableState: this.parseJson(row.mutableStateJson) as WorkflowRunDetailDto["mutableState"],
|
|
213
|
+
slotStates,
|
|
214
|
+
executionInstances,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async listBinaryStorageKeys(runId: RunId): Promise<ReadonlyArray<string>> {
|
|
219
|
+
const id = decodeURIComponent(runId) as RunId;
|
|
220
|
+
const row = await this.prisma.run.findUnique({
|
|
221
|
+
where: { runId: id },
|
|
222
|
+
select: { outputsByNodeJson: true, mutableStateJson: true },
|
|
223
|
+
});
|
|
224
|
+
if (!row) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const instances = (await this.prisma.executionInstance.findMany({
|
|
228
|
+
where: { runId: id },
|
|
229
|
+
select: { inputJson: true, outputJson: true },
|
|
230
|
+
})) as Array<Pick<ExecutionInstanceRow, "inputJson" | "outputJson">>;
|
|
231
|
+
const keys = new Set<string>();
|
|
232
|
+
this.collectBinaryKeysFromJsonText(row.outputsByNodeJson, keys);
|
|
233
|
+
this.collectBinaryKeysFromJsonText(row.mutableStateJson, keys);
|
|
234
|
+
for (const instance of instances) {
|
|
235
|
+
this.collectBinaryKeysFromJsonText(instance.inputJson, keys);
|
|
236
|
+
this.collectBinaryKeysFromJsonText(instance.outputJson, keys);
|
|
237
|
+
}
|
|
238
|
+
return [...keys].sort((left, right) => left.localeCompare(right));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private applyParentToSnapshots(state: PersistedRunState): PersistedRunState {
|
|
242
|
+
if (!state.parent) {
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
const next: Record<NodeId, NodeExecutionSnapshot> = {};
|
|
246
|
+
for (const [id, snap] of Object.entries(state.nodeSnapshotsByNodeId ?? {})) {
|
|
247
|
+
next[id] = { ...snap, parent: state.parent };
|
|
248
|
+
}
|
|
249
|
+
return { ...state, nodeSnapshotsByNodeId: next };
|
|
84
250
|
}
|
|
85
251
|
|
|
86
252
|
async save(state: PersistedRunState): Promise<void> {
|
|
253
|
+
let candidate = state;
|
|
254
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
255
|
+
try {
|
|
256
|
+
await this.saveOnce(candidate);
|
|
257
|
+
return;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (!this.isConcurrentRunUpdateError(error) || attempt === 2) {
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
const latest = await this.load(candidate.runId);
|
|
263
|
+
if (!latest) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
candidate = this.mergeConcurrentState(latest, candidate);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async saveOnce(state: PersistedRunState): Promise<void> {
|
|
87
272
|
const now = new Date().toISOString();
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
273
|
+
const nextRevision = (state.revision ?? 0) + 1;
|
|
274
|
+
const workItems = this.buildWorkItems(state, now);
|
|
275
|
+
const instances = this.buildExecutionInstances(state);
|
|
276
|
+
const projectionJson = this.buildProjectionSlotStatesJson(state);
|
|
277
|
+
|
|
278
|
+
await this.prisma.$transaction(async (tx) => {
|
|
279
|
+
await tx.runWorkItem.deleteMany({ where: { runId: state.runId } });
|
|
280
|
+
if (workItems.length > 0) {
|
|
281
|
+
await tx.runWorkItem.createMany({ data: workItems });
|
|
282
|
+
}
|
|
283
|
+
const existingInstances = await tx.executionInstance.findMany({
|
|
284
|
+
where: { runId: state.runId },
|
|
285
|
+
select: { instanceId: true, slotNodeId: true, runIndex: true },
|
|
286
|
+
});
|
|
287
|
+
const existingById = new Map(existingInstances.map((row) => [row.instanceId, row]));
|
|
288
|
+
const maxRunIndexBySlot = new Map<string, number>();
|
|
289
|
+
for (const row of existingInstances) {
|
|
290
|
+
maxRunIndexBySlot.set(row.slotNodeId, Math.max(maxRunIndexBySlot.get(row.slotNodeId) ?? 0, row.runIndex));
|
|
291
|
+
}
|
|
292
|
+
for (const instance of instances) {
|
|
293
|
+
const existing = existingById.get(instance.instanceId);
|
|
294
|
+
const runIndex = existing?.runIndex ?? this.nextRunIndexForSlot(maxRunIndexBySlot, instance.slotNodeId);
|
|
295
|
+
if (existing) {
|
|
296
|
+
await tx.executionInstance.update({
|
|
297
|
+
where: { instanceId: instance.instanceId },
|
|
298
|
+
data: {
|
|
299
|
+
workflowId: instance.workflowId,
|
|
300
|
+
slotNodeId: instance.slotNodeId,
|
|
301
|
+
workflowNodeId: instance.workflowNodeId,
|
|
302
|
+
kind: instance.kind,
|
|
303
|
+
connectionKind: instance.connectionKind,
|
|
304
|
+
activationId: instance.activationId,
|
|
305
|
+
batchId: instance.batchId,
|
|
306
|
+
parentInstanceId: instance.parentInstanceId,
|
|
307
|
+
parentRunId: instance.parentRunId,
|
|
308
|
+
workerClaimToken: instance.workerClaimToken,
|
|
309
|
+
status: instance.status,
|
|
310
|
+
queuedAt: instance.queuedAt,
|
|
311
|
+
startedAt: instance.startedAt,
|
|
312
|
+
finishedAt: instance.finishedAt,
|
|
313
|
+
updatedAt: instance.updatedAt,
|
|
314
|
+
itemCount: instance.itemCount,
|
|
315
|
+
inputJson: instance.inputJson,
|
|
316
|
+
outputJson: instance.outputJson,
|
|
317
|
+
errorJson: instance.errorJson,
|
|
318
|
+
inputItemIndicesJson: instance.inputItemIndicesJson,
|
|
319
|
+
outputItemCount: instance.outputItemCount,
|
|
320
|
+
successfulItemCount: instance.successfulItemCount,
|
|
321
|
+
failedItemCount: instance.failedItemCount,
|
|
322
|
+
inputStorageKind: instance.inputStorageKind,
|
|
323
|
+
outputStorageKind: instance.outputStorageKind,
|
|
324
|
+
inputBytes: instance.inputBytes,
|
|
325
|
+
outputBytes: instance.outputBytes,
|
|
326
|
+
inputPreviewJson: instance.inputPreviewJson,
|
|
327
|
+
outputPreviewJson: instance.outputPreviewJson,
|
|
328
|
+
inputPayloadRef: instance.inputPayloadRef,
|
|
329
|
+
outputPayloadRef: instance.outputPayloadRef,
|
|
330
|
+
inputTruncated: instance.inputTruncated,
|
|
331
|
+
outputTruncated: instance.outputTruncated,
|
|
332
|
+
usedPinnedOutput: instance.usedPinnedOutput,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
await tx.executionInstance.create({
|
|
338
|
+
data: {
|
|
339
|
+
...instance,
|
|
340
|
+
runIndex,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
await tx.runSlotProjection.upsert({
|
|
345
|
+
where: { runId: state.runId },
|
|
346
|
+
create: {
|
|
347
|
+
runId: state.runId,
|
|
348
|
+
workflowId: state.workflowId,
|
|
349
|
+
revision: nextRevision,
|
|
350
|
+
updatedAt: now,
|
|
351
|
+
slotStatesJson: projectionJson,
|
|
352
|
+
},
|
|
353
|
+
update: {
|
|
354
|
+
workflowId: state.workflowId,
|
|
355
|
+
revision: nextRevision,
|
|
356
|
+
updatedAt: now,
|
|
357
|
+
slotStatesJson: projectionJson,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
const updated = await tx.run.updateMany({
|
|
361
|
+
where: {
|
|
362
|
+
runId: state.runId,
|
|
363
|
+
revision: state.revision ?? 0,
|
|
364
|
+
},
|
|
365
|
+
data: {
|
|
366
|
+
workflowId: state.workflowId,
|
|
367
|
+
startedAt: state.startedAt,
|
|
368
|
+
status: state.status,
|
|
369
|
+
finishedAt: state.finishedAt ?? null,
|
|
370
|
+
updatedAt: now,
|
|
371
|
+
revision: nextRevision,
|
|
372
|
+
parentJson: state.parent ? JSON.stringify(state.parent) : null,
|
|
373
|
+
executionOptionsJson: state.executionOptions ? JSON.stringify(state.executionOptions) : null,
|
|
374
|
+
controlJson: state.control ? JSON.stringify(state.control) : null,
|
|
375
|
+
workflowSnapshotJson: state.workflowSnapshot ? JSON.stringify(state.workflowSnapshot) : null,
|
|
376
|
+
policySnapshotJson: state.policySnapshot ? JSON.stringify(state.policySnapshot) : null,
|
|
377
|
+
engineCountersJson: state.engineCounters ? JSON.stringify(state.engineCounters) : null,
|
|
378
|
+
mutableStateJson: state.mutableState ? JSON.stringify(state.mutableState) : null,
|
|
379
|
+
outputsByNodeJson: JSON.stringify(this.buildPersistedOutputsByNode(state)),
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
if (updated.count !== 1) {
|
|
383
|
+
throw new Error(`Concurrent run update detected for run ${state.runId}.`);
|
|
384
|
+
}
|
|
108
385
|
});
|
|
109
386
|
}
|
|
110
387
|
|
|
@@ -131,7 +408,10 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
131
408
|
const rows = await this.prisma.run.findMany({
|
|
132
409
|
where: {
|
|
133
410
|
status: { in: ["completed", "failed"] },
|
|
134
|
-
|
|
411
|
+
OR: [
|
|
412
|
+
{ AND: [{ finishedAt: { not: null } }, { finishedAt: { lt: args.beforeIso } }] },
|
|
413
|
+
{ AND: [{ finishedAt: null }, { updatedAt: { lt: args.beforeIso } }] },
|
|
414
|
+
],
|
|
135
415
|
},
|
|
136
416
|
orderBy: { updatedAt: "asc" },
|
|
137
417
|
take: limit,
|
|
@@ -139,81 +419,230 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
139
419
|
return rows.map((r) => this.rowToPruneCandidate(r));
|
|
140
420
|
}
|
|
141
421
|
|
|
142
|
-
private
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
422
|
+
private instancesToDomain(
|
|
423
|
+
runId: RunId,
|
|
424
|
+
workflowId: WorkflowId,
|
|
425
|
+
instances: ExecutionInstanceRow[],
|
|
426
|
+
): {
|
|
427
|
+
nodeSnapshotsByNodeId: Record<NodeId, NodeExecutionSnapshot>;
|
|
428
|
+
connectionInvocations: ReadonlyArray<ConnectionInvocationRecord>;
|
|
429
|
+
} {
|
|
430
|
+
const nodeSnapshotsByNodeId: Record<NodeId, NodeExecutionSnapshot> = {};
|
|
431
|
+
const connectionInvocations: ConnectionInvocationRecord[] = [];
|
|
432
|
+
const workflowRows = instances.filter((i) => i.kind === "workflowNodeActivation");
|
|
433
|
+
const byNode = new Map<NodeId, ExecutionInstanceRow>();
|
|
434
|
+
for (const row of workflowRows) {
|
|
435
|
+
const prev = byNode.get(row.slotNodeId);
|
|
436
|
+
if (!prev || row.updatedAt > prev.updatedAt) {
|
|
437
|
+
byNode.set(row.slotNodeId, row);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const [nodeId, row] of byNode.entries()) {
|
|
441
|
+
nodeSnapshotsByNodeId[nodeId] = this.rowToNodeSnapshot(runId, workflowId, nodeId, row);
|
|
442
|
+
}
|
|
443
|
+
const connRows = instances.filter((i) => i.kind === "connectionInvocation").sort((a, b) => a.runIndex - b.runIndex);
|
|
444
|
+
for (const row of connRows) {
|
|
445
|
+
connectionInvocations.push(this.rowToConnectionInvocation(runId, workflowId, row));
|
|
446
|
+
}
|
|
447
|
+
return { nodeSnapshotsByNodeId, connectionInvocations };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private rowToNodeSnapshot(
|
|
451
|
+
runId: RunId,
|
|
452
|
+
workflowId: WorkflowId,
|
|
453
|
+
nodeId: NodeId,
|
|
454
|
+
row: ExecutionInstanceRow,
|
|
455
|
+
): NodeExecutionSnapshot {
|
|
456
|
+
const inputsByPort = row.inputJson
|
|
457
|
+
? (JSON.parse(row.inputJson) as NodeExecutionSnapshot["inputsByPort"])
|
|
458
|
+
: undefined;
|
|
459
|
+
const outputs = row.outputJson ? (JSON.parse(row.outputJson) as NodeOutputs) : undefined;
|
|
460
|
+
const error = row.errorJson
|
|
461
|
+
? (JSON.parse(row.errorJson) as NonNullable<NodeExecutionSnapshot["error"]>)
|
|
462
|
+
: undefined;
|
|
463
|
+
return {
|
|
464
|
+
runId,
|
|
465
|
+
workflowId,
|
|
466
|
+
nodeId,
|
|
467
|
+
activationId: row.activationId ?? undefined,
|
|
468
|
+
status: row.status as NodeExecutionSnapshot["status"],
|
|
469
|
+
usedPinnedOutput: row.usedPinnedOutput ?? undefined,
|
|
470
|
+
queuedAt: row.queuedAt ?? undefined,
|
|
471
|
+
startedAt: row.startedAt ?? undefined,
|
|
472
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
473
|
+
updatedAt: row.updatedAt,
|
|
474
|
+
inputsByPort,
|
|
475
|
+
outputs,
|
|
476
|
+
error,
|
|
154
477
|
};
|
|
155
|
-
return JSON.stringify(blob);
|
|
156
478
|
}
|
|
157
479
|
|
|
158
|
-
private
|
|
159
|
-
|
|
480
|
+
private rowToConnectionInvocation(
|
|
481
|
+
runId: RunId,
|
|
482
|
+
workflowId: WorkflowId,
|
|
483
|
+
row: ExecutionInstanceRow,
|
|
484
|
+
): ConnectionInvocationRecord {
|
|
485
|
+
const err = row.errorJson
|
|
486
|
+
? (JSON.parse(row.errorJson) as NonNullable<ConnectionInvocationRecord["error"]>)
|
|
487
|
+
: undefined;
|
|
160
488
|
return {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
489
|
+
invocationId: row.instanceId,
|
|
490
|
+
runId,
|
|
491
|
+
workflowId,
|
|
492
|
+
connectionNodeId: row.slotNodeId,
|
|
493
|
+
parentAgentNodeId: row.workflowNodeId,
|
|
494
|
+
parentAgentActivationId: row.activationId ?? `synthetic_${row.workflowNodeId}`,
|
|
495
|
+
status: row.status as ConnectionInvocationRecord["status"],
|
|
496
|
+
managedInput: row.inputJson ? JSON.parse(row.inputJson) : undefined,
|
|
497
|
+
managedOutput: row.outputJson ? JSON.parse(row.outputJson) : undefined,
|
|
498
|
+
error: err,
|
|
499
|
+
queuedAt: row.queuedAt ?? undefined,
|
|
500
|
+
startedAt: row.startedAt ?? undefined,
|
|
501
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
502
|
+
updatedAt: row.updatedAt,
|
|
171
503
|
};
|
|
172
504
|
}
|
|
173
505
|
|
|
174
|
-
private
|
|
175
|
-
runId: string;
|
|
176
|
-
workflowId: string;
|
|
177
|
-
startedAt: string;
|
|
178
|
-
status: string;
|
|
179
|
-
parentJson: string | null;
|
|
180
|
-
executionOptionsJson: string | null;
|
|
181
|
-
stateJson: string;
|
|
182
|
-
}): PersistedRunState {
|
|
183
|
-
const blob = this.parseStateBlob(row.stateJson);
|
|
506
|
+
private toExecutionInstanceDto(row: ExecutionInstanceRow): ExecutionInstanceDto {
|
|
184
507
|
return {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
connectionInvocations: blob.connectionInvocations,
|
|
508
|
+
instanceId: row.instanceId,
|
|
509
|
+
slotNodeId: row.slotNodeId as NodeId,
|
|
510
|
+
workflowNodeId: row.workflowNodeId as NodeId,
|
|
511
|
+
parentInstanceId: row.parentInstanceId ?? undefined,
|
|
512
|
+
kind: row.kind as ExecutionInstanceDto["kind"],
|
|
513
|
+
connectionKind: row.connectionKind as ExecutionInstanceDto["connectionKind"],
|
|
514
|
+
runIndex: row.runIndex,
|
|
515
|
+
batchId: row.batchId,
|
|
516
|
+
activationId: row.activationId ?? undefined,
|
|
517
|
+
status: row.status as ExecutionInstanceDto["status"],
|
|
518
|
+
queuedAt: row.queuedAt ?? undefined,
|
|
519
|
+
startedAt: row.startedAt ?? undefined,
|
|
520
|
+
finishedAt: row.finishedAt ?? undefined,
|
|
521
|
+
itemCount: row.itemCount,
|
|
522
|
+
inputJson: row.inputJson ? (JSON.parse(row.inputJson) as ExecutionInstanceDto["inputJson"]) : undefined,
|
|
523
|
+
outputJson: row.outputJson ? (JSON.parse(row.outputJson) as ExecutionInstanceDto["outputJson"]) : undefined,
|
|
524
|
+
error: row.errorJson ? (JSON.parse(row.errorJson) as ExecutionInstanceDto["error"]) : undefined,
|
|
203
525
|
};
|
|
204
526
|
}
|
|
205
527
|
|
|
528
|
+
private buildWorkItems(state: PersistedRunState, nowIso: string): Prisma.RunWorkItemCreateManyInput[] {
|
|
529
|
+
const rows: Prisma.RunWorkItemCreateManyInput[] = [];
|
|
530
|
+
for (const [index, entry] of (state.queue ?? []).entries()) {
|
|
531
|
+
const inputsByPort = this.inputsByPortFromQueueEntry(entry);
|
|
532
|
+
rows.push({
|
|
533
|
+
workItemId: `${state.runId}:queued:${index}:${entry.nodeId}:${entry.batchId ?? "batch_1"}`,
|
|
534
|
+
runId: state.runId,
|
|
535
|
+
workflowId: state.workflowId,
|
|
536
|
+
status: "queued",
|
|
537
|
+
targetNodeId: entry.nodeId,
|
|
538
|
+
batchId: entry.batchId ?? "batch_1",
|
|
539
|
+
availableAt: nowIso,
|
|
540
|
+
enqueuedAt: nowIso,
|
|
541
|
+
itemsIn: this.countItemsByPort(inputsByPort),
|
|
542
|
+
inputsByPortJson: JSON.stringify(inputsByPort),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (state.pending) {
|
|
546
|
+
rows.push({
|
|
547
|
+
workItemId: state.pending.activationId,
|
|
548
|
+
runId: state.runId,
|
|
549
|
+
workflowId: state.workflowId,
|
|
550
|
+
status: "claimed",
|
|
551
|
+
targetNodeId: state.pending.nodeId,
|
|
552
|
+
batchId: state.pending.batchId ?? "batch_1",
|
|
553
|
+
queueName: state.pending.queue,
|
|
554
|
+
claimToken: state.pending.activationId,
|
|
555
|
+
claimedAt: nowIso,
|
|
556
|
+
availableAt: state.pending.enqueuedAt,
|
|
557
|
+
enqueuedAt: state.pending.enqueuedAt,
|
|
558
|
+
itemsIn: state.pending.itemsIn,
|
|
559
|
+
inputsByPortJson: JSON.stringify(state.pending.inputsByPort),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return rows;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private buildExecutionInstances(state: PersistedRunState): Prisma.ExecutionInstanceCreateManyInput[] {
|
|
566
|
+
const rows: Prisma.ExecutionInstanceCreateManyInput[] = [];
|
|
567
|
+
for (const [nodeId, snap] of Object.entries(state.nodeSnapshotsByNodeId ?? {})) {
|
|
568
|
+
const instanceId = `${state.runId}:node:${nodeId}:${snap.activationId ?? "na"}`;
|
|
569
|
+
const itemCount = this.countItemsByPort(snap.inputsByPort) || this.countItemsInOutputs(snap.outputs);
|
|
570
|
+
rows.push({
|
|
571
|
+
instanceId,
|
|
572
|
+
runId: state.runId,
|
|
573
|
+
workflowId: state.workflowId,
|
|
574
|
+
slotNodeId: nodeId,
|
|
575
|
+
workflowNodeId: nodeId,
|
|
576
|
+
kind: "workflowNodeActivation",
|
|
577
|
+
connectionKind: null,
|
|
578
|
+
activationId: snap.activationId ?? null,
|
|
579
|
+
batchId: state.pending?.batchId ?? "batch_1",
|
|
580
|
+
runIndex: 1,
|
|
581
|
+
status: snap.status,
|
|
582
|
+
queuedAt: snap.queuedAt ?? null,
|
|
583
|
+
startedAt: snap.startedAt ?? null,
|
|
584
|
+
finishedAt: snap.finishedAt ?? null,
|
|
585
|
+
updatedAt: snap.updatedAt,
|
|
586
|
+
itemCount,
|
|
587
|
+
inputJson: snap.inputsByPort ? JSON.stringify(snap.inputsByPort) : null,
|
|
588
|
+
outputJson: snap.outputs ? JSON.stringify(snap.outputs) : null,
|
|
589
|
+
errorJson: snap.error ? JSON.stringify(snap.error) : null,
|
|
590
|
+
inputItemIndicesJson: null,
|
|
591
|
+
outputItemCount: snap.outputs ? this.countItemsInOutputs(snap.outputs) : null,
|
|
592
|
+
successfulItemCount: null,
|
|
593
|
+
failedItemCount: snap.status === "failed" ? itemCount : null,
|
|
594
|
+
inputStorageKind: "inline",
|
|
595
|
+
outputStorageKind: "inline",
|
|
596
|
+
usedPinnedOutput: snap.usedPinnedOutput ?? null,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
let cIdx = 0;
|
|
600
|
+
for (const inv of state.connectionInvocations ?? []) {
|
|
601
|
+
rows.push({
|
|
602
|
+
instanceId: inv.invocationId,
|
|
603
|
+
runId: state.runId,
|
|
604
|
+
workflowId: state.workflowId,
|
|
605
|
+
slotNodeId: inv.connectionNodeId,
|
|
606
|
+
workflowNodeId: inv.parentAgentNodeId,
|
|
607
|
+
kind: "connectionInvocation",
|
|
608
|
+
connectionKind: "languageModel",
|
|
609
|
+
activationId: inv.parentAgentActivationId,
|
|
610
|
+
batchId: state.pending?.batchId ?? "batch_1",
|
|
611
|
+
runIndex: cIdx,
|
|
612
|
+
status: inv.status,
|
|
613
|
+
queuedAt: inv.queuedAt ?? null,
|
|
614
|
+
startedAt: inv.startedAt ?? null,
|
|
615
|
+
finishedAt: inv.finishedAt ?? null,
|
|
616
|
+
updatedAt: inv.updatedAt,
|
|
617
|
+
itemCount: 0,
|
|
618
|
+
inputJson: inv.managedInput !== undefined ? JSON.stringify(inv.managedInput) : null,
|
|
619
|
+
outputJson: inv.managedOutput !== undefined ? JSON.stringify(inv.managedOutput) : null,
|
|
620
|
+
errorJson: inv.error ? JSON.stringify(inv.error) : null,
|
|
621
|
+
inputItemIndicesJson: null,
|
|
622
|
+
outputItemCount: null,
|
|
623
|
+
successfulItemCount: null,
|
|
624
|
+
failedItemCount: null,
|
|
625
|
+
inputStorageKind: "inline",
|
|
626
|
+
outputStorageKind: "inline",
|
|
627
|
+
});
|
|
628
|
+
cIdx += 1;
|
|
629
|
+
}
|
|
630
|
+
return rows;
|
|
631
|
+
}
|
|
632
|
+
|
|
206
633
|
private rowToPruneCandidate(row: {
|
|
207
634
|
runId: string;
|
|
208
635
|
workflowId: string;
|
|
209
636
|
startedAt: string;
|
|
210
637
|
updatedAt: string;
|
|
638
|
+
finishedAt: string | null;
|
|
211
639
|
}): RunPruneCandidate {
|
|
640
|
+
const finishedAt = row.finishedAt ?? row.updatedAt;
|
|
212
641
|
return {
|
|
213
642
|
runId: row.runId as RunId,
|
|
214
643
|
workflowId: row.workflowId as WorkflowId,
|
|
215
644
|
startedAt: row.startedAt,
|
|
216
|
-
finishedAt
|
|
645
|
+
finishedAt,
|
|
217
646
|
};
|
|
218
647
|
}
|
|
219
648
|
|
|
@@ -225,9 +654,10 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
225
654
|
parentJson: string | null;
|
|
226
655
|
executionOptionsJson: string | null;
|
|
227
656
|
updatedAt: string;
|
|
657
|
+
finishedAt: string | null;
|
|
228
658
|
}): RunSummary {
|
|
229
659
|
const status = row.status as RunSummary["status"];
|
|
230
|
-
const finishedAt = status === "completed" || status === "failed" ? row.updatedAt : undefined;
|
|
660
|
+
const finishedAt = status === "completed" || status === "failed" ? (row.finishedAt ?? row.updatedAt) : undefined;
|
|
231
661
|
return {
|
|
232
662
|
runId: row.runId as RunId,
|
|
233
663
|
workflowId: row.workflowId as WorkflowId,
|
|
@@ -240,4 +670,261 @@ export class PrismaWorkflowRunRepository implements WorkflowRunRepository, Workf
|
|
|
240
670
|
: undefined,
|
|
241
671
|
};
|
|
242
672
|
}
|
|
673
|
+
|
|
674
|
+
private parseJson<T>(value: string | null): T | undefined {
|
|
675
|
+
if (!value) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
return JSON.parse(value) as T;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private toPendingNodeExecution(row: RunWorkItemRecord): PendingNodeExecution {
|
|
682
|
+
return {
|
|
683
|
+
runId: row.runId as RunId,
|
|
684
|
+
activationId: (row.claimToken ?? row.workItemId) as PendingNodeExecution["activationId"],
|
|
685
|
+
workflowId: row.workflowId as WorkflowId,
|
|
686
|
+
nodeId: row.targetNodeId as NodeId,
|
|
687
|
+
itemsIn: row.itemsIn,
|
|
688
|
+
inputsByPort: JSON.parse(row.inputsByPortJson) as NodeInputsByPort,
|
|
689
|
+
receiptId: row.claimToken ?? row.workItemId,
|
|
690
|
+
queue: row.queueName ?? undefined,
|
|
691
|
+
batchId: row.batchId,
|
|
692
|
+
enqueuedAt: row.enqueuedAt,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private toQueueEntry(row: RunWorkItemRecord): RunQueueEntry {
|
|
697
|
+
const inputsByPort = JSON.parse(row.inputsByPortJson) as NodeInputsByPort;
|
|
698
|
+
const portEntries = Object.entries(inputsByPort);
|
|
699
|
+
if (portEntries.length <= 1) {
|
|
700
|
+
const [portKey, items] = portEntries[0] ?? ["in", []];
|
|
701
|
+
return {
|
|
702
|
+
nodeId: row.targetNodeId as NodeId,
|
|
703
|
+
input: items,
|
|
704
|
+
toInput: portKey === "in" ? undefined : portKey,
|
|
705
|
+
batchId: row.batchId,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return {
|
|
709
|
+
nodeId: row.targetNodeId as NodeId,
|
|
710
|
+
input: [],
|
|
711
|
+
batchId: row.batchId,
|
|
712
|
+
collect: {
|
|
713
|
+
expectedInputs: portEntries.map(([portKey]) => portKey),
|
|
714
|
+
received: inputsByPort,
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private inputsByPortFromQueueEntry(entry: RunQueueEntry): NodeInputsByPort {
|
|
720
|
+
if (entry.collect) {
|
|
721
|
+
return entry.collect.received;
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
[(entry.toInput ?? "in") as string]: entry.input,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private countItemsByPort(inputsByPort: NodeInputsByPort | undefined): number {
|
|
729
|
+
let count = 0;
|
|
730
|
+
for (const items of Object.values(inputsByPort ?? {})) {
|
|
731
|
+
count += items.length;
|
|
732
|
+
}
|
|
733
|
+
return count;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private countItemsInOutputs(outputs: NodeOutputs | undefined): number {
|
|
737
|
+
let count = 0;
|
|
738
|
+
for (const items of Object.values(outputs ?? {})) {
|
|
739
|
+
count += items?.length ?? 0;
|
|
740
|
+
}
|
|
741
|
+
return count;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private buildProjectionSlotStatesJson(state: PersistedRunState): string {
|
|
745
|
+
const slotStatesByNodeId: Record<
|
|
746
|
+
string,
|
|
747
|
+
{
|
|
748
|
+
latestInstanceId?: string;
|
|
749
|
+
latestTerminalInstanceId?: string;
|
|
750
|
+
latestRunningInstanceId?: string;
|
|
751
|
+
latestStatus?: string;
|
|
752
|
+
invocationCount: number;
|
|
753
|
+
runCount: number;
|
|
754
|
+
}
|
|
755
|
+
> = {};
|
|
756
|
+
for (const [nodeId, snapshot] of Object.entries(state.nodeSnapshotsByNodeId ?? {})) {
|
|
757
|
+
const latestInstanceId = `${state.runId}:node:${nodeId}:${snapshot.activationId ?? "na"}`;
|
|
758
|
+
slotStatesByNodeId[nodeId] = {
|
|
759
|
+
latestInstanceId,
|
|
760
|
+
latestTerminalInstanceId:
|
|
761
|
+
snapshot.status === "completed" || snapshot.status === "failed" ? latestInstanceId : undefined,
|
|
762
|
+
latestRunningInstanceId:
|
|
763
|
+
snapshot.status === "queued" || snapshot.status === "running" ? latestInstanceId : undefined,
|
|
764
|
+
latestStatus: snapshot.status,
|
|
765
|
+
invocationCount: 0,
|
|
766
|
+
runCount: snapshot.status === "completed" || snapshot.status === "failed" ? 1 : 0,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
for (const invocation of state.connectionInvocations ?? []) {
|
|
770
|
+
const existing = slotStatesByNodeId[invocation.connectionNodeId] ?? {
|
|
771
|
+
invocationCount: 0,
|
|
772
|
+
runCount: 0,
|
|
773
|
+
};
|
|
774
|
+
existing.invocationCount += 1;
|
|
775
|
+
slotStatesByNodeId[invocation.connectionNodeId] = existing;
|
|
776
|
+
}
|
|
777
|
+
return JSON.stringify({ slotStatesByNodeId });
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private toSlotStateDtos(projection: RunSlotProjectionRow | null): ReadonlyArray<SlotExecutionStateDto> {
|
|
781
|
+
const state = this.parseJson<{ slotStatesByNodeId?: Record<string, SlotExecutionStateDto> }>(
|
|
782
|
+
projection?.slotStatesJson ?? null,
|
|
783
|
+
);
|
|
784
|
+
return Object.entries(state?.slotStatesByNodeId ?? {}).map(([slotNodeId, slotState]) => ({
|
|
785
|
+
slotNodeId: slotNodeId as NodeId,
|
|
786
|
+
latestInstanceId: slotState.latestInstanceId,
|
|
787
|
+
latestTerminalInstanceId: slotState.latestTerminalInstanceId,
|
|
788
|
+
latestRunningInstanceId: slotState.latestRunningInstanceId,
|
|
789
|
+
status:
|
|
790
|
+
slotState.status ??
|
|
791
|
+
(slotState as SlotExecutionStateDto & { latestStatus?: SlotExecutionStateDto["status"] }).latestStatus,
|
|
792
|
+
invocationCount: slotState.invocationCount,
|
|
793
|
+
runCount: slotState.runCount,
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private buildPersistedOutputsByNode(state: PersistedRunState): Record<NodeId, NodeOutputs> {
|
|
798
|
+
const persistedOutputsByNode: Record<NodeId, NodeOutputs> = {};
|
|
799
|
+
for (const [nodeId, outputs] of Object.entries(state.outputsByNode ?? {})) {
|
|
800
|
+
if (state.nodeSnapshotsByNodeId[nodeId]) {
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
persistedOutputsByNode[nodeId as NodeId] = outputs;
|
|
804
|
+
}
|
|
805
|
+
return persistedOutputsByNode;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private mergePersistedOutputsByNode(
|
|
809
|
+
args: Readonly<{
|
|
810
|
+
persistedOutputsByNode: Record<NodeId, NodeOutputs>;
|
|
811
|
+
nodeSnapshotsByNodeId: Record<NodeId, NodeExecutionSnapshot>;
|
|
812
|
+
}>,
|
|
813
|
+
): Record<NodeId, NodeOutputs> {
|
|
814
|
+
const mergedOutputsByNode: Record<NodeId, NodeOutputs> = {
|
|
815
|
+
...args.persistedOutputsByNode,
|
|
816
|
+
};
|
|
817
|
+
for (const [nodeId, snapshot] of Object.entries(args.nodeSnapshotsByNodeId)) {
|
|
818
|
+
if (snapshot.outputs) {
|
|
819
|
+
mergedOutputsByNode[nodeId as NodeId] = snapshot.outputs;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return mergedOutputsByNode;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private nextRunIndexForSlot(maxRunIndexBySlot: Map<string, number>, slotNodeId: string): number {
|
|
826
|
+
const next = (maxRunIndexBySlot.get(slotNodeId) ?? 0) + 1;
|
|
827
|
+
maxRunIndexBySlot.set(slotNodeId, next);
|
|
828
|
+
return next;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private mergeConcurrentState(latest: PersistedRunState, desired: PersistedRunState): PersistedRunState {
|
|
832
|
+
return {
|
|
833
|
+
...latest,
|
|
834
|
+
...desired,
|
|
835
|
+
revision: latest.revision,
|
|
836
|
+
parent: desired.parent ?? latest.parent,
|
|
837
|
+
executionOptions: desired.executionOptions ?? latest.executionOptions,
|
|
838
|
+
control: desired.control ?? latest.control,
|
|
839
|
+
workflowSnapshot: desired.workflowSnapshot ?? latest.workflowSnapshot,
|
|
840
|
+
mutableState: desired.mutableState ?? latest.mutableState,
|
|
841
|
+
policySnapshot: desired.policySnapshot ?? latest.policySnapshot,
|
|
842
|
+
engineCounters: desired.engineCounters ?? latest.engineCounters,
|
|
843
|
+
pending: desired.pending,
|
|
844
|
+
queue: desired.queue,
|
|
845
|
+
outputsByNode: {
|
|
846
|
+
...(latest.outputsByNode ?? {}),
|
|
847
|
+
...(desired.outputsByNode ?? {}),
|
|
848
|
+
},
|
|
849
|
+
nodeSnapshotsByNodeId: this.mergeNodeSnapshots(latest.nodeSnapshotsByNodeId, desired.nodeSnapshotsByNodeId),
|
|
850
|
+
connectionInvocations: this.mergeConnectionInvocations(
|
|
851
|
+
latest.connectionInvocations,
|
|
852
|
+
desired.connectionInvocations,
|
|
853
|
+
),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private mergeNodeSnapshots(
|
|
858
|
+
latest: PersistedRunState["nodeSnapshotsByNodeId"],
|
|
859
|
+
desired: PersistedRunState["nodeSnapshotsByNodeId"],
|
|
860
|
+
): PersistedRunState["nodeSnapshotsByNodeId"] {
|
|
861
|
+
const merged: PersistedRunState["nodeSnapshotsByNodeId"] = {
|
|
862
|
+
...(latest ?? {}),
|
|
863
|
+
};
|
|
864
|
+
for (const [nodeId, snapshot] of Object.entries(desired ?? {})) {
|
|
865
|
+
const current = merged[nodeId as NodeId];
|
|
866
|
+
if (!current || (snapshot.updatedAt ?? "") >= (current.updatedAt ?? "")) {
|
|
867
|
+
merged[nodeId as NodeId] = snapshot;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return merged;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
private mergeConnectionInvocations(
|
|
874
|
+
latest: PersistedRunState["connectionInvocations"],
|
|
875
|
+
desired: PersistedRunState["connectionInvocations"],
|
|
876
|
+
): PersistedRunState["connectionInvocations"] {
|
|
877
|
+
const byId = new Map<string, NonNullable<PersistedRunState["connectionInvocations"]>[number]>();
|
|
878
|
+
for (const record of latest ?? []) {
|
|
879
|
+
byId.set(record.invocationId, record);
|
|
880
|
+
}
|
|
881
|
+
for (const record of desired ?? []) {
|
|
882
|
+
const current = byId.get(record.invocationId);
|
|
883
|
+
if (!current || record.updatedAt >= current.updatedAt) {
|
|
884
|
+
byId.set(record.invocationId, record);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return [...byId.values()].sort(
|
|
888
|
+
(left, right) =>
|
|
889
|
+
left.updatedAt.localeCompare(right.updatedAt) || left.invocationId.localeCompare(right.invocationId),
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private isConcurrentRunUpdateError(error: unknown): boolean {
|
|
894
|
+
return error instanceof Error && error.message.includes("Concurrent run update detected");
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private collectBinaryKeysFromJsonText(value: string | null, keys: Set<string>): void {
|
|
898
|
+
if (!value) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
this.collectBinaryKeysFromValue(JSON.parse(value) as unknown, keys);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private collectBinaryKeysFromValue(value: unknown, keys: Set<string>): void {
|
|
905
|
+
if (Array.isArray(value)) {
|
|
906
|
+
for (const entry of value) {
|
|
907
|
+
this.collectBinaryKeysFromValue(entry, keys);
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!value || typeof value !== "object") {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const record = value as Record<string, unknown>;
|
|
915
|
+
if (
|
|
916
|
+
typeof record.id === "string" &&
|
|
917
|
+
typeof record.storageKey === "string" &&
|
|
918
|
+
typeof record.mimeType === "string" &&
|
|
919
|
+
typeof record.size === "number"
|
|
920
|
+
) {
|
|
921
|
+
if (record.storageKey.length > 0) {
|
|
922
|
+
keys.add(record.storageKey);
|
|
923
|
+
}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
for (const child of Object.values(record)) {
|
|
927
|
+
this.collectBinaryKeysFromValue(child, keys);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
243
930
|
}
|