@knpkv/herdr-work 0.5.1 → 0.5.3
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/dist/store.d.ts.map +1 -1
- package/dist/store.js +612 -605
- package/dist/store.js.map +1 -1
- package/package.json +3 -3
package/dist/store.js
CHANGED
|
@@ -138,544 +138,436 @@ const legacyDecisionEquivalent = Schema.toEquivalence(LegacyDecisionRecord);
|
|
|
138
138
|
const TransactionId = Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(256), Schema.isPattern(/^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/));
|
|
139
139
|
const storeError = (operation) => (cause) => new WorkStoreError({ cause, operation });
|
|
140
140
|
const fromPrivateDatabaseError = (error) => new WorkStoreError({ cause: error.cause, operation: error.operation });
|
|
141
|
+
/**
|
|
142
|
+
* Upgrades pre-session Work tables in place. Runs inside `WorkStore.open`'s
|
|
143
|
+
* schema transaction, so a failure here or in a later schema step leaves the
|
|
144
|
+
* file as it was.
|
|
145
|
+
*/
|
|
141
146
|
const migrateLegacyAuthorityTables = (database) => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
?
|
|
147
|
+
const columns = (table) => Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare(`PRAGMA table_info(${table})`).all()).map(({ name }) => name);
|
|
148
|
+
const laneColumns = columns("work_lane_claims");
|
|
149
|
+
const decisionColumns = columns("work_decision_handoffs");
|
|
150
|
+
const tables = Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all()).map(({ name }) => name);
|
|
151
|
+
const coordinatorDispatchColumns = tables.includes("orchestrator_dispatches")
|
|
152
|
+
? columns("orchestrator_dispatches")
|
|
153
|
+
: [];
|
|
154
|
+
const requireBindingCompanions = (binding, operation) => {
|
|
155
|
+
const laneInput = tables.includes("work_lane_operations")
|
|
156
|
+
? database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
157
|
+
phase, revision, record
|
|
158
|
+
FROM work_lane_operations WHERE operation_id = ?`).get(binding.lane.operationId)
|
|
159
|
+
: undefined;
|
|
160
|
+
const checkpointInput = tables.includes("work_goal_events")
|
|
161
|
+
? database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
162
|
+
FROM work_goal_events WHERE event_id = ?`).get(binding.checkpoint.eventId)
|
|
163
|
+
: undefined;
|
|
164
|
+
const error = agentBindingReadbackError(binding, laneInput === undefined ? undefined : Schema.decodeUnknownSync(AgentBindingLaneOperationRow)(laneInput), checkpointInput === undefined
|
|
165
|
+
? undefined
|
|
166
|
+
: Schema.decodeUnknownSync(AgentBindingGoalEventRow)(checkpointInput), operation);
|
|
167
|
+
if (error !== undefined)
|
|
168
|
+
throw error;
|
|
169
|
+
};
|
|
170
|
+
const coordinatorTablesPresent = {
|
|
171
|
+
dispatch: tables.includes("orchestrator_dispatches"),
|
|
172
|
+
event: tables.includes("orchestrator_events"),
|
|
173
|
+
metadata: tables.includes("orchestrator_dispatch_metadata")
|
|
174
|
+
};
|
|
175
|
+
const requireCoordinatorSchema = (operation) => {
|
|
176
|
+
const present = Object.values(coordinatorTablesPresent).filter(Boolean).length;
|
|
177
|
+
if (present !== 0 && present !== 3) {
|
|
178
|
+
throw new WorkStoreError({ cause: coordinatorTablesPresent, operation: `${operation}.schema` });
|
|
179
|
+
}
|
|
180
|
+
return present === 3;
|
|
181
|
+
};
|
|
182
|
+
requireCoordinatorSchema("open.migrate");
|
|
183
|
+
const readCoordinatorLifecycle = (dispatchRequestId) => ({
|
|
184
|
+
dispatchRows: Schema.decodeUnknownSync(Schema.Array(CoordinatorLifecycleDispatchRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId,
|
|
185
|
+
activity_idempotency_key AS activityIdempotencyKey, command,
|
|
186
|
+
accepted_at AS acceptedAt, status
|
|
187
|
+
FROM orchestrator_dispatches WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId)),
|
|
188
|
+
eventRows: Schema.decodeUnknownSync(Schema.Array(CoordinatorLifecycleEventRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, sequence, type,
|
|
189
|
+
activity_idempotency_key AS activityIdempotencyKey,
|
|
190
|
+
occurred_at AS occurredAt, detail, result
|
|
191
|
+
FROM orchestrator_events WHERE dispatch_request_id = ?
|
|
192
|
+
ORDER BY sequence ASC LIMIT 5`).all(dispatchRequestId))
|
|
193
|
+
});
|
|
194
|
+
const routeStorageAuthority = (dispatchRequestId, operation) => {
|
|
195
|
+
if (!coordinatorDispatchColumns.includes("is_routed")) {
|
|
196
|
+
return { _tag: "legacy_without_routed_discriminator" };
|
|
197
|
+
}
|
|
198
|
+
const rows = Schema.decodeUnknownSync(Schema.Array(CoordinatorRouteDiscriminatorRow))(database.prepare(`SELECT is_routed AS isRouted FROM orchestrator_dispatches
|
|
199
|
+
WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId));
|
|
200
|
+
const row = rows[0];
|
|
201
|
+
if (rows.length !== 1 || row === undefined) {
|
|
202
|
+
throw new WorkStoreError({ cause: { dispatchRequestId, rows }, operation });
|
|
203
|
+
}
|
|
204
|
+
return { _tag: "routed_discriminator", isRouted: row.isRouted };
|
|
205
|
+
};
|
|
206
|
+
const requireLinkedParentAuthority = (linkedRequestId, operation) => {
|
|
207
|
+
if (linkedRequestId === null)
|
|
208
|
+
return;
|
|
209
|
+
const metadataRows = Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
210
|
+
FROM orchestrator_dispatch_metadata WHERE dispatch_request_id = ? LIMIT 2`).all(linkedRequestId));
|
|
211
|
+
const metadata = metadataRows[0];
|
|
212
|
+
if (metadataRows.length !== 1 || metadata === undefined || metadata.workLink !== null) {
|
|
213
|
+
throw new WorkStoreError({ cause: { linkedRequestId, metadataRows }, operation });
|
|
214
|
+
}
|
|
215
|
+
const lifecycle = readCoordinatorLifecycle(linkedRequestId);
|
|
216
|
+
requireCoordinatorFailedLunaAuthority(lifecycle.dispatchRows, lifecycle.eventRows, metadata.route, routeStorageAuthority(linkedRequestId, operation), operation);
|
|
217
|
+
};
|
|
218
|
+
const requireMetadataAuthority = (dispatchRequestId, routeText, lineage, operation) => {
|
|
219
|
+
const commandRows = Schema.decodeUnknownSync(Schema.Array(CoordinatorCommandRow))(database.prepare(`SELECT activity_idempotency_key AS activityIdempotencyKey, command FROM orchestrator_dispatches
|
|
220
|
+
WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId));
|
|
221
|
+
const commandRow = commandRows[0];
|
|
222
|
+
if (commandRows.length !== 1 || commandRow === undefined) {
|
|
223
|
+
throw new WorkStoreError({ cause: { commandRows, dispatchRequestId }, operation });
|
|
224
|
+
}
|
|
225
|
+
const route = requireCoordinatorRouteAuthority(commandRow.command, commandRow.activityIdempotencyKey, routeText, lineage, routeStorageAuthority(dispatchRequestId, operation), operation);
|
|
226
|
+
requireLinkedParentAuthority(route.linkedRequestId, `${operation}.parent`);
|
|
227
|
+
};
|
|
228
|
+
const requireLifecycleAuthority = (dispatchRequestId, expectedRunningAt, operation) => {
|
|
229
|
+
if (!requireCoordinatorSchema(operation))
|
|
230
|
+
return;
|
|
231
|
+
const lifecycle = readCoordinatorLifecycle(dispatchRequestId);
|
|
232
|
+
requireCoordinatorLifecycleAuthority(lifecycle.dispatchRows, lifecycle.eventRows, expectedRunningAt, operation);
|
|
233
|
+
};
|
|
234
|
+
const requireCurrentAgentBindingAuthority = (dispatchRequestId, handoff, operation) => {
|
|
235
|
+
const lifecycle = readCoordinatorLifecycle(dispatchRequestId);
|
|
236
|
+
const runningAt = coordinatorLifecycleRunningAt(lifecycle.dispatchRows, lifecycle.eventRows, `${operation}.lifecycle`);
|
|
237
|
+
const bindingRows = tables.includes("work_agent_bindings")
|
|
238
|
+
? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
239
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
240
|
+
FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId))
|
|
152
241
|
: [];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
if (runningAt === null && bindingRows.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
const bindingRow = bindingRows[0];
|
|
245
|
+
if (runningAt === null || bindingRows.length !== 1 || bindingRow === undefined) {
|
|
246
|
+
throw new WorkStoreError({ cause: { bindingRows, lifecycle }, operation });
|
|
247
|
+
}
|
|
248
|
+
const decoded = decodeAgentBindingRow(bindingRow, { dispatchRequestId, laneId: handoff.laneId }, operation);
|
|
249
|
+
if (decoded._tag === "invalid")
|
|
250
|
+
throw decoded.error;
|
|
251
|
+
if (decoded.binding.request.expectedRevision !== handoff.expectedRevision ||
|
|
252
|
+
decoded.binding.lane.goalId !== handoff.goalId ||
|
|
253
|
+
decoded.binding.checkpoint.occurredAt !== runningAt) {
|
|
254
|
+
throw new WorkStoreError({ cause: { binding: decoded.binding, handoff, runningAt }, operation });
|
|
255
|
+
}
|
|
256
|
+
requireBindingCompanions(decoded.binding, operation);
|
|
257
|
+
const currentLaneRows = Schema.decodeUnknownSync(Schema.Array(LaneRow))(database.prepare(`SELECT lane_id AS laneId, goal_id AS goalId, operation_id AS operationId,
|
|
258
|
+
phase, revision, record
|
|
259
|
+
FROM work_lane_claims WHERE lane_id = ? LIMIT 2`).all(decoded.binding.lane.laneId));
|
|
260
|
+
const currentLaneRow = currentLaneRows[0];
|
|
261
|
+
if (currentLaneRows.length !== 1 || currentLaneRow === undefined) {
|
|
262
|
+
throw new WorkStoreError({
|
|
263
|
+
cause: { binding: decoded.binding, currentLaneRows },
|
|
264
|
+
operation: `${operation}.lane-revision`
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
let currentLane;
|
|
268
|
+
try {
|
|
269
|
+
currentLane = Schema.decodeUnknownSync(WorkLaneClaimed)(JSON.parse(currentLaneRow.record));
|
|
270
|
+
}
|
|
271
|
+
catch (cause) {
|
|
272
|
+
throw new WorkStoreError({ cause, operation: `${operation}.lane-revision` });
|
|
273
|
+
}
|
|
274
|
+
const currentLaneSchema = laneColumns.includes("goal_id");
|
|
275
|
+
const currentOperationRows = currentLaneSchema
|
|
276
|
+
? Schema.decodeUnknownSync(Schema.Array(AgentBindingLaneOperationRow))(database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
156
277
|
phase, revision, record
|
|
157
|
-
FROM work_lane_operations WHERE operation_id =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
278
|
+
FROM work_lane_operations WHERE operation_id = ? LIMIT 2`).all(currentLane.operationId))
|
|
279
|
+
: [];
|
|
280
|
+
const currentOperationError = currentLaneSchema
|
|
281
|
+
? laneOperationReadbackError(currentLane, currentOperationRows.length === 1 ? currentOperationRows[0] : undefined, `${operation}.lane-revision`)
|
|
282
|
+
: undefined;
|
|
283
|
+
if (currentLane.laneId !== currentLaneRow.laneId ||
|
|
284
|
+
currentLane.goalId !== currentLaneRow.goalId ||
|
|
285
|
+
currentLane.operationId !== currentLaneRow.operationId ||
|
|
286
|
+
currentLane.phase !== currentLaneRow.phase ||
|
|
287
|
+
currentLane.revision !== currentLaneRow.revision ||
|
|
288
|
+
currentLane.laneId !== decoded.binding.lane.laneId ||
|
|
289
|
+
currentLane.goalId !== decoded.binding.lane.goalId ||
|
|
290
|
+
currentLane.revision < decoded.binding.lane.revision ||
|
|
291
|
+
(currentLaneSchema && currentLane.revision === decoded.binding.lane.revision &&
|
|
292
|
+
!Equal.equals(currentLane, decoded.binding.lane)) ||
|
|
293
|
+
currentOperationError !== undefined) {
|
|
294
|
+
throw new WorkStoreError({
|
|
295
|
+
cause: { binding: decoded.binding, currentLane, currentLaneRow, currentOperationError },
|
|
296
|
+
operation: `${operation}.lane-revision`
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const dispatches = tables.includes("work_dispatch_handoffs")
|
|
301
|
+
? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
|
|
302
|
+
lane_id AS laneId, occurred_at AS occurredAt, lineage, record
|
|
303
|
+
FROM work_dispatch_handoffs`).all())
|
|
304
|
+
: [];
|
|
305
|
+
const decisionIdentities = decisionColumns.length > 0
|
|
306
|
+
? Schema.decodeUnknownSync(Schema.Array(DecisionIdentityRow))(database.prepare("SELECT handoff_id AS handoffId FROM work_decision_handoffs").all())
|
|
307
|
+
: [];
|
|
308
|
+
const orphanDispatch = dispatches.find(({ handoffId }) => !decisionIdentities.some((decision) => decision.handoffId === handoffId));
|
|
309
|
+
if (orphanDispatch !== undefined) {
|
|
310
|
+
throw new WorkStoreError({
|
|
311
|
+
cause: { decisionIdentities, dispatch: orphanDispatch },
|
|
312
|
+
operation: "open.migrate.dispatch-decision"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
if (tables.includes("orchestrator_dispatch_metadata")) {
|
|
316
|
+
const linkedMetadata = Schema.decodeUnknownSync(Schema.Array(MetadataHandoffIdentityRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId,
|
|
317
|
+
CASE WHEN work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
|
|
318
|
+
CASE WHEN json_valid(work_link) AND json_type(work_link, '$.handoff.id') = 'text'
|
|
319
|
+
THEN json_extract(work_link, '$.handoff.id') END AS handoffId,
|
|
320
|
+
route
|
|
321
|
+
FROM orchestrator_dispatch_metadata AS metadata
|
|
322
|
+
WHERE work_link IS NOT NULL OR EXISTS (
|
|
323
|
+
SELECT 1 FROM orchestrator_dispatch_metadata AS linked
|
|
324
|
+
WHERE linked.dispatch_request_id = metadata.dispatch_request_id
|
|
325
|
+
AND linked.work_link IS NOT NULL
|
|
326
|
+
) OR (json_valid(route) AND json_extract(route, '$.model') = 'gpt-5.6-sol')
|
|
327
|
+
LIMIT ${workDecisionMaxRecords + 1}`).all());
|
|
328
|
+
if (linkedMetadata.length > workDecisionMaxRecords) {
|
|
329
|
+
throw new WorkStoreError({ cause: linkedMetadata.length, operation: "open.migrate.metadata-capacity" });
|
|
330
|
+
}
|
|
331
|
+
const metadataCounts = new Map();
|
|
332
|
+
const dispatchCounts = new Map();
|
|
333
|
+
const decisionCounts = new Map();
|
|
334
|
+
for (const metadata of linkedMetadata) {
|
|
335
|
+
metadataCounts.set(metadata.dispatchRequestId, (metadataCounts.get(metadata.dispatchRequestId) ?? 0) + 1);
|
|
336
|
+
}
|
|
337
|
+
for (const dispatch of dispatches) {
|
|
338
|
+
const identity = JSON.stringify([dispatch.dispatchRequestId, dispatch.handoffId]);
|
|
339
|
+
dispatchCounts.set(identity, (dispatchCounts.get(identity) ?? 0) + 1);
|
|
340
|
+
}
|
|
341
|
+
for (const decision of decisionIdentities) {
|
|
342
|
+
decisionCounts.set(decision.handoffId, (decisionCounts.get(decision.handoffId) ?? 0) + 1);
|
|
343
|
+
}
|
|
344
|
+
const invalidMetadata = linkedMetadata.find((metadata) => {
|
|
345
|
+
if (metadata.route === null)
|
|
346
|
+
return true;
|
|
347
|
+
const requiresWorkLink = coordinatorRouteRequiresWorkLink(metadata.route, routeStorageAuthority(metadata.dispatchRequestId, "open.migrate.metadata-decision"), "open.migrate.metadata-decision");
|
|
348
|
+
if (!requiresWorkLink)
|
|
349
|
+
return metadata.hasWorkLink === 1;
|
|
350
|
+
const dispatchIdentity = JSON.stringify([metadata.dispatchRequestId, metadata.handoffId]);
|
|
351
|
+
return (metadata.handoffId === null || metadataCounts.get(metadata.dispatchRequestId) !== 1 ||
|
|
352
|
+
dispatchCounts.get(dispatchIdentity) !== 1 || decisionCounts.get(metadata.handoffId) !== 1);
|
|
192
353
|
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
354
|
+
if (invalidMetadata !== undefined) {
|
|
355
|
+
throw new WorkStoreError({
|
|
356
|
+
cause: { decisionIdentities, dispatches, linkedMetadata, metadata: invalidMetadata },
|
|
357
|
+
operation: "open.migrate.metadata-decision"
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const lanes = laneColumns.length > 0 && !laneColumns.includes("goal_id")
|
|
362
|
+
? Schema.decodeUnknownSync(Schema.Array(LegacyLaneStoredRow))(database.prepare("SELECT lane_id AS laneId, revision, record FROM work_lane_claims").all()).map((row) => {
|
|
363
|
+
const legacy = Schema.decodeUnknownSync(LegacyLaneRecord)(JSON.parse(row.record));
|
|
364
|
+
if (legacy.laneId !== row.laneId || legacy.revision !== row.revision) {
|
|
365
|
+
throw new WorkStoreError({ cause: { legacy, row }, operation: "open.migrate.lane-identity" });
|
|
196
366
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
367
|
+
return Schema.decodeUnknownSync(WorkLaneClaimed)({
|
|
368
|
+
...legacy,
|
|
369
|
+
goalId: legacy.laneId,
|
|
370
|
+
operationId: legacy.laneId
|
|
371
|
+
});
|
|
372
|
+
})
|
|
373
|
+
: [];
|
|
374
|
+
const decisions = decisionColumns.length > 0 && !decisionColumns.includes("session_id")
|
|
375
|
+
? Schema.decodeUnknownSync(Schema.Array(LegacyDecisionStoredRow))(database.prepare("SELECT handoff_id AS handoffId, record FROM work_decision_handoffs").all()).map((row) => {
|
|
376
|
+
const legacy = Schema.decodeUnknownSync(LegacyDecisionRecord)(JSON.parse(row.record));
|
|
377
|
+
if (legacy.id !== row.handoffId) {
|
|
378
|
+
throw new WorkStoreError({ cause: { legacy, row }, operation: "open.migrate.handoff-identity" });
|
|
202
379
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
const metadataRows = Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
209
|
-
FROM orchestrator_dispatch_metadata WHERE dispatch_request_id = ? LIMIT 2`).all(linkedRequestId));
|
|
210
|
-
const metadata = metadataRows[0];
|
|
211
|
-
if (metadataRows.length !== 1 || metadata === undefined || metadata.workLink !== null) {
|
|
212
|
-
throw new WorkStoreError({ cause: { linkedRequestId, metadataRows }, operation });
|
|
380
|
+
const matchingDispatches = dispatches.filter(({ handoffId }) => handoffId === legacy.id);
|
|
381
|
+
const dispatch = matchingDispatches[0];
|
|
382
|
+
const lane = lanes.find(({ laneId }) => laneId === legacy.laneId);
|
|
383
|
+
if (lane === undefined) {
|
|
384
|
+
throw new WorkStoreError({ cause: legacy, operation: "open.migrate.handoff-lane" });
|
|
213
385
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
386
|
+
const dispatchIds = dispatch === undefined
|
|
387
|
+
? []
|
|
388
|
+
: Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage));
|
|
389
|
+
if (matchingDispatches.length !== 1 || dispatch === undefined) {
|
|
390
|
+
throw new WorkStoreError({
|
|
391
|
+
cause: { legacy, matchingDispatches },
|
|
392
|
+
operation: "open.migrate.legacy-dispatch-cardinality"
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
const dispatchHandoff = Schema.decodeUnknownSync(LegacyDecisionRecord)(JSON.parse(dispatch.record));
|
|
396
|
+
if (dispatch.laneId !== legacy.laneId || dispatch.occurredAt !== legacy.occurredAt ||
|
|
397
|
+
!legacyDecisionEquivalent(dispatchHandoff, legacy)) {
|
|
398
|
+
throw new WorkStoreError({
|
|
399
|
+
cause: { dispatch, dispatchHandoff, legacy },
|
|
400
|
+
operation: "open.migrate.legacy-dispatch-authority"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (tables.includes("orchestrator_dispatch_metadata")) {
|
|
404
|
+
const metadataRows = Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
405
|
+
FROM orchestrator_dispatch_metadata WHERE dispatch_request_id = ? LIMIT 2`).all(dispatch.dispatchRequestId));
|
|
406
|
+
const metadata = metadataRows[0];
|
|
407
|
+
if (metadataRows.length !== 1 || metadata === undefined) {
|
|
408
|
+
throw new WorkStoreError({
|
|
409
|
+
cause: { dispatch, metadataRows },
|
|
410
|
+
operation: "open.migrate.legacy-metadata-authority"
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
if (metadata.workLink === null) {
|
|
414
|
+
throw new WorkStoreError({
|
|
415
|
+
cause: { dispatch, metadata },
|
|
416
|
+
operation: "open.migrate.legacy-metadata-authority"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const workLink = Schema.decodeUnknownSync(LegacyMetadataWorkLink)(JSON.parse(metadata.workLink));
|
|
420
|
+
requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.legacy-metadata-authority");
|
|
421
|
+
if (!legacyDecisionEquivalent(workLink.handoff, legacy) ||
|
|
422
|
+
!workDispatchLineageEquivalent(workLink.lineage, dispatchIds)) {
|
|
423
|
+
throw new WorkStoreError({
|
|
424
|
+
cause: { legacy, workLink },
|
|
425
|
+
operation: "open.migrate.legacy-metadata-authority"
|
|
426
|
+
});
|
|
427
|
+
}
|
|
223
428
|
}
|
|
224
|
-
const route = requireCoordinatorRouteAuthority(commandRow.command, commandRow.activityIdempotencyKey, routeText, lineage, routeStorageAuthority(dispatchRequestId, operation), operation);
|
|
225
|
-
requireLinkedParentAuthority(route.linkedRequestId, `${operation}.parent`);
|
|
226
|
-
};
|
|
227
|
-
const requireLifecycleAuthority = (dispatchRequestId, expectedRunningAt, operation) => {
|
|
228
|
-
if (!requireCoordinatorSchema(operation))
|
|
229
|
-
return;
|
|
230
|
-
const lifecycle = readCoordinatorLifecycle(dispatchRequestId);
|
|
231
|
-
requireCoordinatorLifecycleAuthority(lifecycle.dispatchRows, lifecycle.eventRows, expectedRunningAt, operation);
|
|
232
|
-
};
|
|
233
|
-
const requireCurrentAgentBindingAuthority = (dispatchRequestId, handoff, operation) => {
|
|
234
|
-
const lifecycle = readCoordinatorLifecycle(dispatchRequestId);
|
|
235
|
-
const runningAt = coordinatorLifecycleRunningAt(lifecycle.dispatchRows, lifecycle.eventRows, `${operation}.lifecycle`);
|
|
236
429
|
const bindingRows = tables.includes("work_agent_bindings")
|
|
237
430
|
? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
238
431
|
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
239
|
-
FROM work_agent_bindings
|
|
432
|
+
FROM work_agent_bindings
|
|
433
|
+
WHERE dispatch_request_id = ?`).all(dispatch.dispatchRequestId))
|
|
240
434
|
: [];
|
|
241
|
-
if (runningAt === null && bindingRows.length === 0)
|
|
242
|
-
return;
|
|
243
435
|
const bindingRow = bindingRows[0];
|
|
244
|
-
if (
|
|
245
|
-
throw new WorkStoreError({ cause: { bindingRows, lifecycle }, operation });
|
|
246
|
-
}
|
|
247
|
-
const decoded = decodeAgentBindingRow(bindingRow, { dispatchRequestId, laneId: handoff.laneId }, operation);
|
|
248
|
-
if (decoded._tag === "invalid")
|
|
249
|
-
throw decoded.error;
|
|
250
|
-
if (decoded.binding.request.expectedRevision !== handoff.expectedRevision ||
|
|
251
|
-
decoded.binding.lane.goalId !== handoff.goalId ||
|
|
252
|
-
decoded.binding.checkpoint.occurredAt !== runningAt) {
|
|
253
|
-
throw new WorkStoreError({ cause: { binding: decoded.binding, handoff, runningAt }, operation });
|
|
254
|
-
}
|
|
255
|
-
requireBindingCompanions(decoded.binding, operation);
|
|
256
|
-
const currentLaneRows = Schema.decodeUnknownSync(Schema.Array(LaneRow))(database.prepare(`SELECT lane_id AS laneId, goal_id AS goalId, operation_id AS operationId,
|
|
257
|
-
phase, revision, record
|
|
258
|
-
FROM work_lane_claims WHERE lane_id = ? LIMIT 2`).all(decoded.binding.lane.laneId));
|
|
259
|
-
const currentLaneRow = currentLaneRows[0];
|
|
260
|
-
if (currentLaneRows.length !== 1 || currentLaneRow === undefined) {
|
|
436
|
+
if (bindingRows.length !== 1 || bindingRow === undefined) {
|
|
261
437
|
throw new WorkStoreError({
|
|
262
|
-
cause: {
|
|
263
|
-
operation:
|
|
438
|
+
cause: { bindingRows, dispatch, legacy },
|
|
439
|
+
operation: "open.migrate.legacy-handoff-revision"
|
|
264
440
|
});
|
|
265
441
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
442
|
+
const bindingDecision = decodeAgentBindingRow(bindingRow, { dispatchRequestId: dispatch.dispatchRequestId, laneId: legacy.laneId }, "open.migrate.legacy-agent-binding");
|
|
443
|
+
if (bindingDecision._tag === "invalid")
|
|
444
|
+
throw bindingDecision.error;
|
|
445
|
+
if (bindingDecision.binding.lane.goalId !== legacy.goalId) {
|
|
446
|
+
throw new WorkStoreError({
|
|
447
|
+
cause: { binding: bindingDecision.binding, legacy },
|
|
448
|
+
operation: "open.migrate.legacy-agent-binding.goal"
|
|
449
|
+
});
|
|
272
450
|
}
|
|
273
|
-
|
|
274
|
-
const currentOperationRows = currentLaneSchema
|
|
275
|
-
? Schema.decodeUnknownSync(Schema.Array(AgentBindingLaneOperationRow))(database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
276
|
-
phase, revision, record
|
|
277
|
-
FROM work_lane_operations WHERE operation_id = ? LIMIT 2`).all(currentLane.operationId))
|
|
278
|
-
: [];
|
|
279
|
-
const currentOperationError = currentLaneSchema
|
|
280
|
-
? laneOperationReadbackError(currentLane, currentOperationRows.length === 1 ? currentOperationRows[0] : undefined, `${operation}.lane-revision`)
|
|
281
|
-
: undefined;
|
|
282
|
-
if (currentLane.laneId !== currentLaneRow.laneId ||
|
|
283
|
-
currentLane.goalId !== currentLaneRow.goalId ||
|
|
284
|
-
currentLane.operationId !== currentLaneRow.operationId ||
|
|
285
|
-
currentLane.phase !== currentLaneRow.phase ||
|
|
286
|
-
currentLane.revision !== currentLaneRow.revision ||
|
|
287
|
-
currentLane.laneId !== decoded.binding.lane.laneId ||
|
|
288
|
-
currentLane.goalId !== decoded.binding.lane.goalId ||
|
|
289
|
-
currentLane.revision < decoded.binding.lane.revision ||
|
|
290
|
-
(currentLaneSchema && currentLane.revision === decoded.binding.lane.revision &&
|
|
291
|
-
!Equal.equals(currentLane, decoded.binding.lane)) ||
|
|
292
|
-
currentOperationError !== undefined) {
|
|
451
|
+
if (lane.revision < bindingDecision.binding.lane.revision) {
|
|
293
452
|
throw new WorkStoreError({
|
|
294
|
-
cause: { binding:
|
|
295
|
-
operation:
|
|
453
|
+
cause: { binding: bindingDecision.binding, lane, legacy },
|
|
454
|
+
operation: "open.migrate.legacy-handoff-lane-revision"
|
|
296
455
|
});
|
|
297
456
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
throw new WorkStoreError({
|
|
310
|
-
cause: { decisionIdentities, dispatch: orphanDispatch },
|
|
311
|
-
operation: "open.migrate.dispatch-decision"
|
|
457
|
+
requireBindingCompanions(bindingDecision.binding, "open.migrate.legacy-agent-binding");
|
|
458
|
+
requireLifecycleAuthority(bindingDecision.binding.request.dispatchRequestId, bindingDecision.binding.checkpoint.occurredAt, "open.migrate.legacy-agent-binding.lifecycle");
|
|
459
|
+
return Schema.decodeUnknownSync(WorkDecisionHandoff)({
|
|
460
|
+
...legacy,
|
|
461
|
+
contextDelta: legacy.summary,
|
|
462
|
+
expectedRevision: bindingDecision.binding.request.expectedRevision,
|
|
463
|
+
sessionId: legacy.id,
|
|
464
|
+
dispatchIds,
|
|
465
|
+
blockers: [],
|
|
466
|
+
evidenceRefs: [],
|
|
467
|
+
version: "herdr.work.decision.v2"
|
|
312
468
|
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
for (const metadata of linkedMetadata) {
|
|
334
|
-
metadataCounts.set(metadata.dispatchRequestId, (metadataCounts.get(metadata.dispatchRequestId) ?? 0) + 1);
|
|
335
|
-
}
|
|
469
|
+
})
|
|
470
|
+
: [];
|
|
471
|
+
if (laneColumns.length > 0 && !laneColumns.includes("goal_id")) {
|
|
472
|
+
database.exec("ALTER TABLE work_lane_claims ADD COLUMN goal_id TEXT");
|
|
473
|
+
database.exec("ALTER TABLE work_lane_claims ADD COLUMN operation_id TEXT");
|
|
474
|
+
database.exec("ALTER TABLE work_lane_claims ADD COLUMN phase TEXT");
|
|
475
|
+
const update = database.prepare("UPDATE work_lane_claims SET goal_id = ?, operation_id = ?, phase = ?, record = ? WHERE lane_id = ?");
|
|
476
|
+
for (const lane of lanes)
|
|
477
|
+
update.run(lane.goalId, lane.operationId, lane.phase, JSON.stringify(lane), lane.laneId);
|
|
478
|
+
}
|
|
479
|
+
if (decisionColumns.length > 0 && !decisionColumns.includes("session_id")) {
|
|
480
|
+
database.exec("ALTER TABLE work_decision_handoffs ADD COLUMN session_id TEXT");
|
|
481
|
+
const update = database.prepare("UPDATE work_decision_handoffs SET session_id = ?, record = ? WHERE handoff_id = ?");
|
|
482
|
+
for (const decision of decisions)
|
|
483
|
+
update.run(decision.sessionId, JSON.stringify(decision), decision.id);
|
|
484
|
+
if (dispatches.length > 0) {
|
|
485
|
+
const updateDispatch = database.prepare("UPDATE work_dispatch_handoffs SET record = ? WHERE dispatch_request_id = ?");
|
|
486
|
+
const updateMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
487
|
+
? database.prepare("UPDATE orchestrator_dispatch_metadata SET work_link = ? WHERE dispatch_request_id = ?")
|
|
488
|
+
: null;
|
|
336
489
|
for (const dispatch of dispatches) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
const invalidMetadata = linkedMetadata.find((metadata) => {
|
|
344
|
-
if (metadata.route === null)
|
|
345
|
-
return true;
|
|
346
|
-
const requiresWorkLink = coordinatorRouteRequiresWorkLink(metadata.route, routeStorageAuthority(metadata.dispatchRequestId, "open.migrate.metadata-decision"), "open.migrate.metadata-decision");
|
|
347
|
-
if (!requiresWorkLink)
|
|
348
|
-
return metadata.hasWorkLink === 1;
|
|
349
|
-
const dispatchIdentity = JSON.stringify([metadata.dispatchRequestId, metadata.handoffId]);
|
|
350
|
-
return (metadata.handoffId === null || metadataCounts.get(metadata.dispatchRequestId) !== 1 ||
|
|
351
|
-
dispatchCounts.get(dispatchIdentity) !== 1 || decisionCounts.get(metadata.handoffId) !== 1);
|
|
352
|
-
});
|
|
353
|
-
if (invalidMetadata !== undefined) {
|
|
354
|
-
throw new WorkStoreError({
|
|
355
|
-
cause: { decisionIdentities, dispatches, linkedMetadata, metadata: invalidMetadata },
|
|
356
|
-
operation: "open.migrate.metadata-decision"
|
|
357
|
-
});
|
|
490
|
+
const decision = decisions.find(({ id }) => id === dispatch.handoffId);
|
|
491
|
+
if (decision === undefined)
|
|
492
|
+
continue;
|
|
493
|
+
updateDispatch.run(JSON.stringify(decision), dispatch.dispatchRequestId);
|
|
494
|
+
updateMetadata?.run(JSON.stringify({ handoff: decision, lineage: decision.dispatchIds }), dispatch.dispatchRequestId);
|
|
358
495
|
}
|
|
359
496
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
:
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const dispatchIds = dispatch === undefined
|
|
386
|
-
? []
|
|
387
|
-
: Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage));
|
|
388
|
-
if (matchingDispatches.length !== 1 || dispatch === undefined) {
|
|
389
|
-
throw new WorkStoreError({
|
|
390
|
-
cause: { legacy, matchingDispatches },
|
|
391
|
-
operation: "open.migrate.legacy-dispatch-cardinality"
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
const dispatchHandoff = Schema.decodeUnknownSync(LegacyDecisionRecord)(JSON.parse(dispatch.record));
|
|
395
|
-
if (dispatch.laneId !== legacy.laneId || dispatch.occurredAt !== legacy.occurredAt ||
|
|
396
|
-
!legacyDecisionEquivalent(dispatchHandoff, legacy)) {
|
|
397
|
-
throw new WorkStoreError({
|
|
398
|
-
cause: { dispatch, dispatchHandoff, legacy },
|
|
399
|
-
operation: "open.migrate.legacy-dispatch-authority"
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
if (tables.includes("orchestrator_dispatch_metadata")) {
|
|
403
|
-
const metadataRows = Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
404
|
-
FROM orchestrator_dispatch_metadata WHERE dispatch_request_id = ? LIMIT 2`).all(dispatch.dispatchRequestId));
|
|
405
|
-
const metadata = metadataRows[0];
|
|
406
|
-
if (metadataRows.length !== 1 || metadata === undefined) {
|
|
497
|
+
}
|
|
498
|
+
if (decisionColumns.length > 0) {
|
|
499
|
+
const storedDecisions = Schema.decodeUnknownSync(Schema.Array(DecisionRow))(database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
500
|
+
occurred_at AS occurredAt, record
|
|
501
|
+
FROM work_decision_handoffs`).all());
|
|
502
|
+
const updateDecision = database.prepare("UPDATE work_decision_handoffs SET record = ? WHERE handoff_id = ?");
|
|
503
|
+
const updateDispatch = tables.includes("work_dispatch_handoffs")
|
|
504
|
+
? database.prepare("UPDATE work_dispatch_handoffs SET record = ? WHERE handoff_id = ?")
|
|
505
|
+
: null;
|
|
506
|
+
const updateMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
507
|
+
? database.prepare(`UPDATE orchestrator_dispatch_metadata SET work_link = ?
|
|
508
|
+
WHERE dispatch_request_id = ?`)
|
|
509
|
+
: null;
|
|
510
|
+
const previousDecisions = storedDecisions.flatMap((row) => {
|
|
511
|
+
const input = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(row.record);
|
|
512
|
+
const previous = decodePreviousDecisionHandoff(input);
|
|
513
|
+
const handoffDispatches = tables.includes("work_dispatch_handoffs")
|
|
514
|
+
? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
|
|
515
|
+
lane_id AS laneId, occurred_at AS occurredAt, lineage, record
|
|
516
|
+
FROM work_dispatch_handoffs WHERE handoff_id = ?`).all(row.handoffId))
|
|
517
|
+
: [];
|
|
518
|
+
switch (previous._tag) {
|
|
519
|
+
case "current": {
|
|
520
|
+
if (previous.value.id !== row.handoffId || previous.value.sessionId !== row.sessionId ||
|
|
521
|
+
previous.value.laneId !== row.laneId || previous.value.occurredAt !== row.occurredAt) {
|
|
407
522
|
throw new WorkStoreError({
|
|
408
|
-
cause: {
|
|
409
|
-
operation: "open.migrate.
|
|
523
|
+
cause: { handoff: previous.value, row },
|
|
524
|
+
operation: "open.migrate.handoff-identity"
|
|
410
525
|
});
|
|
411
526
|
}
|
|
412
|
-
if (
|
|
527
|
+
if (handoffDispatches.length > 1) {
|
|
413
528
|
throw new WorkStoreError({
|
|
414
|
-
cause: {
|
|
415
|
-
operation: "open.migrate.
|
|
529
|
+
cause: { handoffDispatches, row },
|
|
530
|
+
operation: "open.migrate.dispatch-cardinality"
|
|
416
531
|
});
|
|
417
532
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
533
|
+
const linkedMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
534
|
+
? Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
535
|
+
FROM orchestrator_dispatch_metadata
|
|
536
|
+
WHERE work_link IS NOT NULL
|
|
537
|
+
AND CASE WHEN json_valid(work_link)
|
|
538
|
+
THEN json_extract(work_link, '$.handoff.id') END = ?`).all(row.handoffId))
|
|
539
|
+
: [];
|
|
540
|
+
if (tables.includes("orchestrator_dispatch_metadata") &&
|
|
541
|
+
(linkedMetadata.length !== handoffDispatches.length ||
|
|
542
|
+
linkedMetadata.some((metadata) => !handoffDispatches.some(({ dispatchRequestId }) => dispatchRequestId === metadata.dispatchRequestId)))) {
|
|
422
543
|
throw new WorkStoreError({
|
|
423
|
-
cause: {
|
|
424
|
-
operation: "open.migrate.
|
|
544
|
+
cause: { handoffDispatches, linkedMetadata, row },
|
|
545
|
+
operation: "open.migrate.metadata-authority"
|
|
425
546
|
});
|
|
426
547
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const bindingRow = bindingRows[0];
|
|
435
|
-
if (bindingRows.length !== 1 || bindingRow === undefined) {
|
|
436
|
-
throw new WorkStoreError({
|
|
437
|
-
cause: { bindingRows, dispatch, legacy },
|
|
438
|
-
operation: "open.migrate.legacy-handoff-revision"
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
const bindingDecision = decodeAgentBindingRow(bindingRow, { dispatchRequestId: dispatch.dispatchRequestId, laneId: legacy.laneId }, "open.migrate.legacy-agent-binding");
|
|
442
|
-
if (bindingDecision._tag === "invalid")
|
|
443
|
-
throw bindingDecision.error;
|
|
444
|
-
if (bindingDecision.binding.lane.goalId !== legacy.goalId) {
|
|
445
|
-
throw new WorkStoreError({
|
|
446
|
-
cause: { binding: bindingDecision.binding, legacy },
|
|
447
|
-
operation: "open.migrate.legacy-agent-binding.goal"
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
if (lane.revision < bindingDecision.binding.lane.revision) {
|
|
451
|
-
throw new WorkStoreError({
|
|
452
|
-
cause: { binding: bindingDecision.binding, lane, legacy },
|
|
453
|
-
operation: "open.migrate.legacy-handoff-lane-revision"
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
requireBindingCompanions(bindingDecision.binding, "open.migrate.legacy-agent-binding");
|
|
457
|
-
requireLifecycleAuthority(bindingDecision.binding.request.dispatchRequestId, bindingDecision.binding.checkpoint.occurredAt, "open.migrate.legacy-agent-binding.lifecycle");
|
|
458
|
-
return Schema.decodeUnknownSync(WorkDecisionHandoff)({
|
|
459
|
-
...legacy,
|
|
460
|
-
contextDelta: legacy.summary,
|
|
461
|
-
expectedRevision: bindingDecision.binding.request.expectedRevision,
|
|
462
|
-
sessionId: legacy.id,
|
|
463
|
-
dispatchIds,
|
|
464
|
-
blockers: [],
|
|
465
|
-
evidenceRefs: [],
|
|
466
|
-
version: "herdr.work.decision.v2"
|
|
467
|
-
});
|
|
468
|
-
})
|
|
469
|
-
: [];
|
|
470
|
-
if (laneColumns.length > 0 && !laneColumns.includes("goal_id")) {
|
|
471
|
-
database.exec("ALTER TABLE work_lane_claims ADD COLUMN goal_id TEXT");
|
|
472
|
-
database.exec("ALTER TABLE work_lane_claims ADD COLUMN operation_id TEXT");
|
|
473
|
-
database.exec("ALTER TABLE work_lane_claims ADD COLUMN phase TEXT");
|
|
474
|
-
const update = database.prepare("UPDATE work_lane_claims SET goal_id = ?, operation_id = ?, phase = ?, record = ? WHERE lane_id = ?");
|
|
475
|
-
for (const lane of lanes)
|
|
476
|
-
update.run(lane.goalId, lane.operationId, lane.phase, JSON.stringify(lane), lane.laneId);
|
|
477
|
-
}
|
|
478
|
-
if (decisionColumns.length > 0 && !decisionColumns.includes("session_id")) {
|
|
479
|
-
database.exec("ALTER TABLE work_decision_handoffs ADD COLUMN session_id TEXT");
|
|
480
|
-
const update = database.prepare("UPDATE work_decision_handoffs SET session_id = ?, record = ? WHERE handoff_id = ?");
|
|
481
|
-
for (const decision of decisions)
|
|
482
|
-
update.run(decision.sessionId, JSON.stringify(decision), decision.id);
|
|
483
|
-
if (dispatches.length > 0) {
|
|
484
|
-
const updateDispatch = database.prepare("UPDATE work_dispatch_handoffs SET record = ? WHERE dispatch_request_id = ?");
|
|
485
|
-
const updateMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
486
|
-
? database.prepare("UPDATE orchestrator_dispatch_metadata SET work_link = ? WHERE dispatch_request_id = ?")
|
|
487
|
-
: null;
|
|
488
|
-
for (const dispatch of dispatches) {
|
|
489
|
-
const decision = decisions.find(({ id }) => id === dispatch.handoffId);
|
|
490
|
-
if (decision === undefined)
|
|
491
|
-
continue;
|
|
492
|
-
updateDispatch.run(JSON.stringify(decision), dispatch.dispatchRequestId);
|
|
493
|
-
updateMetadata?.run(JSON.stringify({ handoff: decision, lineage: decision.dispatchIds }), dispatch.dispatchRequestId);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
if (decisionColumns.length > 0) {
|
|
498
|
-
const storedDecisions = Schema.decodeUnknownSync(Schema.Array(DecisionRow))(database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
499
|
-
occurred_at AS occurredAt, record
|
|
500
|
-
FROM work_decision_handoffs`).all());
|
|
501
|
-
const updateDecision = database.prepare("UPDATE work_decision_handoffs SET record = ? WHERE handoff_id = ?");
|
|
502
|
-
const updateDispatch = tables.includes("work_dispatch_handoffs")
|
|
503
|
-
? database.prepare("UPDATE work_dispatch_handoffs SET record = ? WHERE handoff_id = ?")
|
|
504
|
-
: null;
|
|
505
|
-
const updateMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
506
|
-
? database.prepare(`UPDATE orchestrator_dispatch_metadata SET work_link = ?
|
|
507
|
-
WHERE dispatch_request_id = ?`)
|
|
508
|
-
: null;
|
|
509
|
-
const previousDecisions = storedDecisions.flatMap((row) => {
|
|
510
|
-
const input = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(row.record);
|
|
511
|
-
const previous = decodePreviousDecisionHandoff(input);
|
|
512
|
-
const handoffDispatches = tables.includes("work_dispatch_handoffs")
|
|
513
|
-
? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
|
|
514
|
-
lane_id AS laneId, occurred_at AS occurredAt, lineage, record
|
|
515
|
-
FROM work_dispatch_handoffs WHERE handoff_id = ?`).all(row.handoffId))
|
|
516
|
-
: [];
|
|
517
|
-
switch (previous._tag) {
|
|
518
|
-
case "current": {
|
|
519
|
-
if (previous.value.id !== row.handoffId || previous.value.sessionId !== row.sessionId ||
|
|
520
|
-
previous.value.laneId !== row.laneId || previous.value.occurredAt !== row.occurredAt) {
|
|
521
|
-
throw new WorkStoreError({
|
|
522
|
-
cause: { handoff: previous.value, row },
|
|
523
|
-
operation: "open.migrate.handoff-identity"
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
if (handoffDispatches.length > 1) {
|
|
527
|
-
throw new WorkStoreError({
|
|
528
|
-
cause: { handoffDispatches, row },
|
|
529
|
-
operation: "open.migrate.dispatch-cardinality"
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
const linkedMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
533
|
-
? Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
534
|
-
FROM orchestrator_dispatch_metadata
|
|
535
|
-
WHERE work_link IS NOT NULL
|
|
536
|
-
AND CASE WHEN json_valid(work_link)
|
|
537
|
-
THEN json_extract(work_link, '$.handoff.id') END = ?`).all(row.handoffId))
|
|
538
|
-
: [];
|
|
539
|
-
if (tables.includes("orchestrator_dispatch_metadata") &&
|
|
540
|
-
(linkedMetadata.length !== handoffDispatches.length ||
|
|
541
|
-
linkedMetadata.some((metadata) => !handoffDispatches.some(({ dispatchRequestId }) => dispatchRequestId === metadata.dispatchRequestId)))) {
|
|
542
|
-
throw new WorkStoreError({
|
|
543
|
-
cause: { handoffDispatches, linkedMetadata, row },
|
|
544
|
-
operation: "open.migrate.metadata-authority"
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
for (const dispatch of handoffDispatches) {
|
|
548
|
-
const dispatchAuthority = (() => {
|
|
549
|
-
try {
|
|
550
|
-
return {
|
|
551
|
-
handoff: Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(dispatch.record)),
|
|
552
|
-
lineage: Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage))
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
catch (cause) {
|
|
556
|
-
throw new WorkStoreError({ cause, operation: "open.migrate.decode-dispatch-authority" });
|
|
557
|
-
}
|
|
558
|
-
})();
|
|
559
|
-
if (dispatch.laneId !== previous.value.laneId ||
|
|
560
|
-
dispatch.occurredAt !== previous.value.occurredAt ||
|
|
561
|
-
!currentDecisionHandoffEquivalent(dispatchAuthority.handoff, previous.value) ||
|
|
562
|
-
!workDispatchLineageContainedBy(dispatchAuthority.lineage, previous.value.dispatchIds)) {
|
|
563
|
-
throw new WorkStoreError({
|
|
564
|
-
cause: { dispatch, dispatchAuthority, handoff: previous.value },
|
|
565
|
-
operation: "open.migrate.dispatch-authority"
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
if (!tables.includes("orchestrator_dispatch_metadata"))
|
|
569
|
-
continue;
|
|
570
|
-
const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
|
|
571
|
-
if (metadata?.workLink === null || metadata?.workLink === undefined) {
|
|
572
|
-
throw new WorkStoreError({
|
|
573
|
-
cause: { dispatch, metadata },
|
|
574
|
-
operation: "open.migrate.metadata-authority"
|
|
575
|
-
});
|
|
548
|
+
for (const dispatch of handoffDispatches) {
|
|
549
|
+
const dispatchAuthority = (() => {
|
|
550
|
+
try {
|
|
551
|
+
return {
|
|
552
|
+
handoff: Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(dispatch.record)),
|
|
553
|
+
lineage: Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage))
|
|
554
|
+
};
|
|
576
555
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return Schema.decodeUnknownSync(CurrentMetadataWorkLink)(JSON.parse(metadata.workLink));
|
|
580
|
-
}
|
|
581
|
-
catch (cause) {
|
|
582
|
-
throw new WorkStoreError({ cause, operation: "open.migrate.decode-metadata-authority" });
|
|
583
|
-
}
|
|
584
|
-
})();
|
|
585
|
-
requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.metadata-authority");
|
|
586
|
-
if (!currentDecisionHandoffEquivalent(workLink.handoff, previous.value) ||
|
|
587
|
-
!workDispatchLineageEquivalent(workLink.lineage, dispatchAuthority.lineage)) {
|
|
588
|
-
throw new WorkStoreError({
|
|
589
|
-
cause: { handoff: previous.value, workLink },
|
|
590
|
-
operation: "open.migrate.metadata-authority"
|
|
591
|
-
});
|
|
556
|
+
catch (cause) {
|
|
557
|
+
throw new WorkStoreError({ cause, operation: "open.migrate.decode-dispatch-authority" });
|
|
592
558
|
}
|
|
593
|
-
|
|
559
|
+
})();
|
|
560
|
+
if (dispatch.laneId !== previous.value.laneId ||
|
|
561
|
+
dispatch.occurredAt !== previous.value.occurredAt ||
|
|
562
|
+
!currentDecisionHandoffEquivalent(dispatchAuthority.handoff, previous.value) ||
|
|
563
|
+
!workDispatchLineageContainedBy(dispatchAuthority.lineage, previous.value.dispatchIds)) {
|
|
564
|
+
throw new WorkStoreError({
|
|
565
|
+
cause: { dispatch, dispatchAuthority, handoff: previous.value },
|
|
566
|
+
operation: "open.migrate.dispatch-authority"
|
|
567
|
+
});
|
|
594
568
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
case "invalid":
|
|
598
|
-
throw new WorkStoreError({
|
|
599
|
-
cause: previous.cause,
|
|
600
|
-
operation: "open.migrate.invalid-handoff"
|
|
601
|
-
});
|
|
602
|
-
case "previous":
|
|
603
|
-
return [{ previous: previous.value, row }];
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
for (const { previous, row } of previousDecisions) {
|
|
607
|
-
const laneRevision = Schema.decodeUnknownSync(LaneRevisionRow)(database.prepare("SELECT revision FROM work_lane_claims WHERE lane_id = ?").get(row.laneId));
|
|
608
|
-
const handoffDispatches = tables.includes("work_dispatch_handoffs")
|
|
609
|
-
? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
|
|
610
|
-
lane_id AS laneId, occurred_at AS occurredAt, lineage, record
|
|
611
|
-
FROM work_dispatch_handoffs WHERE handoff_id = ?`).all(row.handoffId))
|
|
612
|
-
: [];
|
|
613
|
-
if (handoffDispatches.length > 1) {
|
|
614
|
-
throw new WorkStoreError({
|
|
615
|
-
cause: { handoffDispatches, row },
|
|
616
|
-
operation: "open.migrate.dispatch-cardinality"
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
const linkedMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
620
|
-
? Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
621
|
-
FROM orchestrator_dispatch_metadata
|
|
622
|
-
WHERE work_link IS NOT NULL
|
|
623
|
-
AND CASE WHEN json_valid(work_link)
|
|
624
|
-
THEN json_extract(work_link, '$.handoff.id') END = ?`).all(row.handoffId))
|
|
625
|
-
: [];
|
|
626
|
-
if (tables.includes("orchestrator_dispatch_metadata") &&
|
|
627
|
-
(linkedMetadata.length !== handoffDispatches.length ||
|
|
628
|
-
linkedMetadata.some((metadata) => !handoffDispatches.some(({ dispatchRequestId }) => dispatchRequestId === metadata.dispatchRequestId)))) {
|
|
629
|
-
throw new WorkStoreError({
|
|
630
|
-
cause: { handoffDispatches, linkedMetadata, row },
|
|
631
|
-
operation: "open.migrate.metadata-authority"
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
const bindingRows = tables.includes("work_agent_bindings") && tables.includes("work_dispatch_handoffs")
|
|
635
|
-
? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT binding.dispatch_request_id AS dispatchRequestId, binding.lane_id AS laneId,
|
|
636
|
-
binding.expected_revision AS expectedRevision, binding.revision,
|
|
637
|
-
binding.agent_id AS agentId, binding.host, binding.record
|
|
638
|
-
FROM work_agent_bindings binding
|
|
639
|
-
JOIN work_dispatch_handoffs dispatch
|
|
640
|
-
ON dispatch.dispatch_request_id = binding.dispatch_request_id
|
|
641
|
-
WHERE dispatch.handoff_id = ?`).all(row.handoffId))
|
|
642
|
-
: [];
|
|
643
|
-
const bindingRow = bindingRows[0];
|
|
644
|
-
const handoffDispatch = bindingRow === undefined
|
|
645
|
-
? undefined
|
|
646
|
-
: handoffDispatches.find(({ dispatchRequestId }) => dispatchRequestId === bindingRow.dispatchRequestId);
|
|
647
|
-
if (bindingRows.length !== 1 || bindingRow === undefined || handoffDispatch === undefined) {
|
|
648
|
-
throw new WorkStoreError({ cause: { bindingRows, row }, operation: "open.migrate.handoff-revision" });
|
|
649
|
-
}
|
|
650
|
-
const bindingDecision = decodeAgentBindingRow(bindingRow, { dispatchRequestId: handoffDispatch.dispatchRequestId, laneId: previous.laneId }, "open.migrate.agent-binding");
|
|
651
|
-
if (bindingDecision._tag === "invalid")
|
|
652
|
-
throw bindingDecision.error;
|
|
653
|
-
if (laneRevision.revision < bindingDecision.binding.lane.revision) {
|
|
654
|
-
throw new WorkStoreError({
|
|
655
|
-
cause: { binding: bindingDecision.binding, laneRevision, previous },
|
|
656
|
-
operation: "open.migrate.handoff-lane-revision"
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
if (bindingDecision.binding.lane.goalId !== previous.goalId) {
|
|
660
|
-
throw new WorkStoreError({
|
|
661
|
-
cause: { binding: bindingDecision.binding, previous },
|
|
662
|
-
operation: "open.migrate.agent-binding.goal"
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
requireBindingCompanions(bindingDecision.binding, "open.migrate.agent-binding");
|
|
666
|
-
requireLifecycleAuthority(bindingDecision.binding.request.dispatchRequestId, bindingDecision.binding.checkpoint.occurredAt, "open.migrate.agent-binding.lifecycle");
|
|
667
|
-
const verifiedDispatches = handoffDispatches.map((dispatch) => {
|
|
668
|
-
const dispatchHandoff = Schema.decodeUnknownSync(PreviousWorkDecisionHandoff)(JSON.parse(dispatch.record));
|
|
669
|
-
const lineage = Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage));
|
|
670
|
-
if (dispatch.laneId !== previous.laneId || dispatch.occurredAt !== previous.occurredAt ||
|
|
671
|
-
!previousDecisionHandoffEquivalent(dispatchHandoff, previous) ||
|
|
672
|
-
!workDispatchLineageContainedBy(lineage, previous.dispatchIds)) {
|
|
673
|
-
throw new WorkStoreError({
|
|
674
|
-
cause: { dispatch, dispatchHandoff, previous },
|
|
675
|
-
operation: "open.migrate.dispatch-authority"
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
if (tables.includes("orchestrator_dispatch_metadata")) {
|
|
569
|
+
if (!tables.includes("orchestrator_dispatch_metadata"))
|
|
570
|
+
continue;
|
|
679
571
|
const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
|
|
680
572
|
if (metadata?.workLink === null || metadata?.workLink === undefined) {
|
|
681
573
|
throw new WorkStoreError({
|
|
@@ -683,116 +575,217 @@ const migrateLegacyAuthorityTables = (database) => {
|
|
|
683
575
|
operation: "open.migrate.metadata-authority"
|
|
684
576
|
});
|
|
685
577
|
}
|
|
686
|
-
const workLink =
|
|
578
|
+
const workLink = (() => {
|
|
579
|
+
try {
|
|
580
|
+
return Schema.decodeUnknownSync(CurrentMetadataWorkLink)(JSON.parse(metadata.workLink));
|
|
581
|
+
}
|
|
582
|
+
catch (cause) {
|
|
583
|
+
throw new WorkStoreError({ cause, operation: "open.migrate.decode-metadata-authority" });
|
|
584
|
+
}
|
|
585
|
+
})();
|
|
687
586
|
requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.metadata-authority");
|
|
688
|
-
if (!
|
|
689
|
-
!workDispatchLineageEquivalent(workLink.lineage, lineage)) {
|
|
587
|
+
if (!currentDecisionHandoffEquivalent(workLink.handoff, previous.value) ||
|
|
588
|
+
!workDispatchLineageEquivalent(workLink.lineage, dispatchAuthority.lineage)) {
|
|
690
589
|
throw new WorkStoreError({
|
|
691
|
-
cause: { previous, workLink },
|
|
590
|
+
cause: { handoff: previous.value, workLink },
|
|
692
591
|
operation: "open.migrate.metadata-authority"
|
|
693
592
|
});
|
|
694
593
|
}
|
|
594
|
+
requireCurrentAgentBindingAuthority(dispatch.dispatchRequestId, previous.value, "open.migrate.current-agent-binding");
|
|
695
595
|
}
|
|
696
|
-
return
|
|
697
|
-
});
|
|
698
|
-
const migrated = upgradePreviousDecisionHandoff(previous, bindingDecision.binding.request.expectedRevision);
|
|
699
|
-
if (migrated.id !== row.handoffId || migrated.sessionId !== row.sessionId ||
|
|
700
|
-
migrated.laneId !== row.laneId || migrated.occurredAt !== row.occurredAt) {
|
|
701
|
-
throw new WorkStoreError({ cause: { migrated, row }, operation: "open.migrate.handoff-identity" });
|
|
702
|
-
}
|
|
703
|
-
const encoded = JSON.stringify(migrated);
|
|
704
|
-
updateDecision.run(encoded, row.handoffId);
|
|
705
|
-
updateDispatch?.run(encoded, row.handoffId);
|
|
706
|
-
for (const { dispatch, lineage } of verifiedDispatches) {
|
|
707
|
-
updateMetadata?.run(JSON.stringify({ handoff: migrated, lineage }), dispatch.dispatchRequestId);
|
|
596
|
+
return [];
|
|
708
597
|
}
|
|
598
|
+
case "invalid":
|
|
599
|
+
throw new WorkStoreError({
|
|
600
|
+
cause: previous.cause,
|
|
601
|
+
operation: "open.migrate.invalid-handoff"
|
|
602
|
+
});
|
|
603
|
+
case "previous":
|
|
604
|
+
return [{ previous: previous.value, row }];
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
for (const { previous, row } of previousDecisions) {
|
|
608
|
+
const laneRevision = Schema.decodeUnknownSync(LaneRevisionRow)(database.prepare("SELECT revision FROM work_lane_claims WHERE lane_id = ?").get(row.laneId));
|
|
609
|
+
const handoffDispatches = tables.includes("work_dispatch_handoffs")
|
|
610
|
+
? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
|
|
611
|
+
lane_id AS laneId, occurred_at AS occurredAt, lineage, record
|
|
612
|
+
FROM work_dispatch_handoffs WHERE handoff_id = ?`).all(row.handoffId))
|
|
613
|
+
: [];
|
|
614
|
+
if (handoffDispatches.length > 1) {
|
|
615
|
+
throw new WorkStoreError({
|
|
616
|
+
cause: { handoffDispatches, row },
|
|
617
|
+
operation: "open.migrate.dispatch-cardinality"
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
const linkedMetadata = tables.includes("orchestrator_dispatch_metadata")
|
|
621
|
+
? Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
|
|
622
|
+
FROM orchestrator_dispatch_metadata
|
|
623
|
+
WHERE work_link IS NOT NULL
|
|
624
|
+
AND CASE WHEN json_valid(work_link)
|
|
625
|
+
THEN json_extract(work_link, '$.handoff.id') END = ?`).all(row.handoffId))
|
|
626
|
+
: [];
|
|
627
|
+
if (tables.includes("orchestrator_dispatch_metadata") &&
|
|
628
|
+
(linkedMetadata.length !== handoffDispatches.length ||
|
|
629
|
+
linkedMetadata.some((metadata) => !handoffDispatches.some(({ dispatchRequestId }) => dispatchRequestId === metadata.dispatchRequestId)))) {
|
|
630
|
+
throw new WorkStoreError({
|
|
631
|
+
cause: { handoffDispatches, linkedMetadata, row },
|
|
632
|
+
operation: "open.migrate.metadata-authority"
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const bindingRows = tables.includes("work_agent_bindings") && tables.includes("work_dispatch_handoffs")
|
|
636
|
+
? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT binding.dispatch_request_id AS dispatchRequestId, binding.lane_id AS laneId,
|
|
637
|
+
binding.expected_revision AS expectedRevision, binding.revision,
|
|
638
|
+
binding.agent_id AS agentId, binding.host, binding.record
|
|
639
|
+
FROM work_agent_bindings binding
|
|
640
|
+
JOIN work_dispatch_handoffs dispatch
|
|
641
|
+
ON dispatch.dispatch_request_id = binding.dispatch_request_id
|
|
642
|
+
WHERE dispatch.handoff_id = ?`).all(row.handoffId))
|
|
643
|
+
: [];
|
|
644
|
+
const bindingRow = bindingRows[0];
|
|
645
|
+
const handoffDispatch = bindingRow === undefined
|
|
646
|
+
? undefined
|
|
647
|
+
: handoffDispatches.find(({ dispatchRequestId }) => dispatchRequestId === bindingRow.dispatchRequestId);
|
|
648
|
+
if (bindingRows.length !== 1 || bindingRow === undefined || handoffDispatch === undefined) {
|
|
649
|
+
throw new WorkStoreError({ cause: { bindingRows, row }, operation: "open.migrate.handoff-revision" });
|
|
650
|
+
}
|
|
651
|
+
const bindingDecision = decodeAgentBindingRow(bindingRow, { dispatchRequestId: handoffDispatch.dispatchRequestId, laneId: previous.laneId }, "open.migrate.agent-binding");
|
|
652
|
+
if (bindingDecision._tag === "invalid")
|
|
653
|
+
throw bindingDecision.error;
|
|
654
|
+
if (laneRevision.revision < bindingDecision.binding.lane.revision) {
|
|
655
|
+
throw new WorkStoreError({
|
|
656
|
+
cause: { binding: bindingDecision.binding, laneRevision, previous },
|
|
657
|
+
operation: "open.migrate.handoff-lane-revision"
|
|
658
|
+
});
|
|
709
659
|
}
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
660
|
+
if (bindingDecision.binding.lane.goalId !== previous.goalId) {
|
|
661
|
+
throw new WorkStoreError({
|
|
662
|
+
cause: { binding: bindingDecision.binding, previous },
|
|
663
|
+
operation: "open.migrate.agent-binding.goal"
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
requireBindingCompanions(bindingDecision.binding, "open.migrate.agent-binding");
|
|
667
|
+
requireLifecycleAuthority(bindingDecision.binding.request.dispatchRequestId, bindingDecision.binding.checkpoint.occurredAt, "open.migrate.agent-binding.lifecycle");
|
|
668
|
+
const verifiedDispatches = handoffDispatches.map((dispatch) => {
|
|
669
|
+
const dispatchHandoff = Schema.decodeUnknownSync(PreviousWorkDecisionHandoff)(JSON.parse(dispatch.record));
|
|
670
|
+
const lineage = Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage));
|
|
671
|
+
if (dispatch.laneId !== previous.laneId || dispatch.occurredAt !== previous.occurredAt ||
|
|
672
|
+
!previousDecisionHandoffEquivalent(dispatchHandoff, previous) ||
|
|
673
|
+
!workDispatchLineageContainedBy(lineage, previous.dispatchIds)) {
|
|
716
674
|
throw new WorkStoreError({
|
|
717
|
-
cause:
|
|
718
|
-
operation: "open.migrate.
|
|
675
|
+
cause: { dispatch, dispatchHandoff, previous },
|
|
676
|
+
operation: "open.migrate.dispatch-authority"
|
|
719
677
|
});
|
|
720
678
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
? database.prepare(`SELECT CAST(metadata.rowid AS TEXT) AS rowId,
|
|
729
|
-
metadata.dispatch_request_id AS dispatchRequestId,
|
|
730
|
-
dispatch.activity_idempotency_key AS activityIdempotencyKey,
|
|
731
|
-
dispatch.command, dispatch.is_routed AS isRouted,
|
|
732
|
-
CASE WHEN metadata.work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
|
|
733
|
-
metadata.route
|
|
734
|
-
FROM orchestrator_dispatch_metadata AS metadata
|
|
735
|
-
LEFT JOIN orchestrator_dispatches AS dispatch
|
|
736
|
-
ON dispatch.dispatch_request_id = metadata.dispatch_request_id
|
|
737
|
-
ORDER BY metadata.rowid ASC LIMIT ?`).all(pageSize)
|
|
738
|
-
: database.prepare(`SELECT CAST(metadata.rowid AS TEXT) AS rowId,
|
|
739
|
-
metadata.dispatch_request_id AS dispatchRequestId,
|
|
740
|
-
dispatch.activity_idempotency_key AS activityIdempotencyKey,
|
|
741
|
-
dispatch.command, dispatch.is_routed AS isRouted,
|
|
742
|
-
CASE WHEN metadata.work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
|
|
743
|
-
metadata.route
|
|
744
|
-
FROM orchestrator_dispatch_metadata AS metadata
|
|
745
|
-
LEFT JOIN orchestrator_dispatches AS dispatch
|
|
746
|
-
ON dispatch.dispatch_request_id = metadata.dispatch_request_id
|
|
747
|
-
WHERE metadata.rowid > CAST(? AS INTEGER)
|
|
748
|
-
ORDER BY metadata.rowid ASC LIMIT ?`).all(cursor, pageSize);
|
|
749
|
-
const rows = Schema.decodeUnknownSync(Schema.Array(CoordinatorMetadataRouteRow))(input);
|
|
750
|
-
for (const row of rows) {
|
|
751
|
-
try {
|
|
752
|
-
const isLegacyUnroutedOrphan = row.activityIdempotencyKey === null && row.command === null &&
|
|
753
|
-
row.isRouted === null && row.route === null && row.hasWorkLink === 0;
|
|
754
|
-
if (isLegacyUnroutedOrphan)
|
|
755
|
-
continue;
|
|
756
|
-
if (row.activityIdempotencyKey === null || row.command === null ||
|
|
757
|
-
row.route === null || row.isRouted === null) {
|
|
758
|
-
throw new WorkStoreError({ cause: row, operation: "open.migrate.metadata-route" });
|
|
759
|
-
}
|
|
760
|
-
requireCoordinatorRouteBinding(row.command, row.activityIdempotencyKey, row.route, row.hasWorkLink === 1, { _tag: "routed_discriminator", isRouted: row.isRouted }, "open.migrate.metadata-route");
|
|
679
|
+
if (tables.includes("orchestrator_dispatch_metadata")) {
|
|
680
|
+
const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
|
|
681
|
+
if (metadata?.workLink === null || metadata?.workLink === undefined) {
|
|
682
|
+
throw new WorkStoreError({
|
|
683
|
+
cause: { dispatch, metadata },
|
|
684
|
+
operation: "open.migrate.metadata-authority"
|
|
685
|
+
});
|
|
761
686
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
687
|
+
const workLink = Schema.decodeUnknownSync(PreviousMetadataWorkLink)(JSON.parse(metadata.workLink));
|
|
688
|
+
requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.metadata-authority");
|
|
689
|
+
if (!previousDecisionHandoffEquivalent(workLink.handoff, previous) ||
|
|
690
|
+
!workDispatchLineageEquivalent(workLink.lineage, lineage)) {
|
|
691
|
+
throw new WorkStoreError({
|
|
692
|
+
cause: { previous, workLink },
|
|
693
|
+
operation: "open.migrate.metadata-authority"
|
|
694
|
+
});
|
|
766
695
|
}
|
|
767
696
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
697
|
+
return { dispatch, lineage };
|
|
698
|
+
});
|
|
699
|
+
const migrated = upgradePreviousDecisionHandoff(previous, bindingDecision.binding.request.expectedRevision);
|
|
700
|
+
if (migrated.id !== row.handoffId || migrated.sessionId !== row.sessionId ||
|
|
701
|
+
migrated.laneId !== row.laneId || migrated.occurredAt !== row.occurredAt) {
|
|
702
|
+
throw new WorkStoreError({ cause: { migrated, row }, operation: "open.migrate.handoff-identity" });
|
|
703
|
+
}
|
|
704
|
+
const encoded = JSON.stringify(migrated);
|
|
705
|
+
updateDecision.run(encoded, row.handoffId);
|
|
706
|
+
updateDispatch?.run(encoded, row.handoffId);
|
|
707
|
+
for (const { dispatch, lineage } of verifiedDispatches) {
|
|
708
|
+
updateMetadata?.run(JSON.stringify({ handoff: migrated, lineage }), dispatch.dispatchRequestId);
|
|
772
709
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
HAVING COUNT(metadata.dispatch_request_id) <> 1
|
|
781
|
-
LIMIT 1`).all())[0];
|
|
782
|
-
if (invalidRoutedMetadata !== undefined) {
|
|
710
|
+
}
|
|
711
|
+
if (decisions.length > 0 || previousDecisions.length > 0) {
|
|
712
|
+
const migratedLedger = Schema.decodeUnknownSync(LedgerBytesRow)(database.prepare(`SELECT COALESCE(SUM(
|
|
713
|
+
length(CAST(handoff_id AS BLOB)) + length(CAST(record AS BLOB))
|
|
714
|
+
), 0) AS bytes
|
|
715
|
+
FROM work_decision_handoffs`).get());
|
|
716
|
+
if (migratedLedger.bytes > workDecisionMaxBytes) {
|
|
783
717
|
throw new WorkStoreError({
|
|
784
|
-
cause:
|
|
785
|
-
operation: "open.migrate.
|
|
718
|
+
cause: migratedLedger,
|
|
719
|
+
operation: "open.migrate.handoff-capacity"
|
|
786
720
|
});
|
|
787
721
|
}
|
|
788
722
|
}
|
|
789
|
-
database.exec("COMMIT");
|
|
790
|
-
transaction = false;
|
|
791
723
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
724
|
+
if (tables.includes("orchestrator_dispatch_metadata") && coordinatorDispatchColumns.includes("is_routed")) {
|
|
725
|
+
const pageSize = 512;
|
|
726
|
+
let cursor = null;
|
|
727
|
+
while (true) {
|
|
728
|
+
const input = cursor === null
|
|
729
|
+
? database.prepare(`SELECT CAST(metadata.rowid AS TEXT) AS rowId,
|
|
730
|
+
metadata.dispatch_request_id AS dispatchRequestId,
|
|
731
|
+
dispatch.activity_idempotency_key AS activityIdempotencyKey,
|
|
732
|
+
dispatch.command, dispatch.is_routed AS isRouted,
|
|
733
|
+
CASE WHEN metadata.work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
|
|
734
|
+
metadata.route
|
|
735
|
+
FROM orchestrator_dispatch_metadata AS metadata
|
|
736
|
+
LEFT JOIN orchestrator_dispatches AS dispatch
|
|
737
|
+
ON dispatch.dispatch_request_id = metadata.dispatch_request_id
|
|
738
|
+
ORDER BY metadata.rowid ASC LIMIT ?`).all(pageSize)
|
|
739
|
+
: database.prepare(`SELECT CAST(metadata.rowid AS TEXT) AS rowId,
|
|
740
|
+
metadata.dispatch_request_id AS dispatchRequestId,
|
|
741
|
+
dispatch.activity_idempotency_key AS activityIdempotencyKey,
|
|
742
|
+
dispatch.command, dispatch.is_routed AS isRouted,
|
|
743
|
+
CASE WHEN metadata.work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
|
|
744
|
+
metadata.route
|
|
745
|
+
FROM orchestrator_dispatch_metadata AS metadata
|
|
746
|
+
LEFT JOIN orchestrator_dispatches AS dispatch
|
|
747
|
+
ON dispatch.dispatch_request_id = metadata.dispatch_request_id
|
|
748
|
+
WHERE metadata.rowid > CAST(? AS INTEGER)
|
|
749
|
+
ORDER BY metadata.rowid ASC LIMIT ?`).all(cursor, pageSize);
|
|
750
|
+
const rows = Schema.decodeUnknownSync(Schema.Array(CoordinatorMetadataRouteRow))(input);
|
|
751
|
+
for (const row of rows) {
|
|
752
|
+
try {
|
|
753
|
+
const isLegacyUnroutedOrphan = row.activityIdempotencyKey === null && row.command === null &&
|
|
754
|
+
row.isRouted === null && row.route === null && row.hasWorkLink === 0;
|
|
755
|
+
if (isLegacyUnroutedOrphan)
|
|
756
|
+
continue;
|
|
757
|
+
if (row.activityIdempotencyKey === null || row.command === null ||
|
|
758
|
+
row.route === null || row.isRouted === null) {
|
|
759
|
+
throw new WorkStoreError({ cause: row, operation: "open.migrate.metadata-route" });
|
|
760
|
+
}
|
|
761
|
+
requireCoordinatorRouteBinding(row.command, row.activityIdempotencyKey, row.route, row.hasWorkLink === 1, { _tag: "routed_discriminator", isRouted: row.isRouted }, "open.migrate.metadata-route");
|
|
762
|
+
}
|
|
763
|
+
catch (cause) {
|
|
764
|
+
if (Schema.is(WorkStoreError)(cause))
|
|
765
|
+
throw cause;
|
|
766
|
+
throw new WorkStoreError({ cause, operation: "open.migrate.metadata-route" });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const last = rows.at(-1);
|
|
770
|
+
if (rows.length < pageSize || last === undefined)
|
|
771
|
+
break;
|
|
772
|
+
cursor = last.rowId;
|
|
773
|
+
}
|
|
774
|
+
const invalidRoutedMetadata = Schema.decodeUnknownSync(Schema.Array(RoutedMetadataCardinalityRow))(database.prepare(`SELECT dispatch.dispatch_request_id AS dispatchRequestId,
|
|
775
|
+
COUNT(metadata.dispatch_request_id) AS metadataCount
|
|
776
|
+
FROM orchestrator_dispatches AS dispatch
|
|
777
|
+
LEFT JOIN orchestrator_dispatch_metadata AS metadata
|
|
778
|
+
ON metadata.dispatch_request_id = dispatch.dispatch_request_id
|
|
779
|
+
WHERE dispatch.is_routed = 1
|
|
780
|
+
GROUP BY dispatch.dispatch_request_id
|
|
781
|
+
HAVING COUNT(metadata.dispatch_request_id) <> 1
|
|
782
|
+
LIMIT 1`).all())[0];
|
|
783
|
+
if (invalidRoutedMetadata !== undefined) {
|
|
784
|
+
throw new WorkStoreError({
|
|
785
|
+
cause: invalidRoutedMetadata,
|
|
786
|
+
operation: "open.migrate.routed-metadata"
|
|
787
|
+
});
|
|
788
|
+
}
|
|
796
789
|
}
|
|
797
790
|
};
|
|
798
791
|
const readTransactionLedgerTotals = (database) => Schema.decodeUnknownSync(TransactionLedgerTotalsRow)(database.prepare(`SELECT transaction_count AS transactionCount, transaction_bytes AS transactionBytes
|
|
@@ -1209,9 +1202,14 @@ export class WorkStore {
|
|
|
1209
1202
|
const opened = yield* openPrivateSqlite(path, {
|
|
1210
1203
|
busyTimeoutMillis: workStoreBusyTimeoutMillis,
|
|
1211
1204
|
initialize: (database) => {
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1205
|
+
// One transaction: legacy migration and schema creation succeed or fail
|
|
1206
|
+
// together, and their statements share one commit instead of syncing
|
|
1207
|
+
// the file once each.
|
|
1208
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1209
|
+
try {
|
|
1210
|
+
migrateLegacyAuthorityTables(database);
|
|
1211
|
+
const hadLaneOperationLedger = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'work_lane_operations'").get() !== undefined;
|
|
1212
|
+
database.exec(`
|
|
1215
1213
|
CREATE TABLE IF NOT EXISTS work_goal_events (
|
|
1216
1214
|
event_id TEXT PRIMARY KEY,
|
|
1217
1215
|
goal_id TEXT NOT NULL,
|
|
@@ -1317,19 +1315,19 @@ export class WorkStore {
|
|
|
1317
1315
|
CREATE INDEX IF NOT EXISTS work_decision_handoffs_session
|
|
1318
1316
|
ON work_decision_handoffs (session_id);
|
|
1319
1317
|
`);
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1318
|
+
const columns = Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare("PRAGMA table_info(work_goal_events)").all());
|
|
1319
|
+
if (!columns.some(({ name }) => name === "transaction_id")) {
|
|
1320
|
+
database.exec("ALTER TABLE work_goal_events ADD COLUMN transaction_id TEXT");
|
|
1321
|
+
}
|
|
1322
|
+
if (!hadLaneOperationLedger) {
|
|
1323
|
+
database.exec(`
|
|
1326
1324
|
INSERT OR IGNORE INTO work_lane_operations
|
|
1327
1325
|
(operation_id, lane_id, goal_id, phase, revision, record)
|
|
1328
1326
|
SELECT operation_id, lane_id, goal_id, phase, revision, record
|
|
1329
1327
|
FROM work_lane_claims
|
|
1330
1328
|
`);
|
|
1331
|
-
|
|
1332
|
-
|
|
1329
|
+
}
|
|
1330
|
+
database.exec(`
|
|
1333
1331
|
INSERT OR IGNORE INTO work_lane_operation_totals
|
|
1334
1332
|
(singleton, operation_count, operation_bytes)
|
|
1335
1333
|
SELECT 1, COUNT(*), COALESCE(SUM(
|
|
@@ -1337,7 +1335,7 @@ export class WorkStore {
|
|
|
1337
1335
|
), 0)
|
|
1338
1336
|
FROM work_lane_operations
|
|
1339
1337
|
`);
|
|
1340
|
-
|
|
1338
|
+
database.exec(`
|
|
1341
1339
|
INSERT OR IGNORE INTO work_goal_transaction_totals
|
|
1342
1340
|
(singleton, transaction_count, transaction_bytes)
|
|
1343
1341
|
SELECT 1, COUNT(*), COALESCE(SUM(
|
|
@@ -1345,7 +1343,7 @@ export class WorkStore {
|
|
|
1345
1343
|
), 0)
|
|
1346
1344
|
FROM work_goal_transactions
|
|
1347
1345
|
`);
|
|
1348
|
-
|
|
1346
|
+
database.exec(`
|
|
1349
1347
|
INSERT OR REPLACE INTO work_decision_totals
|
|
1350
1348
|
(singleton, decision_count, decision_bytes)
|
|
1351
1349
|
SELECT 1, COUNT(*), COALESCE(SUM(
|
|
@@ -1353,6 +1351,15 @@ export class WorkStore {
|
|
|
1353
1351
|
), 0)
|
|
1354
1352
|
FROM work_decision_handoffs
|
|
1355
1353
|
`);
|
|
1354
|
+
database.exec("COMMIT");
|
|
1355
|
+
}
|
|
1356
|
+
catch (error) {
|
|
1357
|
+
// SQLite may already have rolled back (SQLITE_FULL, IOERR); a second
|
|
1358
|
+
// ROLLBACK would throw and replace the real error.
|
|
1359
|
+
if (database.isTransaction)
|
|
1360
|
+
database.exec("ROLLBACK");
|
|
1361
|
+
throw error;
|
|
1362
|
+
}
|
|
1356
1363
|
}
|
|
1357
1364
|
}).pipe(Effect.mapError(fromPrivateDatabaseError));
|
|
1358
1365
|
return new WorkStore(path, opened, cryptoService);
|