@knpkv/herdr-work 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +86 -0
  2. package/dist/display-state.d.ts +10 -0
  3. package/dist/display-state.d.ts.map +1 -0
  4. package/dist/display-state.js +5 -0
  5. package/dist/display-state.js.map +1 -0
  6. package/dist/errors.d.ts +21 -0
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +12 -0
  9. package/dist/errors.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/internal/legacy-lane-claim.d.ts +74 -0
  15. package/dist/internal/legacy-lane-claim.d.ts.map +1 -0
  16. package/dist/internal/legacy-lane-claim.js +81 -0
  17. package/dist/internal/legacy-lane-claim.js.map +1 -0
  18. package/dist/internal/session-index.d.ts +18 -0
  19. package/dist/internal/session-index.d.ts.map +1 -0
  20. package/dist/internal/session-index.js +23 -0
  21. package/dist/internal/session-index.js.map +1 -0
  22. package/dist/internal/terminal-reconcile.d.ts +29 -0
  23. package/dist/internal/terminal-reconcile.d.ts.map +1 -0
  24. package/dist/internal/terminal-reconcile.js +79 -0
  25. package/dist/internal/terminal-reconcile.js.map +1 -0
  26. package/dist/model.d.ts +909 -29
  27. package/dist/model.d.ts.map +1 -1
  28. package/dist/model.js +236 -7
  29. package/dist/model.js.map +1 -1
  30. package/dist/observed.d.ts +77 -0
  31. package/dist/observed.d.ts.map +1 -0
  32. package/dist/observed.js +240 -0
  33. package/dist/observed.js.map +1 -0
  34. package/dist/projection.d.ts +192 -12
  35. package/dist/projection.d.ts.map +1 -1
  36. package/dist/service.d.ts +121 -992
  37. package/dist/service.d.ts.map +1 -1
  38. package/dist/service.js +13 -3
  39. package/dist/service.js.map +1 -1
  40. package/dist/sql.d.ts.map +1 -1
  41. package/dist/sql.js +76 -3
  42. package/dist/sql.js.map +1 -1
  43. package/dist/store.d.ts +193 -11
  44. package/dist/store.d.ts.map +1 -1
  45. package/dist/store.js +1296 -639
  46. package/dist/store.js.map +1 -1
  47. package/dist/view.d.ts.map +1 -1
  48. package/dist/view.js +169 -103
  49. package/dist/view.js.map +1 -1
  50. package/dist/work-triage.d.ts +72 -0
  51. package/dist/work-triage.d.ts.map +1 -0
  52. package/dist/work-triage.js +114 -0
  53. package/dist/work-triage.js.map +1 -0
  54. package/package.json +3 -3
  55. package/src/styles.css +229 -237
package/dist/store.js CHANGED
@@ -1,16 +1,20 @@
1
- import { agentConnectTarget, fleetResponseBodyMaxBytes, workReassignActivitySummary } from "@knpkv/herdr-fleet";
1
+ import { agentConnectTarget, fleetResponseBodyMaxBytes, workAbandonActivitySummary, workReassignActivitySummary } from "@knpkv/herdr-fleet";
2
2
  import { openPrivateSqlite } from "@knpkv/herdr-fleet/sqlite";
3
- import { Clock, Crypto, Effect, Equal, Option, Schema } from "effect";
3
+ import { Clock, Crypto, Effect, Equal, Option, Result, Schema } from "effect";
4
4
  import { Hex } from "effect/encoding";
5
5
  import { makeWorkAgentBinding } from "./agent-binding.js";
6
- import { WorkAdmissionConflictError, WorkAgentBindingAuthorityError, WorkAgentBindingConflictError, WorkCheckpointConflictError, WorkCoordinatorHandoffConflictError, WorkDecisionAuthorityConflictError, WorkDecisionHandoffConflictError, WorkDecisionRevisionConflictError, WorkGoalAgentTargetConflictError, WorkGoalBindingRequiresAgentError, WorkGoalOwnerMismatchError, WorkGoalReassignmentConflictError, WorkGoalRevisionConflictError, WorkLaneClaimConflictError, WorkLaneGoalConflictError, WorkLaneOperationConflictError, WorkProjectionError, WorkPullRequestLinkError, WorkStoreError, WorkTransactionConflictError } from "./errors.js";
6
+ import { WorkAdmissionConflictError, WorkAgentBindingAuthorityError, WorkAgentBindingConflictError, WorkCheckpointConflictError, WorkCoordinatorHandoffConflictError, WorkDecisionAuthorityConflictError, WorkDecisionHandoffConflictError, WorkDecisionRevisionConflictError, WorkGoalAbandonmentConflictError, WorkGoalAgentTargetConflictError, WorkGoalBindingRequiresAgentError, WorkGoalLaneActiveError, WorkGoalOwnerMismatchError, WorkGoalReassignmentConflictError, WorkGoalRevisionConflictError, WorkGoalTerminalError, WorkLaneClaimConflictError, WorkLaneGoalConflictError, WorkLaneOperationConflictError, WorkProjectionError, WorkPullRequestLinkError, WorkStoreError, WorkTransactionConflictError } from "./errors.js";
7
7
  import { validateGoalFamilyHistory } from "./goal-family.js";
8
8
  import { agentBindingAdmissionError, workAgentBindingLaneOperationMaxBytes, workAgentBindingLaneOperationMaxRecords, workAgentBindingMaximumSnapshotBytes, workAgentBindingSnapshotEnvelopeMaxBytes, workMaximumSnapshotBytesForHistory } from "./internal/agent-binding-admission.js";
9
9
  import { AgentBindingGoalEventRow, AgentBindingLaneOperationRow, agentBindingReadbackError, AgentBindingRow, decodeAgentBindingGoalEvent, decodeAgentBindingRow, laneOperationReadbackError } from "./internal/agent-binding-readback.js";
10
10
  import { CoordinatorCommandRow, CoordinatorLifecycleDispatchRow, CoordinatorLifecycleEventRow, coordinatorLifecycleRunningAt, CoordinatorRouteDiscriminatorRow, coordinatorRouteRequiresWorkLink, requireCoordinatorFailedLunaAuthority, requireCoordinatorLifecycleAuthority, requireCoordinatorRouteAuthority, requireCoordinatorRouteBinding } from "./internal/coordinator-authority.js";
11
11
  import { currentDecisionHandoffEquivalent, CurrentMetadataWorkLink, decodePreviousDecisionHandoff, previousDecisionHandoffEquivalent, PreviousWorkDecisionHandoff, upgradePreviousDecisionHandoff, workDispatchLineageContainedBy, workDispatchLineageEquivalent } from "./internal/decision-handoff-migration.js";
12
12
  import { workHistoryError } from "./internal/history-validation.js";
13
- import { WorkAdmissionTarget, WorkAgentBinding, WorkAgentBindingRequest, WorkCoordinatorSessionId, WorkDecisionHandoff, WorkDispatchHandoff, WorkExistingGoalRecovery, WorkGoal, WorkGoalCheckpoint, WorkGoalId, WorkGoalReassigned, WorkGoalReassignment, workHistoryMaxEvents, WorkLaneClaim, WorkLaneClaimed, WorkProspectiveAdmission, WorkPullRequestLink, WorkRecoveryTarget, workSnapshotMaxGoals } from "./model.js";
13
+ import { LaneOperationLedgerRow, LaneOperationTotalsRow, planLegacyLaneOperations, resolveLegacyLaneClaim } from "./internal/legacy-lane-claim.js";
14
+ import { enforcesOneHandoffPerSession, SessionIndexDefinition, sessionIndexDefinitionQuery } from "./internal/session-index.js";
15
+ import { goalActivityLimit, terminalCandidates, terminalCheckpoint, withoutBlockers } from "./internal/terminal-reconcile.js";
16
+ import { admissionEvidence, isTerminalWorkState, WorkAdmissionTarget, WorkAgentBinding, WorkAgentBindingRequest, WorkCoordinatorSessionId, WorkDecisionHandoff, WorkDispatchHandoff, WorkExistingGoalRecovery, WorkGoal, WorkGoalAbandoned, WorkGoalAbandonment, WorkGoalCheckpoint, WorkGoalId, WorkGoalReassigned, WorkGoalReassignment, workHistoryMaxEvents, WorkLaneClaim, WorkLaneClaimed, WorkObservationEnvelope, workObservationMaxSkewMillis, WorkObservationSubject, WorkObservedAdmission, WorkObservedFact, workObservedFactMaxBytes, workObservedFactMaxRecords, WorkObservedFailure, WorkProspectiveAdmission, WorkPullRequestLink, workReconcilerHeadroom, WorkRecoveryTarget, workSnapshotMaxGoals } from "./model.js";
17
+ import { asciiLower, canonicalSubject, observationSubject, reconcilerEventPrefix } from "./observed.js";
14
18
  const StoredEventRow = Schema.Struct({ record: Schema.String });
15
19
  const StoredEventRows = Schema.Array(StoredEventRow);
16
20
  const StoredEventWithTransactionRow = Schema.Struct({
@@ -138,544 +142,498 @@ const legacyDecisionEquivalent = Schema.toEquivalence(LegacyDecisionRecord);
138
142
  const TransactionId = Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(256), Schema.isPattern(/^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/));
139
143
  const storeError = (operation) => (cause) => new WorkStoreError({ cause, operation });
140
144
  const fromPrivateDatabaseError = (error) => new WorkStoreError({ cause: error.cause, operation: error.operation });
145
+ /**
146
+ * Files written before the session index was unique carry a plain index under
147
+ * the same name, which `CREATE UNIQUE INDEX IF NOT EXISTS` leaves in place.
148
+ * Replace it, failing closed when two handoffs already share a session. Runs
149
+ * inside `WorkStore.open`'s schema transaction, so a failure leaves the plain
150
+ * index in place.
151
+ */
152
+ const requireUniqueSessionIndex = (database) => {
153
+ const sessionIndex = Schema.decodeUnknownSync(Schema.Array(SessionIndexDefinition))(database.prepare(sessionIndexDefinitionQuery).all())[0];
154
+ if (sessionIndex === undefined || enforcesOneHandoffPerSession(sessionIndex))
155
+ return;
156
+ // NULL sessions never collide under a UNIQUE index, so they are not duplicates.
157
+ const duplicateGroups = `SELECT session_id AS sessionId, COUNT(*) AS rows, group_concat(handoff_id) AS handoffIds
158
+ FROM work_decision_handoffs WHERE session_id IS NOT NULL
159
+ GROUP BY session_id HAVING COUNT(*) > 1`;
160
+ const duplicates = database.prepare(`${duplicateGroups} LIMIT 10`).all();
161
+ if (duplicates.length > 0) {
162
+ const total = database.prepare(`SELECT COUNT(*) AS sessions FROM (${duplicateGroups})`).get();
163
+ throw new WorkStoreError({ cause: { duplicates, total }, operation: "open.migrate.session-index" });
164
+ }
165
+ database.exec(`
166
+ DROP INDEX work_decision_handoffs_session;
167
+ CREATE UNIQUE INDEX work_decision_handoffs_session ON work_decision_handoffs (session_id);
168
+ `);
169
+ };
170
+ /**
171
+ * Upgrades pre-session Work tables in place. Runs inside `WorkStore.open`'s
172
+ * schema transaction, so a failure here or in a later schema step leaves the
173
+ * file as it was.
174
+ */
141
175
  const migrateLegacyAuthorityTables = (database) => {
142
- let transaction = false;
143
- try {
144
- database.exec("BEGIN IMMEDIATE");
145
- transaction = true;
146
- const columns = (table) => Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare(`PRAGMA table_info(${table})`).all()).map(({ name }) => name);
147
- const laneColumns = columns("work_lane_claims");
148
- const decisionColumns = columns("work_decision_handoffs");
149
- 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);
150
- const coordinatorDispatchColumns = tables.includes("orchestrator_dispatches")
151
- ? columns("orchestrator_dispatches")
176
+ const columns = (table) => Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare(`PRAGMA table_info(${table})`).all()).map(({ name }) => name);
177
+ const laneColumns = columns("work_lane_claims");
178
+ const decisionColumns = columns("work_decision_handoffs");
179
+ 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);
180
+ const coordinatorDispatchColumns = tables.includes("orchestrator_dispatches")
181
+ ? columns("orchestrator_dispatches")
182
+ : [];
183
+ const requireBindingCompanions = (binding, operation) => {
184
+ const laneInput = tables.includes("work_lane_operations")
185
+ ? database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
186
+ phase, revision, record
187
+ FROM work_lane_operations WHERE operation_id = ?`).get(binding.lane.operationId)
188
+ : undefined;
189
+ const checkpointInput = tables.includes("work_goal_events")
190
+ ? database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
191
+ FROM work_goal_events WHERE event_id = ?`).get(binding.checkpoint.eventId)
192
+ : undefined;
193
+ const error = agentBindingReadbackError(binding, laneInput === undefined ? undefined : Schema.decodeUnknownSync(AgentBindingLaneOperationRow)(laneInput), checkpointInput === undefined
194
+ ? undefined
195
+ : Schema.decodeUnknownSync(AgentBindingGoalEventRow)(checkpointInput), operation);
196
+ if (error !== undefined)
197
+ throw error;
198
+ };
199
+ const coordinatorTablesPresent = {
200
+ dispatch: tables.includes("orchestrator_dispatches"),
201
+ event: tables.includes("orchestrator_events"),
202
+ metadata: tables.includes("orchestrator_dispatch_metadata")
203
+ };
204
+ const requireCoordinatorSchema = (operation) => {
205
+ const present = Object.values(coordinatorTablesPresent).filter(Boolean).length;
206
+ if (present !== 0 && present !== 3) {
207
+ throw new WorkStoreError({ cause: coordinatorTablesPresent, operation: `${operation}.schema` });
208
+ }
209
+ return present === 3;
210
+ };
211
+ requireCoordinatorSchema("open.migrate");
212
+ const readCoordinatorLifecycle = (dispatchRequestId) => ({
213
+ dispatchRows: Schema.decodeUnknownSync(Schema.Array(CoordinatorLifecycleDispatchRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId,
214
+ activity_idempotency_key AS activityIdempotencyKey, command,
215
+ accepted_at AS acceptedAt, status
216
+ FROM orchestrator_dispatches WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId)),
217
+ eventRows: Schema.decodeUnknownSync(Schema.Array(CoordinatorLifecycleEventRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, sequence, type,
218
+ activity_idempotency_key AS activityIdempotencyKey,
219
+ occurred_at AS occurredAt, detail, result
220
+ FROM orchestrator_events WHERE dispatch_request_id = ?
221
+ ORDER BY sequence ASC LIMIT 5`).all(dispatchRequestId))
222
+ });
223
+ const routeStorageAuthority = (dispatchRequestId, operation) => {
224
+ if (!coordinatorDispatchColumns.includes("is_routed")) {
225
+ return { _tag: "legacy_without_routed_discriminator" };
226
+ }
227
+ const rows = Schema.decodeUnknownSync(Schema.Array(CoordinatorRouteDiscriminatorRow))(database.prepare(`SELECT is_routed AS isRouted FROM orchestrator_dispatches
228
+ WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId));
229
+ const row = rows[0];
230
+ if (rows.length !== 1 || row === undefined) {
231
+ throw new WorkStoreError({ cause: { dispatchRequestId, rows }, operation });
232
+ }
233
+ return { _tag: "routed_discriminator", isRouted: row.isRouted };
234
+ };
235
+ const requireLinkedParentAuthority = (linkedRequestId, operation) => {
236
+ if (linkedRequestId === null)
237
+ return;
238
+ const metadataRows = Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
239
+ FROM orchestrator_dispatch_metadata WHERE dispatch_request_id = ? LIMIT 2`).all(linkedRequestId));
240
+ const metadata = metadataRows[0];
241
+ if (metadataRows.length !== 1 || metadata === undefined || metadata.workLink !== null) {
242
+ throw new WorkStoreError({ cause: { linkedRequestId, metadataRows }, operation });
243
+ }
244
+ const lifecycle = readCoordinatorLifecycle(linkedRequestId);
245
+ requireCoordinatorFailedLunaAuthority(lifecycle.dispatchRows, lifecycle.eventRows, metadata.route, routeStorageAuthority(linkedRequestId, operation), operation);
246
+ };
247
+ const requireMetadataAuthority = (dispatchRequestId, routeText, lineage, operation) => {
248
+ const commandRows = Schema.decodeUnknownSync(Schema.Array(CoordinatorCommandRow))(database.prepare(`SELECT activity_idempotency_key AS activityIdempotencyKey, command FROM orchestrator_dispatches
249
+ WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId));
250
+ const commandRow = commandRows[0];
251
+ if (commandRows.length !== 1 || commandRow === undefined) {
252
+ throw new WorkStoreError({ cause: { commandRows, dispatchRequestId }, operation });
253
+ }
254
+ const route = requireCoordinatorRouteAuthority(commandRow.command, commandRow.activityIdempotencyKey, routeText, lineage, routeStorageAuthority(dispatchRequestId, operation), operation);
255
+ requireLinkedParentAuthority(route.linkedRequestId, `${operation}.parent`);
256
+ };
257
+ const requireLifecycleAuthority = (dispatchRequestId, expectedRunningAt, operation) => {
258
+ if (!requireCoordinatorSchema(operation))
259
+ return;
260
+ const lifecycle = readCoordinatorLifecycle(dispatchRequestId);
261
+ requireCoordinatorLifecycleAuthority(lifecycle.dispatchRows, lifecycle.eventRows, expectedRunningAt, operation);
262
+ };
263
+ const requireCurrentAgentBindingAuthority = (dispatchRequestId, handoff, operation) => {
264
+ const lifecycle = readCoordinatorLifecycle(dispatchRequestId);
265
+ const runningAt = coordinatorLifecycleRunningAt(lifecycle.dispatchRows, lifecycle.eventRows, `${operation}.lifecycle`);
266
+ const bindingRows = tables.includes("work_agent_bindings")
267
+ ? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
268
+ expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
269
+ FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId))
152
270
  : [];
153
- const requireBindingCompanions = (binding, operation) => {
154
- const laneInput = tables.includes("work_lane_operations")
155
- ? database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
271
+ if (runningAt === null && bindingRows.length === 0)
272
+ return;
273
+ const bindingRow = bindingRows[0];
274
+ if (runningAt === null || bindingRows.length !== 1 || bindingRow === undefined) {
275
+ throw new WorkStoreError({ cause: { bindingRows, lifecycle }, operation });
276
+ }
277
+ const decoded = decodeAgentBindingRow(bindingRow, { dispatchRequestId, laneId: handoff.laneId }, operation);
278
+ if (decoded._tag === "invalid")
279
+ throw decoded.error;
280
+ if (decoded.binding.request.expectedRevision !== handoff.expectedRevision ||
281
+ decoded.binding.lane.goalId !== handoff.goalId ||
282
+ decoded.binding.checkpoint.occurredAt !== runningAt) {
283
+ throw new WorkStoreError({ cause: { binding: decoded.binding, handoff, runningAt }, operation });
284
+ }
285
+ requireBindingCompanions(decoded.binding, operation);
286
+ const currentLaneRows = Schema.decodeUnknownSync(Schema.Array(LaneRow))(database.prepare(`SELECT lane_id AS laneId, goal_id AS goalId, operation_id AS operationId,
287
+ phase, revision, record
288
+ FROM work_lane_claims WHERE lane_id = ? LIMIT 2`).all(decoded.binding.lane.laneId));
289
+ const currentLaneRow = currentLaneRows[0];
290
+ if (currentLaneRows.length !== 1 || currentLaneRow === undefined) {
291
+ throw new WorkStoreError({
292
+ cause: { binding: decoded.binding, currentLaneRows },
293
+ operation: `${operation}.lane-revision`
294
+ });
295
+ }
296
+ let currentLane;
297
+ try {
298
+ currentLane = Schema.decodeUnknownSync(WorkLaneClaimed)(JSON.parse(currentLaneRow.record));
299
+ }
300
+ catch (cause) {
301
+ throw new WorkStoreError({ cause, operation: `${operation}.lane-revision` });
302
+ }
303
+ const currentLaneSchema = laneColumns.includes("goal_id");
304
+ const currentOperationRows = currentLaneSchema
305
+ ? Schema.decodeUnknownSync(Schema.Array(AgentBindingLaneOperationRow))(database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
156
306
  phase, revision, record
157
- FROM work_lane_operations WHERE operation_id = ?`).get(binding.lane.operationId)
158
- : undefined;
159
- const checkpointInput = tables.includes("work_goal_events")
160
- ? database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
161
- FROM work_goal_events WHERE event_id = ?`).get(binding.checkpoint.eventId)
162
- : undefined;
163
- const error = agentBindingReadbackError(binding, laneInput === undefined ? undefined : Schema.decodeUnknownSync(AgentBindingLaneOperationRow)(laneInput), checkpointInput === undefined
164
- ? undefined
165
- : Schema.decodeUnknownSync(AgentBindingGoalEventRow)(checkpointInput), operation);
166
- if (error !== undefined)
167
- throw error;
168
- };
169
- const coordinatorTablesPresent = {
170
- dispatch: tables.includes("orchestrator_dispatches"),
171
- event: tables.includes("orchestrator_events"),
172
- metadata: tables.includes("orchestrator_dispatch_metadata")
173
- };
174
- const requireCoordinatorSchema = (operation) => {
175
- const present = Object.values(coordinatorTablesPresent).filter(Boolean).length;
176
- if (present !== 0 && present !== 3) {
177
- throw new WorkStoreError({ cause: coordinatorTablesPresent, operation: `${operation}.schema` });
178
- }
179
- return present === 3;
180
- };
181
- requireCoordinatorSchema("open.migrate");
182
- const readCoordinatorLifecycle = (dispatchRequestId) => ({
183
- dispatchRows: Schema.decodeUnknownSync(Schema.Array(CoordinatorLifecycleDispatchRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId,
184
- activity_idempotency_key AS activityIdempotencyKey, command,
185
- accepted_at AS acceptedAt, status
186
- FROM orchestrator_dispatches WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId)),
187
- eventRows: Schema.decodeUnknownSync(Schema.Array(CoordinatorLifecycleEventRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, sequence, type,
188
- activity_idempotency_key AS activityIdempotencyKey,
189
- occurred_at AS occurredAt, detail, result
190
- FROM orchestrator_events WHERE dispatch_request_id = ?
191
- ORDER BY sequence ASC LIMIT 5`).all(dispatchRequestId))
307
+ FROM work_lane_operations WHERE operation_id = ? LIMIT 2`).all(currentLane.operationId))
308
+ : [];
309
+ const currentOperationError = currentLaneSchema
310
+ ? laneOperationReadbackError(currentLane, currentOperationRows.length === 1 ? currentOperationRows[0] : undefined, `${operation}.lane-revision`)
311
+ : undefined;
312
+ if (currentLane.laneId !== currentLaneRow.laneId ||
313
+ currentLane.goalId !== currentLaneRow.goalId ||
314
+ currentLane.operationId !== currentLaneRow.operationId ||
315
+ currentLane.phase !== currentLaneRow.phase ||
316
+ currentLane.revision !== currentLaneRow.revision ||
317
+ currentLane.laneId !== decoded.binding.lane.laneId ||
318
+ currentLane.goalId !== decoded.binding.lane.goalId ||
319
+ currentLane.revision < decoded.binding.lane.revision ||
320
+ (currentLaneSchema && currentLane.revision === decoded.binding.lane.revision &&
321
+ !Equal.equals(currentLane, decoded.binding.lane)) ||
322
+ currentOperationError !== undefined) {
323
+ throw new WorkStoreError({
324
+ cause: { binding: decoded.binding, currentLane, currentLaneRow, currentOperationError },
325
+ operation: `${operation}.lane-revision`
326
+ });
327
+ }
328
+ };
329
+ const dispatches = tables.includes("work_dispatch_handoffs")
330
+ ? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
331
+ lane_id AS laneId, occurred_at AS occurredAt, lineage, record
332
+ FROM work_dispatch_handoffs`).all())
333
+ : [];
334
+ const decisionIdentities = decisionColumns.length > 0
335
+ ? Schema.decodeUnknownSync(Schema.Array(DecisionIdentityRow))(database.prepare("SELECT handoff_id AS handoffId FROM work_decision_handoffs").all())
336
+ : [];
337
+ const orphanDispatch = dispatches.find(({ handoffId }) => !decisionIdentities.some((decision) => decision.handoffId === handoffId));
338
+ if (orphanDispatch !== undefined) {
339
+ throw new WorkStoreError({
340
+ cause: { decisionIdentities, dispatch: orphanDispatch },
341
+ operation: "open.migrate.dispatch-decision"
342
+ });
343
+ }
344
+ if (tables.includes("orchestrator_dispatch_metadata")) {
345
+ const linkedMetadata = Schema.decodeUnknownSync(Schema.Array(MetadataHandoffIdentityRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId,
346
+ CASE WHEN work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
347
+ CASE WHEN json_valid(work_link) AND json_type(work_link, '$.handoff.id') = 'text'
348
+ THEN json_extract(work_link, '$.handoff.id') END AS handoffId,
349
+ route
350
+ FROM orchestrator_dispatch_metadata AS metadata
351
+ WHERE work_link IS NOT NULL OR EXISTS (
352
+ SELECT 1 FROM orchestrator_dispatch_metadata AS linked
353
+ WHERE linked.dispatch_request_id = metadata.dispatch_request_id
354
+ AND linked.work_link IS NOT NULL
355
+ ) OR (json_valid(route) AND json_extract(route, '$.model') = 'gpt-5.6-sol')
356
+ LIMIT ${workDecisionMaxRecords + 1}`).all());
357
+ if (linkedMetadata.length > workDecisionMaxRecords) {
358
+ throw new WorkStoreError({ cause: linkedMetadata.length, operation: "open.migrate.metadata-capacity" });
359
+ }
360
+ const metadataCounts = new Map();
361
+ const dispatchCounts = new Map();
362
+ const decisionCounts = new Map();
363
+ for (const metadata of linkedMetadata) {
364
+ metadataCounts.set(metadata.dispatchRequestId, (metadataCounts.get(metadata.dispatchRequestId) ?? 0) + 1);
365
+ }
366
+ for (const dispatch of dispatches) {
367
+ const identity = JSON.stringify([dispatch.dispatchRequestId, dispatch.handoffId]);
368
+ dispatchCounts.set(identity, (dispatchCounts.get(identity) ?? 0) + 1);
369
+ }
370
+ for (const decision of decisionIdentities) {
371
+ decisionCounts.set(decision.handoffId, (decisionCounts.get(decision.handoffId) ?? 0) + 1);
372
+ }
373
+ const invalidMetadata = linkedMetadata.find((metadata) => {
374
+ if (metadata.route === null)
375
+ return true;
376
+ const requiresWorkLink = coordinatorRouteRequiresWorkLink(metadata.route, routeStorageAuthority(metadata.dispatchRequestId, "open.migrate.metadata-decision"), "open.migrate.metadata-decision");
377
+ if (!requiresWorkLink)
378
+ return metadata.hasWorkLink === 1;
379
+ const dispatchIdentity = JSON.stringify([metadata.dispatchRequestId, metadata.handoffId]);
380
+ return (metadata.handoffId === null || metadataCounts.get(metadata.dispatchRequestId) !== 1 ||
381
+ dispatchCounts.get(dispatchIdentity) !== 1 || decisionCounts.get(metadata.handoffId) !== 1);
192
382
  });
193
- const routeStorageAuthority = (dispatchRequestId, operation) => {
194
- if (!coordinatorDispatchColumns.includes("is_routed")) {
195
- return { _tag: "legacy_without_routed_discriminator" };
383
+ if (invalidMetadata !== undefined) {
384
+ throw new WorkStoreError({
385
+ cause: { decisionIdentities, dispatches, linkedMetadata, metadata: invalidMetadata },
386
+ operation: "open.migrate.metadata-decision"
387
+ });
388
+ }
389
+ }
390
+ const lanes = laneColumns.length > 0 && !laneColumns.includes("goal_id")
391
+ ? Schema.decodeUnknownSync(Schema.Array(LegacyLaneStoredRow))(database.prepare("SELECT lane_id AS laneId, revision, record FROM work_lane_claims").all()).map((row) => {
392
+ const legacy = Schema.decodeUnknownSync(LegacyLaneRecord)(JSON.parse(row.record));
393
+ if (legacy.laneId !== row.laneId || legacy.revision !== row.revision) {
394
+ throw new WorkStoreError({ cause: { legacy, row }, operation: "open.migrate.lane-identity" });
196
395
  }
197
- const rows = Schema.decodeUnknownSync(Schema.Array(CoordinatorRouteDiscriminatorRow))(database.prepare(`SELECT is_routed AS isRouted FROM orchestrator_dispatches
198
- WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId));
199
- const row = rows[0];
200
- if (rows.length !== 1 || row === undefined) {
201
- throw new WorkStoreError({ cause: { dispatchRequestId, rows }, operation });
396
+ return Schema.decodeUnknownSync(WorkLaneClaimed)({
397
+ ...legacy,
398
+ goalId: legacy.laneId,
399
+ operationId: legacy.laneId
400
+ });
401
+ })
402
+ : [];
403
+ // Lanes the running bindings of legacy handoffs recorded; see resolveLegacyLaneClaim.
404
+ const bindingLanes = [];
405
+ const decisions = decisionColumns.length > 0 && !decisionColumns.includes("session_id")
406
+ ? Schema.decodeUnknownSync(Schema.Array(LegacyDecisionStoredRow))(database.prepare("SELECT handoff_id AS handoffId, record FROM work_decision_handoffs").all()).map((row) => {
407
+ const legacy = Schema.decodeUnknownSync(LegacyDecisionRecord)(JSON.parse(row.record));
408
+ if (legacy.id !== row.handoffId) {
409
+ throw new WorkStoreError({ cause: { legacy, row }, operation: "open.migrate.handoff-identity" });
202
410
  }
203
- return { _tag: "routed_discriminator", isRouted: row.isRouted };
204
- };
205
- const requireLinkedParentAuthority = (linkedRequestId, operation) => {
206
- if (linkedRequestId === null)
207
- return;
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 });
411
+ const matchingDispatches = dispatches.filter(({ handoffId }) => handoffId === legacy.id);
412
+ const dispatch = matchingDispatches[0];
413
+ const lane = lanes.find(({ laneId }) => laneId === legacy.laneId);
414
+ if (lane === undefined) {
415
+ throw new WorkStoreError({ cause: legacy, operation: "open.migrate.handoff-lane" });
213
416
  }
214
- const lifecycle = readCoordinatorLifecycle(linkedRequestId);
215
- requireCoordinatorFailedLunaAuthority(lifecycle.dispatchRows, lifecycle.eventRows, metadata.route, routeStorageAuthority(linkedRequestId, operation), operation);
216
- };
217
- const requireMetadataAuthority = (dispatchRequestId, routeText, lineage, operation) => {
218
- const commandRows = Schema.decodeUnknownSync(Schema.Array(CoordinatorCommandRow))(database.prepare(`SELECT activity_idempotency_key AS activityIdempotencyKey, command FROM orchestrator_dispatches
219
- WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId));
220
- const commandRow = commandRows[0];
221
- if (commandRows.length !== 1 || commandRow === undefined) {
222
- throw new WorkStoreError({ cause: { commandRows, dispatchRequestId }, operation });
417
+ const dispatchIds = dispatch === undefined
418
+ ? []
419
+ : Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage));
420
+ if (matchingDispatches.length !== 1 || dispatch === undefined) {
421
+ throw new WorkStoreError({
422
+ cause: { legacy, matchingDispatches },
423
+ operation: "open.migrate.legacy-dispatch-cardinality"
424
+ });
425
+ }
426
+ const dispatchHandoff = Schema.decodeUnknownSync(LegacyDecisionRecord)(JSON.parse(dispatch.record));
427
+ if (dispatch.laneId !== legacy.laneId || dispatch.occurredAt !== legacy.occurredAt ||
428
+ !legacyDecisionEquivalent(dispatchHandoff, legacy)) {
429
+ throw new WorkStoreError({
430
+ cause: { dispatch, dispatchHandoff, legacy },
431
+ operation: "open.migrate.legacy-dispatch-authority"
432
+ });
433
+ }
434
+ if (tables.includes("orchestrator_dispatch_metadata")) {
435
+ const metadataRows = Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
436
+ FROM orchestrator_dispatch_metadata WHERE dispatch_request_id = ? LIMIT 2`).all(dispatch.dispatchRequestId));
437
+ const metadata = metadataRows[0];
438
+ if (metadataRows.length !== 1 || metadata === undefined) {
439
+ throw new WorkStoreError({
440
+ cause: { dispatch, metadataRows },
441
+ operation: "open.migrate.legacy-metadata-authority"
442
+ });
443
+ }
444
+ if (metadata.workLink === null) {
445
+ throw new WorkStoreError({
446
+ cause: { dispatch, metadata },
447
+ operation: "open.migrate.legacy-metadata-authority"
448
+ });
449
+ }
450
+ const workLink = Schema.decodeUnknownSync(LegacyMetadataWorkLink)(JSON.parse(metadata.workLink));
451
+ requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.legacy-metadata-authority");
452
+ if (!legacyDecisionEquivalent(workLink.handoff, legacy) ||
453
+ !workDispatchLineageEquivalent(workLink.lineage, dispatchIds)) {
454
+ throw new WorkStoreError({
455
+ cause: { legacy, workLink },
456
+ operation: "open.migrate.legacy-metadata-authority"
457
+ });
458
+ }
223
459
  }
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
460
  const bindingRows = tables.includes("work_agent_bindings")
237
461
  ? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
238
462
  expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
239
- FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId))
463
+ FROM work_agent_bindings
464
+ WHERE dispatch_request_id = ?`).all(dispatch.dispatchRequestId))
240
465
  : [];
241
- if (runningAt === null && bindingRows.length === 0)
242
- return;
243
466
  const bindingRow = bindingRows[0];
244
- if (runningAt === null || bindingRows.length !== 1 || bindingRow === undefined) {
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) {
467
+ if (bindingRows.length !== 1 || bindingRow === undefined) {
261
468
  throw new WorkStoreError({
262
- cause: { binding: decoded.binding, currentLaneRows },
263
- operation: `${operation}.lane-revision`
469
+ cause: { bindingRows, dispatch, legacy },
470
+ operation: "open.migrate.legacy-handoff-revision"
264
471
  });
265
472
  }
266
- let currentLane;
267
- try {
268
- currentLane = Schema.decodeUnknownSync(WorkLaneClaimed)(JSON.parse(currentLaneRow.record));
269
- }
270
- catch (cause) {
271
- throw new WorkStoreError({ cause, operation: `${operation}.lane-revision` });
473
+ const bindingDecision = decodeAgentBindingRow(bindingRow, { dispatchRequestId: dispatch.dispatchRequestId, laneId: legacy.laneId }, "open.migrate.legacy-agent-binding");
474
+ if (bindingDecision._tag === "invalid")
475
+ throw bindingDecision.error;
476
+ if (bindingDecision.binding.lane.goalId !== legacy.goalId) {
477
+ throw new WorkStoreError({
478
+ cause: { binding: bindingDecision.binding, legacy },
479
+ operation: "open.migrate.legacy-agent-binding.goal"
480
+ });
272
481
  }
273
- const currentLaneSchema = laneColumns.includes("goal_id");
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) {
482
+ if (lane.revision < bindingDecision.binding.lane.revision) {
293
483
  throw new WorkStoreError({
294
- cause: { binding: decoded.binding, currentLane, currentLaneRow, currentOperationError },
295
- operation: `${operation}.lane-revision`
484
+ cause: { binding: bindingDecision.binding, lane, legacy },
485
+ operation: "open.migrate.legacy-handoff-lane-revision"
296
486
  });
297
487
  }
298
- };
299
- const dispatches = tables.includes("work_dispatch_handoffs")
300
- ? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
301
- lane_id AS laneId, occurred_at AS occurredAt, lineage, record
302
- FROM work_dispatch_handoffs`).all())
303
- : [];
304
- const decisionIdentities = decisionColumns.length > 0
305
- ? Schema.decodeUnknownSync(Schema.Array(DecisionIdentityRow))(database.prepare("SELECT handoff_id AS handoffId FROM work_decision_handoffs").all())
306
- : [];
307
- const orphanDispatch = dispatches.find(({ handoffId }) => !decisionIdentities.some((decision) => decision.handoffId === handoffId));
308
- if (orphanDispatch !== undefined) {
309
- throw new WorkStoreError({
310
- cause: { decisionIdentities, dispatch: orphanDispatch },
311
- operation: "open.migrate.dispatch-decision"
488
+ bindingLanes.push(bindingDecision.binding.lane);
489
+ requireBindingCompanions(bindingDecision.binding, "open.migrate.legacy-agent-binding");
490
+ requireLifecycleAuthority(bindingDecision.binding.request.dispatchRequestId, bindingDecision.binding.checkpoint.occurredAt, "open.migrate.legacy-agent-binding.lifecycle");
491
+ return Schema.decodeUnknownSync(WorkDecisionHandoff)({
492
+ ...legacy,
493
+ contextDelta: legacy.summary,
494
+ expectedRevision: bindingDecision.binding.request.expectedRevision,
495
+ sessionId: legacy.id,
496
+ dispatchIds,
497
+ blockers: [],
498
+ evidenceRefs: [],
499
+ version: "herdr.work.decision.v2"
312
500
  });
501
+ })
502
+ : [];
503
+ const migratedClaims = lanes.map((lane) => resolveLegacyLaneClaim(lane, bindingLanes));
504
+ if (laneColumns.length > 0 && !laneColumns.includes("goal_id")) {
505
+ database.exec("ALTER TABLE work_lane_claims ADD COLUMN goal_id TEXT");
506
+ database.exec("ALTER TABLE work_lane_claims ADD COLUMN operation_id TEXT");
507
+ database.exec("ALTER TABLE work_lane_claims ADD COLUMN phase TEXT");
508
+ const update = database.prepare("UPDATE work_lane_claims SET goal_id = ?, operation_id = ?, phase = ?, record = ? WHERE lane_id = ?");
509
+ // An existing lane-operation ledger is not backfilled later, so record each
510
+ // migrated claim's operation here; the claim's readback requires it.
511
+ // Without a ledger, schema creation below creates it and backfills every
512
+ // claim, so the same bound applies to an empty ledger.
513
+ const hasLedger = tables.includes("work_lane_operations");
514
+ const operations = planLegacyLaneOperations(migratedClaims.map(({ lane }) => lane), new Map(hasLedger
515
+ ? migratedClaims.flatMap(({ lane }) => {
516
+ const matches = database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId, phase, revision, record
517
+ FROM work_lane_operations WHERE CAST(operation_id AS TEXT) = ?`).all(lane.operationId);
518
+ const rows = Schema.decodeUnknownSync(Schema.Array(LaneOperationLedgerRow))(matches);
519
+ // A key stored as a blob, or twice, is a collision even when its bytes match.
520
+ const found = rows.find(({ operationId }) => operationId !== lane.operationId) ?? rows[0];
521
+ return found === undefined ? [] : [[lane.operationId, found]];
522
+ })
523
+ : []), hasLedger
524
+ ? Schema.decodeUnknownSync(LaneOperationTotalsRow)(database.prepare(`SELECT COUNT(*) AS count, COALESCE(SUM(
525
+ length(CAST(operation_id AS BLOB)) + length(CAST(record AS BLOB))), 0) AS bytes
526
+ FROM work_lane_operations`).get())
527
+ : { bytes: 0, count: 0 }, { bytes: workLaneOperationMaxBytes, records: workLaneOperationMaxRecords });
528
+ if (operations._tag === "collision") {
529
+ throw new WorkStoreError({ cause: operations, operation: "open.migrate.lane-operation-collision" });
313
530
  }
314
- if (tables.includes("orchestrator_dispatch_metadata")) {
315
- const linkedMetadata = Schema.decodeUnknownSync(Schema.Array(MetadataHandoffIdentityRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId,
316
- CASE WHEN work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
317
- CASE WHEN json_valid(work_link) AND json_type(work_link, '$.handoff.id') = 'text'
318
- THEN json_extract(work_link, '$.handoff.id') END AS handoffId,
319
- route
320
- FROM orchestrator_dispatch_metadata AS metadata
321
- WHERE work_link IS NOT NULL OR EXISTS (
322
- SELECT 1 FROM orchestrator_dispatch_metadata AS linked
323
- WHERE linked.dispatch_request_id = metadata.dispatch_request_id
324
- AND linked.work_link IS NOT NULL
325
- ) OR (json_valid(route) AND json_extract(route, '$.model') = 'gpt-5.6-sol')
326
- LIMIT ${workDecisionMaxRecords + 1}`).all());
327
- if (linkedMetadata.length > workDecisionMaxRecords) {
328
- throw new WorkStoreError({ cause: linkedMetadata.length, operation: "open.migrate.metadata-capacity" });
329
- }
330
- const metadataCounts = new Map();
331
- const dispatchCounts = new Map();
332
- const decisionCounts = new Map();
333
- for (const metadata of linkedMetadata) {
334
- metadataCounts.set(metadata.dispatchRequestId, (metadataCounts.get(metadata.dispatchRequestId) ?? 0) + 1);
531
+ if (operations._tag === "capacity") {
532
+ throw new WorkStoreError({ cause: operations, operation: "open.migrate.lane-operation-capacity" });
533
+ }
534
+ for (const { lane } of migratedClaims) {
535
+ update.run(lane.goalId, lane.operationId, lane.phase, JSON.stringify(lane), lane.laneId);
536
+ }
537
+ if (hasLedger && operations.inserts.length > 0) {
538
+ const recordOperation = database.prepare(`INSERT INTO work_lane_operations
539
+ (operation_id, lane_id, goal_id, phase, revision, record) VALUES (?, ?, ?, ?, ?, ?)`);
540
+ for (const lane of operations.inserts) {
541
+ recordOperation.run(lane.operationId, lane.laneId, lane.goalId, lane.phase, lane.revision, JSON.stringify(lane));
335
542
  }
543
+ }
544
+ }
545
+ if (decisionColumns.length > 0 && !decisionColumns.includes("session_id")) {
546
+ database.exec("ALTER TABLE work_decision_handoffs ADD COLUMN session_id TEXT");
547
+ const update = database.prepare("UPDATE work_decision_handoffs SET session_id = ?, record = ? WHERE handoff_id = ?");
548
+ for (const decision of decisions)
549
+ update.run(decision.sessionId, JSON.stringify(decision), decision.id);
550
+ if (dispatches.length > 0) {
551
+ const updateDispatch = database.prepare("UPDATE work_dispatch_handoffs SET record = ? WHERE dispatch_request_id = ?");
552
+ const updateMetadata = tables.includes("orchestrator_dispatch_metadata")
553
+ ? database.prepare("UPDATE orchestrator_dispatch_metadata SET work_link = ? WHERE dispatch_request_id = ?")
554
+ : null;
336
555
  for (const dispatch of dispatches) {
337
- const identity = JSON.stringify([dispatch.dispatchRequestId, dispatch.handoffId]);
338
- dispatchCounts.set(identity, (dispatchCounts.get(identity) ?? 0) + 1);
339
- }
340
- for (const decision of decisionIdentities) {
341
- decisionCounts.set(decision.handoffId, (decisionCounts.get(decision.handoffId) ?? 0) + 1);
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
- });
556
+ const decision = decisions.find(({ id }) => id === dispatch.handoffId);
557
+ if (decision === undefined)
558
+ continue;
559
+ updateDispatch.run(JSON.stringify(decision), dispatch.dispatchRequestId);
560
+ updateMetadata?.run(JSON.stringify({ handoff: decision, lineage: decision.dispatchIds }), dispatch.dispatchRequestId);
358
561
  }
359
562
  }
360
- const lanes = laneColumns.length > 0 && !laneColumns.includes("goal_id")
361
- ? Schema.decodeUnknownSync(Schema.Array(LegacyLaneStoredRow))(database.prepare("SELECT lane_id AS laneId, revision, record FROM work_lane_claims").all()).map((row) => {
362
- const legacy = Schema.decodeUnknownSync(LegacyLaneRecord)(JSON.parse(row.record));
363
- if (legacy.laneId !== row.laneId || legacy.revision !== row.revision) {
364
- throw new WorkStoreError({ cause: { legacy, row }, operation: "open.migrate.lane-identity" });
365
- }
366
- return Schema.decodeUnknownSync(WorkLaneClaimed)({
367
- ...legacy,
368
- goalId: legacy.laneId,
369
- operationId: legacy.laneId
370
- });
371
- })
372
- : [];
373
- const decisions = decisionColumns.length > 0 && !decisionColumns.includes("session_id")
374
- ? Schema.decodeUnknownSync(Schema.Array(LegacyDecisionStoredRow))(database.prepare("SELECT handoff_id AS handoffId, record FROM work_decision_handoffs").all()).map((row) => {
375
- const legacy = Schema.decodeUnknownSync(LegacyDecisionRecord)(JSON.parse(row.record));
376
- if (legacy.id !== row.handoffId) {
377
- throw new WorkStoreError({ cause: { legacy, row }, operation: "open.migrate.handoff-identity" });
378
- }
379
- const matchingDispatches = dispatches.filter(({ handoffId }) => handoffId === legacy.id);
380
- const dispatch = matchingDispatches[0];
381
- const lane = lanes.find(({ laneId }) => laneId === legacy.laneId);
382
- if (lane === undefined) {
383
- throw new WorkStoreError({ cause: legacy, operation: "open.migrate.handoff-lane" });
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) {
563
+ }
564
+ if (decisionColumns.length > 0) {
565
+ const storedDecisions = Schema.decodeUnknownSync(Schema.Array(DecisionRow))(database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
566
+ occurred_at AS occurredAt, record
567
+ FROM work_decision_handoffs`).all());
568
+ const updateDecision = database.prepare("UPDATE work_decision_handoffs SET record = ? WHERE handoff_id = ?");
569
+ const updateDispatch = tables.includes("work_dispatch_handoffs")
570
+ ? database.prepare("UPDATE work_dispatch_handoffs SET record = ? WHERE handoff_id = ?")
571
+ : null;
572
+ const updateMetadata = tables.includes("orchestrator_dispatch_metadata")
573
+ ? database.prepare(`UPDATE orchestrator_dispatch_metadata SET work_link = ?
574
+ WHERE dispatch_request_id = ?`)
575
+ : null;
576
+ const previousDecisions = storedDecisions.flatMap((row) => {
577
+ const input = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Json))(row.record);
578
+ const previous = decodePreviousDecisionHandoff(input);
579
+ const handoffDispatches = tables.includes("work_dispatch_handoffs")
580
+ ? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
581
+ lane_id AS laneId, occurred_at AS occurredAt, lineage, record
582
+ FROM work_dispatch_handoffs WHERE handoff_id = ?`).all(row.handoffId))
583
+ : [];
584
+ switch (previous._tag) {
585
+ case "current": {
586
+ if (previous.value.id !== row.handoffId || previous.value.sessionId !== row.sessionId ||
587
+ previous.value.laneId !== row.laneId || previous.value.occurredAt !== row.occurredAt) {
407
588
  throw new WorkStoreError({
408
- cause: { dispatch, metadataRows },
409
- operation: "open.migrate.legacy-metadata-authority"
589
+ cause: { handoff: previous.value, row },
590
+ operation: "open.migrate.handoff-identity"
410
591
  });
411
592
  }
412
- if (metadata.workLink === null) {
593
+ if (handoffDispatches.length > 1) {
413
594
  throw new WorkStoreError({
414
- cause: { dispatch, metadata },
415
- operation: "open.migrate.legacy-metadata-authority"
595
+ cause: { handoffDispatches, row },
596
+ operation: "open.migrate.dispatch-cardinality"
416
597
  });
417
598
  }
418
- const workLink = Schema.decodeUnknownSync(LegacyMetadataWorkLink)(JSON.parse(metadata.workLink));
419
- requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.legacy-metadata-authority");
420
- if (!legacyDecisionEquivalent(workLink.handoff, legacy) ||
421
- !workDispatchLineageEquivalent(workLink.lineage, dispatchIds)) {
599
+ const linkedMetadata = tables.includes("orchestrator_dispatch_metadata")
600
+ ? Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
601
+ FROM orchestrator_dispatch_metadata
602
+ WHERE work_link IS NOT NULL
603
+ AND CASE WHEN json_valid(work_link)
604
+ THEN json_extract(work_link, '$.handoff.id') END = ?`).all(row.handoffId))
605
+ : [];
606
+ if (tables.includes("orchestrator_dispatch_metadata") &&
607
+ (linkedMetadata.length !== handoffDispatches.length ||
608
+ linkedMetadata.some((metadata) => !handoffDispatches.some(({ dispatchRequestId }) => dispatchRequestId === metadata.dispatchRequestId)))) {
422
609
  throw new WorkStoreError({
423
- cause: { legacy, workLink },
424
- operation: "open.migrate.legacy-metadata-authority"
610
+ cause: { handoffDispatches, linkedMetadata, row },
611
+ operation: "open.migrate.metadata-authority"
425
612
  });
426
613
  }
427
- }
428
- const bindingRows = tables.includes("work_agent_bindings")
429
- ? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
430
- expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
431
- FROM work_agent_bindings
432
- WHERE dispatch_request_id = ?`).all(dispatch.dispatchRequestId))
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
- });
614
+ for (const dispatch of handoffDispatches) {
615
+ const dispatchAuthority = (() => {
616
+ try {
617
+ return {
618
+ handoff: Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(dispatch.record)),
619
+ lineage: Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage))
620
+ };
567
621
  }
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
- });
622
+ catch (cause) {
623
+ throw new WorkStoreError({ cause, operation: "open.migrate.decode-dispatch-authority" });
576
624
  }
577
- const workLink = (() => {
578
- try {
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
- });
592
- }
593
- requireCurrentAgentBindingAuthority(dispatch.dispatchRequestId, previous.value, "open.migrate.current-agent-binding");
625
+ })();
626
+ if (dispatch.laneId !== previous.value.laneId ||
627
+ dispatch.occurredAt !== previous.value.occurredAt ||
628
+ !currentDecisionHandoffEquivalent(dispatchAuthority.handoff, previous.value) ||
629
+ !workDispatchLineageContainedBy(dispatchAuthority.lineage, previous.value.dispatchIds)) {
630
+ throw new WorkStoreError({
631
+ cause: { dispatch, dispatchAuthority, handoff: previous.value },
632
+ operation: "open.migrate.dispatch-authority"
633
+ });
594
634
  }
595
- return [];
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")) {
635
+ if (!tables.includes("orchestrator_dispatch_metadata"))
636
+ continue;
679
637
  const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
680
638
  if (metadata?.workLink === null || metadata?.workLink === undefined) {
681
639
  throw new WorkStoreError({
@@ -683,116 +641,227 @@ const migrateLegacyAuthorityTables = (database) => {
683
641
  operation: "open.migrate.metadata-authority"
684
642
  });
685
643
  }
686
- const workLink = Schema.decodeUnknownSync(PreviousMetadataWorkLink)(JSON.parse(metadata.workLink));
644
+ const workLink = (() => {
645
+ try {
646
+ return Schema.decodeUnknownSync(CurrentMetadataWorkLink)(JSON.parse(metadata.workLink));
647
+ }
648
+ catch (cause) {
649
+ throw new WorkStoreError({ cause, operation: "open.migrate.decode-metadata-authority" });
650
+ }
651
+ })();
687
652
  requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.metadata-authority");
688
- if (!previousDecisionHandoffEquivalent(workLink.handoff, previous) ||
689
- !workDispatchLineageEquivalent(workLink.lineage, lineage)) {
653
+ if (!currentDecisionHandoffEquivalent(workLink.handoff, previous.value) ||
654
+ !workDispatchLineageEquivalent(workLink.lineage, dispatchAuthority.lineage)) {
690
655
  throw new WorkStoreError({
691
- cause: { previous, workLink },
656
+ cause: { handoff: previous.value, workLink },
692
657
  operation: "open.migrate.metadata-authority"
693
658
  });
694
659
  }
660
+ requireCurrentAgentBindingAuthority(dispatch.dispatchRequestId, previous.value, "open.migrate.current-agent-binding");
695
661
  }
696
- return { dispatch, lineage };
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);
662
+ return [];
708
663
  }
664
+ case "invalid":
665
+ throw new WorkStoreError({
666
+ cause: previous.cause,
667
+ operation: "open.migrate.invalid-handoff"
668
+ });
669
+ case "previous":
670
+ return [{ previous: previous.value, row }];
671
+ }
672
+ });
673
+ for (const { previous, row } of previousDecisions) {
674
+ const laneRevision = Schema.decodeUnknownSync(LaneRevisionRow)(database.prepare("SELECT revision FROM work_lane_claims WHERE lane_id = ?").get(row.laneId));
675
+ const handoffDispatches = tables.includes("work_dispatch_handoffs")
676
+ ? Schema.decodeUnknownSync(Schema.Array(LegacyDispatchStoredRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, handoff_id AS handoffId,
677
+ lane_id AS laneId, occurred_at AS occurredAt, lineage, record
678
+ FROM work_dispatch_handoffs WHERE handoff_id = ?`).all(row.handoffId))
679
+ : [];
680
+ if (handoffDispatches.length > 1) {
681
+ throw new WorkStoreError({
682
+ cause: { handoffDispatches, row },
683
+ operation: "open.migrate.dispatch-cardinality"
684
+ });
685
+ }
686
+ const linkedMetadata = tables.includes("orchestrator_dispatch_metadata")
687
+ ? Schema.decodeUnknownSync(Schema.Array(MetadataWorkLinkRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, route, work_link AS workLink
688
+ FROM orchestrator_dispatch_metadata
689
+ WHERE work_link IS NOT NULL
690
+ AND CASE WHEN json_valid(work_link)
691
+ THEN json_extract(work_link, '$.handoff.id') END = ?`).all(row.handoffId))
692
+ : [];
693
+ if (tables.includes("orchestrator_dispatch_metadata") &&
694
+ (linkedMetadata.length !== handoffDispatches.length ||
695
+ linkedMetadata.some((metadata) => !handoffDispatches.some(({ dispatchRequestId }) => dispatchRequestId === metadata.dispatchRequestId)))) {
696
+ throw new WorkStoreError({
697
+ cause: { handoffDispatches, linkedMetadata, row },
698
+ operation: "open.migrate.metadata-authority"
699
+ });
700
+ }
701
+ const bindingRows = tables.includes("work_agent_bindings") && tables.includes("work_dispatch_handoffs")
702
+ ? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT binding.dispatch_request_id AS dispatchRequestId, binding.lane_id AS laneId,
703
+ binding.expected_revision AS expectedRevision, binding.revision,
704
+ binding.agent_id AS agentId, binding.host, binding.record
705
+ FROM work_agent_bindings binding
706
+ JOIN work_dispatch_handoffs dispatch
707
+ ON dispatch.dispatch_request_id = binding.dispatch_request_id
708
+ WHERE dispatch.handoff_id = ?`).all(row.handoffId))
709
+ : [];
710
+ const bindingRow = bindingRows[0];
711
+ const handoffDispatch = bindingRow === undefined
712
+ ? undefined
713
+ : handoffDispatches.find(({ dispatchRequestId }) => dispatchRequestId === bindingRow.dispatchRequestId);
714
+ if (bindingRows.length !== 1 || bindingRow === undefined || handoffDispatch === undefined) {
715
+ throw new WorkStoreError({ cause: { bindingRows, row }, operation: "open.migrate.handoff-revision" });
709
716
  }
710
- if (decisions.length > 0 || previousDecisions.length > 0) {
711
- const migratedLedger = Schema.decodeUnknownSync(LedgerBytesRow)(database.prepare(`SELECT COALESCE(SUM(
712
- length(CAST(handoff_id AS BLOB)) + length(CAST(record AS BLOB))
713
- ), 0) AS bytes
714
- FROM work_decision_handoffs`).get());
715
- if (migratedLedger.bytes > workDecisionMaxBytes) {
717
+ const bindingDecision = decodeAgentBindingRow(bindingRow, { dispatchRequestId: handoffDispatch.dispatchRequestId, laneId: previous.laneId }, "open.migrate.agent-binding");
718
+ if (bindingDecision._tag === "invalid")
719
+ throw bindingDecision.error;
720
+ if (laneRevision.revision < bindingDecision.binding.lane.revision) {
721
+ throw new WorkStoreError({
722
+ cause: { binding: bindingDecision.binding, laneRevision, previous },
723
+ operation: "open.migrate.handoff-lane-revision"
724
+ });
725
+ }
726
+ if (bindingDecision.binding.lane.goalId !== previous.goalId) {
727
+ throw new WorkStoreError({
728
+ cause: { binding: bindingDecision.binding, previous },
729
+ operation: "open.migrate.agent-binding.goal"
730
+ });
731
+ }
732
+ requireBindingCompanions(bindingDecision.binding, "open.migrate.agent-binding");
733
+ requireLifecycleAuthority(bindingDecision.binding.request.dispatchRequestId, bindingDecision.binding.checkpoint.occurredAt, "open.migrate.agent-binding.lifecycle");
734
+ const verifiedDispatches = handoffDispatches.map((dispatch) => {
735
+ const dispatchHandoff = Schema.decodeUnknownSync(PreviousWorkDecisionHandoff)(JSON.parse(dispatch.record));
736
+ const lineage = Schema.decodeUnknownSync(WorkDispatchHandoff.fields.lineage)(JSON.parse(dispatch.lineage));
737
+ if (dispatch.laneId !== previous.laneId || dispatch.occurredAt !== previous.occurredAt ||
738
+ !previousDecisionHandoffEquivalent(dispatchHandoff, previous) ||
739
+ !workDispatchLineageContainedBy(lineage, previous.dispatchIds)) {
716
740
  throw new WorkStoreError({
717
- cause: migratedLedger,
718
- operation: "open.migrate.handoff-capacity"
741
+ cause: { dispatch, dispatchHandoff, previous },
742
+ operation: "open.migrate.dispatch-authority"
719
743
  });
720
744
  }
721
- }
722
- }
723
- if (tables.includes("orchestrator_dispatch_metadata") && coordinatorDispatchColumns.includes("is_routed")) {
724
- const pageSize = 512;
725
- let cursor = null;
726
- while (true) {
727
- const input = cursor === null
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");
745
+ if (tables.includes("orchestrator_dispatch_metadata")) {
746
+ const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
747
+ if (metadata?.workLink === null || metadata?.workLink === undefined) {
748
+ throw new WorkStoreError({
749
+ cause: { dispatch, metadata },
750
+ operation: "open.migrate.metadata-authority"
751
+ });
761
752
  }
762
- catch (cause) {
763
- if (Schema.is(WorkStoreError)(cause))
764
- throw cause;
765
- throw new WorkStoreError({ cause, operation: "open.migrate.metadata-route" });
753
+ const workLink = Schema.decodeUnknownSync(PreviousMetadataWorkLink)(JSON.parse(metadata.workLink));
754
+ requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.metadata-authority");
755
+ if (!previousDecisionHandoffEquivalent(workLink.handoff, previous) ||
756
+ !workDispatchLineageEquivalent(workLink.lineage, lineage)) {
757
+ throw new WorkStoreError({
758
+ cause: { previous, workLink },
759
+ operation: "open.migrate.metadata-authority"
760
+ });
766
761
  }
767
762
  }
768
- const last = rows.at(-1);
769
- if (rows.length < pageSize || last === undefined)
770
- break;
771
- cursor = last.rowId;
763
+ return { dispatch, lineage };
764
+ });
765
+ const migrated = upgradePreviousDecisionHandoff(previous, bindingDecision.binding.request.expectedRevision);
766
+ if (migrated.id !== row.handoffId || migrated.sessionId !== row.sessionId ||
767
+ migrated.laneId !== row.laneId || migrated.occurredAt !== row.occurredAt) {
768
+ throw new WorkStoreError({ cause: { migrated, row }, operation: "open.migrate.handoff-identity" });
769
+ }
770
+ const encoded = JSON.stringify(migrated);
771
+ updateDecision.run(encoded, row.handoffId);
772
+ updateDispatch?.run(encoded, row.handoffId);
773
+ for (const { dispatch, lineage } of verifiedDispatches) {
774
+ updateMetadata?.run(JSON.stringify({ handoff: migrated, lineage }), dispatch.dispatchRequestId);
772
775
  }
773
- const invalidRoutedMetadata = Schema.decodeUnknownSync(Schema.Array(RoutedMetadataCardinalityRow))(database.prepare(`SELECT dispatch.dispatch_request_id AS dispatchRequestId,
774
- COUNT(metadata.dispatch_request_id) AS metadataCount
775
- FROM orchestrator_dispatches AS dispatch
776
- LEFT JOIN orchestrator_dispatch_metadata AS metadata
777
- ON metadata.dispatch_request_id = dispatch.dispatch_request_id
778
- WHERE dispatch.is_routed = 1
779
- GROUP BY dispatch.dispatch_request_id
780
- HAVING COUNT(metadata.dispatch_request_id) <> 1
781
- LIMIT 1`).all())[0];
782
- if (invalidRoutedMetadata !== undefined) {
776
+ }
777
+ if (decisions.length > 0 || previousDecisions.length > 0) {
778
+ const migratedLedger = Schema.decodeUnknownSync(LedgerBytesRow)(database.prepare(`SELECT COALESCE(SUM(
779
+ length(CAST(handoff_id AS BLOB)) + length(CAST(record AS BLOB))
780
+ ), 0) AS bytes
781
+ FROM work_decision_handoffs`).get());
782
+ if (migratedLedger.bytes > workDecisionMaxBytes) {
783
783
  throw new WorkStoreError({
784
- cause: invalidRoutedMetadata,
785
- operation: "open.migrate.routed-metadata"
784
+ cause: migratedLedger,
785
+ operation: "open.migrate.handoff-capacity"
786
786
  });
787
787
  }
788
788
  }
789
- database.exec("COMMIT");
790
- transaction = false;
791
789
  }
792
- catch (error) {
793
- if (transaction)
794
- database.exec("ROLLBACK");
795
- throw error;
790
+ // Reported after capacity: lane CAS allows one binding per revision, so
791
+ // several mean the file is inconsistent rather than merely large.
792
+ const ambiguousClaims = migratedClaims.filter(({ _tag }) => _tag === "ambiguous");
793
+ if (ambiguousClaims.length > 0) {
794
+ throw new WorkStoreError({ cause: { claims: ambiguousClaims }, operation: "open.migrate.lane-binding-ambiguous" });
795
+ }
796
+ const mismatchedClaims = migratedClaims.filter(({ _tag }) => _tag === "mismatch");
797
+ if (mismatchedClaims.length > 0) {
798
+ throw new WorkStoreError({ cause: { claims: mismatchedClaims }, operation: "open.migrate.lane-binding-mismatch" });
799
+ }
800
+ if (tables.includes("orchestrator_dispatch_metadata") && coordinatorDispatchColumns.includes("is_routed")) {
801
+ const pageSize = 512;
802
+ let cursor = null;
803
+ while (true) {
804
+ const input = cursor === null
805
+ ? database.prepare(`SELECT CAST(metadata.rowid AS TEXT) AS rowId,
806
+ metadata.dispatch_request_id AS dispatchRequestId,
807
+ dispatch.activity_idempotency_key AS activityIdempotencyKey,
808
+ dispatch.command, dispatch.is_routed AS isRouted,
809
+ CASE WHEN metadata.work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
810
+ metadata.route
811
+ FROM orchestrator_dispatch_metadata AS metadata
812
+ LEFT JOIN orchestrator_dispatches AS dispatch
813
+ ON dispatch.dispatch_request_id = metadata.dispatch_request_id
814
+ ORDER BY metadata.rowid ASC LIMIT ?`).all(pageSize)
815
+ : database.prepare(`SELECT CAST(metadata.rowid AS TEXT) AS rowId,
816
+ metadata.dispatch_request_id AS dispatchRequestId,
817
+ dispatch.activity_idempotency_key AS activityIdempotencyKey,
818
+ dispatch.command, dispatch.is_routed AS isRouted,
819
+ CASE WHEN metadata.work_link IS NULL THEN 0 ELSE 1 END AS hasWorkLink,
820
+ metadata.route
821
+ FROM orchestrator_dispatch_metadata AS metadata
822
+ LEFT JOIN orchestrator_dispatches AS dispatch
823
+ ON dispatch.dispatch_request_id = metadata.dispatch_request_id
824
+ WHERE metadata.rowid > CAST(? AS INTEGER)
825
+ ORDER BY metadata.rowid ASC LIMIT ?`).all(cursor, pageSize);
826
+ const rows = Schema.decodeUnknownSync(Schema.Array(CoordinatorMetadataRouteRow))(input);
827
+ for (const row of rows) {
828
+ try {
829
+ const isLegacyUnroutedOrphan = row.activityIdempotencyKey === null && row.command === null &&
830
+ row.isRouted === null && row.route === null && row.hasWorkLink === 0;
831
+ if (isLegacyUnroutedOrphan)
832
+ continue;
833
+ if (row.activityIdempotencyKey === null || row.command === null ||
834
+ row.route === null || row.isRouted === null) {
835
+ throw new WorkStoreError({ cause: row, operation: "open.migrate.metadata-route" });
836
+ }
837
+ requireCoordinatorRouteBinding(row.command, row.activityIdempotencyKey, row.route, row.hasWorkLink === 1, { _tag: "routed_discriminator", isRouted: row.isRouted }, "open.migrate.metadata-route");
838
+ }
839
+ catch (cause) {
840
+ if (Schema.is(WorkStoreError)(cause))
841
+ throw cause;
842
+ throw new WorkStoreError({ cause, operation: "open.migrate.metadata-route" });
843
+ }
844
+ }
845
+ const last = rows.at(-1);
846
+ if (rows.length < pageSize || last === undefined)
847
+ break;
848
+ cursor = last.rowId;
849
+ }
850
+ const invalidRoutedMetadata = Schema.decodeUnknownSync(Schema.Array(RoutedMetadataCardinalityRow))(database.prepare(`SELECT dispatch.dispatch_request_id AS dispatchRequestId,
851
+ COUNT(metadata.dispatch_request_id) AS metadataCount
852
+ FROM orchestrator_dispatches AS dispatch
853
+ LEFT JOIN orchestrator_dispatch_metadata AS metadata
854
+ ON metadata.dispatch_request_id = dispatch.dispatch_request_id
855
+ WHERE dispatch.is_routed = 1
856
+ GROUP BY dispatch.dispatch_request_id
857
+ HAVING COUNT(metadata.dispatch_request_id) <> 1
858
+ LIMIT 1`).all())[0];
859
+ if (invalidRoutedMetadata !== undefined) {
860
+ throw new WorkStoreError({
861
+ cause: invalidRoutedMetadata,
862
+ operation: "open.migrate.routed-metadata"
863
+ });
864
+ }
796
865
  }
797
866
  };
798
867
  const readTransactionLedgerTotals = (database) => Schema.decodeUnknownSync(TransactionLedgerTotalsRow)(database.prepare(`SELECT transaction_count AS transactionCount, transaction_bytes AS transactionBytes
@@ -801,6 +870,7 @@ const readDecisionLedgerTotals = (database) => Schema.decodeUnknownSync(Decision
801
870
  FROM work_decision_totals WHERE singleton = 1`).get());
802
871
  const readLaneOperationLedgerTotals = (database) => Schema.decodeUnknownSync(LaneOperationLedgerTotalsRow)(database.prepare(`SELECT operation_count AS operationCount, operation_bytes AS operationBytes
803
872
  FROM work_lane_operation_totals WHERE singleton = 1`).get());
873
+ const ObservationIdRow = Schema.Struct({ observationId: Schema.String });
804
874
  const ReassignmentRow = Schema.Struct({ approvalJobId: Schema.String, goalId: Schema.String, record: Schema.String });
805
875
  /** Applies a reassignment's agent choice; `keep` is only reached when the goal has no target. */
806
876
  const withReassignedAgent = (goal, toAgent) => {
@@ -953,11 +1023,11 @@ const admissionState = (database, target) => {
953
1023
  (claim.worktree === target.worktree && claim.laneId !== target.laneId)) ||
954
1024
  authority.latest.some(({ request }) => request.worker.agentId === target.worker.agentId && request.laneId !== target.laneId) ||
955
1025
  // A session id stays claimed by its lane even after a reassignment supersedes that binding.
956
- bindings.some(({ request }) => (request.prospectiveAdmission?.sessionId === target.sessionId && request.laneId !== target.laneId) ||
1026
+ bindings.some(({ request }) => (admissionEvidence(request)?.sessionId === target.sessionId && request.laneId !== target.laneId) ||
957
1027
  (request.existingGoalRecovery?.sessionId === target.sessionId && request.laneId !== target.laneId)) || authority.ambiguousLanes.has(target.laneId);
958
1028
  const exact = goal !== undefined && lane !== undefined && binding !== undefined &&
959
- goal.goal.id === target.goalId && goal.goal.state !== "completed" &&
960
- goal.goal.state !== "deployed" && goal.goal.goalFamily?.role === "canonical" &&
1029
+ goal.goal.id === target.goalId && !isTerminalWorkState(goal.goal.state) &&
1030
+ goal.goal.goalFamily?.role === "canonical" &&
961
1031
  goal.goal.goalFamily.canonicalGoalId === target.goalId &&
962
1032
  Equal.equals(goal.goal.owner, target.owner) &&
963
1033
  lane.goalId === target.goalId && lane.head === target.head &&
@@ -965,9 +1035,9 @@ const admissionState = (database, target) => {
965
1035
  lane.phase !== "shipped" && Equal.equals(lane.owner, target.owner) &&
966
1036
  binding.lane.laneId === target.laneId &&
967
1037
  Equal.equals(binding.request.worker, target.worker) &&
968
- (binding.request.prospectiveAdmission === undefined || (binding.request.prospectiveAdmission.sessionId === target.sessionId &&
969
- binding.request.prospectiveAdmission.workAssignment === target.expectedWork &&
970
- binding.request.prospectiveAdmission.baseHead === target.baseHead)) &&
1038
+ (admissionEvidence(binding.request) === undefined || (admissionEvidence(binding.request)?.sessionId === target.sessionId &&
1039
+ admissionEvidence(binding.request)?.workAssignment === target.expectedWork &&
1040
+ admissionEvidence(binding.request)?.baseHead === target.baseHead)) &&
971
1041
  (binding.request.existingGoalRecovery === undefined ||
972
1042
  (binding.request.existingGoalRecovery.sessionId === target.sessionId &&
973
1043
  binding.request.existingGoalRecovery.workAssignment === target.expectedWork &&
@@ -994,7 +1064,7 @@ const admissionState = (database, target) => {
994
1064
  entry.goalFamily?.canonicalGoalId === target.goalId ||
995
1065
  entry.agentHierarchy?.agent.agentId === target.worker.agentId) || lanes.entries.some(({ claim }) => claim.laneId === target.laneId || claim.goalId === target.goalId ||
996
1066
  claim.worktree === target.worktree) || authority.latest.some(({ request }) => request.worker.agentId === target.worker.agentId) ||
997
- bindings.some(({ request }) => request.prospectiveAdmission?.sessionId === target.sessionId ||
1067
+ bindings.some(({ request }) => admissionEvidence(request)?.sessionId === target.sessionId ||
998
1068
  request.existingGoalRecovery?.sessionId === target.sessionId);
999
1069
  if (conflicts) {
1000
1070
  return { _tag: "conflict", target, reason: "durable PR, goal, lane, worker, or worktree is already bound" };
@@ -1131,8 +1201,7 @@ const recoveryState = (database, target) => {
1131
1201
  goal.connectTarget !== null ||
1132
1202
  (goal.agentHierarchy !== undefined && goal.agentHierarchy !== null) ||
1133
1203
  (goal.review?.url !== null && goal.review?.url !== undefined && goal.review.url !== target.reviewUrl) ||
1134
- goal.state === "completed" ||
1135
- goal.state === "deployed")) {
1204
+ isTerminalWorkState(goal.state))) {
1136
1205
  return conflict("canonical goal history is linked, terminal, or belongs to another owner");
1137
1206
  }
1138
1207
  const bindingRows = Schema.decodeUnknownSync(AgentBindingRows)(database
@@ -1165,7 +1234,7 @@ const recoveryState = (database, target) => {
1165
1234
  operations.claims.some(({ goalId, laneId, worktree }) => goalId === target.goalId || laneId === target.laneId || worktree === target.worktree) ||
1166
1235
  bindings.some(({ request }) => request.laneId === target.laneId ||
1167
1236
  request.worker.agentId === target.worker.agentId ||
1168
- request.prospectiveAdmission?.sessionId === target.sessionId ||
1237
+ admissionEvidence(request)?.sessionId === target.sessionId ||
1169
1238
  request.existingGoalRecovery?.sessionId === target.sessionId) ||
1170
1239
  decisions.some(({ record }) => {
1171
1240
  const handoff = Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(record));
@@ -1193,6 +1262,140 @@ const readRecoveryState = (database, target) => {
1193
1262
  throw cause;
1194
1263
  }
1195
1264
  };
1265
+ const ReassignmentApprovalRow = Schema.Struct({ approvalJobId: Schema.String, goalId: Schema.String });
1266
+ const ReconcilerEventRow = Schema.Struct({ eventId: Schema.String, goalId: Schema.String });
1267
+ const ObservedFactRow = Schema.Struct({
1268
+ subject: Schema.String,
1269
+ observationId: Schema.String,
1270
+ observedAt: Schema.Number,
1271
+ confirmedAt: Schema.Number,
1272
+ record: Schema.String
1273
+ });
1274
+ const StoredFactRow = Schema.Struct({ observationId: Schema.String, confirmedAt: Schema.Number });
1275
+ const FactTotalsRow = Schema.Struct({ count: Schema.Number, bytes: Schema.Number });
1276
+ const EvictionRow = Schema.Struct({ subject: Schema.String, bytes: Schema.Number });
1277
+ const canonicalObservation = (observation) => observation._tag === "agent"
1278
+ ? { ...observation, host: asciiLower(observation.host) }
1279
+ : { ...observation, repository: asciiLower(observation.repository) };
1280
+ /**
1281
+ * Applies each observation in one transaction, then evicts the oldest rows
1282
+ * until facts and failures are each within their bounds.
1283
+ *
1284
+ * - The same facts again keep their first-seen time and move `confirmed_at`.
1285
+ * - Different facts replace the stored ones, unless the stored ones were
1286
+ * confirmed at or after this observation (then it is stale).
1287
+ * - A good read ends the subject's run of failures. A failure starts a run,
1288
+ * or keeps the running one's `since`; one older than the last good read is stale.
1289
+ */
1290
+ const writeObservations = (database, prepared) => {
1291
+ database.exec("BEGIN IMMEDIATE");
1292
+ try {
1293
+ const readFact = database.prepare(`SELECT observation_id AS observationId, confirmed_at AS confirmedAt
1294
+ FROM work_observed_facts WHERE subject = ?`);
1295
+ const upsertFact = database.prepare(`INSERT INTO work_observed_facts (subject, observation_id, observed_at, confirmed_at, record)
1296
+ VALUES (?, ?, ?, ?, ?)
1297
+ ON CONFLICT (subject) DO UPDATE SET observation_id = excluded.observation_id,
1298
+ observed_at = excluded.observed_at, confirmed_at = excluded.confirmed_at, record = excluded.record`);
1299
+ const confirmFact = database.prepare("UPDATE work_observed_facts SET confirmed_at = max(confirmed_at, ?) WHERE subject = ?");
1300
+ // A good read ends a run of failures only if it is newer than the run's
1301
+ // latest failure.
1302
+ const clearFailures = database.prepare("DELETE FROM work_observed_failures WHERE subject = ? AND last_at <= ?");
1303
+ // We keep only the run's first and latest failure, so a run split by a
1304
+ // delayed good read restarts at its latest failure: a real failed read.
1305
+ const trimFailures = database.prepare("UPDATE work_observed_failures SET since = last_at WHERE subject = ? AND since <= ? AND last_at > ?");
1306
+ const endFailures = {
1307
+ run: (subject, observedAt) => {
1308
+ clearFailures.run(subject, observedAt);
1309
+ trimFailures.run(subject, observedAt, observedAt);
1310
+ }
1311
+ };
1312
+ // `since` is the earliest failure of the run; source and reason come from
1313
+ // the latest one, whatever order the failures arrive in.
1314
+ const recordFailure = database.prepare(`INSERT INTO work_observed_failures (subject, source, reason, since, last_at) VALUES (?, ?, ?, ?, ?)
1315
+ ON CONFLICT (subject) DO UPDATE SET
1316
+ since = min(since, excluded.since),
1317
+ source = CASE WHEN excluded.last_at >= last_at THEN excluded.source ELSE source END,
1318
+ reason = CASE WHEN excluded.last_at >= last_at THEN excluded.reason ELSE reason END,
1319
+ last_at = max(last_at, excluded.last_at)`);
1320
+ const outcomes = prepared.map((item) => {
1321
+ if (item._tag === "future")
1322
+ return { _tag: "stale", subject: item.subject };
1323
+ const row = readFact.get(item.subject);
1324
+ const stored = row === undefined ? undefined : Schema.decodeUnknownSync(StoredFactRow)(row);
1325
+ if (item._tag === "unknown") {
1326
+ if (stored !== undefined && stored.confirmedAt >= item.observedAt) {
1327
+ return { _tag: "stale", subject: item.subject };
1328
+ }
1329
+ recordFailure.run(item.subject, item.source, item.reason, item.observedAt, item.observedAt);
1330
+ return { _tag: "unknown", reason: item.reason, subject: item.subject };
1331
+ }
1332
+ if (stored?.observationId === item.observationId) {
1333
+ confirmFact.run(item.observedAt, item.subject);
1334
+ endFailures.run(item.subject, item.observedAt);
1335
+ return { _tag: "unchanged", subject: item.subject };
1336
+ }
1337
+ if (stored !== undefined && stored.confirmedAt >= item.observedAt)
1338
+ return { _tag: "stale", subject: item.subject };
1339
+ upsertFact.run(item.subject, item.observationId, item.observedAt, item.observedAt, item.record);
1340
+ endFailures.run(item.subject, item.observedAt);
1341
+ return { _tag: "stored", subject: item.subject };
1342
+ });
1343
+ // Totals are read once and kept current as rows go, so eviction is one
1344
+ // indexed lookup and delete per row, not a full rescan per row.
1345
+ const evict = (table, age, payload) => {
1346
+ const size = `length(CAST(subject AS BLOB)) + length(CAST(${payload} AS BLOB))`;
1347
+ const totals = Schema.decodeUnknownSync(FactTotalsRow)(database.prepare(`SELECT COUNT(*) AS count, COALESCE(SUM(${size}), 0) AS bytes FROM ${table}`).get());
1348
+ const oldest = database.prepare(`SELECT subject, ${size} AS bytes FROM ${table} ORDER BY ${age} ASC, subject ASC LIMIT 1`);
1349
+ const remove = database.prepare(`DELETE FROM ${table} WHERE subject = ?`);
1350
+ let { bytes, count } = totals;
1351
+ let evicted = 0;
1352
+ while (count > workObservedFactMaxRecords || bytes > workObservedFactMaxBytes) {
1353
+ const row = Schema.decodeUnknownSync(EvictionRow)(oldest.get());
1354
+ remove.run(row.subject);
1355
+ count -= 1;
1356
+ bytes -= row.bytes;
1357
+ evicted += 1;
1358
+ }
1359
+ return evicted;
1360
+ };
1361
+ // The least recently read rows go first: a fact by its last confirmation,
1362
+ // a failure by its latest failed read.
1363
+ const evicted = evict("work_observed_facts", "confirmed_at", "record") +
1364
+ evict("work_observed_failures", "last_at", "reason");
1365
+ database.exec("COMMIT");
1366
+ return { evicted, outcomes };
1367
+ }
1368
+ catch (cause) {
1369
+ if (database.isTransaction)
1370
+ database.exec("ROLLBACK");
1371
+ throw cause;
1372
+ }
1373
+ };
1374
+ const admissionSummary = (provenance) => provenance._tag === "approved"
1375
+ ? `Prospective admission of an existing owner by approved Fleet job ${provenance.approvalJobId}`
1376
+ : `Admission of an observed worker by the reconciler from observation ${provenance.observationId}`;
1377
+ /** The binding-request key that records how a worker was admitted. */
1378
+ const admissionRecord = (provenance, evidence) => provenance._tag === "approved"
1379
+ ? {
1380
+ prospectiveAdmission: {
1381
+ ...evidence,
1382
+ approvalJobId: provenance.approvalJobId,
1383
+ approvalActor: provenance.approvalActor
1384
+ }
1385
+ }
1386
+ : { observedAdmission: { ...evidence, actor: "reconciler", observationId: provenance.observationId } };
1387
+ /** Whether a stored binding was admitted by exactly this provenance and absence evidence (an exact replay). */
1388
+ const sameAdmission = (request, expectedAbsenceToken, provenance) => {
1389
+ if (provenance._tag === "approved") {
1390
+ const stored = request.prospectiveAdmission;
1391
+ return stored !== undefined &&
1392
+ stored.expectedAbsenceToken === expectedAbsenceToken &&
1393
+ stored.approvalJobId === provenance.approvalJobId && stored.approvalActor === provenance.approvalActor;
1394
+ }
1395
+ const stored = request.observedAdmission;
1396
+ return stored !== undefined &&
1397
+ stored.expectedAbsenceToken === expectedAbsenceToken && stored.observationId === provenance.observationId;
1398
+ };
1196
1399
  export class WorkStore {
1197
1400
  #database;
1198
1401
  #cryptoService;
@@ -1209,9 +1412,14 @@ export class WorkStore {
1209
1412
  const opened = yield* openPrivateSqlite(path, {
1210
1413
  busyTimeoutMillis: workStoreBusyTimeoutMillis,
1211
1414
  initialize: (database) => {
1212
- migrateLegacyAuthorityTables(database);
1213
- const hadLaneOperationLedger = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'work_lane_operations'").get() !== undefined;
1214
- database.exec(`
1415
+ // One transaction: legacy migration and schema creation succeed or fail
1416
+ // together, and their statements share one commit instead of syncing
1417
+ // the file once each.
1418
+ database.exec("BEGIN IMMEDIATE");
1419
+ try {
1420
+ migrateLegacyAuthorityTables(database);
1421
+ const hadLaneOperationLedger = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'work_lane_operations'").get() !== undefined;
1422
+ database.exec(`
1215
1423
  CREATE TABLE IF NOT EXISTS work_goal_events (
1216
1424
  event_id TEXT PRIMARY KEY,
1217
1425
  goal_id TEXT NOT NULL,
@@ -1291,6 +1499,13 @@ export class WorkStore {
1291
1499
  goal_id TEXT NOT NULL,
1292
1500
  record TEXT NOT NULL
1293
1501
  );
1502
+ -- One row per approved abandonment, committed with the goal checkpoint whose
1503
+ -- event id equals its approval job id, so work_goal_events bounds it too.
1504
+ CREATE TABLE IF NOT EXISTS work_goal_abandonments (
1505
+ approval_job_id TEXT PRIMARY KEY,
1506
+ goal_id TEXT NOT NULL,
1507
+ record TEXT NOT NULL
1508
+ );
1294
1509
  CREATE TABLE IF NOT EXISTS work_decision_handoffs (
1295
1510
  handoff_id TEXT PRIMARY KEY,
1296
1511
  session_id TEXT NOT NULL UNIQUE,
@@ -1314,22 +1529,45 @@ export class WorkStore {
1314
1529
  END;
1315
1530
  CREATE INDEX IF NOT EXISTS work_decision_handoffs_lane_time
1316
1531
  ON work_decision_handoffs (lane_id, occurred_at, handoff_id);
1317
- CREATE INDEX IF NOT EXISTS work_decision_handoffs_session
1532
+ CREATE UNIQUE INDEX IF NOT EXISTS work_decision_handoffs_session
1318
1533
  ON work_decision_handoffs (session_id);
1534
+ CREATE TABLE IF NOT EXISTS work_observed_facts (
1535
+ subject TEXT PRIMARY KEY,
1536
+ observation_id TEXT NOT NULL,
1537
+ observed_at INTEGER NOT NULL,
1538
+ confirmed_at INTEGER NOT NULL,
1539
+ record TEXT NOT NULL
1540
+ );
1541
+ CREATE INDEX IF NOT EXISTS work_observed_facts_age
1542
+ ON work_observed_facts (confirmed_at, subject);
1543
+ CREATE TABLE IF NOT EXISTS work_reconciler_events (
1544
+ event_id TEXT PRIMARY KEY,
1545
+ goal_id TEXT NOT NULL
1546
+ );
1547
+ CREATE TABLE IF NOT EXISTS work_observed_failures (
1548
+ subject TEXT PRIMARY KEY,
1549
+ source TEXT NOT NULL,
1550
+ reason TEXT NOT NULL,
1551
+ since INTEGER NOT NULL,
1552
+ last_at INTEGER NOT NULL
1553
+ );
1554
+ CREATE INDEX IF NOT EXISTS work_observed_failures_age
1555
+ ON work_observed_failures (last_at, subject);
1319
1556
  `);
1320
- const columns = Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare("PRAGMA table_info(work_goal_events)").all());
1321
- if (!columns.some(({ name }) => name === "transaction_id")) {
1322
- database.exec("ALTER TABLE work_goal_events ADD COLUMN transaction_id TEXT");
1323
- }
1324
- if (!hadLaneOperationLedger) {
1325
- database.exec(`
1557
+ requireUniqueSessionIndex(database);
1558
+ const columns = Schema.decodeUnknownSync(Schema.Array(Schema.Struct({ name: Schema.String })))(database.prepare("PRAGMA table_info(work_goal_events)").all());
1559
+ if (!columns.some(({ name }) => name === "transaction_id")) {
1560
+ database.exec("ALTER TABLE work_goal_events ADD COLUMN transaction_id TEXT");
1561
+ }
1562
+ if (!hadLaneOperationLedger) {
1563
+ database.exec(`
1326
1564
  INSERT OR IGNORE INTO work_lane_operations
1327
1565
  (operation_id, lane_id, goal_id, phase, revision, record)
1328
1566
  SELECT operation_id, lane_id, goal_id, phase, revision, record
1329
1567
  FROM work_lane_claims
1330
1568
  `);
1331
- }
1332
- database.exec(`
1569
+ }
1570
+ database.exec(`
1333
1571
  INSERT OR IGNORE INTO work_lane_operation_totals
1334
1572
  (singleton, operation_count, operation_bytes)
1335
1573
  SELECT 1, COUNT(*), COALESCE(SUM(
@@ -1337,7 +1575,7 @@ export class WorkStore {
1337
1575
  ), 0)
1338
1576
  FROM work_lane_operations
1339
1577
  `);
1340
- database.exec(`
1578
+ database.exec(`
1341
1579
  INSERT OR IGNORE INTO work_goal_transaction_totals
1342
1580
  (singleton, transaction_count, transaction_bytes)
1343
1581
  SELECT 1, COUNT(*), COALESCE(SUM(
@@ -1345,7 +1583,7 @@ export class WorkStore {
1345
1583
  ), 0)
1346
1584
  FROM work_goal_transactions
1347
1585
  `);
1348
- database.exec(`
1586
+ database.exec(`
1349
1587
  INSERT OR REPLACE INTO work_decision_totals
1350
1588
  (singleton, decision_count, decision_bytes)
1351
1589
  SELECT 1, COUNT(*), COALESCE(SUM(
@@ -1353,6 +1591,15 @@ export class WorkStore {
1353
1591
  ), 0)
1354
1592
  FROM work_decision_handoffs
1355
1593
  `);
1594
+ database.exec("COMMIT");
1595
+ }
1596
+ catch (error) {
1597
+ // SQLite may already have rolled back (SQLITE_FULL, IOERR); a second
1598
+ // ROLLBACK would throw and replace the real error.
1599
+ if (database.isTransaction)
1600
+ database.exec("ROLLBACK");
1601
+ throw error;
1602
+ }
1356
1603
  }
1357
1604
  }).pipe(Effect.mapError(fromPrivateDatabaseError));
1358
1605
  return new WorkStore(path, opened, cryptoService);
@@ -1869,6 +2116,155 @@ export class WorkStore {
1869
2116
  return yield* decision.error;
1870
2117
  return decision.result;
1871
2118
  });
2119
+ /**
2120
+ * Applies an approved `work.abandon` job: the goal, owned by exactly the
2121
+ * approved owner and still at the approved head, becomes `abandoned` with its
2122
+ * blocker cleared and a status activity naming the job. A goal with an
2123
+ * active lane, or already finished, is refused; nothing else is written. An
2124
+ * exact replay of the job returns the stored result.
2125
+ */
2126
+ abandon = Effect.fn("WorkStore.abandon")(function* (request) {
2127
+ const decoded = yield* Schema.decodeUnknownEffect(WorkGoalAbandonment)(request).pipe(Effect.mapError(storeError("abandon.decode")));
2128
+ const jobId = decoded.approvalJobId;
2129
+ const observedAt = yield* Clock.currentTimeMillis;
2130
+ yield* this.secureFiles();
2131
+ const decision = yield* Effect.try({
2132
+ try: () => {
2133
+ let transaction = false;
2134
+ const reject = (error) => {
2135
+ this.#database.exec("ROLLBACK");
2136
+ transaction = false;
2137
+ return { _tag: "rejected", error };
2138
+ };
2139
+ try {
2140
+ this.#database.exec("BEGIN IMMEDIATE");
2141
+ transaction = true;
2142
+ const priorRaw = this.#database
2143
+ .prepare(`SELECT approval_job_id AS approvalJobId, goal_id AS goalId, record
2144
+ FROM work_goal_abandonments WHERE approval_job_id = ?`)
2145
+ .get(jobId);
2146
+ if (priorRaw !== undefined) {
2147
+ const row = Schema.decodeUnknownSync(ReassignmentRow)(priorRaw);
2148
+ const prior = Schema.decodeUnknownSync(WorkGoalAbandoned)(JSON.parse(row.record));
2149
+ if (row.approvalJobId !== prior.abandonment.approvalJobId || row.goalId !== prior.abandonment.goalId) {
2150
+ return reject(new WorkStoreError({ cause: { prior, row }, operation: "abandon.replay.identity-mismatch" }));
2151
+ }
2152
+ if (!Equal.equals(prior.abandonment, decoded)) {
2153
+ return reject(new WorkGoalAbandonmentConflictError({ approvalJobId: jobId, reason: "payload_mismatch" }));
2154
+ }
2155
+ this.#database.exec("ROLLBACK");
2156
+ transaction = false;
2157
+ return { _tag: "abandoned", result: prior };
2158
+ }
2159
+ const collision = this.#database
2160
+ .prepare(`SELECT 1 FROM work_goal_events WHERE event_id = ?
2161
+ UNION ALL SELECT 1 FROM work_lane_operations WHERE operation_id = ?
2162
+ UNION ALL SELECT 1 FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 1`)
2163
+ .get(jobId, jobId, jobId);
2164
+ if (collision !== undefined) {
2165
+ return reject(new WorkGoalAbandonmentConflictError({ approvalJobId: jobId, reason: "identifier_in_use" }));
2166
+ }
2167
+ const eventRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingGoalEventRow))(this.#database
2168
+ .prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
2169
+ FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC LIMIT ?`)
2170
+ .all(workHistoryMaxEvents + 1));
2171
+ const history = [];
2172
+ for (const row of eventRows) {
2173
+ const event = decodeAgentBindingGoalEvent(row, "abandon.history");
2174
+ if (event._tag === "invalid")
2175
+ return reject(event.error);
2176
+ history.push(event.checkpoint);
2177
+ }
2178
+ const head = history.filter(({ goal }) => goal.id === decoded.goalId).at(-1);
2179
+ if (head === undefined ||
2180
+ head.eventId !== decoded.expectedGoalEventId ||
2181
+ head.goal.updatedAt !== decoded.expectedGoalUpdatedAt) {
2182
+ return reject(new WorkGoalRevisionConflictError({
2183
+ goalId: decoded.goalId,
2184
+ expectedEventId: decoded.expectedGoalEventId,
2185
+ expectedUpdatedAt: decoded.expectedGoalUpdatedAt,
2186
+ actualEventId: head?.eventId ?? null,
2187
+ actualUpdatedAt: head?.goal.updatedAt ?? null
2188
+ }));
2189
+ }
2190
+ if (!Equal.equals(head.goal.owner, decoded.owner)) {
2191
+ return reject(new WorkGoalOwnerMismatchError({
2192
+ goalId: decoded.goalId,
2193
+ laneId: null,
2194
+ expectedOwner: { id: decoded.owner.id, name: decoded.owner.name },
2195
+ actualOwner: { id: head.goal.owner.id, name: head.goal.owner.name }
2196
+ }));
2197
+ }
2198
+ if (isTerminalWorkState(head.goal.state)) {
2199
+ return reject(new WorkGoalTerminalError({ goalId: decoded.goalId, state: head.goal.state }));
2200
+ }
2201
+ const ledger = readValidatedLaneLedger(this.#database, "abandon.lane");
2202
+ if (ledger._tag === "invalid")
2203
+ return reject(ledger.error);
2204
+ const active = ledger.entries.find(({ claim }) => claim.goalId === decoded.goalId && claim.phase !== "shipped");
2205
+ if (active !== undefined) {
2206
+ return reject(new WorkGoalLaneActiveError({ goalId: decoded.goalId, laneId: active.claim.laneId }));
2207
+ }
2208
+ // occurredAt equals updatedAt for every valid checkpoint; both bound the ordering defensively.
2209
+ const at = Math.max(observedAt, head.occurredAt + 1, head.goal.updatedAt + 1);
2210
+ const existingActivity = head.goal.activity ?? [];
2211
+ const goal = Schema.decodeUnknownSync(WorkGoal)({
2212
+ ...withoutBlockers(head.goal),
2213
+ state: "abandoned",
2214
+ // A full activity list keeps the owner's entries and takes no note; the
2215
+ // checkpoint's event id and the abandonments table still name the job.
2216
+ activity: existingActivity.length >= goalActivityLimit ? existingActivity : [
2217
+ ...existingActivity,
2218
+ {
2219
+ id: jobId,
2220
+ kind: "status",
2221
+ summary: workAbandonActivitySummary(decoded, jobId, decoded.approvalHash),
2222
+ occurredAt: at
2223
+ }
2224
+ ],
2225
+ updatedAt: at
2226
+ });
2227
+ const checkpoint = Schema.decodeUnknownSync(WorkGoalCheckpoint)({
2228
+ version: "herdr.work.event.v1",
2229
+ eventId: jobId,
2230
+ occurredAt: at,
2231
+ goal
2232
+ });
2233
+ const result = Schema.decodeUnknownSync(WorkGoalAbandoned)({ abandonment: decoded, checkpoint });
2234
+ const familyError = validateGoalFamilyHistory([...history, checkpoint]);
2235
+ if (familyError !== undefined)
2236
+ return reject(familyError);
2237
+ const capacity = agentBindingAdmissionError({
2238
+ history,
2239
+ candidate: checkpoint,
2240
+ operationCount: 0,
2241
+ operationBytes: 0,
2242
+ candidateOperationBytes: 0
2243
+ });
2244
+ if (capacity !== undefined)
2245
+ return reject(capacity);
2246
+ this.#database
2247
+ .prepare("INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record) VALUES (?, ?, ?, ?)")
2248
+ .run(checkpoint.eventId, checkpoint.goal.id, checkpoint.occurredAt, JSON.stringify(checkpoint));
2249
+ this.#database
2250
+ .prepare("INSERT INTO work_goal_abandonments (approval_job_id, goal_id, record) VALUES (?, ?, ?)")
2251
+ .run(jobId, decoded.goalId, JSON.stringify(result));
2252
+ this.#database.exec("COMMIT");
2253
+ transaction = false;
2254
+ return { _tag: "abandoned", result };
2255
+ }
2256
+ catch (cause) {
2257
+ if (transaction)
2258
+ this.#database.exec("ROLLBACK");
2259
+ throw cause;
2260
+ }
2261
+ },
2262
+ catch: storeError("abandon.transaction")
2263
+ });
2264
+ if (decision._tag === "rejected")
2265
+ return yield* decision.error;
2266
+ return decision.result;
2267
+ });
1872
2268
  admissionPreflight = Effect.fn("WorkStore.admissionPreflight")(function* (target) {
1873
2269
  const decoded = yield* Schema.decodeUnknownEffect(WorkAdmissionTarget)(target).pipe(Effect.mapError(storeError("admission.preflight.decode")));
1874
2270
  const state = yield* Effect.try({
@@ -1881,8 +2277,27 @@ export class WorkStore {
1881
2277
  const preflight = { _tag: "prospective", target: state.target, absenceToken };
1882
2278
  return preflight;
1883
2279
  });
2280
+ /**
2281
+ * Admits an existing owner after an approved Fleet job. Its absence token is
2282
+ * re-checked inside the write; an exact replay returns the stored link.
2283
+ */
1884
2284
  admitExistingOwner = Effect.fn("WorkStore.admitExistingOwner")(function* (request) {
1885
2285
  const decoded = yield* Schema.decodeUnknownEffect(WorkProspectiveAdmission)(request).pipe(Effect.mapError(storeError("admission.decode")));
2286
+ return yield* this.#admit(decoded, {
2287
+ _tag: "approved",
2288
+ approvalActor: decoded.approvalActor,
2289
+ approvalJobId: decoded.approvalJobId
2290
+ });
2291
+ });
2292
+ /**
2293
+ * Admits a worker the reconciler observed, without an approval: the same
2294
+ * write and checks as `admitExistingOwner`, credited to the observation.
2295
+ */
2296
+ admitObserved = Effect.fn("WorkStore.admitObserved")(function* (request) {
2297
+ const decoded = yield* Schema.decodeUnknownEffect(WorkObservedAdmission)(request).pipe(Effect.mapError(storeError("admission.decode")));
2298
+ return yield* this.#admit(decoded, { _tag: "observed", observationId: decoded.observationId });
2299
+ });
2300
+ #admit = Effect.fn("WorkStore.admit")(function* (decoded, provenance) {
1886
2301
  const target = Schema.decodeUnknownSync(WorkAdmissionTarget)(decoded);
1887
2302
  const now = yield* Clock.currentTimeMillis;
1888
2303
  yield* this.secureFiles();
@@ -1906,13 +2321,10 @@ export class WorkStore {
1906
2321
  const state = admissionState(this.#database, target);
1907
2322
  if (state._tag === "existing") {
1908
2323
  const existing = state.link;
1909
- const provenance = existing.binding.request.prospectiveAdmission;
1910
2324
  this.#database.exec("ROLLBACK");
1911
2325
  transaction = false;
1912
2326
  return existing.binding.request.dispatchRequestId === decoded.operationId &&
1913
- provenance?.expectedAbsenceToken === decoded.expectedAbsenceToken &&
1914
- provenance?.approvalJobId === decoded.approvalJobId &&
1915
- provenance.approvalActor === decoded.approvalActor &&
2327
+ sameAdmission(existing.binding.request, decoded.expectedAbsenceToken, provenance) &&
1916
2328
  existing.goal.title === decoded.title &&
1917
2329
  existing.goal.summary === decoded.summary &&
1918
2330
  existing.goal.detail === decoded.detail
@@ -1955,7 +2367,7 @@ export class WorkStore {
1955
2367
  activity: [{
1956
2368
  id: `${decoded.operationId}.admission`,
1957
2369
  kind: "status",
1958
- summary: `Prospective admission of an existing owner by approved Fleet job ${decoded.approvalJobId}`,
2370
+ summary: admissionSummary(provenance),
1959
2371
  occurredAt: createdAt
1960
2372
  }],
1961
2373
  review: { state: "requested", summary: null, updatedAt: createdAt, url: decoded.reviewUrl },
@@ -1989,14 +2401,12 @@ export class WorkStore {
1989
2401
  laneId: decoded.laneId,
1990
2402
  expectedRevision: 0,
1991
2403
  worker: decoded.worker,
1992
- prospectiveAdmission: {
2404
+ ...admissionRecord(provenance, {
1993
2405
  sessionId: decoded.sessionId,
1994
2406
  workAssignment: decoded.expectedWork,
1995
2407
  baseHead: decoded.baseHead,
1996
- expectedAbsenceToken: decoded.expectedAbsenceToken,
1997
- approvalJobId: decoded.approvalJobId,
1998
- approvalActor: decoded.approvalActor
1999
- }
2408
+ expectedAbsenceToken: decoded.expectedAbsenceToken
2409
+ })
2000
2410
  },
2001
2411
  lane,
2002
2412
  checkpoint: {
@@ -2043,7 +2453,7 @@ export class WorkStore {
2043
2453
  _tag: "rejected",
2044
2454
  error: capacity ?? new WorkProjectionError({
2045
2455
  cause: decoded,
2046
- detail: "prospective admission exceeds Work capacity",
2456
+ detail: "admission exceeds Work capacity",
2047
2457
  reason: "capacity_exceeded"
2048
2458
  })
2049
2459
  };
@@ -2190,7 +2600,7 @@ export class WorkStore {
2190
2600
  if (currentDecision._tag === "invalid")
2191
2601
  return reject(currentDecision.error);
2192
2602
  const current = currentDecision.checkpoint;
2193
- if (current.goal.state === "completed") {
2603
+ if (current.goal.state === "completed" || current.goal.state === "abandoned") {
2194
2604
  return reject(new WorkAgentBindingAuthorityError({
2195
2605
  actualRevision: lane.revision,
2196
2606
  expectedRevision: decoded.expectedRevision,
@@ -2327,8 +2737,26 @@ export class WorkStore {
2327
2737
  const entries = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ dispatchRequestId: WorkAgentBindingRequest.fields.dispatchRequestId })))(rows).pipe(Effect.mapError(storeError("agent-binding.lane.rows")));
2328
2738
  return entries.map(({ dispatchRequestId }) => dispatchRequestId);
2329
2739
  });
2330
- append = Effect.fn("WorkStore.append")(function* (event) {
2740
+ append = (event) => this.appendAt(event, null).pipe(
2741
+ // Only an expected head can raise it; a plain append never checks one.
2742
+ Effect.catchTag("WorkGoalRevisionConflictError", Effect.die));
2743
+ /**
2744
+ * Appends one checkpoint. With `expectedHead`, the goal's latest checkpoint
2745
+ * must still be that one when the write commits (an exact replay always
2746
+ * succeeds), so a writer that planned from the head cannot bury a newer
2747
+ * owner checkpoint.
2748
+ */
2749
+ appendAt = Effect.fn("WorkStore.append")(function* (event, guard) {
2331
2750
  const decoded = yield* Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("append.decode")));
2751
+ // The reconciler's event and activity ids are its provenance; no other
2752
+ // writer may use them.
2753
+ if (guard === null && decoded.eventId.startsWith(reconcilerEventPrefix)) {
2754
+ return yield* new WorkProjectionError({
2755
+ cause: decoded.eventId,
2756
+ detail: `event ids starting with ${reconcilerEventPrefix} are reserved for the reconciler`,
2757
+ reason: "malformed"
2758
+ });
2759
+ }
2332
2760
  yield* this.secureFiles();
2333
2761
  const decision = yield* Effect.try({
2334
2762
  try: () => {
@@ -2345,12 +2773,37 @@ export class WorkStore {
2345
2773
  .prepare(`SELECT record FROM work_goal_events
2346
2774
  WHERE event_id = ? OR (goal_id = ? AND occurred_at = ?)`)
2347
2775
  .all(decoded.eventId, decoded.goal.id, decoded.occurredAt)).map(({ record }) => Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(record)));
2348
- if (collisions.length > 0) {
2349
- if (collisions.every((existing) => Equal.equals(existing, decoded))) {
2350
- this.#database.exec("ROLLBACK");
2351
- transaction = false;
2352
- return { _tag: "replayed", event: decoded };
2776
+ if (collisions.length > 0 && collisions.every((existing) => Equal.equals(existing, decoded))) {
2777
+ this.#database.exec("ROLLBACK");
2778
+ transaction = false;
2779
+ return { _tag: "replayed", event: decoded };
2780
+ }
2781
+ if (guard !== null) {
2782
+ // Planned from this head and this fact: if either moved, the plan is stale.
2783
+ const headRow = this.#database.prepare(`SELECT record FROM work_goal_events WHERE goal_id = ?
2784
+ ORDER BY occurred_at DESC, event_id DESC LIMIT 1`).get(decoded.goal.id);
2785
+ const head = headRow === undefined ? null : Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(Schema.decodeUnknownSync(StoredEventRow)(headRow).record));
2786
+ const factRow = this.#database.prepare("SELECT observation_id AS observationId FROM work_observed_facts WHERE subject = ?").get(guard.fact.subject);
2787
+ const fact = factRow === undefined ? null : Schema.decodeUnknownSync(ObservationIdRow)(factRow);
2788
+ if (head?.eventId !== guard.head.eventId || fact?.observationId !== guard.fact.observationId) {
2789
+ return reject(new WorkGoalRevisionConflictError({
2790
+ actualEventId: head?.eventId ?? null,
2791
+ actualUpdatedAt: head?.goal.updatedAt ?? null,
2792
+ expectedEventId: guard.head.eventId,
2793
+ expectedUpdatedAt: guard.head.goal.updatedAt,
2794
+ goalId: decoded.goal.id
2795
+ }));
2796
+ }
2797
+ const count = Schema.decodeUnknownSync(CountRow)(this.#database.prepare("SELECT COUNT(*) AS count FROM work_goal_events").get()).count;
2798
+ if (count + 1 > workHistoryMaxEvents - guard.reserve) {
2799
+ return reject(new WorkProjectionError({
2800
+ cause: count,
2801
+ detail: `the reconciler leaves ${guard.reserve} of ${workHistoryMaxEvents} checkpoints free`,
2802
+ reason: "capacity_exceeded"
2803
+ }));
2353
2804
  }
2805
+ }
2806
+ if (collisions.length > 0) {
2354
2807
  return reject(new WorkCheckpointConflictError({
2355
2808
  eventId: decoded.eventId,
2356
2809
  goalId: decoded.goal.id,
@@ -2405,6 +2858,11 @@ export class WorkStore {
2405
2858
  }
2406
2859
  }
2407
2860
  const result = this.#database.prepare("INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record) VALUES (?, ?, ?, ?)").run(decoded.eventId, decoded.goal.id, decoded.occurredAt, JSON.stringify(decoded));
2861
+ // The reconciler's own record of what it wrote: its authorship and its
2862
+ // once-per-goal rule come from this table, never from an id's spelling.
2863
+ if (guard !== null) {
2864
+ this.#database.prepare("INSERT INTO work_reconciler_events (event_id, goal_id) VALUES (?, ?)").run(decoded.eventId, decoded.goal.id);
2865
+ }
2408
2866
  this.#database.exec("COMMIT");
2409
2867
  transaction = false;
2410
2868
  return { _tag: "inserted", changes: result.changes };
@@ -2436,6 +2894,14 @@ export class WorkStore {
2436
2894
  }
2437
2895
  const transaction = yield* Schema.decodeUnknownEffect(TransactionId)(transactionId).pipe(Effect.mapError(storeError("appendMany.decode.transaction")));
2438
2896
  const decoded = yield* Effect.forEach(events, (event) => Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("appendMany.decode.event"))));
2897
+ const reserved = decoded.find(({ eventId }) => eventId.startsWith(reconcilerEventPrefix));
2898
+ if (reserved !== undefined) {
2899
+ return yield* new WorkProjectionError({
2900
+ cause: reserved.eventId,
2901
+ detail: `event ids starting with ${reconcilerEventPrefix} are reserved for the reconciler`,
2902
+ reason: "malformed"
2903
+ });
2904
+ }
2439
2905
  if (decoded.length === 0) {
2440
2906
  return yield* new WorkProjectionError({
2441
2907
  cause: events,
@@ -2937,7 +3403,7 @@ export class WorkStore {
2937
3403
  goal.goalFamily?.role !== "canonical" ||
2938
3404
  goal.goalFamily.canonicalGoalId !== goal.id)
2939
3405
  return rejectLink("missing_provenance");
2940
- if (goal.state === "completed" || goal.state === "deployed")
3406
+ if (isTerminalWorkState(goal.state))
2941
3407
  return rejectLink("terminal_goal");
2942
3408
  if (!Equal.equals(goal.owner, reconciliation.expectedOwner))
2943
3409
  return rejectLink("owner_mismatch");
@@ -3353,9 +3819,18 @@ export class WorkStore {
3353
3819
  phase, revision, record
3354
3820
  FROM work_lane_operations ORDER BY operation_id ASC
3355
3821
  LIMIT ?`).all(workLaneOperationMaxRecords + 1);
3822
+ const facts = this.#database.prepare(`SELECT subject, observation_id AS observationId, observed_at AS observedAt,
3823
+ confirmed_at AS confirmedAt, record
3824
+ FROM work_observed_facts ORDER BY subject ASC
3825
+ LIMIT ?`).all(workObservedFactMaxRecords + 1);
3826
+ const failures = this.#database.prepare(`SELECT subject, source, reason, since, last_at AS lastAt
3827
+ FROM work_observed_failures ORDER BY subject ASC LIMIT ?`).all(workObservedFactMaxRecords + 1);
3828
+ const reconcilerRows = this.#database.prepare("SELECT event_id AS eventId, goal_id AS goalId FROM work_reconciler_events ORDER BY event_id ASC LIMIT ?").all(workHistoryMaxEvents + 1);
3829
+ const reassignments = this.#database.prepare(`SELECT approval_job_id AS approvalJobId, goal_id AS goalId FROM work_goal_reassignments
3830
+ UNION ALL SELECT approval_job_id AS approvalJobId, goal_id AS goalId FROM work_goal_abandonments`).all();
3356
3831
  this.#database.exec("COMMIT");
3357
3832
  inTransaction = false;
3358
- return { bindings, events, laneOperations };
3833
+ return { bindings, events, facts, failures, laneOperations, reassignments, reconcilerRows };
3359
3834
  }
3360
3835
  catch (cause) {
3361
3836
  if (inTransaction)
@@ -3380,6 +3855,23 @@ export class WorkStore {
3380
3855
  const eventById = new Map(eventRows.map((row) => [row.eventId, row]));
3381
3856
  const laneByOperation = new Map(laneRows.map((row) => [row.operationId, row]));
3382
3857
  let logicalObservedAt = null;
3858
+ // An approved job's activity id is its operation (recovery), the operation
3859
+ // plus `.admission` (prospective admission), or the job id (reassignment).
3860
+ const reassignmentRows = yield* Schema.decodeUnknownEffect(Schema.Array(ReassignmentApprovalRow))(source.reassignments).pipe(Effect.mapError(storeError("snapshot-input.decode-reassignments")));
3861
+ // An approved reassignment or abandonment may be stamped past the clock
3862
+ // (one past a future-dated head); like a binding, it moves logical time so
3863
+ // a default snapshot shows what the approved job wrote.
3864
+ for (const { approvalJobId } of reassignmentRows) {
3865
+ const written = eventById.get(approvalJobId);
3866
+ if (written !== undefined)
3867
+ logicalObservedAt = Math.max(logicalObservedAt ?? 0, written.occurredAt);
3868
+ }
3869
+ const approvals = reassignmentRows.map(({ approvalJobId, goalId }) => ({
3870
+ activityId: approvalJobId,
3871
+ approvalJobId,
3872
+ goalId
3873
+ }));
3874
+ const observedAdmissions = [];
3383
3875
  for (const row of bindingRows) {
3384
3876
  const binding = yield* Effect.try({
3385
3877
  try: () => Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(row.record)),
@@ -3400,8 +3892,173 @@ export class WorkStore {
3400
3892
  if (readbackError !== undefined)
3401
3893
  return yield* readbackError;
3402
3894
  logicalObservedAt = Math.max(logicalObservedAt ?? 0, binding.checkpoint.occurredAt);
3895
+ const goalId = binding.checkpoint.goal.id;
3896
+ const { dispatchRequestId, existingGoalRecovery, observedAdmission, ownerReassignment, prospectiveAdmission } = binding.request;
3897
+ if (observedAdmission !== undefined) {
3898
+ observedAdmissions.push({ eventId: `${dispatchRequestId}.admission`, goalId });
3899
+ }
3900
+ if (prospectiveAdmission !== undefined) {
3901
+ approvals.push({
3902
+ activityId: `${dispatchRequestId}.admission`,
3903
+ approvalJobId: prospectiveAdmission.approvalJobId,
3904
+ goalId
3905
+ });
3906
+ }
3907
+ if (existingGoalRecovery !== undefined) {
3908
+ approvals.push({ activityId: dispatchRequestId, approvalJobId: existingGoalRecovery.approvalJobId, goalId });
3909
+ }
3910
+ if (ownerReassignment !== undefined) {
3911
+ approvals.push({
3912
+ activityId: ownerReassignment.approvalJobId,
3913
+ approvalJobId: ownerReassignment.approvalJobId,
3914
+ goalId
3915
+ });
3916
+ }
3917
+ }
3918
+ const factRows = yield* Schema.decodeUnknownEffect(Schema.Array(ObservedFactRow))(source.facts).pipe(Effect.mapError(storeError("snapshot-input.decode-fact-rows")));
3919
+ const failures = yield* Schema.decodeUnknownEffect(Schema.Array(WorkObservedFailure))(source.failures).pipe(Effect.mapError(storeError("snapshot-input.decode-failures")));
3920
+ if (factRows.length > workObservedFactMaxRecords || failures.length > workObservedFactMaxRecords) {
3921
+ return yield* new WorkStoreError({ cause: factRows.length, operation: "snapshot-input.fact-capacity" });
3922
+ }
3923
+ const facts = yield* Effect.forEach(factRows, (row) => Effect.try({
3924
+ try: () => Schema.decodeUnknownSync(WorkObservedFact)({
3925
+ confirmedAt: row.confirmedAt,
3926
+ observation: JSON.parse(row.record),
3927
+ observationId: row.observationId,
3928
+ observedAt: row.observedAt,
3929
+ subject: row.subject
3930
+ }),
3931
+ catch: storeError("snapshot-input.decode-fact")
3932
+ }));
3933
+ // A row is filed under its own facts' subject; anything else is a corrupt row.
3934
+ const misfiled = facts.find((fact) => fact.subject !== observationSubject(fact.observation));
3935
+ if (misfiled !== undefined) {
3936
+ return yield* new WorkStoreError({ cause: misfiled.subject, operation: "snapshot-input.fact-subject" });
3937
+ }
3938
+ const reconcilerEvents = yield* Schema.decodeUnknownEffect(Schema.Array(ReconcilerEventRow))(source.reconcilerRows)
3939
+ .pipe(Effect.mapError(storeError("snapshot-input.decode-reconciler-events")));
3940
+ // Each current activity's origin: the checkpoint that started its current
3941
+ // unchanged run. An activity that disappears, or reads differently, starts
3942
+ // a new run, so a later owner rewrite or re-creation is never credited to
3943
+ // whoever wrote an earlier version.
3944
+ const activityOrigins = new Map();
3945
+ const previousByGoal = new Map();
3946
+ const ordered = events.toSorted((left, right) => left.occurredAt - right.occurredAt || (left.eventId < right.eventId ? -1 : left.eventId > right.eventId ? 1 : 0));
3947
+ for (const event of ordered) {
3948
+ const previous = previousByGoal.get(event.goal.id) ?? new Map();
3949
+ const current = new Map((event.goal.activity ?? []).map((activity) => [activity.id, activity]));
3950
+ for (const [id, activity] of current) {
3951
+ const before = previous.get(id);
3952
+ const unchanged = before !== undefined && before.kind === activity.kind &&
3953
+ before.summary === activity.summary &&
3954
+ before.occurredAt === activity.occurredAt;
3955
+ if (!unchanged)
3956
+ activityOrigins.set(`${event.goal.id}\u0000${id}`, { activity, eventId: event.eventId });
3957
+ }
3958
+ previousByGoal.set(event.goal.id, current);
3403
3959
  }
3404
- return { events, logicalObservedAt };
3960
+ return {
3961
+ activityOrigins,
3962
+ approvals,
3963
+ events,
3964
+ facts,
3965
+ failures,
3966
+ logicalObservedAt,
3967
+ observedAdmissions,
3968
+ reconcilerEvents
3969
+ };
3970
+ });
3971
+ reconcile = Effect.fn("WorkStore.reconcile")(function* () {
3972
+ const source = yield* this.snapshotInput();
3973
+ if (workHistoryMaxEvents - source.events.length < workReconcilerHeadroom) {
3974
+ return yield* new WorkProjectionError({
3975
+ cause: source.events.length,
3976
+ detail: `the reconciler leaves ${workReconcilerHeadroom} of ${workHistoryMaxEvents} checkpoints free`,
3977
+ reason: "capacity_exceeded"
3978
+ });
3979
+ }
3980
+ // A goal is stamped by the reconciler at most once, whatever its facts
3981
+ // look like later: a reopened goal stays the owner's.
3982
+ const stamped = new Map(source.reconcilerEvents.map(({ eventId, goalId }) => [goalId, eventId]));
3983
+ const cryptoService = this.#cryptoService;
3984
+ const now = yield* Clock.currentTimeMillis;
3985
+ const appendAt = (checkpoint, guard) => this.appendAt(checkpoint, guard);
3986
+ return yield* Effect.forEach(terminalCandidates(source.events, source.facts), Effect.fnUntraced(function* (candidate) {
3987
+ const goalId = candidate.head.goal.id;
3988
+ const previous = stamped.get(goalId);
3989
+ if (previous !== undefined)
3990
+ return { _tag: "recorded", eventId: previous, goalId };
3991
+ const digest = yield* cryptoService.digest("SHA-256", utf8.encode(`${candidate.fact.observationId}\u0000${goalId}`)).pipe(Effect.mapError(storeError("reconcile.digest")));
3992
+ const eventId = `${reconcilerEventPrefix}${Hex.encode(digest)}`;
3993
+ const checkpoint = terminalCheckpoint(candidate, eventId, now);
3994
+ // A goal that cannot take its terminal checkpoint (an activity already
3995
+ // holds the reconciler's id, a timestamp past the bound) is left for
3996
+ // its owner: reported as a conflict, never failing the other goals.
3997
+ if (Result.isFailure(checkpoint))
3998
+ return { _tag: "conflict", goalId };
3999
+ return yield* appendAt(checkpoint.success, {
4000
+ fact: { observationId: candidate.fact.observationId, subject: candidate.fact.subject },
4001
+ head: candidate.head,
4002
+ reserve: workReconcilerHeadroom
4003
+ }).pipe(Effect.as({
4004
+ _tag: "applied",
4005
+ eventId,
4006
+ goalId,
4007
+ state: candidate.pullRequest.state === "merged" ? "completed" : "abandoned"
4008
+ }), Effect.catchTag("WorkGoalRevisionConflictError", () => Effect.succeed({ _tag: "conflict", goalId })));
4009
+ }));
4010
+ });
4011
+ observe = Effect.fn("WorkStore.observe")(function* (envelopes) {
4012
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(WorkObservationEnvelope).check(Schema.isMaxLength(workObservedFactMaxRecords)))(envelopes).pipe(Effect.mapError(storeError("observe.decode")));
4013
+ const cryptoService = this.#cryptoService;
4014
+ // An observation from further ahead than clock skew allows would outrank
4015
+ // every real reading until wall time caught up, so it is skipped as stale.
4016
+ const clockNow = yield* Clock.currentTimeMillis;
4017
+ const latestAllowed = clockNow + workObservationMaxSkewMillis;
4018
+ const prepared = yield* Effect.forEach(decoded, Effect.fnUntraced(function* (envelope) {
4019
+ // Composite subjects are bounded on their own; check before anything is written.
4020
+ const subject = yield* Schema.decodeUnknownEffect(WorkObservationSubject)(observationSubject(envelope.observation)).pipe(Effect.mapError(storeError("observe.subject")));
4021
+ if (envelope.observedAt > latestAllowed)
4022
+ return { _tag: "future", subject };
4023
+ if (envelope.observation._tag === "unknown") {
4024
+ if (canonicalSubject(envelope.observation.source, envelope.observation.subject) === null) {
4025
+ return yield* new WorkStoreError({ cause: envelope.observation, operation: "observe.subject" });
4026
+ }
4027
+ return {
4028
+ _tag: "unknown",
4029
+ // Within the skew allowance, a reading from slightly ahead counts
4030
+ // as now, so it is visible to a snapshot taken now.
4031
+ observedAt: Math.min(envelope.observedAt, clockNow),
4032
+ reason: envelope.observation.reason,
4033
+ source: envelope.observation.source,
4034
+ subject
4035
+ };
4036
+ }
4037
+ // Case-insensitive identities are lowercased so a spelling change is the
4038
+ // same fact; encoding emits keys in schema field order, so equal facts
4039
+ // give equal text and one id.
4040
+ const record = JSON.stringify(Schema.encodeSync(WorkObservationEnvelope)({
4041
+ ...envelope,
4042
+ observation: canonicalObservation(envelope.observation)
4043
+ })
4044
+ .observation);
4045
+ const digest = yield* cryptoService.digest("SHA-256", utf8.encode(record)).pipe(Effect.mapError(storeError("observe.digest")));
4046
+ return {
4047
+ _tag: "fact",
4048
+ observationId: Hex.encode(digest),
4049
+ observedAt: Math.min(envelope.observedAt, clockNow),
4050
+ record,
4051
+ subject
4052
+ };
4053
+ }));
4054
+ yield* this.secureFiles();
4055
+ const report = yield* Effect.try({
4056
+ try: () => writeObservations(this.#database, prepared),
4057
+ catch: storeError("observe.write")
4058
+ });
4059
+ // The write may have created the WAL siblings; they get the same private mode.
4060
+ yield* this.secureFiles();
4061
+ return report;
3405
4062
  });
3406
4063
  secureFiles() {
3407
4064
  return this.#secureFiles;