@knpkv/herdr-work 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -1
- package/dist/agent-binding.d.ts +4 -0
- package/dist/agent-binding.d.ts.map +1 -0
- package/dist/agent-binding.js +26 -0
- package/dist/agent-binding.js.map +1 -0
- package/dist/errors.d.ts +141 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +97 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/agent-binding-admission.d.ts +15 -0
- package/dist/internal/agent-binding-admission.d.ts.map +1 -0
- package/dist/internal/agent-binding-admission.js +92 -0
- package/dist/internal/agent-binding-admission.js.map +1 -0
- package/dist/internal/agent-binding-readback.d.ts +55 -0
- package/dist/internal/agent-binding-readback.d.ts.map +1 -0
- package/dist/internal/agent-binding-readback.js +124 -0
- package/dist/internal/agent-binding-readback.js.map +1 -0
- package/dist/internal/coordinator-authority.d.ts +62 -0
- package/dist/internal/coordinator-authority.d.ts.map +1 -0
- package/dist/internal/coordinator-authority.js +214 -0
- package/dist/internal/coordinator-authority.js.map +1 -0
- package/dist/internal/decision-handoff-migration.d.ts +126 -0
- package/dist/internal/decision-handoff-migration.d.ts.map +1 -0
- package/dist/internal/decision-handoff-migration.js +51 -0
- package/dist/internal/decision-handoff-migration.js.map +1 -0
- package/dist/internal/history-validation.d.ts +5 -0
- package/dist/internal/history-validation.d.ts.map +1 -0
- package/dist/internal/history-validation.js +33 -0
- package/dist/internal/history-validation.js.map +1 -0
- package/dist/model.d.ts +3075 -51
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +334 -12
- package/dist/model.js.map +1 -1
- package/dist/navigation.d.ts +17 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +62 -0
- package/dist/navigation.js.map +1 -0
- package/dist/projection.d.ts +644 -0
- package/dist/projection.d.ts.map +1 -1
- package/dist/projection.js +65 -30
- package/dist/projection.js.map +1 -1
- package/dist/service.d.ts +1870 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +138 -5
- package/dist/service.js.map +1 -1
- package/dist/sql.d.ts +30 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +1454 -0
- package/dist/sql.js.map +1 -0
- package/dist/store.d.ts +1068 -4
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +3275 -51
- package/dist/store.js.map +1 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +154 -8
- package/dist/view.js.map +1 -1
- package/package.json +25 -13
- package/src/styles.css +91 -17
package/dist/store.js
CHANGED
|
@@ -1,48 +1,1217 @@
|
|
|
1
|
-
import { fleetResponseBodyMaxBytes } from "@knpkv/herdr-fleet";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { agentConnectTarget, fleetResponseBodyMaxBytes, workReassignActivitySummary } from "@knpkv/herdr-fleet";
|
|
2
|
+
import { openPrivateSqlite } from "@knpkv/herdr-fleet/sqlite";
|
|
3
|
+
import { Clock, Crypto, Effect, Equal, Option, Schema } from "effect";
|
|
4
|
+
import { Hex } from "effect/encoding";
|
|
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";
|
|
5
7
|
import { validateGoalFamilyHistory } from "./goal-family.js";
|
|
6
|
-
import {
|
|
8
|
+
import { agentBindingAdmissionError, workAgentBindingLaneOperationMaxBytes, workAgentBindingLaneOperationMaxRecords, workAgentBindingMaximumSnapshotBytes, workAgentBindingSnapshotEnvelopeMaxBytes, workMaximumSnapshotBytesForHistory } from "./internal/agent-binding-admission.js";
|
|
9
|
+
import { AgentBindingGoalEventRow, AgentBindingLaneOperationRow, agentBindingReadbackError, AgentBindingRow, decodeAgentBindingGoalEvent, decodeAgentBindingRow, laneOperationReadbackError } from "./internal/agent-binding-readback.js";
|
|
10
|
+
import { CoordinatorCommandRow, CoordinatorLifecycleDispatchRow, CoordinatorLifecycleEventRow, coordinatorLifecycleRunningAt, CoordinatorRouteDiscriminatorRow, coordinatorRouteRequiresWorkLink, requireCoordinatorFailedLunaAuthority, requireCoordinatorLifecycleAuthority, requireCoordinatorRouteAuthority, requireCoordinatorRouteBinding } from "./internal/coordinator-authority.js";
|
|
11
|
+
import { currentDecisionHandoffEquivalent, CurrentMetadataWorkLink, decodePreviousDecisionHandoff, previousDecisionHandoffEquivalent, PreviousWorkDecisionHandoff, upgradePreviousDecisionHandoff, workDispatchLineageContainedBy, workDispatchLineageEquivalent } from "./internal/decision-handoff-migration.js";
|
|
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";
|
|
7
14
|
const StoredEventRow = Schema.Struct({ record: Schema.String });
|
|
8
15
|
const StoredEventRows = Schema.Array(StoredEventRow);
|
|
16
|
+
const StoredEventWithTransactionRow = Schema.Struct({
|
|
17
|
+
eventId: Schema.String,
|
|
18
|
+
goalId: Schema.String,
|
|
19
|
+
occurredAt: Schema.Number,
|
|
20
|
+
record: Schema.String,
|
|
21
|
+
transactionId: Schema.NullOr(Schema.String)
|
|
22
|
+
});
|
|
23
|
+
const StoredEventWithTransactionRows = Schema.Array(StoredEventWithTransactionRow);
|
|
24
|
+
const TransactionRow = Schema.Struct({ record: Schema.String });
|
|
25
|
+
const TransactionEventIdentity = Schema.Struct({
|
|
26
|
+
eventId: Schema.String,
|
|
27
|
+
goalId: Schema.String,
|
|
28
|
+
occurredAt: Schema.Number
|
|
29
|
+
});
|
|
30
|
+
const LegacyCompactTransactionRecord = Schema.Struct({
|
|
31
|
+
events: Schema.Array(TransactionEventIdentity),
|
|
32
|
+
version: Schema.Literal("herdr.work.transaction.v1")
|
|
33
|
+
});
|
|
34
|
+
const CompactTransactionRecord = Schema.Struct({
|
|
35
|
+
digest: Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)),
|
|
36
|
+
version: Schema.Literal("herdr.work.transaction.v3")
|
|
37
|
+
});
|
|
38
|
+
const PreviousCompactTransactionRecord = Schema.Struct({
|
|
39
|
+
digest: Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)),
|
|
40
|
+
version: Schema.Literal("herdr.work.transaction.v2")
|
|
41
|
+
});
|
|
42
|
+
const LaneRow = Schema.Struct({
|
|
43
|
+
goalId: Schema.String,
|
|
44
|
+
laneId: Schema.String,
|
|
45
|
+
operationId: Schema.String,
|
|
46
|
+
phase: Schema.String,
|
|
47
|
+
record: Schema.String,
|
|
48
|
+
revision: Schema.Number
|
|
49
|
+
});
|
|
50
|
+
const DecisionRow = Schema.Struct({
|
|
51
|
+
handoffId: Schema.String,
|
|
52
|
+
sessionId: Schema.String,
|
|
53
|
+
laneId: Schema.String,
|
|
54
|
+
occurredAt: Schema.Number,
|
|
55
|
+
record: Schema.String
|
|
56
|
+
});
|
|
57
|
+
const DecisionIdentityRow = Schema.Struct({ handoffId: Schema.String });
|
|
58
|
+
const AgentBindingRows = Schema.Array(AgentBindingRow);
|
|
9
59
|
const CountRow = Schema.Struct({ count: Schema.Number });
|
|
60
|
+
const DecisionLedgerTotalsRow = Schema.Struct({
|
|
61
|
+
decisionBytes: Schema.Number,
|
|
62
|
+
decisionCount: Schema.Number
|
|
63
|
+
});
|
|
64
|
+
const LedgerBytesRow = Schema.Struct({ bytes: Schema.Number });
|
|
65
|
+
const LaneOperationLedgerTotalsRow = Schema.Struct({
|
|
66
|
+
operationBytes: Schema.Number,
|
|
67
|
+
operationCount: Schema.Number
|
|
68
|
+
});
|
|
69
|
+
const TransactionLedgerTotalsRow = Schema.Struct({
|
|
70
|
+
transactionBytes: Schema.Number,
|
|
71
|
+
transactionCount: Schema.Number
|
|
72
|
+
});
|
|
73
|
+
const LegacyLaneStoredRow = Schema.Struct({ laneId: Schema.String, record: Schema.String, revision: Schema.Number });
|
|
74
|
+
const LegacyLaneRecord = Schema.Struct({
|
|
75
|
+
laneId: Schema.String,
|
|
76
|
+
worktree: Schema.String,
|
|
77
|
+
branch: Schema.String,
|
|
78
|
+
head: Schema.String,
|
|
79
|
+
owner: Schema.Struct({ id: Schema.String, name: Schema.String }),
|
|
80
|
+
parent: Schema.NullOr(Schema.String),
|
|
81
|
+
phase: Schema.String,
|
|
82
|
+
expectedRevision: Schema.Number,
|
|
83
|
+
revision: Schema.Number
|
|
84
|
+
});
|
|
85
|
+
const LegacyDecisionStoredRow = Schema.Struct({ handoffId: Schema.String, record: Schema.String });
|
|
86
|
+
const LegacyDispatchStoredRow = Schema.Struct({
|
|
87
|
+
dispatchRequestId: Schema.String,
|
|
88
|
+
handoffId: Schema.String,
|
|
89
|
+
laneId: Schema.String,
|
|
90
|
+
occurredAt: Schema.Number,
|
|
91
|
+
lineage: Schema.String,
|
|
92
|
+
record: Schema.String
|
|
93
|
+
});
|
|
94
|
+
const LaneRevisionRow = Schema.Struct({ revision: Schema.Number });
|
|
95
|
+
const MetadataWorkLinkRow = Schema.Struct({
|
|
96
|
+
dispatchRequestId: Schema.String,
|
|
97
|
+
route: Schema.String,
|
|
98
|
+
workLink: Schema.NullOr(Schema.String)
|
|
99
|
+
});
|
|
100
|
+
const MetadataHandoffIdentityRow = Schema.Struct({
|
|
101
|
+
dispatchRequestId: Schema.String,
|
|
102
|
+
hasWorkLink: Schema.Literals([0, 1]),
|
|
103
|
+
handoffId: Schema.NullOr(Schema.String),
|
|
104
|
+
route: Schema.NullOr(Schema.String)
|
|
105
|
+
});
|
|
106
|
+
const RoutedMetadataCardinalityRow = Schema.Struct({
|
|
107
|
+
dispatchRequestId: Schema.String,
|
|
108
|
+
metadataCount: Schema.Number
|
|
109
|
+
});
|
|
110
|
+
const CoordinatorMetadataRouteRow = Schema.Struct({
|
|
111
|
+
activityIdempotencyKey: Schema.NullOr(Schema.String),
|
|
112
|
+
command: Schema.NullOr(Schema.String),
|
|
113
|
+
dispatchRequestId: Schema.String,
|
|
114
|
+
hasWorkLink: Schema.Literals([0, 1]),
|
|
115
|
+
isRouted: Schema.NullOr(Schema.Literals([0, 1])),
|
|
116
|
+
route: Schema.NullOr(Schema.String),
|
|
117
|
+
rowId: Schema.String
|
|
118
|
+
});
|
|
119
|
+
const LegacyDecisionRecord = Schema.Struct({
|
|
120
|
+
version: Schema.Literal("herdr.work.decision.v1"),
|
|
121
|
+
id: Schema.String,
|
|
122
|
+
laneId: Schema.String,
|
|
123
|
+
goalId: Schema.String,
|
|
124
|
+
decision: Schema.String,
|
|
125
|
+
summary: Schema.String,
|
|
126
|
+
owner: Schema.Struct({ id: Schema.String, name: Schema.String }),
|
|
127
|
+
occurredAt: Schema.Number
|
|
128
|
+
});
|
|
129
|
+
const PreviousMetadataWorkLink = Schema.Struct({
|
|
130
|
+
handoff: PreviousWorkDecisionHandoff,
|
|
131
|
+
lineage: WorkDispatchHandoff.fields.lineage
|
|
132
|
+
});
|
|
133
|
+
const LegacyMetadataWorkLink = Schema.Struct({
|
|
134
|
+
handoff: LegacyDecisionRecord,
|
|
135
|
+
lineage: WorkDispatchHandoff.fields.lineage
|
|
136
|
+
});
|
|
137
|
+
const legacyDecisionEquivalent = Schema.toEquivalence(LegacyDecisionRecord);
|
|
138
|
+
const TransactionId = Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(256), Schema.isPattern(/^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/));
|
|
10
139
|
const storeError = (operation) => (cause) => new WorkStoreError({ cause, operation });
|
|
140
|
+
const fromPrivateDatabaseError = (error) => new WorkStoreError({ cause: error.cause, operation: error.operation });
|
|
141
|
+
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")
|
|
152
|
+
: [];
|
|
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,
|
|
156
|
+
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))
|
|
192
|
+
});
|
|
193
|
+
const routeStorageAuthority = (dispatchRequestId, operation) => {
|
|
194
|
+
if (!coordinatorDispatchColumns.includes("is_routed")) {
|
|
195
|
+
return { _tag: "legacy_without_routed_discriminator" };
|
|
196
|
+
}
|
|
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 });
|
|
202
|
+
}
|
|
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 });
|
|
213
|
+
}
|
|
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 });
|
|
223
|
+
}
|
|
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
|
+
const bindingRows = tables.includes("work_agent_bindings")
|
|
237
|
+
? Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
238
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
239
|
+
FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 2`).all(dispatchRequestId))
|
|
240
|
+
: [];
|
|
241
|
+
if (runningAt === null && bindingRows.length === 0)
|
|
242
|
+
return;
|
|
243
|
+
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) {
|
|
261
|
+
throw new WorkStoreError({
|
|
262
|
+
cause: { binding: decoded.binding, currentLaneRows },
|
|
263
|
+
operation: `${operation}.lane-revision`
|
|
264
|
+
});
|
|
265
|
+
}
|
|
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` });
|
|
272
|
+
}
|
|
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) {
|
|
293
|
+
throw new WorkStoreError({
|
|
294
|
+
cause: { binding: decoded.binding, currentLane, currentLaneRow, currentOperationError },
|
|
295
|
+
operation: `${operation}.lane-revision`
|
|
296
|
+
});
|
|
297
|
+
}
|
|
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"
|
|
312
|
+
});
|
|
313
|
+
}
|
|
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);
|
|
335
|
+
}
|
|
336
|
+
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
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
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) {
|
|
407
|
+
throw new WorkStoreError({
|
|
408
|
+
cause: { dispatch, metadataRows },
|
|
409
|
+
operation: "open.migrate.legacy-metadata-authority"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (metadata.workLink === null) {
|
|
413
|
+
throw new WorkStoreError({
|
|
414
|
+
cause: { dispatch, metadata },
|
|
415
|
+
operation: "open.migrate.legacy-metadata-authority"
|
|
416
|
+
});
|
|
417
|
+
}
|
|
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)) {
|
|
422
|
+
throw new WorkStoreError({
|
|
423
|
+
cause: { legacy, workLink },
|
|
424
|
+
operation: "open.migrate.legacy-metadata-authority"
|
|
425
|
+
});
|
|
426
|
+
}
|
|
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
|
+
});
|
|
567
|
+
}
|
|
568
|
+
if (!tables.includes("orchestrator_dispatch_metadata"))
|
|
569
|
+
continue;
|
|
570
|
+
const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
|
|
571
|
+
if (metadata?.workLink === null || metadata?.workLink === undefined) {
|
|
572
|
+
throw new WorkStoreError({
|
|
573
|
+
cause: { dispatch, metadata },
|
|
574
|
+
operation: "open.migrate.metadata-authority"
|
|
575
|
+
});
|
|
576
|
+
}
|
|
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");
|
|
594
|
+
}
|
|
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")) {
|
|
679
|
+
const metadata = linkedMetadata.find(({ dispatchRequestId }) => dispatchRequestId === dispatch.dispatchRequestId);
|
|
680
|
+
if (metadata?.workLink === null || metadata?.workLink === undefined) {
|
|
681
|
+
throw new WorkStoreError({
|
|
682
|
+
cause: { dispatch, metadata },
|
|
683
|
+
operation: "open.migrate.metadata-authority"
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
const workLink = Schema.decodeUnknownSync(PreviousMetadataWorkLink)(JSON.parse(metadata.workLink));
|
|
687
|
+
requireMetadataAuthority(dispatch.dispatchRequestId, metadata.route, workLink.lineage, "open.migrate.metadata-authority");
|
|
688
|
+
if (!previousDecisionHandoffEquivalent(workLink.handoff, previous) ||
|
|
689
|
+
!workDispatchLineageEquivalent(workLink.lineage, lineage)) {
|
|
690
|
+
throw new WorkStoreError({
|
|
691
|
+
cause: { previous, workLink },
|
|
692
|
+
operation: "open.migrate.metadata-authority"
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
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);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
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) {
|
|
716
|
+
throw new WorkStoreError({
|
|
717
|
+
cause: migratedLedger,
|
|
718
|
+
operation: "open.migrate.handoff-capacity"
|
|
719
|
+
});
|
|
720
|
+
}
|
|
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");
|
|
761
|
+
}
|
|
762
|
+
catch (cause) {
|
|
763
|
+
if (Schema.is(WorkStoreError)(cause))
|
|
764
|
+
throw cause;
|
|
765
|
+
throw new WorkStoreError({ cause, operation: "open.migrate.metadata-route" });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const last = rows.at(-1);
|
|
769
|
+
if (rows.length < pageSize || last === undefined)
|
|
770
|
+
break;
|
|
771
|
+
cursor = last.rowId;
|
|
772
|
+
}
|
|
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) {
|
|
783
|
+
throw new WorkStoreError({
|
|
784
|
+
cause: invalidRoutedMetadata,
|
|
785
|
+
operation: "open.migrate.routed-metadata"
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
database.exec("COMMIT");
|
|
790
|
+
transaction = false;
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
if (transaction)
|
|
794
|
+
database.exec("ROLLBACK");
|
|
795
|
+
throw error;
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
const readTransactionLedgerTotals = (database) => Schema.decodeUnknownSync(TransactionLedgerTotalsRow)(database.prepare(`SELECT transaction_count AS transactionCount, transaction_bytes AS transactionBytes
|
|
799
|
+
FROM work_goal_transaction_totals WHERE singleton = 1`).get());
|
|
800
|
+
const readDecisionLedgerTotals = (database) => Schema.decodeUnknownSync(DecisionLedgerTotalsRow)(database.prepare(`SELECT decision_count AS decisionCount, decision_bytes AS decisionBytes
|
|
801
|
+
FROM work_decision_totals WHERE singleton = 1`).get());
|
|
802
|
+
const readLaneOperationLedgerTotals = (database) => Schema.decodeUnknownSync(LaneOperationLedgerTotalsRow)(database.prepare(`SELECT operation_count AS operationCount, operation_bytes AS operationBytes
|
|
803
|
+
FROM work_lane_operation_totals WHERE singleton = 1`).get());
|
|
804
|
+
const ReassignmentRow = Schema.Struct({ approvalJobId: Schema.String, goalId: Schema.String, record: Schema.String });
|
|
805
|
+
/** Applies a reassignment's agent choice; `keep` is only reached when the goal has no target. */
|
|
806
|
+
const withReassignedAgent = (goal, toAgent) => {
|
|
807
|
+
switch (toAgent._tag) {
|
|
808
|
+
case "set":
|
|
809
|
+
return { ...goal, agentHierarchy: { agent: toAgent.agent }, connectTarget: agentConnectTarget(toAgent.agent) };
|
|
810
|
+
case "clear":
|
|
811
|
+
return { ...goal, agentHierarchy: null, connectTarget: null };
|
|
812
|
+
case "keep":
|
|
813
|
+
return goal;
|
|
814
|
+
}
|
|
815
|
+
};
|
|
11
816
|
const utf8 = new TextEncoder();
|
|
12
817
|
const encodedBytes = (value) => utf8.encode(JSON.stringify(value)).byteLength;
|
|
13
818
|
const maximumTimestamp = 8_640_000_000_000_000;
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
819
|
+
const workTransactionMaxRecords = 16_384;
|
|
820
|
+
const workTransactionMaxBytes = 2 * 1024 * 1024;
|
|
821
|
+
const workLaneMaxRecords = workSnapshotMaxGoals;
|
|
822
|
+
const workLaneMaxBytes = 2 * 1024 * 1024;
|
|
823
|
+
const workLaneOperationMaxRecords = workAgentBindingLaneOperationMaxRecords;
|
|
824
|
+
const workLaneOperationMaxBytes = workAgentBindingLaneOperationMaxBytes;
|
|
825
|
+
const workDecisionMaxRecords = 16_384;
|
|
826
|
+
const workDecisionMaxBytes = 2 * 1024 * 1024;
|
|
827
|
+
const workStoreBusyTimeoutMillis = 5_000;
|
|
828
|
+
const workSnapshotEnvelopeMaxBytes = workAgentBindingSnapshotEnvelopeMaxBytes;
|
|
829
|
+
const maximumSnapshotBytes = workAgentBindingMaximumSnapshotBytes;
|
|
830
|
+
const transactionContent = (events) => JSON.stringify(events);
|
|
831
|
+
export const __herdrWorkMaximumSnapshotBytesForTest = maximumSnapshotBytes;
|
|
832
|
+
export const __herdrWorkEncodedBytesForTest = encodedBytes;
|
|
833
|
+
export const __herdrWorkSnapshotEnvelopeMaxBytesForTest = workSnapshotEnvelopeMaxBytes;
|
|
834
|
+
export const __herdrWorkLaneOperationMaxBytesForTest = workLaneOperationMaxBytes;
|
|
31
835
|
const decodeRow = (row) => Schema.decodeUnknownEffect(StoredEventRow)(row).pipe(Effect.mapError(storeError("decode.row")), Effect.flatMap(({ record }) => Effect.try({
|
|
32
836
|
try: () => Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(record)),
|
|
33
837
|
catch: storeError("decode.event")
|
|
34
838
|
})));
|
|
839
|
+
const claimInputFromClaimed = (claim) => {
|
|
840
|
+
const input = {
|
|
841
|
+
branch: claim.branch,
|
|
842
|
+
expectedRevision: claim.expectedRevision,
|
|
843
|
+
goalId: claim.goalId,
|
|
844
|
+
head: claim.head,
|
|
845
|
+
laneId: claim.laneId,
|
|
846
|
+
operationId: claim.operationId,
|
|
847
|
+
owner: claim.owner,
|
|
848
|
+
parent: claim.parent,
|
|
849
|
+
phase: claim.phase
|
|
850
|
+
};
|
|
851
|
+
return claim.reconciliation === undefined
|
|
852
|
+
? { ...input, worktree: claim.worktree }
|
|
853
|
+
: { ...input, reconciliation: claim.reconciliation, worktree: claim.worktree };
|
|
854
|
+
};
|
|
855
|
+
const readValidatedLaneLedger = (database, operation) => {
|
|
856
|
+
try {
|
|
857
|
+
const rows = Schema.decodeUnknownSync(Schema.Array(LaneRow))(database.prepare(`SELECT lane_id AS laneId, goal_id AS goalId, operation_id AS operationId,
|
|
858
|
+
phase, revision, record
|
|
859
|
+
FROM work_lane_claims ORDER BY lane_id ASC LIMIT ?`).all(workLaneMaxRecords + 1));
|
|
860
|
+
if (rows.length > workLaneMaxRecords) {
|
|
861
|
+
return {
|
|
862
|
+
_tag: "invalid",
|
|
863
|
+
error: new WorkStoreError({ cause: rows.length, operation: `${operation}.capacity` })
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const entries = [];
|
|
867
|
+
for (const row of rows) {
|
|
868
|
+
const claim = Schema.decodeUnknownSync(WorkLaneClaimed)(JSON.parse(row.record));
|
|
869
|
+
if (claim.goalId !== row.goalId ||
|
|
870
|
+
claim.laneId !== row.laneId ||
|
|
871
|
+
claim.operationId !== row.operationId ||
|
|
872
|
+
claim.phase !== row.phase ||
|
|
873
|
+
claim.revision !== row.revision) {
|
|
874
|
+
return {
|
|
875
|
+
_tag: "invalid",
|
|
876
|
+
error: new WorkStoreError({ cause: { claim, row }, operation: `${operation}.identity-mismatch` })
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
entries.push({ claim, row });
|
|
880
|
+
}
|
|
881
|
+
return { _tag: "valid", entries };
|
|
882
|
+
}
|
|
883
|
+
catch (cause) {
|
|
884
|
+
return { _tag: "invalid", error: new WorkStoreError({ cause, operation: `${operation}.decode` }) };
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const admissionConflict = (target, reason) => new WorkAdmissionConflictError({ goalId: target.goalId, laneId: target.laneId, reason });
|
|
888
|
+
/**
|
|
889
|
+
* Splits bindings into each lane's authoritative row (highest lane revision)
|
|
890
|
+
* and the lanes where that revision is shared by more than one row. Earlier
|
|
891
|
+
* rows on a lane were superseded, for example by an approved reassignment.
|
|
892
|
+
*/
|
|
893
|
+
const authoritativeBindings = (bindings) => {
|
|
894
|
+
const maxRevision = new Map();
|
|
895
|
+
for (const binding of bindings) {
|
|
896
|
+
const laneId = binding.request.laneId;
|
|
897
|
+
maxRevision.set(laneId, Math.max(maxRevision.get(laneId) ?? 0, binding.lane.revision));
|
|
898
|
+
}
|
|
899
|
+
const latest = bindings.filter((binding) => binding.lane.revision === maxRevision.get(binding.request.laneId));
|
|
900
|
+
const ambiguousLanes = new Set(latest
|
|
901
|
+
.map(({ request }) => request.laneId)
|
|
902
|
+
.filter((laneId, index, laneIds) => laneIds.indexOf(laneId) !== index));
|
|
903
|
+
return { latest, ambiguousLanes };
|
|
904
|
+
};
|
|
905
|
+
/** Reads every durable identity row, including entries omitted by time-window projections. */
|
|
906
|
+
const admissionState = (database, target) => {
|
|
907
|
+
const eventRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingGoalEventRow))(database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
908
|
+
FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC LIMIT ?`).all(workHistoryMaxEvents + 1));
|
|
909
|
+
const lanes = readValidatedLaneLedger(database, "admission.inspect");
|
|
910
|
+
if (lanes._tag === "invalid")
|
|
911
|
+
throw lanes.error;
|
|
912
|
+
const bindingRows = Schema.decodeUnknownSync(AgentBindingRows)(database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
913
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
914
|
+
FROM work_agent_bindings ORDER BY dispatch_request_id ASC LIMIT ?`).all(workLaneOperationMaxRecords + 1));
|
|
915
|
+
const operationRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingLaneOperationRow))(database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
916
|
+
phase, revision, record FROM work_lane_operations ORDER BY operation_id ASC LIMIT ?`).all(workLaneOperationMaxRecords + 1));
|
|
917
|
+
if (eventRows.length > workHistoryMaxEvents || bindingRows.length > workLaneOperationMaxRecords ||
|
|
918
|
+
operationRows.length > workLaneOperationMaxRecords) {
|
|
919
|
+
throw new WorkStoreError({ cause: target, operation: "admission.inspect.capacity" });
|
|
920
|
+
}
|
|
921
|
+
const events = eventRows.map((row) => {
|
|
922
|
+
const decision = decodeAgentBindingGoalEvent(row, "admission.inspect.event");
|
|
923
|
+
if (decision._tag === "invalid")
|
|
924
|
+
throw decision.error;
|
|
925
|
+
return decision.checkpoint;
|
|
926
|
+
});
|
|
927
|
+
const historyError = workHistoryError(events);
|
|
928
|
+
if (historyError !== undefined)
|
|
929
|
+
throw historyError;
|
|
930
|
+
const bindings = bindingRows.map((row) => {
|
|
931
|
+
const binding = Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(row.record));
|
|
932
|
+
if (row.dispatchRequestId !== binding.request.dispatchRequestId ||
|
|
933
|
+
row.laneId !== binding.request.laneId || row.revision !== binding.lane.revision ||
|
|
934
|
+
row.agentId !== binding.request.worker.agentId ||
|
|
935
|
+
row.host.toLowerCase() !== binding.request.worker.host.toLowerCase()) {
|
|
936
|
+
throw new WorkStoreError({ cause: row, operation: "admission.inspect.binding-identity" });
|
|
937
|
+
}
|
|
938
|
+
const readback = agentBindingReadbackError(binding, operationRows.find(({ operationId }) => operationId === binding.lane.operationId), eventRows.find(({ eventId }) => eventId === binding.checkpoint.eventId), "admission.inspect.binding");
|
|
939
|
+
if (readback !== undefined)
|
|
940
|
+
throw readback;
|
|
941
|
+
return binding;
|
|
942
|
+
});
|
|
943
|
+
const latest = new Map();
|
|
944
|
+
for (const event of events)
|
|
945
|
+
latest.set(event.goal.id, event);
|
|
946
|
+
const goal = [...latest.values()].find(({ goal }) => goal.review?.url === target.reviewUrl);
|
|
947
|
+
const lane = lanes.entries.find(({ claim }) => claim.laneId === target.laneId)?.claim;
|
|
948
|
+
const authority = authoritativeBindings(bindings);
|
|
949
|
+
const binding = authority.latest.find(({ lane: bound }) => bound.laneId === target.laneId);
|
|
950
|
+
const foreignConflict = events.some(({ goal: entry }) => (entry.review?.url === target.reviewUrl && entry.id !== target.goalId) ||
|
|
951
|
+
(entry.agentHierarchy?.agent.agentId === target.worker.agentId && entry.id !== target.goalId) ||
|
|
952
|
+
(entry.goalFamily?.canonicalGoalId === target.goalId && entry.id !== target.goalId)) || lanes.entries.some(({ claim }) => (claim.goalId === target.goalId && claim.laneId !== target.laneId) ||
|
|
953
|
+
(claim.worktree === target.worktree && claim.laneId !== target.laneId)) ||
|
|
954
|
+
authority.latest.some(({ request }) => request.worker.agentId === target.worker.agentId && request.laneId !== target.laneId) ||
|
|
955
|
+
// 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) ||
|
|
957
|
+
(request.existingGoalRecovery?.sessionId === target.sessionId && request.laneId !== target.laneId)) || authority.ambiguousLanes.has(target.laneId);
|
|
958
|
+
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" &&
|
|
961
|
+
goal.goal.goalFamily.canonicalGoalId === target.goalId &&
|
|
962
|
+
Equal.equals(goal.goal.owner, target.owner) &&
|
|
963
|
+
lane.goalId === target.goalId && lane.head === target.head &&
|
|
964
|
+
lane.worktree === target.worktree && lane.branch === target.branch &&
|
|
965
|
+
lane.phase !== "shipped" && Equal.equals(lane.owner, target.owner) &&
|
|
966
|
+
binding.lane.laneId === target.laneId &&
|
|
967
|
+
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)) &&
|
|
971
|
+
(binding.request.existingGoalRecovery === undefined ||
|
|
972
|
+
(binding.request.existingGoalRecovery.sessionId === target.sessionId &&
|
|
973
|
+
binding.request.existingGoalRecovery.workAssignment === target.expectedWork &&
|
|
974
|
+
binding.request.existingGoalRecovery.baseHead === target.baseHead));
|
|
975
|
+
if (exact && !foreignConflict) {
|
|
976
|
+
return {
|
|
977
|
+
_tag: "existing",
|
|
978
|
+
target,
|
|
979
|
+
link: Schema.decodeUnknownSync(WorkPullRequestLink)({
|
|
980
|
+
request: {
|
|
981
|
+
repository: target.repository,
|
|
982
|
+
pullRequest: target.pullRequest,
|
|
983
|
+
goalId: target.goalId,
|
|
984
|
+
laneId: target.laneId
|
|
985
|
+
},
|
|
986
|
+
goalEventId: goal.eventId,
|
|
987
|
+
goal: goal.goal,
|
|
988
|
+
lane,
|
|
989
|
+
binding
|
|
990
|
+
})
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
const conflicts = events.some(({ goal: entry }) => entry.id === target.goalId || entry.review?.url === target.reviewUrl ||
|
|
994
|
+
entry.goalFamily?.canonicalGoalId === target.goalId ||
|
|
995
|
+
entry.agentHierarchy?.agent.agentId === target.worker.agentId) || lanes.entries.some(({ claim }) => claim.laneId === target.laneId || claim.goalId === target.goalId ||
|
|
996
|
+
claim.worktree === target.worktree) || authority.latest.some(({ request }) => request.worker.agentId === target.worker.agentId) ||
|
|
997
|
+
bindings.some(({ request }) => request.prospectiveAdmission?.sessionId === target.sessionId ||
|
|
998
|
+
request.existingGoalRecovery?.sessionId === target.sessionId);
|
|
999
|
+
if (conflicts) {
|
|
1000
|
+
return { _tag: "conflict", target, reason: "durable PR, goal, lane, worker, or worktree is already bound" };
|
|
1001
|
+
}
|
|
1002
|
+
const snapshot = JSON.stringify({
|
|
1003
|
+
target,
|
|
1004
|
+
eventRows,
|
|
1005
|
+
lanes: lanes.entries.map(({ row }) => row),
|
|
1006
|
+
bindingRows,
|
|
1007
|
+
operationRows
|
|
1008
|
+
});
|
|
1009
|
+
return { _tag: "prospective", target, snapshot };
|
|
1010
|
+
};
|
|
1011
|
+
const readAdmissionState = (database, target) => {
|
|
1012
|
+
database.exec("BEGIN");
|
|
1013
|
+
try {
|
|
1014
|
+
const state = admissionState(database, target);
|
|
1015
|
+
database.exec("ROLLBACK");
|
|
1016
|
+
return state;
|
|
1017
|
+
}
|
|
1018
|
+
catch (cause) {
|
|
1019
|
+
database.exec("ROLLBACK");
|
|
1020
|
+
throw cause;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
const recoveryTables = [
|
|
1024
|
+
"work_goal_events",
|
|
1025
|
+
"work_lane_claims",
|
|
1026
|
+
"work_lane_operations",
|
|
1027
|
+
"work_agent_bindings",
|
|
1028
|
+
"work_goal_transactions",
|
|
1029
|
+
"work_goal_transaction_totals",
|
|
1030
|
+
"work_lane_operation_totals",
|
|
1031
|
+
"work_decision_handoffs",
|
|
1032
|
+
"work_decision_totals"
|
|
1033
|
+
];
|
|
1034
|
+
/** Validates every historical operation against its indexed row and current lane. */
|
|
1035
|
+
const readRecoveryOperations = (database) => {
|
|
1036
|
+
const lanes = readValidatedLaneLedger(database, "recovery.preflight");
|
|
1037
|
+
if (lanes._tag === "invalid")
|
|
1038
|
+
throw lanes.error;
|
|
1039
|
+
const rows = Schema.decodeUnknownSync(Schema.Array(AgentBindingLaneOperationRow))(database
|
|
1040
|
+
.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
1041
|
+
phase, revision, record FROM work_lane_operations ORDER BY operation_id ASC LIMIT ?`)
|
|
1042
|
+
.all(workLaneOperationMaxRecords + 1));
|
|
1043
|
+
if (rows.length > workLaneOperationMaxRecords) {
|
|
1044
|
+
throw new WorkStoreError({ cause: rows.length, operation: "recovery.preflight.ledger-capacity" });
|
|
1045
|
+
}
|
|
1046
|
+
const current = new Map(lanes.entries.map(({ claim, row }) => [claim.laneId, { claim, row }]));
|
|
1047
|
+
const revisions = new Set();
|
|
1048
|
+
const claims = rows.map((row) => {
|
|
1049
|
+
let claim;
|
|
1050
|
+
try {
|
|
1051
|
+
claim = Schema.decodeUnknownSync(WorkLaneClaimed)(JSON.parse(row.record));
|
|
1052
|
+
}
|
|
1053
|
+
catch (cause) {
|
|
1054
|
+
throw new WorkStoreError({ cause, operation: "recovery.preflight.operation-decode" });
|
|
1055
|
+
}
|
|
1056
|
+
if (claim.operationId !== row.operationId || claim.laneId !== row.laneId ||
|
|
1057
|
+
claim.goalId !== row.goalId || claim.phase !== row.phase || claim.revision !== row.revision) {
|
|
1058
|
+
throw new WorkStoreError({ cause: { claim, row }, operation: "recovery.preflight.operation-identity" });
|
|
1059
|
+
}
|
|
1060
|
+
const lane = current.get(claim.laneId);
|
|
1061
|
+
const key = `${claim.laneId}\u0000${String(claim.revision)}`;
|
|
1062
|
+
if (lane === undefined || lane.claim.goalId !== claim.goalId ||
|
|
1063
|
+
claim.revision > lane.claim.revision || revisions.has(key) ||
|
|
1064
|
+
(claim.revision === lane.claim.revision && row.record !== lane.row.record)) {
|
|
1065
|
+
throw new WorkStoreError({ cause: { claim, row, lane }, operation: "recovery.preflight.operation-companion" });
|
|
1066
|
+
}
|
|
1067
|
+
revisions.add(key);
|
|
1068
|
+
return claim;
|
|
1069
|
+
});
|
|
1070
|
+
const byOperation = new Map(rows.map((row) => [row.operationId, row]));
|
|
1071
|
+
if (lanes.entries.some(({ claim, row }) => {
|
|
1072
|
+
const latest = byOperation.get(claim.operationId);
|
|
1073
|
+
return latest === undefined || latest.revision !== claim.revision || latest.record !== row.record;
|
|
1074
|
+
})) {
|
|
1075
|
+
throw new WorkStoreError({ cause: lanes.entries, operation: "recovery.preflight.missing-latest-operation" });
|
|
1076
|
+
}
|
|
1077
|
+
return { lanes, rows, claims };
|
|
1078
|
+
};
|
|
1079
|
+
/** Reads the complete Work authority, including rows outside every projection window. */
|
|
1080
|
+
const recoveryState = (database, target) => {
|
|
1081
|
+
const inspected = admissionState(database, target);
|
|
1082
|
+
const operations = readRecoveryOperations(database);
|
|
1083
|
+
if (inspected._tag === "existing")
|
|
1084
|
+
return { _tag: "existing", target, link: inspected.link };
|
|
1085
|
+
const conflict = (reason) => ({
|
|
1086
|
+
_tag: "conflict",
|
|
1087
|
+
target,
|
|
1088
|
+
reason
|
|
1089
|
+
});
|
|
1090
|
+
const tables = recoveryTables.map((name) => {
|
|
1091
|
+
const rows = database.prepare(`SELECT * FROM ${name} ORDER BY rowid ASC LIMIT ?`).all(workHistoryMaxEvents + 1);
|
|
1092
|
+
if (rows.length > workHistoryMaxEvents) {
|
|
1093
|
+
throw new WorkStoreError({
|
|
1094
|
+
cause: name,
|
|
1095
|
+
operation: "recovery.preflight.capacity"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
return { name, rows };
|
|
1099
|
+
});
|
|
1100
|
+
const eventRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingGoalEventRow))(database
|
|
1101
|
+
.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
1102
|
+
FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC LIMIT ?`)
|
|
1103
|
+
.all(workHistoryMaxEvents + 1));
|
|
1104
|
+
if (eventRows.length > workHistoryMaxEvents) {
|
|
1105
|
+
throw new WorkStoreError({
|
|
1106
|
+
cause: target,
|
|
1107
|
+
operation: "recovery.preflight.history-capacity"
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
const history = eventRows.map((row) => {
|
|
1111
|
+
const decision = decodeAgentBindingGoalEvent(row, "recovery.preflight.event");
|
|
1112
|
+
if (decision._tag === "invalid")
|
|
1113
|
+
throw decision.error;
|
|
1114
|
+
return decision.checkpoint;
|
|
1115
|
+
});
|
|
1116
|
+
const historyError = workHistoryError(history);
|
|
1117
|
+
if (historyError !== undefined)
|
|
1118
|
+
throw historyError;
|
|
1119
|
+
const own = history.filter(({ goal }) => goal.id === target.goalId);
|
|
1120
|
+
const original = own.at(-1);
|
|
1121
|
+
if (original === undefined)
|
|
1122
|
+
return conflict("canonical goal does not exist");
|
|
1123
|
+
if (original.eventId !== target.expectedGoalEventId || original.goal.updatedAt !== target.expectedGoalUpdatedAt) {
|
|
1124
|
+
return conflict("approved goal event or revision is stale");
|
|
1125
|
+
}
|
|
1126
|
+
if (own.some(({ goal }) => goal.goalFamily?.role !== "canonical" ||
|
|
1127
|
+
goal.goalFamily.canonicalGoalId !== target.goalId ||
|
|
1128
|
+
!Equal.equals(goal.owner, target.owner) ||
|
|
1129
|
+
(goal.repository.repository !== target.repository &&
|
|
1130
|
+
(goal.repository.repository !== target.worktree || goal.repository.branch !== target.branch)) ||
|
|
1131
|
+
goal.connectTarget !== null ||
|
|
1132
|
+
(goal.agentHierarchy !== undefined && goal.agentHierarchy !== null) ||
|
|
1133
|
+
(goal.review?.url !== null && goal.review?.url !== undefined && goal.review.url !== target.reviewUrl) ||
|
|
1134
|
+
goal.state === "completed" ||
|
|
1135
|
+
goal.state === "deployed")) {
|
|
1136
|
+
return conflict("canonical goal history is linked, terminal, or belongs to another owner");
|
|
1137
|
+
}
|
|
1138
|
+
const bindingRows = Schema.decodeUnknownSync(AgentBindingRows)(database
|
|
1139
|
+
.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
1140
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
1141
|
+
FROM work_agent_bindings ORDER BY dispatch_request_id ASC LIMIT ?`)
|
|
1142
|
+
.all(workLaneOperationMaxRecords + 1));
|
|
1143
|
+
if (bindingRows.length > workLaneOperationMaxRecords) {
|
|
1144
|
+
throw new WorkStoreError({
|
|
1145
|
+
cause: target,
|
|
1146
|
+
operation: "recovery.preflight.ledger-capacity"
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
const bindings = bindingRows.map((row) => Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(row.record)));
|
|
1150
|
+
const decisions = Schema.decodeUnknownSync(Schema.Array(DecisionRow))(database
|
|
1151
|
+
.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
1152
|
+
occurred_at AS occurredAt, record FROM work_decision_handoffs ORDER BY handoff_id ASC LIMIT ?`)
|
|
1153
|
+
.all(workHistoryMaxEvents + 1));
|
|
1154
|
+
if (decisions.length > workHistoryMaxEvents) {
|
|
1155
|
+
throw new WorkStoreError({
|
|
1156
|
+
cause: target,
|
|
1157
|
+
operation: "recovery.preflight.decision-capacity"
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (history.some(({ goal }) => goal.id !== target.goalId &&
|
|
1161
|
+
(goal.review?.url === target.reviewUrl ||
|
|
1162
|
+
goal.goalFamily?.canonicalGoalId === target.goalId ||
|
|
1163
|
+
goal.agentHierarchy?.agent.agentId === target.worker.agentId)) ||
|
|
1164
|
+
operations.lanes.entries.some(({ claim }) => claim.goalId === target.goalId || claim.laneId === target.laneId || claim.worktree === target.worktree) ||
|
|
1165
|
+
operations.claims.some(({ goalId, laneId, worktree }) => goalId === target.goalId || laneId === target.laneId || worktree === target.worktree) ||
|
|
1166
|
+
bindings.some(({ request }) => request.laneId === target.laneId ||
|
|
1167
|
+
request.worker.agentId === target.worker.agentId ||
|
|
1168
|
+
request.prospectiveAdmission?.sessionId === target.sessionId ||
|
|
1169
|
+
request.existingGoalRecovery?.sessionId === target.sessionId) ||
|
|
1170
|
+
decisions.some(({ record }) => {
|
|
1171
|
+
const handoff = Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(record));
|
|
1172
|
+
return (handoff.goalId === target.goalId || handoff.laneId === target.laneId || handoff.sessionId === target.sessionId);
|
|
1173
|
+
})) {
|
|
1174
|
+
return conflict("historical PR, goal, lane, worker, session, or worktree linkage conflicts");
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
_tag: "recoverable",
|
|
1178
|
+
target,
|
|
1179
|
+
original,
|
|
1180
|
+
history,
|
|
1181
|
+
snapshot: JSON.stringify({ target, tables })
|
|
1182
|
+
};
|
|
1183
|
+
};
|
|
1184
|
+
const readRecoveryState = (database, target) => {
|
|
1185
|
+
database.exec("BEGIN");
|
|
1186
|
+
try {
|
|
1187
|
+
const state = recoveryState(database, target);
|
|
1188
|
+
database.exec("ROLLBACK");
|
|
1189
|
+
return state;
|
|
1190
|
+
}
|
|
1191
|
+
catch (cause) {
|
|
1192
|
+
database.exec("ROLLBACK");
|
|
1193
|
+
throw cause;
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
35
1196
|
export class WorkStore {
|
|
36
1197
|
#database;
|
|
37
|
-
#
|
|
1198
|
+
#cryptoService;
|
|
1199
|
+
#secureFiles;
|
|
38
1200
|
path;
|
|
39
|
-
constructor(path,
|
|
1201
|
+
constructor(path, opened, cryptoService) {
|
|
40
1202
|
this.path = path;
|
|
41
|
-
this.#
|
|
42
|
-
this.#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1203
|
+
this.#database = opened.database;
|
|
1204
|
+
this.#secureFiles = opened.secureFiles.pipe(Effect.mapError(fromPrivateDatabaseError));
|
|
1205
|
+
this.#cryptoService = cryptoService;
|
|
1206
|
+
}
|
|
1207
|
+
static open = Effect.fn("WorkStore.open")(function* (path) {
|
|
1208
|
+
const cryptoService = yield* Crypto.Crypto;
|
|
1209
|
+
const opened = yield* openPrivateSqlite(path, {
|
|
1210
|
+
busyTimeoutMillis: workStoreBusyTimeoutMillis,
|
|
1211
|
+
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(`
|
|
46
1215
|
CREATE TABLE IF NOT EXISTS work_goal_events (
|
|
47
1216
|
event_id TEXT PRIMARY KEY,
|
|
48
1217
|
goal_id TEXT NOT NULL,
|
|
@@ -50,28 +1219,1117 @@ export class WorkStore {
|
|
|
50
1219
|
record TEXT NOT NULL,
|
|
51
1220
|
UNIQUE (goal_id, occurred_at)
|
|
52
1221
|
);
|
|
1222
|
+
CREATE TABLE IF NOT EXISTS work_agent_bindings (
|
|
1223
|
+
dispatch_request_id TEXT PRIMARY KEY,
|
|
1224
|
+
lane_id TEXT NOT NULL,
|
|
1225
|
+
expected_revision INTEGER NOT NULL,
|
|
1226
|
+
revision INTEGER NOT NULL,
|
|
1227
|
+
agent_id TEXT NOT NULL,
|
|
1228
|
+
host TEXT NOT NULL,
|
|
1229
|
+
record TEXT NOT NULL
|
|
1230
|
+
);
|
|
1231
|
+
CREATE INDEX IF NOT EXISTS work_agent_bindings_lane_revision
|
|
1232
|
+
ON work_agent_bindings (lane_id, revision);
|
|
1233
|
+
CREATE TABLE IF NOT EXISTS work_goal_transactions (
|
|
1234
|
+
transaction_id TEXT PRIMARY KEY,
|
|
1235
|
+
record TEXT NOT NULL
|
|
1236
|
+
);
|
|
1237
|
+
CREATE TABLE IF NOT EXISTS work_goal_transaction_totals (
|
|
1238
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
1239
|
+
transaction_count INTEGER NOT NULL CHECK (transaction_count >= 0),
|
|
1240
|
+
transaction_bytes INTEGER NOT NULL CHECK (transaction_bytes >= 0)
|
|
1241
|
+
);
|
|
1242
|
+
CREATE TRIGGER IF NOT EXISTS work_goal_transactions_after_insert
|
|
1243
|
+
AFTER INSERT ON work_goal_transactions
|
|
1244
|
+
BEGIN
|
|
1245
|
+
UPDATE work_goal_transaction_totals
|
|
1246
|
+
SET transaction_count = transaction_count + 1,
|
|
1247
|
+
transaction_bytes = transaction_bytes +
|
|
1248
|
+
length(CAST(NEW.transaction_id AS BLOB)) + length(CAST(NEW.record AS BLOB))
|
|
1249
|
+
WHERE singleton = 1;
|
|
1250
|
+
END;
|
|
1251
|
+
CREATE TABLE IF NOT EXISTS work_lane_claims (
|
|
1252
|
+
lane_id TEXT PRIMARY KEY,
|
|
1253
|
+
goal_id TEXT NOT NULL,
|
|
1254
|
+
operation_id TEXT NOT NULL UNIQUE,
|
|
1255
|
+
phase TEXT NOT NULL,
|
|
1256
|
+
revision INTEGER NOT NULL,
|
|
1257
|
+
record TEXT NOT NULL
|
|
1258
|
+
);
|
|
1259
|
+
CREATE UNIQUE INDEX IF NOT EXISTS work_lane_claims_one_active_goal
|
|
1260
|
+
ON work_lane_claims (goal_id)
|
|
1261
|
+
WHERE phase <> 'shipped';
|
|
1262
|
+
CREATE TABLE IF NOT EXISTS work_lane_operations (
|
|
1263
|
+
operation_id TEXT PRIMARY KEY,
|
|
1264
|
+
lane_id TEXT NOT NULL,
|
|
1265
|
+
goal_id TEXT NOT NULL,
|
|
1266
|
+
phase TEXT NOT NULL,
|
|
1267
|
+
revision INTEGER NOT NULL,
|
|
1268
|
+
record TEXT NOT NULL
|
|
1269
|
+
);
|
|
1270
|
+
CREATE INDEX IF NOT EXISTS work_lane_operations_lane_revision
|
|
1271
|
+
ON work_lane_operations (lane_id, revision);
|
|
1272
|
+
CREATE TABLE IF NOT EXISTS work_lane_operation_totals (
|
|
1273
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
1274
|
+
operation_count INTEGER NOT NULL CHECK (operation_count >= 0),
|
|
1275
|
+
operation_bytes INTEGER NOT NULL CHECK (operation_bytes >= 0)
|
|
1276
|
+
);
|
|
1277
|
+
CREATE TRIGGER IF NOT EXISTS work_lane_operations_after_insert
|
|
1278
|
+
AFTER INSERT ON work_lane_operations
|
|
1279
|
+
BEGIN
|
|
1280
|
+
UPDATE work_lane_operation_totals
|
|
1281
|
+
SET operation_count = operation_count + 1,
|
|
1282
|
+
operation_bytes = operation_bytes +
|
|
1283
|
+
length(CAST(NEW.operation_id AS BLOB)) + length(CAST(NEW.record AS BLOB))
|
|
1284
|
+
WHERE singleton = 1;
|
|
1285
|
+
END;
|
|
1286
|
+
-- One row per approved reassignment. Every row commits together with the
|
|
1287
|
+
-- goal checkpoint whose event id equals its approval job id, so the
|
|
1288
|
+
-- work_goal_events capacity bounds this table too.
|
|
1289
|
+
CREATE TABLE IF NOT EXISTS work_goal_reassignments (
|
|
1290
|
+
approval_job_id TEXT PRIMARY KEY,
|
|
1291
|
+
goal_id TEXT NOT NULL,
|
|
1292
|
+
record TEXT NOT NULL
|
|
1293
|
+
);
|
|
1294
|
+
CREATE TABLE IF NOT EXISTS work_decision_handoffs (
|
|
1295
|
+
handoff_id TEXT PRIMARY KEY,
|
|
1296
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
1297
|
+
lane_id TEXT NOT NULL,
|
|
1298
|
+
occurred_at INTEGER NOT NULL,
|
|
1299
|
+
record TEXT NOT NULL
|
|
1300
|
+
);
|
|
1301
|
+
CREATE TABLE IF NOT EXISTS work_decision_totals (
|
|
1302
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
1303
|
+
decision_count INTEGER NOT NULL CHECK (decision_count >= 0),
|
|
1304
|
+
decision_bytes INTEGER NOT NULL CHECK (decision_bytes >= 0)
|
|
1305
|
+
);
|
|
1306
|
+
CREATE TRIGGER IF NOT EXISTS work_decision_handoffs_after_insert
|
|
1307
|
+
AFTER INSERT ON work_decision_handoffs
|
|
1308
|
+
BEGIN
|
|
1309
|
+
UPDATE work_decision_totals
|
|
1310
|
+
SET decision_count = decision_count + 1,
|
|
1311
|
+
decision_bytes = decision_bytes +
|
|
1312
|
+
length(CAST(NEW.handoff_id AS BLOB)) + length(CAST(NEW.record AS BLOB))
|
|
1313
|
+
WHERE singleton = 1;
|
|
1314
|
+
END;
|
|
1315
|
+
CREATE INDEX IF NOT EXISTS work_decision_handoffs_lane_time
|
|
1316
|
+
ON work_decision_handoffs (lane_id, occurred_at, handoff_id);
|
|
1317
|
+
CREATE INDEX IF NOT EXISTS work_decision_handoffs_session
|
|
1318
|
+
ON work_decision_handoffs (session_id);
|
|
53
1319
|
`);
|
|
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(`
|
|
1326
|
+
INSERT OR IGNORE INTO work_lane_operations
|
|
1327
|
+
(operation_id, lane_id, goal_id, phase, revision, record)
|
|
1328
|
+
SELECT operation_id, lane_id, goal_id, phase, revision, record
|
|
1329
|
+
FROM work_lane_claims
|
|
1330
|
+
`);
|
|
1331
|
+
}
|
|
1332
|
+
database.exec(`
|
|
1333
|
+
INSERT OR IGNORE INTO work_lane_operation_totals
|
|
1334
|
+
(singleton, operation_count, operation_bytes)
|
|
1335
|
+
SELECT 1, COUNT(*), COALESCE(SUM(
|
|
1336
|
+
length(CAST(operation_id AS BLOB)) + length(CAST(record AS BLOB))
|
|
1337
|
+
), 0)
|
|
1338
|
+
FROM work_lane_operations
|
|
1339
|
+
`);
|
|
1340
|
+
database.exec(`
|
|
1341
|
+
INSERT OR IGNORE INTO work_goal_transaction_totals
|
|
1342
|
+
(singleton, transaction_count, transaction_bytes)
|
|
1343
|
+
SELECT 1, COUNT(*), COALESCE(SUM(
|
|
1344
|
+
length(CAST(transaction_id AS BLOB)) + length(CAST(record AS BLOB))
|
|
1345
|
+
), 0)
|
|
1346
|
+
FROM work_goal_transactions
|
|
1347
|
+
`);
|
|
1348
|
+
database.exec(`
|
|
1349
|
+
INSERT OR REPLACE INTO work_decision_totals
|
|
1350
|
+
(singleton, decision_count, decision_bytes)
|
|
1351
|
+
SELECT 1, COUNT(*), COALESCE(SUM(
|
|
1352
|
+
length(CAST(handoff_id AS BLOB)) + length(CAST(record AS BLOB))
|
|
1353
|
+
), 0)
|
|
1354
|
+
FROM work_decision_handoffs
|
|
1355
|
+
`);
|
|
1356
|
+
}
|
|
1357
|
+
}).pipe(Effect.mapError(fromPrivateDatabaseError));
|
|
1358
|
+
return new WorkStore(path, opened, cryptoService);
|
|
1359
|
+
});
|
|
1360
|
+
recoveryPreflight = Effect.fn("WorkStore.recoveryPreflight")(function* (target) {
|
|
1361
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkRecoveryTarget)(target).pipe(Effect.mapError(storeError("recovery.preflight.decode")));
|
|
1362
|
+
const state = yield* Effect.try({
|
|
1363
|
+
try: () => readRecoveryState(this.#database, decoded),
|
|
1364
|
+
catch: storeError("recovery.preflight.read")
|
|
1365
|
+
});
|
|
1366
|
+
if (state._tag !== "recoverable")
|
|
1367
|
+
return state;
|
|
1368
|
+
const historyToken = yield* this.#cryptoService
|
|
1369
|
+
.digest("SHA-256", utf8.encode(state.snapshot))
|
|
1370
|
+
.pipe(Effect.mapError(storeError("recovery.preflight.digest")), Effect.map(Hex.encode));
|
|
1371
|
+
const preflight = {
|
|
1372
|
+
_tag: "recoverable",
|
|
1373
|
+
target: decoded,
|
|
1374
|
+
historyToken
|
|
1375
|
+
};
|
|
1376
|
+
return preflight;
|
|
1377
|
+
});
|
|
1378
|
+
recoverExistingGoal = Effect.fn("WorkStore.recoverExistingGoal")(function* (request) {
|
|
1379
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkExistingGoalRecovery)(request).pipe(Effect.mapError(storeError("recovery.decode")));
|
|
1380
|
+
const target = Schema.decodeUnknownSync(WorkRecoveryTarget)(decoded);
|
|
1381
|
+
const observedAt = yield* Clock.currentTimeMillis;
|
|
1382
|
+
yield* this.secureFiles();
|
|
1383
|
+
const observed = yield* Effect.try({
|
|
1384
|
+
try: () => readRecoveryState(this.#database, target),
|
|
1385
|
+
catch: storeError("recovery.inspect")
|
|
1386
|
+
});
|
|
1387
|
+
const historyToken = observed._tag === "recoverable"
|
|
1388
|
+
? yield* this.#cryptoService
|
|
1389
|
+
.digest("SHA-256", utf8.encode(observed.snapshot))
|
|
1390
|
+
.pipe(Effect.mapError(storeError("recovery.digest")), Effect.map(Hex.encode))
|
|
1391
|
+
: undefined;
|
|
1392
|
+
const decision = yield* Effect.try({
|
|
1393
|
+
try: () => {
|
|
1394
|
+
let transaction = false;
|
|
1395
|
+
const reject = (reason) => ({
|
|
1396
|
+
_tag: "rejected",
|
|
1397
|
+
error: admissionConflict(target, reason)
|
|
1398
|
+
});
|
|
1399
|
+
try {
|
|
1400
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
1401
|
+
transaction = true;
|
|
1402
|
+
const state = recoveryState(this.#database, target);
|
|
1403
|
+
if (state._tag === "existing") {
|
|
1404
|
+
const existing = state.link;
|
|
1405
|
+
const provenance = existing.binding.request.existingGoalRecovery;
|
|
1406
|
+
this.#database.exec("ROLLBACK");
|
|
1407
|
+
transaction = false;
|
|
1408
|
+
return existing.goalEventId === existing.binding.checkpoint.eventId &&
|
|
1409
|
+
existing.lane.revision === 1 &&
|
|
1410
|
+
Equal.equals(existing.goal, existing.binding.checkpoint.goal) &&
|
|
1411
|
+
existing.binding.request.dispatchRequestId === decoded.operationId &&
|
|
1412
|
+
provenance?.expectedGoalEventId === decoded.expectedGoalEventId &&
|
|
1413
|
+
provenance.expectedGoalUpdatedAt === decoded.expectedGoalUpdatedAt &&
|
|
1414
|
+
provenance.expectedHistoryToken === decoded.expectedHistoryToken &&
|
|
1415
|
+
provenance.approvalJobId === decoded.approvalJobId &&
|
|
1416
|
+
provenance.approvalActor === decoded.approvalActor &&
|
|
1417
|
+
provenance.approvalApprovedBy === decoded.approvalApprovedBy &&
|
|
1418
|
+
provenance.approvalApprovedAt === decoded.approvalApprovedAt &&
|
|
1419
|
+
provenance.approvalHash === decoded.approvalHash
|
|
1420
|
+
? { _tag: "linked", link: existing }
|
|
1421
|
+
: reject("existing linkage is not this exact approved recovery");
|
|
1422
|
+
}
|
|
1423
|
+
if (observed._tag !== "recoverable" ||
|
|
1424
|
+
state._tag !== "recoverable" ||
|
|
1425
|
+
state.snapshot !== observed.snapshot ||
|
|
1426
|
+
historyToken !== decoded.expectedHistoryToken) {
|
|
1427
|
+
this.#database.exec("ROLLBACK");
|
|
1428
|
+
transaction = false;
|
|
1429
|
+
return reject(state._tag === "conflict" ? state.reason : "stale complete-history evidence");
|
|
1430
|
+
}
|
|
1431
|
+
if (this.#database
|
|
1432
|
+
.prepare(`SELECT 1 FROM work_lane_operations WHERE operation_id = ?
|
|
1433
|
+
UNION ALL SELECT 1 FROM work_goal_events WHERE event_id = ?
|
|
1434
|
+
UNION ALL SELECT 1 FROM work_agent_bindings WHERE dispatch_request_id = ?
|
|
1435
|
+
UNION ALL SELECT 1 FROM work_goal_transactions WHERE transaction_id = ?
|
|
1436
|
+
UNION ALL SELECT 1 FROM work_decision_handoffs WHERE handoff_id = ? LIMIT 1`)
|
|
1437
|
+
.get(decoded.operationId, decoded.operationId, decoded.operationId, decoded.operationId, decoded.operationId) !== undefined) {
|
|
1438
|
+
this.#database.exec("ROLLBACK");
|
|
1439
|
+
transaction = false;
|
|
1440
|
+
return reject("operation ID is already used by another durable record");
|
|
1441
|
+
}
|
|
1442
|
+
const at = Math.max(observedAt, state.original.goal.updatedAt + 1);
|
|
1443
|
+
const previous = state.original.goal;
|
|
1444
|
+
const goal = Schema.decodeUnknownSync(WorkGoal)({
|
|
1445
|
+
...previous,
|
|
1446
|
+
repository: {
|
|
1447
|
+
repository: decoded.repository,
|
|
1448
|
+
branch: decoded.branch
|
|
1449
|
+
},
|
|
1450
|
+
review: previous.review === null || previous.review === undefined
|
|
1451
|
+
? {
|
|
1452
|
+
state: "not_requested",
|
|
1453
|
+
summary: null,
|
|
1454
|
+
updatedAt: at,
|
|
1455
|
+
url: decoded.reviewUrl
|
|
1456
|
+
}
|
|
1457
|
+
: { ...previous.review, url: decoded.reviewUrl },
|
|
1458
|
+
agentHierarchy: { agent: decoded.worker },
|
|
1459
|
+
connectTarget: agentConnectTarget(decoded.worker),
|
|
1460
|
+
activity: [
|
|
1461
|
+
...(previous.activity ?? []),
|
|
1462
|
+
{
|
|
1463
|
+
id: decoded.operationId,
|
|
1464
|
+
kind: "status",
|
|
1465
|
+
summary: `Existing canonical goal linked to settled owner by approved Fleet job ${decoded.approvalJobId}`,
|
|
1466
|
+
occurredAt: at
|
|
1467
|
+
}
|
|
1468
|
+
],
|
|
1469
|
+
updatedAt: at
|
|
1470
|
+
});
|
|
1471
|
+
const checkpoint = Schema.decodeUnknownSync(WorkGoalCheckpoint)({
|
|
1472
|
+
version: "herdr.work.event.v1",
|
|
1473
|
+
eventId: decoded.operationId,
|
|
1474
|
+
occurredAt: at,
|
|
1475
|
+
goal
|
|
1476
|
+
});
|
|
1477
|
+
const lane = Schema.decodeUnknownSync(WorkLaneClaimed)({
|
|
1478
|
+
operationId: decoded.operationId,
|
|
1479
|
+
goalId: decoded.goalId,
|
|
1480
|
+
laneId: decoded.laneId,
|
|
1481
|
+
worktree: decoded.worktree,
|
|
1482
|
+
branch: decoded.branch,
|
|
1483
|
+
head: decoded.head,
|
|
1484
|
+
owner: decoded.owner,
|
|
1485
|
+
parent: null,
|
|
1486
|
+
phase: "claim",
|
|
1487
|
+
expectedRevision: 0,
|
|
1488
|
+
revision: 1
|
|
1489
|
+
});
|
|
1490
|
+
const binding = Schema.decodeUnknownSync(WorkAgentBinding)({
|
|
1491
|
+
version: "herdr.work.agent-binding.v1",
|
|
1492
|
+
request: {
|
|
1493
|
+
version: "herdr.work.agent-binding-request.v1",
|
|
1494
|
+
dispatchRequestId: decoded.operationId,
|
|
1495
|
+
laneId: decoded.laneId,
|
|
1496
|
+
expectedRevision: 0,
|
|
1497
|
+
worker: decoded.worker,
|
|
1498
|
+
existingGoalRecovery: {
|
|
1499
|
+
sessionId: decoded.sessionId,
|
|
1500
|
+
workAssignment: decoded.expectedWork,
|
|
1501
|
+
baseHead: decoded.baseHead,
|
|
1502
|
+
expectedHistoryToken: decoded.expectedHistoryToken,
|
|
1503
|
+
expectedGoalEventId: decoded.expectedGoalEventId,
|
|
1504
|
+
expectedGoalUpdatedAt: decoded.expectedGoalUpdatedAt,
|
|
1505
|
+
approvalJobId: decoded.approvalJobId,
|
|
1506
|
+
approvalActor: decoded.approvalActor,
|
|
1507
|
+
approvalApprovedBy: decoded.approvalApprovedBy,
|
|
1508
|
+
approvalApprovedAt: decoded.approvalApprovedAt,
|
|
1509
|
+
approvalHash: decoded.approvalHash
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
1512
|
+
lane,
|
|
1513
|
+
checkpoint
|
|
1514
|
+
});
|
|
1515
|
+
const operationTotals = readLaneOperationLedgerTotals(this.#database);
|
|
1516
|
+
const laneRecord = JSON.stringify(lane);
|
|
1517
|
+
const operationBytes = utf8.encode(decoded.operationId).byteLength + utf8.encode(laneRecord).byteLength;
|
|
1518
|
+
const capacity = agentBindingAdmissionError({
|
|
1519
|
+
history: state.history,
|
|
1520
|
+
candidate: checkpoint,
|
|
1521
|
+
operationCount: operationTotals.operationCount,
|
|
1522
|
+
operationBytes: operationTotals.operationBytes,
|
|
1523
|
+
candidateOperationBytes: operationBytes
|
|
1524
|
+
});
|
|
1525
|
+
const lanes = readValidatedLaneLedger(this.#database, "recovery.capacity");
|
|
1526
|
+
if (lanes._tag === "invalid")
|
|
1527
|
+
throw lanes.error;
|
|
1528
|
+
const claimBytes = Schema.decodeUnknownSync(LedgerBytesRow)(this.#database
|
|
1529
|
+
.prepare(`SELECT COALESCE(SUM(length(CAST(lane_id AS BLOB)) + length(CAST(record AS BLOB))), 0) AS bytes
|
|
1530
|
+
FROM work_lane_claims`)
|
|
1531
|
+
.get()).bytes;
|
|
1532
|
+
if (capacity !== undefined ||
|
|
1533
|
+
lanes.entries.length >= workLaneMaxRecords ||
|
|
1534
|
+
claimBytes + utf8.encode(decoded.laneId).byteLength + utf8.encode(laneRecord).byteLength > workLaneMaxBytes) {
|
|
1535
|
+
this.#database.exec("ROLLBACK");
|
|
1536
|
+
transaction = false;
|
|
1537
|
+
return {
|
|
1538
|
+
_tag: "rejected",
|
|
1539
|
+
error: capacity ??
|
|
1540
|
+
new WorkProjectionError({
|
|
1541
|
+
cause: decoded,
|
|
1542
|
+
detail: "existing-goal recovery exceeds Work capacity",
|
|
1543
|
+
reason: "capacity_exceeded"
|
|
1544
|
+
})
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
const link = Schema.decodeUnknownSync(WorkPullRequestLink)({
|
|
1548
|
+
request: {
|
|
1549
|
+
repository: decoded.repository,
|
|
1550
|
+
pullRequest: decoded.pullRequest,
|
|
1551
|
+
goalId: decoded.goalId,
|
|
1552
|
+
laneId: decoded.laneId
|
|
1553
|
+
},
|
|
1554
|
+
goalEventId: checkpoint.eventId,
|
|
1555
|
+
goal,
|
|
1556
|
+
lane,
|
|
1557
|
+
binding
|
|
1558
|
+
});
|
|
1559
|
+
this.#database
|
|
1560
|
+
.prepare(`INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record)
|
|
1561
|
+
VALUES (?, ?, ?, ?)`)
|
|
1562
|
+
.run(checkpoint.eventId, checkpoint.goal.id, checkpoint.occurredAt, JSON.stringify(checkpoint));
|
|
1563
|
+
this.#database
|
|
1564
|
+
.prepare(`INSERT INTO work_lane_claims (lane_id, goal_id, operation_id, phase, revision, record)
|
|
1565
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
1566
|
+
.run(lane.laneId, lane.goalId, lane.operationId, lane.phase, lane.revision, laneRecord);
|
|
1567
|
+
this.#database
|
|
1568
|
+
.prepare(`INSERT INTO work_lane_operations (operation_id, lane_id, goal_id, phase, revision, record)
|
|
1569
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
1570
|
+
.run(lane.operationId, lane.laneId, lane.goalId, lane.phase, lane.revision, laneRecord);
|
|
1571
|
+
this.#database
|
|
1572
|
+
.prepare(`INSERT INTO work_agent_bindings
|
|
1573
|
+
(dispatch_request_id, lane_id, expected_revision, revision, agent_id, host, record)
|
|
1574
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
1575
|
+
.run(decoded.operationId, decoded.laneId, 0, 1, decoded.worker.agentId, decoded.worker.host, JSON.stringify(binding));
|
|
1576
|
+
this.#database.exec("COMMIT");
|
|
1577
|
+
transaction = false;
|
|
1578
|
+
return { _tag: "linked", link };
|
|
1579
|
+
}
|
|
1580
|
+
catch (cause) {
|
|
1581
|
+
if (transaction)
|
|
1582
|
+
this.#database.exec("ROLLBACK");
|
|
1583
|
+
throw cause;
|
|
1584
|
+
}
|
|
1585
|
+
},
|
|
1586
|
+
catch: storeError("recovery.transaction")
|
|
1587
|
+
});
|
|
1588
|
+
if (decision._tag === "rejected")
|
|
1589
|
+
return yield* decision.error;
|
|
1590
|
+
return decision.link;
|
|
1591
|
+
});
|
|
1592
|
+
reassign = Effect.fn("WorkStore.reassign")(function* (request) {
|
|
1593
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkGoalReassignment)(request).pipe(Effect.mapError(storeError("reassign.decode")));
|
|
1594
|
+
const jobId = decoded.approvalJobId;
|
|
1595
|
+
const observedAt = yield* Clock.currentTimeMillis;
|
|
1596
|
+
yield* this.secureFiles();
|
|
1597
|
+
const decision = yield* Effect.try({
|
|
1598
|
+
try: () => {
|
|
1599
|
+
let transaction = false;
|
|
1600
|
+
const reject = (error) => {
|
|
1601
|
+
this.#database.exec("ROLLBACK");
|
|
1602
|
+
transaction = false;
|
|
1603
|
+
return { _tag: "rejected", error };
|
|
1604
|
+
};
|
|
1605
|
+
const ownerMismatch = (laneId, actual) => reject(new WorkGoalOwnerMismatchError({
|
|
1606
|
+
goalId: decoded.goalId,
|
|
1607
|
+
laneId,
|
|
1608
|
+
expectedOwner: { id: decoded.from.id, name: decoded.from.name },
|
|
1609
|
+
actualOwner: { id: actual.id, name: actual.name }
|
|
1610
|
+
}));
|
|
1611
|
+
try {
|
|
1612
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
1613
|
+
transaction = true;
|
|
1614
|
+
const priorRaw = this.#database
|
|
1615
|
+
.prepare(`SELECT approval_job_id AS approvalJobId, goal_id AS goalId, record
|
|
1616
|
+
FROM work_goal_reassignments WHERE approval_job_id = ?`)
|
|
1617
|
+
.get(jobId);
|
|
1618
|
+
if (priorRaw !== undefined) {
|
|
1619
|
+
const row = Schema.decodeUnknownSync(ReassignmentRow)(priorRaw);
|
|
1620
|
+
const prior = Schema.decodeUnknownSync(WorkGoalReassigned)(JSON.parse(row.record));
|
|
1621
|
+
if (row.approvalJobId !== prior.reassignment.approvalJobId || row.goalId !== prior.reassignment.goalId) {
|
|
1622
|
+
return reject(new WorkStoreError({ cause: { prior, row }, operation: "reassign.replay.identity-mismatch" }));
|
|
1623
|
+
}
|
|
1624
|
+
if (!Equal.equals(prior.reassignment, decoded)) {
|
|
1625
|
+
return reject(new WorkGoalReassignmentConflictError({ approvalJobId: jobId, reason: "payload_mismatch" }));
|
|
1626
|
+
}
|
|
1627
|
+
this.#database.exec("ROLLBACK");
|
|
1628
|
+
transaction = false;
|
|
1629
|
+
return { _tag: "reassigned", result: prior };
|
|
1630
|
+
}
|
|
1631
|
+
const collision = this.#database
|
|
1632
|
+
.prepare(`SELECT 1 FROM work_goal_events WHERE event_id = ?
|
|
1633
|
+
UNION ALL SELECT 1 FROM work_lane_operations WHERE operation_id = ?
|
|
1634
|
+
UNION ALL SELECT 1 FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 1`)
|
|
1635
|
+
.get(jobId, jobId, jobId);
|
|
1636
|
+
if (collision !== undefined) {
|
|
1637
|
+
return reject(new WorkGoalReassignmentConflictError({ approvalJobId: jobId, reason: "identifier_in_use" }));
|
|
1638
|
+
}
|
|
1639
|
+
const eventRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingGoalEventRow))(this.#database
|
|
1640
|
+
.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
1641
|
+
FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC LIMIT ?`)
|
|
1642
|
+
.all(workHistoryMaxEvents + 1));
|
|
1643
|
+
const history = [];
|
|
1644
|
+
for (const row of eventRows) {
|
|
1645
|
+
const event = decodeAgentBindingGoalEvent(row, "reassign.history");
|
|
1646
|
+
if (event._tag === "invalid")
|
|
1647
|
+
return reject(event.error);
|
|
1648
|
+
history.push(event.checkpoint);
|
|
1649
|
+
}
|
|
1650
|
+
const head = history.filter(({ goal }) => goal.id === decoded.goalId).at(-1);
|
|
1651
|
+
if (head === undefined ||
|
|
1652
|
+
head.eventId !== decoded.expectedGoalEventId ||
|
|
1653
|
+
head.goal.updatedAt !== decoded.expectedGoalUpdatedAt) {
|
|
1654
|
+
return reject(new WorkGoalRevisionConflictError({
|
|
1655
|
+
goalId: decoded.goalId,
|
|
1656
|
+
expectedEventId: decoded.expectedGoalEventId,
|
|
1657
|
+
expectedUpdatedAt: decoded.expectedGoalUpdatedAt,
|
|
1658
|
+
actualEventId: head?.eventId ?? null,
|
|
1659
|
+
actualUpdatedAt: head?.goal.updatedAt ?? null
|
|
1660
|
+
}));
|
|
1661
|
+
}
|
|
1662
|
+
if (!Equal.equals(head.goal.owner, decoded.from))
|
|
1663
|
+
return ownerMismatch(null, head.goal.owner);
|
|
1664
|
+
const ledger = readValidatedLaneLedger(this.#database, "reassign.lane");
|
|
1665
|
+
if (ledger._tag === "invalid")
|
|
1666
|
+
return reject(ledger.error);
|
|
1667
|
+
const active = ledger.entries.filter(({ claim }) => claim.goalId === decoded.goalId && claim.phase !== "shipped");
|
|
1668
|
+
if (active.length > 1) {
|
|
1669
|
+
return reject(new WorkStoreError({ cause: active, operation: "reassign.lane.ambiguous" }));
|
|
1670
|
+
}
|
|
1671
|
+
const current = active[0];
|
|
1672
|
+
if (current !== undefined && !Equal.equals(current.claim.owner, decoded.from)) {
|
|
1673
|
+
return ownerMismatch(current.claim.laneId, current.claim.owner);
|
|
1674
|
+
}
|
|
1675
|
+
const latestBindingRaw = current === undefined ? undefined : this.#database
|
|
1676
|
+
.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
1677
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
1678
|
+
FROM work_agent_bindings WHERE lane_id = ? ORDER BY revision DESC LIMIT 1`)
|
|
1679
|
+
.get(current.claim.laneId);
|
|
1680
|
+
const latestBindingRow = latestBindingRaw === undefined
|
|
1681
|
+
? undefined
|
|
1682
|
+
: Schema.decodeUnknownSync(AgentBindingRow)(latestBindingRaw);
|
|
1683
|
+
const bindingDecision = latestBindingRow === undefined ? null : decodeAgentBindingRow(latestBindingRow, { dispatchRequestId: latestBindingRow.dispatchRequestId, laneId: latestBindingRow.laneId }, "reassign.binding");
|
|
1684
|
+
if (bindingDecision?._tag === "invalid")
|
|
1685
|
+
return reject(bindingDecision.error);
|
|
1686
|
+
const previousBinding = bindingDecision === null ? null : bindingDecision.binding;
|
|
1687
|
+
if (previousBinding !== null && decoded.toAgent._tag !== "set") {
|
|
1688
|
+
return reject(new WorkGoalBindingRequiresAgentError({
|
|
1689
|
+
goalId: decoded.goalId,
|
|
1690
|
+
laneId: previousBinding.request.laneId,
|
|
1691
|
+
dispatchRequestId: previousBinding.request.dispatchRequestId
|
|
1692
|
+
}));
|
|
1693
|
+
}
|
|
1694
|
+
const previous = head.goal;
|
|
1695
|
+
const currentAgentId = previous.agentHierarchy?.agent.agentId ?? previous.connectTarget?.agentId;
|
|
1696
|
+
if (decoded.toAgent._tag === "keep" && currentAgentId !== undefined) {
|
|
1697
|
+
return reject(new WorkGoalAgentTargetConflictError({
|
|
1698
|
+
goalId: decoded.goalId,
|
|
1699
|
+
agentId: currentAgentId,
|
|
1700
|
+
reason: "keep_existing_target",
|
|
1701
|
+
holderId: decoded.goalId
|
|
1702
|
+
}));
|
|
1703
|
+
}
|
|
1704
|
+
if (decoded.toAgent._tag === "set") {
|
|
1705
|
+
const agentId = decoded.toAgent.agent.agentId;
|
|
1706
|
+
const agentConflict = (reason, holderId) => reject(new WorkGoalAgentTargetConflictError({ goalId: decoded.goalId, agentId, reason, holderId }));
|
|
1707
|
+
const latestGoals = new Map();
|
|
1708
|
+
for (const { goal } of history)
|
|
1709
|
+
latestGoals.set(goal.id, goal);
|
|
1710
|
+
const goalHolder = [...latestGoals.values()].find((goal) => goal.id !== decoded.goalId &&
|
|
1711
|
+
(goal.agentHierarchy?.agent.agentId === agentId || goal.connectTarget?.agentId === agentId));
|
|
1712
|
+
if (goalHolder !== undefined)
|
|
1713
|
+
return agentConflict("held_by_other_goal", goalHolder.id);
|
|
1714
|
+
const holderRows = Schema.decodeUnknownSync(AgentBindingRows)(this.#database
|
|
1715
|
+
.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
1716
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
1717
|
+
FROM work_agent_bindings ORDER BY dispatch_request_id ASC LIMIT ?`)
|
|
1718
|
+
.all(workLaneOperationMaxRecords + 1));
|
|
1719
|
+
if (holderRows.length > workLaneOperationMaxRecords) {
|
|
1720
|
+
return reject(new WorkStoreError({ cause: holderRows.length, operation: "reassign.agent-holders.capacity" }));
|
|
1721
|
+
}
|
|
1722
|
+
const holders = [];
|
|
1723
|
+
for (const row of holderRows) {
|
|
1724
|
+
const holder = decodeAgentBindingRow(row, { dispatchRequestId: row.dispatchRequestId, laneId: row.laneId }, "reassign.agent-holders");
|
|
1725
|
+
if (holder._tag === "invalid")
|
|
1726
|
+
return reject(holder.error);
|
|
1727
|
+
holders.push(holder.binding);
|
|
1728
|
+
}
|
|
1729
|
+
const laneHolder = authoritativeBindings(holders).latest.find(({ lane, request }) => lane.goalId !== decoded.goalId && request.worker.agentId === agentId);
|
|
1730
|
+
if (laneHolder !== undefined)
|
|
1731
|
+
return agentConflict("held_by_other_lane", laneHolder.request.laneId);
|
|
1732
|
+
}
|
|
1733
|
+
// occurredAt equals updatedAt for every valid checkpoint; both bound the ordering defensively.
|
|
1734
|
+
const at = Math.max(observedAt, head.occurredAt + 1, head.goal.updatedAt + 1);
|
|
1735
|
+
const reassigned = {
|
|
1736
|
+
...previous,
|
|
1737
|
+
owner: decoded.to,
|
|
1738
|
+
activity: [
|
|
1739
|
+
...(previous.activity ?? []),
|
|
1740
|
+
{
|
|
1741
|
+
id: jobId,
|
|
1742
|
+
kind: "status",
|
|
1743
|
+
summary: workReassignActivitySummary(decoded, jobId, decoded.approvalHash),
|
|
1744
|
+
occurredAt: at
|
|
1745
|
+
}
|
|
1746
|
+
],
|
|
1747
|
+
updatedAt: at
|
|
1748
|
+
};
|
|
1749
|
+
const goal = Schema.decodeUnknownSync(WorkGoal)(withReassignedAgent(reassigned, decoded.toAgent));
|
|
1750
|
+
const checkpoint = Schema.decodeUnknownSync(WorkGoalCheckpoint)({
|
|
1751
|
+
version: "herdr.work.event.v1",
|
|
1752
|
+
eventId: jobId,
|
|
1753
|
+
occurredAt: at,
|
|
1754
|
+
goal
|
|
1755
|
+
});
|
|
1756
|
+
const lane = current === undefined ? null : Schema.decodeUnknownSync(WorkLaneClaimed)({
|
|
1757
|
+
operationId: jobId,
|
|
1758
|
+
goalId: current.claim.goalId,
|
|
1759
|
+
laneId: current.claim.laneId,
|
|
1760
|
+
worktree: current.claim.worktree,
|
|
1761
|
+
branch: current.claim.branch,
|
|
1762
|
+
head: current.claim.head,
|
|
1763
|
+
owner: decoded.to,
|
|
1764
|
+
parent: current.claim.parent,
|
|
1765
|
+
phase: current.claim.phase,
|
|
1766
|
+
expectedRevision: current.claim.revision,
|
|
1767
|
+
revision: current.claim.revision + 1
|
|
1768
|
+
});
|
|
1769
|
+
const binding = previousBinding === null || lane === null || decoded.toAgent._tag !== "set"
|
|
1770
|
+
? null
|
|
1771
|
+
: Schema.decodeUnknownSync(WorkAgentBinding)({
|
|
1772
|
+
version: "herdr.work.agent-binding.v1",
|
|
1773
|
+
request: {
|
|
1774
|
+
version: "herdr.work.agent-binding-request.v1",
|
|
1775
|
+
dispatchRequestId: jobId,
|
|
1776
|
+
laneId: lane.laneId,
|
|
1777
|
+
expectedRevision: lane.expectedRevision,
|
|
1778
|
+
worker: decoded.toAgent.agent,
|
|
1779
|
+
ownerReassignment: {
|
|
1780
|
+
previousDispatchRequestId: previousBinding.request.dispatchRequestId,
|
|
1781
|
+
approvalJobId: jobId,
|
|
1782
|
+
approvalActor: decoded.approvalActor,
|
|
1783
|
+
approvalApprovedBy: decoded.approvalApprovedBy,
|
|
1784
|
+
approvalApprovedAt: decoded.approvalApprovedAt,
|
|
1785
|
+
approvalHash: decoded.approvalHash
|
|
1786
|
+
}
|
|
1787
|
+
},
|
|
1788
|
+
lane,
|
|
1789
|
+
checkpoint
|
|
1790
|
+
});
|
|
1791
|
+
const result = Schema.decodeUnknownSync(WorkGoalReassigned)({
|
|
1792
|
+
reassignment: decoded,
|
|
1793
|
+
checkpoint,
|
|
1794
|
+
lane,
|
|
1795
|
+
binding
|
|
1796
|
+
});
|
|
1797
|
+
const familyError = validateGoalFamilyHistory([...history, checkpoint]);
|
|
1798
|
+
if (familyError !== undefined)
|
|
1799
|
+
return reject(familyError);
|
|
1800
|
+
const laneRecord = lane === null ? null : JSON.stringify(lane);
|
|
1801
|
+
// A goal without a lane appends no lane operation, so only a lane rewrite is held to that ledger.
|
|
1802
|
+
const operationTotals = lane === null
|
|
1803
|
+
? { operationCount: 0, operationBytes: 0 }
|
|
1804
|
+
: readLaneOperationLedgerTotals(this.#database);
|
|
1805
|
+
const capacity = agentBindingAdmissionError({
|
|
1806
|
+
history,
|
|
1807
|
+
candidate: checkpoint,
|
|
1808
|
+
operationCount: operationTotals.operationCount,
|
|
1809
|
+
operationBytes: operationTotals.operationBytes,
|
|
1810
|
+
candidateOperationBytes: laneRecord === null
|
|
1811
|
+
? 0
|
|
1812
|
+
: utf8.encode(jobId).byteLength + utf8.encode(laneRecord).byteLength
|
|
1813
|
+
});
|
|
1814
|
+
if (capacity !== undefined)
|
|
1815
|
+
return reject(capacity);
|
|
1816
|
+
if (current !== undefined && laneRecord !== null) {
|
|
1817
|
+
const claimBytes = Schema.decodeUnknownSync(LedgerBytesRow)(this.#database
|
|
1818
|
+
.prepare(`SELECT COALESCE(SUM(length(CAST(lane_id AS BLOB)) + length(CAST(record AS BLOB))), 0) AS bytes
|
|
1819
|
+
FROM work_lane_claims`)
|
|
1820
|
+
.get()).bytes;
|
|
1821
|
+
if (claimBytes - utf8.encode(current.row.record).byteLength + utf8.encode(laneRecord).byteLength >
|
|
1822
|
+
workLaneMaxBytes) {
|
|
1823
|
+
return reject(new WorkProjectionError({
|
|
1824
|
+
cause: decoded,
|
|
1825
|
+
detail: `work lane claims cannot exceed ${workLaneMaxBytes} encoded bytes`,
|
|
1826
|
+
reason: "capacity_exceeded"
|
|
1827
|
+
}));
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
this.#database
|
|
1831
|
+
.prepare("INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record) VALUES (?, ?, ?, ?)")
|
|
1832
|
+
.run(checkpoint.eventId, checkpoint.goal.id, checkpoint.occurredAt, JSON.stringify(checkpoint));
|
|
1833
|
+
if (lane !== null && laneRecord !== null) {
|
|
1834
|
+
const changes = this.#database
|
|
1835
|
+
.prepare(`UPDATE work_lane_claims SET operation_id = ?, revision = ?, record = ?
|
|
1836
|
+
WHERE lane_id = ? AND revision = ?`)
|
|
1837
|
+
.run(lane.operationId, lane.revision, laneRecord, lane.laneId, lane.expectedRevision).changes;
|
|
1838
|
+
if (changes !== 1 && changes !== 1n) {
|
|
1839
|
+
return reject(new WorkStoreError({ cause: lane, operation: "reassign.lane.update" }));
|
|
1840
|
+
}
|
|
1841
|
+
this.#database
|
|
1842
|
+
.prepare(`INSERT INTO work_lane_operations (operation_id, lane_id, goal_id, phase, revision, record)
|
|
1843
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
1844
|
+
.run(lane.operationId, lane.laneId, lane.goalId, lane.phase, lane.revision, laneRecord);
|
|
1845
|
+
}
|
|
1846
|
+
if (binding !== null) {
|
|
1847
|
+
this.#database
|
|
1848
|
+
.prepare(`INSERT INTO work_agent_bindings
|
|
1849
|
+
(dispatch_request_id, lane_id, expected_revision, revision, agent_id, host, record)
|
|
1850
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
1851
|
+
.run(binding.request.dispatchRequestId, binding.request.laneId, binding.request.expectedRevision, binding.lane.revision, binding.request.worker.agentId, binding.request.worker.host, JSON.stringify(binding));
|
|
1852
|
+
}
|
|
1853
|
+
this.#database
|
|
1854
|
+
.prepare("INSERT INTO work_goal_reassignments (approval_job_id, goal_id, record) VALUES (?, ?, ?)")
|
|
1855
|
+
.run(jobId, decoded.goalId, JSON.stringify(result));
|
|
1856
|
+
this.#database.exec("COMMIT");
|
|
1857
|
+
transaction = false;
|
|
1858
|
+
return { _tag: "reassigned", result };
|
|
1859
|
+
}
|
|
1860
|
+
catch (cause) {
|
|
1861
|
+
if (transaction)
|
|
1862
|
+
this.#database.exec("ROLLBACK");
|
|
1863
|
+
throw cause;
|
|
1864
|
+
}
|
|
1865
|
+
},
|
|
1866
|
+
catch: storeError("reassign.transaction")
|
|
1867
|
+
});
|
|
1868
|
+
if (decision._tag === "rejected")
|
|
1869
|
+
return yield* decision.error;
|
|
1870
|
+
return decision.result;
|
|
1871
|
+
});
|
|
1872
|
+
admissionPreflight = Effect.fn("WorkStore.admissionPreflight")(function* (target) {
|
|
1873
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkAdmissionTarget)(target).pipe(Effect.mapError(storeError("admission.preflight.decode")));
|
|
1874
|
+
const state = yield* Effect.try({
|
|
1875
|
+
try: () => readAdmissionState(this.#database, decoded),
|
|
1876
|
+
catch: storeError("admission.preflight.read")
|
|
1877
|
+
});
|
|
1878
|
+
if (state._tag !== "prospective")
|
|
1879
|
+
return state;
|
|
1880
|
+
const absenceToken = yield* this.#cryptoService.digest("SHA-256", new TextEncoder().encode(state.snapshot)).pipe(Effect.mapError(storeError("admission.preflight.digest")), Effect.map(Hex.encode));
|
|
1881
|
+
const preflight = { _tag: "prospective", target: state.target, absenceToken };
|
|
1882
|
+
return preflight;
|
|
1883
|
+
});
|
|
1884
|
+
admitExistingOwner = Effect.fn("WorkStore.admitExistingOwner")(function* (request) {
|
|
1885
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkProspectiveAdmission)(request).pipe(Effect.mapError(storeError("admission.decode")));
|
|
1886
|
+
const target = Schema.decodeUnknownSync(WorkAdmissionTarget)(decoded);
|
|
1887
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1888
|
+
yield* this.secureFiles();
|
|
1889
|
+
const observed = yield* Effect.try({
|
|
1890
|
+
try: () => readAdmissionState(this.#database, target),
|
|
1891
|
+
catch: storeError("admission.inspect")
|
|
1892
|
+
});
|
|
1893
|
+
const absenceToken = observed._tag === "prospective"
|
|
1894
|
+
? yield* this.#cryptoService.digest("SHA-256", new TextEncoder().encode(observed.snapshot)).pipe(Effect.mapError(storeError("admission.digest")), Effect.map(Hex.encode))
|
|
1895
|
+
: undefined;
|
|
1896
|
+
const decision = yield* Effect.try({
|
|
1897
|
+
try: () => {
|
|
1898
|
+
let transaction = false;
|
|
1899
|
+
try {
|
|
1900
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
1901
|
+
transaction = true;
|
|
1902
|
+
const reject = (reason) => ({
|
|
1903
|
+
_tag: "rejected",
|
|
1904
|
+
error: admissionConflict(target, reason)
|
|
1905
|
+
});
|
|
1906
|
+
const state = admissionState(this.#database, target);
|
|
1907
|
+
if (state._tag === "existing") {
|
|
1908
|
+
const existing = state.link;
|
|
1909
|
+
const provenance = existing.binding.request.prospectiveAdmission;
|
|
1910
|
+
this.#database.exec("ROLLBACK");
|
|
1911
|
+
transaction = false;
|
|
1912
|
+
return existing.binding.request.dispatchRequestId === decoded.operationId &&
|
|
1913
|
+
provenance?.expectedAbsenceToken === decoded.expectedAbsenceToken &&
|
|
1914
|
+
provenance?.approvalJobId === decoded.approvalJobId &&
|
|
1915
|
+
provenance.approvalActor === decoded.approvalActor &&
|
|
1916
|
+
existing.goal.title === decoded.title &&
|
|
1917
|
+
existing.goal.summary === decoded.summary &&
|
|
1918
|
+
existing.goal.detail === decoded.detail
|
|
1919
|
+
? { _tag: "admitted", link: existing }
|
|
1920
|
+
: reject("existing admission is not this exact approved operation");
|
|
1921
|
+
}
|
|
1922
|
+
if (state._tag !== "prospective" || observed._tag !== "prospective" ||
|
|
1923
|
+
state.snapshot !== observed.snapshot || absenceToken !== decoded.expectedAbsenceToken) {
|
|
1924
|
+
this.#database.exec("ROLLBACK");
|
|
1925
|
+
transaction = false;
|
|
1926
|
+
return reject(state._tag === "conflict" ? state.reason : "stale absence evidence");
|
|
1927
|
+
}
|
|
1928
|
+
if (this.#database.prepare(`SELECT 1 FROM work_lane_operations WHERE operation_id = ?
|
|
1929
|
+
UNION ALL SELECT 1 FROM work_goal_events WHERE event_id IN (?, ?)
|
|
1930
|
+
UNION ALL SELECT 1 FROM work_agent_bindings WHERE dispatch_request_id = ? LIMIT 1`).get(decoded.operationId, decoded.operationId, `${decoded.operationId}.admission`, decoded.operationId) !==
|
|
1931
|
+
undefined) {
|
|
1932
|
+
this.#database.exec("ROLLBACK");
|
|
1933
|
+
transaction = false;
|
|
1934
|
+
return reject("operation ID is already used by another durable record");
|
|
1935
|
+
}
|
|
1936
|
+
if (now < 1) {
|
|
1937
|
+
this.#database.exec("ROLLBACK");
|
|
1938
|
+
transaction = false;
|
|
1939
|
+
return reject("admission clock cannot produce two ordered checkpoints");
|
|
1940
|
+
}
|
|
1941
|
+
const createdAt = now - 1;
|
|
1942
|
+
const goal = Schema.decodeUnknownSync(WorkGoal)({
|
|
1943
|
+
id: decoded.goalId,
|
|
1944
|
+
title: decoded.title,
|
|
1945
|
+
summary: decoded.summary,
|
|
1946
|
+
detail: decoded.detail,
|
|
1947
|
+
state: "review",
|
|
1948
|
+
owner: decoded.owner,
|
|
1949
|
+
repository: { repository: decoded.repository, branch: decoded.branch },
|
|
1950
|
+
spend: null,
|
|
1951
|
+
delivery: "pull_request",
|
|
1952
|
+
blocker: null,
|
|
1953
|
+
connectTarget: null,
|
|
1954
|
+
goalFamily: { canonicalGoalId: decoded.goalId, role: "canonical" },
|
|
1955
|
+
activity: [{
|
|
1956
|
+
id: `${decoded.operationId}.admission`,
|
|
1957
|
+
kind: "status",
|
|
1958
|
+
summary: `Prospective admission of an existing owner by approved Fleet job ${decoded.approvalJobId}`,
|
|
1959
|
+
occurredAt: createdAt
|
|
1960
|
+
}],
|
|
1961
|
+
review: { state: "requested", summary: null, updatedAt: createdAt, url: decoded.reviewUrl },
|
|
1962
|
+
createdAt,
|
|
1963
|
+
updatedAt: createdAt
|
|
1964
|
+
});
|
|
1965
|
+
const initial = Schema.decodeUnknownSync(WorkGoalCheckpoint)({
|
|
1966
|
+
version: "herdr.work.event.v1",
|
|
1967
|
+
eventId: `${decoded.operationId}.admission`,
|
|
1968
|
+
occurredAt: createdAt,
|
|
1969
|
+
goal
|
|
1970
|
+
});
|
|
1971
|
+
const lane = Schema.decodeUnknownSync(WorkLaneClaimed)({
|
|
1972
|
+
operationId: decoded.operationId,
|
|
1973
|
+
goalId: decoded.goalId,
|
|
1974
|
+
laneId: decoded.laneId,
|
|
1975
|
+
worktree: decoded.worktree,
|
|
1976
|
+
branch: decoded.branch,
|
|
1977
|
+
head: decoded.head,
|
|
1978
|
+
owner: decoded.owner,
|
|
1979
|
+
parent: null,
|
|
1980
|
+
phase: "review",
|
|
1981
|
+
expectedRevision: 0,
|
|
1982
|
+
revision: 1
|
|
1983
|
+
});
|
|
1984
|
+
const binding = Schema.decodeUnknownSync(WorkAgentBinding)({
|
|
1985
|
+
version: "herdr.work.agent-binding.v1",
|
|
1986
|
+
request: {
|
|
1987
|
+
version: "herdr.work.agent-binding-request.v1",
|
|
1988
|
+
dispatchRequestId: decoded.operationId,
|
|
1989
|
+
laneId: decoded.laneId,
|
|
1990
|
+
expectedRevision: 0,
|
|
1991
|
+
worker: decoded.worker,
|
|
1992
|
+
prospectiveAdmission: {
|
|
1993
|
+
sessionId: decoded.sessionId,
|
|
1994
|
+
workAssignment: decoded.expectedWork,
|
|
1995
|
+
baseHead: decoded.baseHead,
|
|
1996
|
+
expectedAbsenceToken: decoded.expectedAbsenceToken,
|
|
1997
|
+
approvalJobId: decoded.approvalJobId,
|
|
1998
|
+
approvalActor: decoded.approvalActor
|
|
1999
|
+
}
|
|
2000
|
+
},
|
|
2001
|
+
lane,
|
|
2002
|
+
checkpoint: {
|
|
2003
|
+
version: "herdr.work.event.v1",
|
|
2004
|
+
eventId: decoded.operationId,
|
|
2005
|
+
occurredAt: now,
|
|
2006
|
+
goal: {
|
|
2007
|
+
...goal,
|
|
2008
|
+
agentHierarchy: { agent: decoded.worker },
|
|
2009
|
+
connectTarget: agentConnectTarget(decoded.worker),
|
|
2010
|
+
updatedAt: now
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
const existingRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingGoalEventRow))(this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
2015
|
+
FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC LIMIT ?`).all(workHistoryMaxEvents + 1));
|
|
2016
|
+
const history = existingRows.map((row) => {
|
|
2017
|
+
const result = decodeAgentBindingGoalEvent(row, "admission.history");
|
|
2018
|
+
if (result._tag === "invalid")
|
|
2019
|
+
throw result.error;
|
|
2020
|
+
return result.checkpoint;
|
|
2021
|
+
});
|
|
2022
|
+
const operationTotals = readLaneOperationLedgerTotals(this.#database);
|
|
2023
|
+
const laneRecord = JSON.stringify(lane);
|
|
2024
|
+
const operationBytes = utf8.encode(decoded.operationId).byteLength + utf8.encode(laneRecord).byteLength;
|
|
2025
|
+
const claimBytes = Schema.decodeUnknownSync(LedgerBytesRow)(this.#database.prepare(`SELECT COALESCE(SUM(length(CAST(lane_id AS BLOB)) + length(CAST(record AS BLOB))), 0) AS bytes
|
|
2026
|
+
FROM work_lane_claims`).get()).bytes;
|
|
2027
|
+
const capacity = agentBindingAdmissionError({
|
|
2028
|
+
history: [...history, initial],
|
|
2029
|
+
candidate: binding.checkpoint,
|
|
2030
|
+
operationCount: operationTotals.operationCount,
|
|
2031
|
+
operationBytes: operationTotals.operationBytes,
|
|
2032
|
+
candidateOperationBytes: operationBytes
|
|
2033
|
+
});
|
|
2034
|
+
const lanes = readValidatedLaneLedger(this.#database, "admission.capacity");
|
|
2035
|
+
if (lanes._tag === "invalid")
|
|
2036
|
+
throw lanes.error;
|
|
2037
|
+
if (capacity !== undefined || lanes.entries.length >= workLaneMaxRecords ||
|
|
2038
|
+
new Set(history.map(({ goal: previous }) => previous.id)).size >= workSnapshotMaxGoals ||
|
|
2039
|
+
claimBytes + utf8.encode(decoded.laneId).byteLength + utf8.encode(laneRecord).byteLength > workLaneMaxBytes) {
|
|
2040
|
+
this.#database.exec("ROLLBACK");
|
|
2041
|
+
transaction = false;
|
|
2042
|
+
return {
|
|
2043
|
+
_tag: "rejected",
|
|
2044
|
+
error: capacity ?? new WorkProjectionError({
|
|
2045
|
+
cause: decoded,
|
|
2046
|
+
detail: "prospective admission exceeds Work capacity",
|
|
2047
|
+
reason: "capacity_exceeded"
|
|
2048
|
+
})
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
const link = Schema.decodeUnknownSync(WorkPullRequestLink)({
|
|
2052
|
+
request: {
|
|
2053
|
+
repository: decoded.repository,
|
|
2054
|
+
pullRequest: decoded.pullRequest,
|
|
2055
|
+
goalId: decoded.goalId,
|
|
2056
|
+
laneId: decoded.laneId
|
|
2057
|
+
},
|
|
2058
|
+
goalEventId: binding.checkpoint.eventId,
|
|
2059
|
+
goal: binding.checkpoint.goal,
|
|
2060
|
+
lane,
|
|
2061
|
+
binding
|
|
2062
|
+
});
|
|
2063
|
+
this.#database.prepare(`INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record)
|
|
2064
|
+
VALUES (?, ?, ?, ?)`).run(initial.eventId, initial.goal.id, initial.occurredAt, JSON.stringify(initial));
|
|
2065
|
+
this.#database.prepare(`INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record)
|
|
2066
|
+
VALUES (?, ?, ?, ?)`).run(binding.checkpoint.eventId, decoded.goalId, now, JSON.stringify(binding.checkpoint));
|
|
2067
|
+
this.#database.prepare(`INSERT INTO work_lane_claims (lane_id, goal_id, operation_id, phase, revision, record)
|
|
2068
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(lane.laneId, lane.goalId, lane.operationId, lane.phase, lane.revision, laneRecord);
|
|
2069
|
+
this.#database.prepare(`INSERT INTO work_lane_operations (operation_id, lane_id, goal_id, phase, revision, record)
|
|
2070
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(lane.operationId, lane.laneId, lane.goalId, lane.phase, lane.revision, laneRecord);
|
|
2071
|
+
this.#database.prepare(`INSERT INTO work_agent_bindings
|
|
2072
|
+
(dispatch_request_id, lane_id, expected_revision, revision, agent_id, host, record)
|
|
2073
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(decoded.operationId, decoded.laneId, 0, 1, decoded.worker.agentId, decoded.worker.host, JSON.stringify(binding));
|
|
2074
|
+
this.#database.exec("COMMIT");
|
|
2075
|
+
transaction = false;
|
|
2076
|
+
return { _tag: "admitted", link };
|
|
2077
|
+
}
|
|
2078
|
+
catch (cause) {
|
|
2079
|
+
if (transaction)
|
|
2080
|
+
this.#database.exec("ROLLBACK");
|
|
2081
|
+
throw cause;
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
catch: storeError("admission.transaction")
|
|
2085
|
+
});
|
|
2086
|
+
if (decision._tag === "rejected")
|
|
2087
|
+
return yield* decision.error;
|
|
2088
|
+
return decision.link;
|
|
2089
|
+
});
|
|
2090
|
+
bindAgent = Effect.fn("WorkStore.bindAgent")(function* (request) {
|
|
2091
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkAgentBindingRequest)(request).pipe(Effect.mapError(storeError("agent-binding.decode")));
|
|
2092
|
+
const observedAt = yield* Clock.currentTimeMillis;
|
|
2093
|
+
yield* this.secureFiles();
|
|
2094
|
+
const decision = yield* Effect.try({
|
|
2095
|
+
try: () => {
|
|
2096
|
+
let inTransaction = false;
|
|
2097
|
+
try {
|
|
2098
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
2099
|
+
inTransaction = true;
|
|
2100
|
+
const reject = (error) => {
|
|
2101
|
+
this.#database.exec("ROLLBACK");
|
|
2102
|
+
inTransaction = false;
|
|
2103
|
+
return { _tag: "rejected", error };
|
|
2104
|
+
};
|
|
2105
|
+
const existingRaw = this.#database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
2106
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
2107
|
+
FROM work_agent_bindings WHERE dispatch_request_id = ?`).get(decoded.dispatchRequestId);
|
|
2108
|
+
if (existingRaw !== undefined) {
|
|
2109
|
+
const row = Schema.decodeUnknownSync(AgentBindingRow)(existingRaw);
|
|
2110
|
+
const binding = Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(row.record));
|
|
2111
|
+
if (row.dispatchRequestId !== binding.request.dispatchRequestId ||
|
|
2112
|
+
row.laneId !== binding.request.laneId ||
|
|
2113
|
+
row.expectedRevision !== binding.request.expectedRevision ||
|
|
2114
|
+
row.revision !== binding.lane.revision ||
|
|
2115
|
+
row.agentId !== binding.request.worker.agentId ||
|
|
2116
|
+
row.host.toLowerCase() !== binding.request.worker.host.toLowerCase()) {
|
|
2117
|
+
return reject(new WorkStoreError({
|
|
2118
|
+
cause: { binding, row },
|
|
2119
|
+
operation: "agent-binding.identity-mismatch"
|
|
2120
|
+
}));
|
|
2121
|
+
}
|
|
2122
|
+
const laneRow = this.#database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
2123
|
+
phase, revision, record
|
|
2124
|
+
FROM work_lane_operations WHERE operation_id = ?`).get(binding.lane.operationId);
|
|
2125
|
+
const checkpointRow = this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
2126
|
+
FROM work_goal_events WHERE event_id = ?`).get(binding.checkpoint.eventId);
|
|
2127
|
+
const readbackError = agentBindingReadbackError(binding, laneRow === undefined ? undefined : Schema.decodeUnknownSync(AgentBindingLaneOperationRow)(laneRow), checkpointRow === undefined
|
|
2128
|
+
? undefined
|
|
2129
|
+
: Schema.decodeUnknownSync(AgentBindingGoalEventRow)(checkpointRow), "agent-binding.replay");
|
|
2130
|
+
if (readbackError !== undefined)
|
|
2131
|
+
return reject(readbackError);
|
|
2132
|
+
this.#database.exec("ROLLBACK");
|
|
2133
|
+
inTransaction = false;
|
|
2134
|
+
return Equal.equals(binding.request, decoded)
|
|
2135
|
+
? { _tag: "bound", binding }
|
|
2136
|
+
: {
|
|
2137
|
+
_tag: "rejected",
|
|
2138
|
+
error: new WorkAgentBindingConflictError({ dispatchRequestId: decoded.dispatchRequestId })
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const laneLedger = readValidatedLaneLedger(this.#database, "agent-binding.authority");
|
|
2142
|
+
if (laneLedger._tag === "invalid")
|
|
2143
|
+
return reject(laneLedger.error);
|
|
2144
|
+
const lane = laneLedger.entries.find(({ claim }) => claim.laneId === decoded.laneId)?.claim;
|
|
2145
|
+
if (lane === undefined) {
|
|
2146
|
+
return reject(new WorkAgentBindingAuthorityError({
|
|
2147
|
+
actualRevision: 0,
|
|
2148
|
+
expectedRevision: decoded.expectedRevision,
|
|
2149
|
+
laneId: decoded.laneId,
|
|
2150
|
+
reason: "missing_lane"
|
|
2151
|
+
}));
|
|
2152
|
+
}
|
|
2153
|
+
if (lane.revision !== decoded.expectedRevision) {
|
|
2154
|
+
return reject(new WorkAgentBindingAuthorityError({
|
|
2155
|
+
actualRevision: lane.revision,
|
|
2156
|
+
expectedRevision: decoded.expectedRevision,
|
|
2157
|
+
laneId: decoded.laneId,
|
|
2158
|
+
reason: "stale_revision"
|
|
2159
|
+
}));
|
|
2160
|
+
}
|
|
2161
|
+
if (lane.phase === "shipped") {
|
|
2162
|
+
return reject(new WorkAgentBindingAuthorityError({
|
|
2163
|
+
actualRevision: lane.revision,
|
|
2164
|
+
expectedRevision: decoded.expectedRevision,
|
|
2165
|
+
laneId: decoded.laneId,
|
|
2166
|
+
reason: "shipped_lane"
|
|
2167
|
+
}));
|
|
2168
|
+
}
|
|
2169
|
+
const activeGoalClaims = laneLedger.entries
|
|
2170
|
+
.map(({ claim }) => claim)
|
|
2171
|
+
.filter((claim) => claim.goalId === lane.goalId && claim.phase !== "shipped");
|
|
2172
|
+
if (activeGoalClaims.length !== 1 || activeGoalClaims[0]?.laneId !== lane.laneId) {
|
|
2173
|
+
return reject(new WorkStoreError({
|
|
2174
|
+
cause: { activeGoalClaims, lane },
|
|
2175
|
+
operation: "agent-binding.goal-authority-conflict"
|
|
2176
|
+
}));
|
|
2177
|
+
}
|
|
2178
|
+
const currentRaw = this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
2179
|
+
FROM work_goal_events
|
|
2180
|
+
WHERE goal_id = ? ORDER BY occurred_at DESC, event_id DESC LIMIT 1`).get(lane.goalId);
|
|
2181
|
+
if (currentRaw === undefined) {
|
|
2182
|
+
return reject(new WorkAgentBindingAuthorityError({
|
|
2183
|
+
actualRevision: lane.revision,
|
|
2184
|
+
expectedRevision: decoded.expectedRevision,
|
|
2185
|
+
laneId: decoded.laneId,
|
|
2186
|
+
reason: "missing_goal"
|
|
2187
|
+
}));
|
|
2188
|
+
}
|
|
2189
|
+
const currentDecision = decodeAgentBindingGoalEvent(Schema.decodeUnknownSync(AgentBindingGoalEventRow)(currentRaw), "agent-binding.current-goal");
|
|
2190
|
+
if (currentDecision._tag === "invalid")
|
|
2191
|
+
return reject(currentDecision.error);
|
|
2192
|
+
const current = currentDecision.checkpoint;
|
|
2193
|
+
if (current.goal.state === "completed") {
|
|
2194
|
+
return reject(new WorkAgentBindingAuthorityError({
|
|
2195
|
+
actualRevision: lane.revision,
|
|
2196
|
+
expectedRevision: decoded.expectedRevision,
|
|
2197
|
+
laneId: decoded.laneId,
|
|
2198
|
+
reason: "terminal_goal"
|
|
2199
|
+
}));
|
|
2200
|
+
}
|
|
2201
|
+
if (current.occurredAt > observedAt) {
|
|
2202
|
+
return reject(new WorkProjectionError({
|
|
2203
|
+
cause: { checkpoint: current, observedAt },
|
|
2204
|
+
detail: "work agent binding cannot advance beyond the observed clock",
|
|
2205
|
+
reason: "inconsistent_history"
|
|
2206
|
+
}));
|
|
2207
|
+
}
|
|
2208
|
+
if (current.occurredAt >= maximumTimestamp) {
|
|
2209
|
+
return reject(new WorkProjectionError({
|
|
2210
|
+
cause: current,
|
|
2211
|
+
detail: "work agent binding timestamp cannot advance",
|
|
2212
|
+
reason: "capacity_exceeded"
|
|
2213
|
+
}));
|
|
2214
|
+
}
|
|
2215
|
+
const occurredAt = Math.max(observedAt, current.occurredAt + 1);
|
|
2216
|
+
const binding = makeWorkAgentBinding(decoded, lane, current, occurredAt);
|
|
2217
|
+
const historyRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingGoalEventRow))(this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
2218
|
+
FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC`).all());
|
|
2219
|
+
const history = [];
|
|
2220
|
+
for (const row of historyRows) {
|
|
2221
|
+
const historyDecision = decodeAgentBindingGoalEvent(row, "agent-binding.history");
|
|
2222
|
+
if (historyDecision._tag === "invalid")
|
|
2223
|
+
return reject(historyDecision.error);
|
|
2224
|
+
history.push(historyDecision.checkpoint);
|
|
2225
|
+
}
|
|
2226
|
+
const collision = this.#database.prepare(`SELECT record FROM work_goal_events
|
|
2227
|
+
WHERE event_id = ? OR (goal_id = ? AND occurred_at = ?)`).get(binding.checkpoint.eventId, binding.checkpoint.goal.id, binding.checkpoint.occurredAt);
|
|
2228
|
+
if (collision !== undefined) {
|
|
2229
|
+
return reject(new WorkStoreError({ cause: collision, operation: "agent-binding.event-collision" }));
|
|
2230
|
+
}
|
|
2231
|
+
const operationTotals = readLaneOperationLedgerTotals(this.#database);
|
|
2232
|
+
const encodedLane = JSON.stringify(binding.lane);
|
|
2233
|
+
const operationBytes = utf8.encode(binding.lane.operationId).byteLength + utf8.encode(encodedLane).byteLength;
|
|
2234
|
+
const admissionError = agentBindingAdmissionError({
|
|
2235
|
+
candidate: binding.checkpoint,
|
|
2236
|
+
candidateOperationBytes: operationBytes,
|
|
2237
|
+
history,
|
|
2238
|
+
operationBytes: operationTotals.operationBytes,
|
|
2239
|
+
operationCount: operationTotals.operationCount
|
|
2240
|
+
});
|
|
2241
|
+
if (admissionError !== undefined)
|
|
2242
|
+
return reject(admissionError);
|
|
2243
|
+
const laneChanges = this.#database.prepare(`UPDATE work_lane_claims
|
|
2244
|
+
SET operation_id = ?, revision = ?, record = ?
|
|
2245
|
+
WHERE lane_id = ? AND revision = ?`).run(binding.lane.operationId, binding.lane.revision, encodedLane, decoded.laneId, decoded.expectedRevision).changes;
|
|
2246
|
+
if (laneChanges !== 1 && laneChanges !== 1n) {
|
|
2247
|
+
return reject(new WorkAgentBindingAuthorityError({
|
|
2248
|
+
actualRevision: lane.revision,
|
|
2249
|
+
expectedRevision: decoded.expectedRevision,
|
|
2250
|
+
laneId: decoded.laneId,
|
|
2251
|
+
reason: "stale_revision"
|
|
2252
|
+
}));
|
|
2253
|
+
}
|
|
2254
|
+
this.#database.prepare(`INSERT INTO work_lane_operations
|
|
2255
|
+
(operation_id, lane_id, goal_id, phase, revision, record)
|
|
2256
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(binding.lane.operationId, binding.lane.laneId, binding.lane.goalId, binding.lane.phase, binding.lane.revision, encodedLane);
|
|
2257
|
+
this.#database.prepare(`INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record)
|
|
2258
|
+
VALUES (?, ?, ?, ?)`).run(binding.checkpoint.eventId, binding.checkpoint.goal.id, binding.checkpoint.occurredAt, JSON.stringify(binding.checkpoint));
|
|
2259
|
+
this.#database.prepare(`INSERT INTO work_agent_bindings
|
|
2260
|
+
(dispatch_request_id, lane_id, expected_revision, revision, agent_id, host, record)
|
|
2261
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(decoded.dispatchRequestId, decoded.laneId, decoded.expectedRevision, binding.lane.revision, decoded.worker.agentId, decoded.worker.host, JSON.stringify(binding));
|
|
2262
|
+
this.#database.exec("COMMIT");
|
|
2263
|
+
inTransaction = false;
|
|
2264
|
+
return { _tag: "bound", binding };
|
|
2265
|
+
}
|
|
2266
|
+
catch (error) {
|
|
2267
|
+
if (inTransaction)
|
|
2268
|
+
this.#database.exec("ROLLBACK");
|
|
2269
|
+
throw error;
|
|
2270
|
+
}
|
|
2271
|
+
},
|
|
2272
|
+
catch: storeError("agent-binding.write")
|
|
2273
|
+
});
|
|
2274
|
+
if (decision._tag === "rejected")
|
|
2275
|
+
return yield* decision.error;
|
|
2276
|
+
return decision.binding;
|
|
2277
|
+
});
|
|
2278
|
+
agentBinding = Effect.fn("WorkStore.agentBinding")(function* (dispatchRequestId) {
|
|
2279
|
+
const decodedId = yield* Schema.decodeUnknownEffect(WorkAgentBindingRequest.fields.dispatchRequestId)(dispatchRequestId).pipe(Effect.mapError(storeError("agent-binding.read.decode")));
|
|
2280
|
+
const raw = yield* Effect.try({
|
|
2281
|
+
try: () => this.#database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
2282
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
2283
|
+
FROM work_agent_bindings WHERE dispatch_request_id = ?`).get(decodedId),
|
|
2284
|
+
catch: storeError("agent-binding.read")
|
|
2285
|
+
});
|
|
2286
|
+
if (raw === undefined)
|
|
2287
|
+
return Option.none();
|
|
2288
|
+
const row = yield* Schema.decodeUnknownEffect(AgentBindingRow)(raw).pipe(Effect.mapError(storeError("agent-binding.read.row")));
|
|
2289
|
+
const binding = yield* Effect.try({
|
|
2290
|
+
try: () => Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(row.record)),
|
|
2291
|
+
catch: storeError("agent-binding.read.record")
|
|
2292
|
+
});
|
|
2293
|
+
if (row.dispatchRequestId !== binding.request.dispatchRequestId ||
|
|
2294
|
+
row.laneId !== binding.request.laneId ||
|
|
2295
|
+
row.expectedRevision !== binding.request.expectedRevision ||
|
|
2296
|
+
row.revision !== binding.lane.revision ||
|
|
2297
|
+
row.agentId !== binding.request.worker.agentId ||
|
|
2298
|
+
row.host.toLowerCase() !== binding.request.worker.host.toLowerCase()) {
|
|
2299
|
+
return yield* new WorkStoreError({ cause: { binding, row }, operation: "agent-binding.read.identity-mismatch" });
|
|
54
2300
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
2301
|
+
const companions = yield* Effect.try({
|
|
2302
|
+
try: () => ({
|
|
2303
|
+
checkpoint: this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
2304
|
+
FROM work_goal_events WHERE event_id = ?`).get(binding.checkpoint.eventId),
|
|
2305
|
+
lane: this.#database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
2306
|
+
phase, revision, record
|
|
2307
|
+
FROM work_lane_operations WHERE operation_id = ?`).get(binding.lane.operationId)
|
|
2308
|
+
}),
|
|
2309
|
+
catch: storeError("agent-binding.read.companions")
|
|
2310
|
+
});
|
|
2311
|
+
const readbackError = agentBindingReadbackError(binding, companions.lane === undefined
|
|
2312
|
+
? undefined
|
|
2313
|
+
: yield* Schema.decodeUnknownEffect(AgentBindingLaneOperationRow)(companions.lane).pipe(Effect.mapError(storeError("agent-binding.read.lane-companion"))), companions.checkpoint === undefined
|
|
2314
|
+
? undefined
|
|
2315
|
+
: yield* Schema.decodeUnknownEffect(AgentBindingGoalEventRow)(companions.checkpoint).pipe(Effect.mapError(storeError("agent-binding.read.checkpoint-companion"))), "agent-binding.readback");
|
|
2316
|
+
if (readbackError !== undefined)
|
|
2317
|
+
return yield* readbackError;
|
|
2318
|
+
return Option.some(binding);
|
|
2319
|
+
});
|
|
2320
|
+
bindingDispatchesForLane = Effect.fn("WorkStore.bindingDispatchesForLane")(function* (laneId) {
|
|
2321
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkGoalId)(laneId).pipe(Effect.mapError(storeError("agent-binding.lane.decode")));
|
|
2322
|
+
const rows = yield* Effect.try({
|
|
2323
|
+
try: () => this.#database.prepare(`SELECT dispatch_request_id AS dispatchRequestId
|
|
2324
|
+
FROM work_agent_bindings WHERE lane_id = ? ORDER BY revision DESC`).all(decoded),
|
|
2325
|
+
catch: storeError("agent-binding.lane.read")
|
|
2326
|
+
});
|
|
2327
|
+
const entries = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ dispatchRequestId: WorkAgentBindingRequest.fields.dispatchRequestId })))(rows).pipe(Effect.mapError(storeError("agent-binding.lane.rows")));
|
|
2328
|
+
return entries.map(({ dispatchRequestId }) => dispatchRequestId);
|
|
72
2329
|
});
|
|
73
2330
|
append = Effect.fn("WorkStore.append")(function* (event) {
|
|
74
2331
|
const decoded = yield* Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("append.decode")));
|
|
2332
|
+
yield* this.secureFiles();
|
|
75
2333
|
const decision = yield* Effect.try({
|
|
76
2334
|
try: () => {
|
|
77
2335
|
let transaction = false;
|
|
@@ -166,9 +2424,910 @@ export class WorkStore {
|
|
|
166
2424
|
if (decision.changes !== 1 && decision.changes !== 1n) {
|
|
167
2425
|
return yield* storeError("append.insert.count")(decision.changes);
|
|
168
2426
|
}
|
|
169
|
-
yield* this.secureFiles();
|
|
170
2427
|
return decoded;
|
|
171
2428
|
});
|
|
2429
|
+
appendMany = Effect.fn("WorkStore.appendMany")(function* (transactionId, events) {
|
|
2430
|
+
if (events.length > workHistoryMaxEvents) {
|
|
2431
|
+
return yield* new WorkProjectionError({
|
|
2432
|
+
cause: events.length,
|
|
2433
|
+
detail: `work history cannot exceed ${workHistoryMaxEvents} checkpoints`,
|
|
2434
|
+
reason: "capacity_exceeded"
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
const transaction = yield* Schema.decodeUnknownEffect(TransactionId)(transactionId).pipe(Effect.mapError(storeError("appendMany.decode.transaction")));
|
|
2438
|
+
const decoded = yield* Effect.forEach(events, (event) => Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("appendMany.decode.event"))));
|
|
2439
|
+
if (decoded.length === 0) {
|
|
2440
|
+
return yield* new WorkProjectionError({
|
|
2441
|
+
cause: events,
|
|
2442
|
+
detail: "a checkpoint transaction must contain at least one event",
|
|
2443
|
+
reason: "malformed"
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
const digest = yield* this.#cryptoService.digest("SHA-256", new TextEncoder().encode(transactionContent(decoded))).pipe(Effect.mapError(storeError("appendMany.digest")), Effect.map(Hex.encode));
|
|
2447
|
+
const transactionRecord = JSON.stringify({ digest, version: "herdr.work.transaction.v3" });
|
|
2448
|
+
const transactionEntryBytes = utf8.encode(transaction).byteLength + utf8.encode(transactionRecord).byteLength;
|
|
2449
|
+
yield* this.secureFiles();
|
|
2450
|
+
const decision = yield* Effect.try({
|
|
2451
|
+
try: () => {
|
|
2452
|
+
let inTransaction = false;
|
|
2453
|
+
try {
|
|
2454
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
2455
|
+
inTransaction = true;
|
|
2456
|
+
const storedTransaction = this.#database.prepare("SELECT record FROM work_goal_transactions WHERE transaction_id = ?").get(transaction);
|
|
2457
|
+
let compactTransaction;
|
|
2458
|
+
let legacyCompactTransaction;
|
|
2459
|
+
let unsupportedCompactTransaction = false;
|
|
2460
|
+
let legacyTransaction;
|
|
2461
|
+
if (storedTransaction !== undefined) {
|
|
2462
|
+
const stored = Schema.decodeUnknownSync(TransactionRow)(storedTransaction);
|
|
2463
|
+
const previous = JSON.parse(stored.record);
|
|
2464
|
+
const compact = Schema.decodeUnknownResult(CompactTransactionRecord)(previous);
|
|
2465
|
+
if (compact._tag === "Success") {
|
|
2466
|
+
compactTransaction = compact.success;
|
|
2467
|
+
}
|
|
2468
|
+
else {
|
|
2469
|
+
const legacyCompact = Schema.decodeUnknownResult(LegacyCompactTransactionRecord)(previous);
|
|
2470
|
+
if (legacyCompact._tag === "Success") {
|
|
2471
|
+
legacyCompactTransaction = legacyCompact.success;
|
|
2472
|
+
unsupportedCompactTransaction = true;
|
|
2473
|
+
}
|
|
2474
|
+
else {
|
|
2475
|
+
const previousCompact = Schema.decodeUnknownResult(PreviousCompactTransactionRecord)(previous);
|
|
2476
|
+
if (previousCompact._tag === "Success") {
|
|
2477
|
+
unsupportedCompactTransaction = true;
|
|
2478
|
+
}
|
|
2479
|
+
else {
|
|
2480
|
+
legacyTransaction = Schema.decodeUnknownSync(Schema.Array(WorkGoalCheckpoint))(previous);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const duplicateEventIds = new Set();
|
|
2486
|
+
const duplicateGoalTimes = new Set();
|
|
2487
|
+
for (const event of decoded) {
|
|
2488
|
+
const goalTime = `${event.goal.id}\u0000${event.occurredAt}`;
|
|
2489
|
+
if (duplicateEventIds.has(event.eventId) || duplicateGoalTimes.has(goalTime)) {
|
|
2490
|
+
this.#database.exec("ROLLBACK");
|
|
2491
|
+
inTransaction = false;
|
|
2492
|
+
return {
|
|
2493
|
+
_tag: "rejected",
|
|
2494
|
+
error: new WorkProjectionError({
|
|
2495
|
+
cause: event,
|
|
2496
|
+
detail: "a checkpoint transaction contains a duplicate event identity",
|
|
2497
|
+
reason: "duplicate_event"
|
|
2498
|
+
})
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
duplicateEventIds.add(event.eventId);
|
|
2502
|
+
duplicateGoalTimes.add(goalTime);
|
|
2503
|
+
}
|
|
2504
|
+
const rows = Schema.decodeUnknownSync(StoredEventWithTransactionRows)(this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt,
|
|
2505
|
+
record, transaction_id AS transactionId
|
|
2506
|
+
FROM work_goal_events`).all());
|
|
2507
|
+
const rowsByEventId = new Map(rows.map((row) => [row.eventId, row]));
|
|
2508
|
+
const rowsByGoalTime = new Map(rows.map((row) => [
|
|
2509
|
+
JSON.stringify([row.goalId, row.occurredAt]),
|
|
2510
|
+
row
|
|
2511
|
+
]));
|
|
2512
|
+
const decodedRows = rows.map((row) => ({
|
|
2513
|
+
event: Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(row.record)),
|
|
2514
|
+
row
|
|
2515
|
+
}));
|
|
2516
|
+
const decodedEventsByEventId = new Map(decodedRows.map(({ event, row }) => [row.eventId, event]));
|
|
2517
|
+
if (legacyTransaction !== undefined) {
|
|
2518
|
+
const legacyRows = legacyTransaction.map((event) => rowsByEventId.get(event.eventId));
|
|
2519
|
+
if (legacyRows.some((row, index) => {
|
|
2520
|
+
const event = legacyTransaction[index];
|
|
2521
|
+
return (row === undefined ||
|
|
2522
|
+
event === undefined ||
|
|
2523
|
+
row.goalId !== event.goal.id ||
|
|
2524
|
+
row.occurredAt !== event.occurredAt);
|
|
2525
|
+
})) {
|
|
2526
|
+
this.#database.exec("ROLLBACK");
|
|
2527
|
+
inTransaction = false;
|
|
2528
|
+
return {
|
|
2529
|
+
_tag: "rejected",
|
|
2530
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
const legacyEvents = legacyRows.map((row) => row === undefined
|
|
2534
|
+
? undefined
|
|
2535
|
+
: Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(row.record)));
|
|
2536
|
+
if (legacyEvents.some((event) => event === undefined) ||
|
|
2537
|
+
legacyEvents.some((event, index) => event !== undefined && !Equal.equals(event, legacyTransaction[index]))) {
|
|
2538
|
+
this.#database.exec("ROLLBACK");
|
|
2539
|
+
inTransaction = false;
|
|
2540
|
+
return {
|
|
2541
|
+
_tag: "rejected",
|
|
2542
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const existing = decoded.map((event) => {
|
|
2547
|
+
const row = rowsByEventId.get(event.eventId) ??
|
|
2548
|
+
rowsByGoalTime.get(JSON.stringify([event.goal.id, event.occurredAt]));
|
|
2549
|
+
return row === undefined
|
|
2550
|
+
? undefined
|
|
2551
|
+
: decodedEventsByEventId.get(row.eventId);
|
|
2552
|
+
});
|
|
2553
|
+
const conflicting = existing.find((candidate, index) => candidate !== undefined && !Equal.equals(candidate, decoded[index]));
|
|
2554
|
+
if (conflicting !== undefined) {
|
|
2555
|
+
this.#database.exec("ROLLBACK");
|
|
2556
|
+
inTransaction = false;
|
|
2557
|
+
const index = existing.findIndex((candidate) => candidate === conflicting);
|
|
2558
|
+
const event = decoded[index];
|
|
2559
|
+
if (event === undefined) {
|
|
2560
|
+
return {
|
|
2561
|
+
_tag: "rejected",
|
|
2562
|
+
error: new WorkStoreError({
|
|
2563
|
+
cause: decoded,
|
|
2564
|
+
operation: "appendMany.collision-candidate"
|
|
2565
|
+
})
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
return {
|
|
2569
|
+
_tag: "rejected",
|
|
2570
|
+
error: new WorkCheckpointConflictError({
|
|
2571
|
+
eventId: event.eventId,
|
|
2572
|
+
goalId: event.goal.id,
|
|
2573
|
+
occurredAt: event.occurredAt
|
|
2574
|
+
})
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
if (compactTransaction !== undefined) {
|
|
2578
|
+
const denormalizedMismatch = decoded.some((event) => {
|
|
2579
|
+
const row = rowsByEventId.get(event.eventId) ??
|
|
2580
|
+
rowsByGoalTime.get(JSON.stringify([event.goal.id, event.occurredAt]));
|
|
2581
|
+
return (row === undefined ||
|
|
2582
|
+
row.eventId !== event.eventId ||
|
|
2583
|
+
row.goalId !== event.goal.id ||
|
|
2584
|
+
row.occurredAt !== event.occurredAt);
|
|
2585
|
+
});
|
|
2586
|
+
if (denormalizedMismatch) {
|
|
2587
|
+
this.#database.exec("ROLLBACK");
|
|
2588
|
+
inTransaction = false;
|
|
2589
|
+
return {
|
|
2590
|
+
_tag: "rejected",
|
|
2591
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (legacyCompactTransaction !== undefined) {
|
|
2596
|
+
const denormalizedMismatch = legacyCompactTransaction.events.some((identity, index) => {
|
|
2597
|
+
const event = decoded[index];
|
|
2598
|
+
const row = rowsByEventId.get(identity.eventId);
|
|
2599
|
+
return (row === undefined ||
|
|
2600
|
+
event === undefined ||
|
|
2601
|
+
row.goalId !== event.goal.id ||
|
|
2602
|
+
row.occurredAt !== event.occurredAt);
|
|
2603
|
+
});
|
|
2604
|
+
if (denormalizedMismatch) {
|
|
2605
|
+
this.#database.exec("ROLLBACK");
|
|
2606
|
+
inTransaction = false;
|
|
2607
|
+
return {
|
|
2608
|
+
_tag: "rejected",
|
|
2609
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
const corruptedRow = decodedRows.find(({ event, row }) => row.eventId !== event.eventId || row.goalId !== event.goal.id || row.occurredAt !== event.occurredAt);
|
|
2614
|
+
if (corruptedRow !== undefined) {
|
|
2615
|
+
this.#database.exec("ROLLBACK");
|
|
2616
|
+
inTransaction = false;
|
|
2617
|
+
return {
|
|
2618
|
+
_tag: "rejected",
|
|
2619
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
if (unsupportedCompactTransaction) {
|
|
2623
|
+
this.#database.exec("ROLLBACK");
|
|
2624
|
+
inTransaction = false;
|
|
2625
|
+
return {
|
|
2626
|
+
_tag: "rejected",
|
|
2627
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
if (legacyTransaction !== undefined && !Equal.equals(legacyTransaction, decoded)) {
|
|
2631
|
+
this.#database.exec("ROLLBACK");
|
|
2632
|
+
inTransaction = false;
|
|
2633
|
+
return {
|
|
2634
|
+
_tag: "rejected",
|
|
2635
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
const newEvents = decoded.filter((_, index) => existing[index] === undefined);
|
|
2639
|
+
if (compactTransaction !== undefined &&
|
|
2640
|
+
newEvents.length !== 0) {
|
|
2641
|
+
this.#database.exec("ROLLBACK");
|
|
2642
|
+
inTransaction = false;
|
|
2643
|
+
return {
|
|
2644
|
+
_tag: "rejected",
|
|
2645
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
if (newEvents.length !== 0 && existing.some((candidate) => candidate !== undefined)) {
|
|
2649
|
+
this.#database.exec("ROLLBACK");
|
|
2650
|
+
inTransaction = false;
|
|
2651
|
+
const event = decoded.find((candidate, index) => existing[index] !== undefined);
|
|
2652
|
+
if (event === undefined) {
|
|
2653
|
+
return {
|
|
2654
|
+
_tag: "rejected",
|
|
2655
|
+
error: new WorkStoreError({
|
|
2656
|
+
cause: decoded,
|
|
2657
|
+
operation: "appendMany.partial-replay-candidate"
|
|
2658
|
+
})
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
_tag: "rejected",
|
|
2663
|
+
error: new WorkCheckpointConflictError({
|
|
2664
|
+
eventId: event.eventId,
|
|
2665
|
+
goalId: event.goal.id,
|
|
2666
|
+
occurredAt: event.occurredAt
|
|
2667
|
+
})
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
if (newEvents.length === 0) {
|
|
2671
|
+
if (compactTransaction !== undefined) {
|
|
2672
|
+
if (compactTransaction.digest !== digest) {
|
|
2673
|
+
this.#database.exec("ROLLBACK");
|
|
2674
|
+
inTransaction = false;
|
|
2675
|
+
return {
|
|
2676
|
+
_tag: "rejected",
|
|
2677
|
+
error: new WorkTransactionConflictError({ transactionId: transaction })
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
this.#database.exec("ROLLBACK");
|
|
2681
|
+
inTransaction = false;
|
|
2682
|
+
return { _tag: "replayed", events: decoded };
|
|
2683
|
+
}
|
|
2684
|
+
if (legacyTransaction !== undefined) {
|
|
2685
|
+
this.#database.exec("ROLLBACK");
|
|
2686
|
+
inTransaction = false;
|
|
2687
|
+
return { _tag: "replayed", events: decoded };
|
|
2688
|
+
}
|
|
2689
|
+
const transactionLedgerTotals = readTransactionLedgerTotals(this.#database);
|
|
2690
|
+
if (transactionLedgerTotals.transactionCount >= workTransactionMaxRecords) {
|
|
2691
|
+
this.#database.exec("ROLLBACK");
|
|
2692
|
+
inTransaction = false;
|
|
2693
|
+
return {
|
|
2694
|
+
_tag: "rejected",
|
|
2695
|
+
error: new WorkProjectionError({
|
|
2696
|
+
cause: decoded,
|
|
2697
|
+
detail: `work transaction history cannot exceed ${workTransactionMaxRecords} transaction IDs`,
|
|
2698
|
+
reason: "capacity_exceeded"
|
|
2699
|
+
})
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
if (transactionLedgerTotals.transactionBytes + transactionEntryBytes > workTransactionMaxBytes) {
|
|
2703
|
+
this.#database.exec("ROLLBACK");
|
|
2704
|
+
inTransaction = false;
|
|
2705
|
+
return {
|
|
2706
|
+
_tag: "rejected",
|
|
2707
|
+
error: new WorkProjectionError({
|
|
2708
|
+
cause: decoded,
|
|
2709
|
+
detail: `work transaction history cannot exceed ${workTransactionMaxBytes} encoded bytes`,
|
|
2710
|
+
reason: "capacity_exceeded"
|
|
2711
|
+
})
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
this.#database.prepare("INSERT INTO work_goal_transactions (transaction_id, record) VALUES (?, ?)").run(transaction, transactionRecord);
|
|
2715
|
+
this.#database.exec("COMMIT");
|
|
2716
|
+
inTransaction = false;
|
|
2717
|
+
return { _tag: "replayed", events: decoded };
|
|
2718
|
+
}
|
|
2719
|
+
const eventCount = Schema.decodeUnknownSync(CountRow)(this.#database.prepare("SELECT COUNT(*) AS count FROM work_goal_events").get()).count;
|
|
2720
|
+
if (eventCount + newEvents.length > workHistoryMaxEvents) {
|
|
2721
|
+
this.#database.exec("ROLLBACK");
|
|
2722
|
+
inTransaction = false;
|
|
2723
|
+
return {
|
|
2724
|
+
_tag: "rejected",
|
|
2725
|
+
error: new WorkProjectionError({
|
|
2726
|
+
cause: decoded,
|
|
2727
|
+
detail: `work history cannot exceed ${workHistoryMaxEvents} checkpoints`,
|
|
2728
|
+
reason: "capacity_exceeded"
|
|
2729
|
+
})
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
const history = decodedRows.map(({ event }) => event);
|
|
2733
|
+
const prospective = [...history, ...newEvents].toSorted((left, right) => left.occurredAt - right.occurredAt || left.eventId.localeCompare(right.eventId));
|
|
2734
|
+
const familyError = validateGoalFamilyHistory(prospective);
|
|
2735
|
+
if (familyError !== undefined) {
|
|
2736
|
+
this.#database.exec("ROLLBACK");
|
|
2737
|
+
inTransaction = false;
|
|
2738
|
+
return { _tag: "rejected", error: familyError };
|
|
2739
|
+
}
|
|
2740
|
+
if (workMaximumSnapshotBytesForHistory(prospective) > fleetResponseBodyMaxBytes) {
|
|
2741
|
+
this.#database.exec("ROLLBACK");
|
|
2742
|
+
inTransaction = false;
|
|
2743
|
+
return {
|
|
2744
|
+
_tag: "rejected",
|
|
2745
|
+
error: new WorkProjectionError({
|
|
2746
|
+
cause: decoded,
|
|
2747
|
+
detail: `work snapshots cannot exceed ${fleetResponseBodyMaxBytes} encoded bytes`,
|
|
2748
|
+
reason: "capacity_exceeded"
|
|
2749
|
+
})
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
const goalIds = new Set(history.map(({ goal }) => goal.id));
|
|
2753
|
+
for (const event of newEvents)
|
|
2754
|
+
goalIds.add(event.goal.id);
|
|
2755
|
+
if (goalIds.size > workSnapshotMaxGoals) {
|
|
2756
|
+
this.#database.exec("ROLLBACK");
|
|
2757
|
+
inTransaction = false;
|
|
2758
|
+
return {
|
|
2759
|
+
_tag: "rejected",
|
|
2760
|
+
error: new WorkProjectionError({
|
|
2761
|
+
cause: decoded,
|
|
2762
|
+
detail: `work snapshots cannot exceed ${workSnapshotMaxGoals} goals`,
|
|
2763
|
+
reason: "capacity_exceeded"
|
|
2764
|
+
})
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
const creationTimes = new Map();
|
|
2768
|
+
for (const event of prospective) {
|
|
2769
|
+
const creationTime = creationTimes.get(event.goal.id);
|
|
2770
|
+
if (creationTime === undefined) {
|
|
2771
|
+
if (event.occurredAt !== event.goal.createdAt) {
|
|
2772
|
+
this.#database.exec("ROLLBACK");
|
|
2773
|
+
inTransaction = false;
|
|
2774
|
+
return {
|
|
2775
|
+
_tag: "rejected",
|
|
2776
|
+
error: new WorkProjectionError({
|
|
2777
|
+
cause: event,
|
|
2778
|
+
detail: `goal ${event.goal.id} must begin at its creation timestamp`,
|
|
2779
|
+
reason: "inconsistent_history"
|
|
2780
|
+
})
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
creationTimes.set(event.goal.id, event.goal.createdAt);
|
|
2784
|
+
}
|
|
2785
|
+
else if (creationTime !== event.goal.createdAt) {
|
|
2786
|
+
this.#database.exec("ROLLBACK");
|
|
2787
|
+
inTransaction = false;
|
|
2788
|
+
return {
|
|
2789
|
+
_tag: "rejected",
|
|
2790
|
+
error: new WorkProjectionError({
|
|
2791
|
+
cause: event,
|
|
2792
|
+
detail: `goal ${event.goal.id} changed its creation timestamp`,
|
|
2793
|
+
reason: "inconsistent_history"
|
|
2794
|
+
})
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
const transactionLedgerTotals = readTransactionLedgerTotals(this.#database);
|
|
2799
|
+
if (transactionLedgerTotals.transactionCount >= workTransactionMaxRecords) {
|
|
2800
|
+
this.#database.exec("ROLLBACK");
|
|
2801
|
+
inTransaction = false;
|
|
2802
|
+
return {
|
|
2803
|
+
_tag: "rejected",
|
|
2804
|
+
error: new WorkProjectionError({
|
|
2805
|
+
cause: decoded,
|
|
2806
|
+
detail: `work transaction history cannot exceed ${workTransactionMaxRecords} transaction IDs`,
|
|
2807
|
+
reason: "capacity_exceeded"
|
|
2808
|
+
})
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
if (transactionLedgerTotals.transactionBytes + transactionEntryBytes > workTransactionMaxBytes) {
|
|
2812
|
+
this.#database.exec("ROLLBACK");
|
|
2813
|
+
inTransaction = false;
|
|
2814
|
+
return {
|
|
2815
|
+
_tag: "rejected",
|
|
2816
|
+
error: new WorkProjectionError({
|
|
2817
|
+
cause: decoded,
|
|
2818
|
+
detail: `work transaction history cannot exceed ${workTransactionMaxBytes} encoded bytes`,
|
|
2819
|
+
reason: "capacity_exceeded"
|
|
2820
|
+
})
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
const insert = this.#database.prepare(`INSERT INTO work_goal_events
|
|
2824
|
+
(event_id, goal_id, occurred_at, record, transaction_id)
|
|
2825
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
2826
|
+
for (const event of newEvents) {
|
|
2827
|
+
insert.run(event.eventId, event.goal.id, event.occurredAt, JSON.stringify(event), transaction);
|
|
2828
|
+
}
|
|
2829
|
+
this.#database.prepare("INSERT INTO work_goal_transactions (transaction_id, record) VALUES (?, ?)").run(transaction, transactionRecord);
|
|
2830
|
+
this.#database.exec("COMMIT");
|
|
2831
|
+
inTransaction = false;
|
|
2832
|
+
return { _tag: "inserted", events: decoded };
|
|
2833
|
+
}
|
|
2834
|
+
catch (error) {
|
|
2835
|
+
if (inTransaction)
|
|
2836
|
+
this.#database.exec("ROLLBACK");
|
|
2837
|
+
throw error;
|
|
2838
|
+
}
|
|
2839
|
+
},
|
|
2840
|
+
catch: storeError("appendMany.insert")
|
|
2841
|
+
});
|
|
2842
|
+
if (decision._tag === "rejected")
|
|
2843
|
+
return yield* decision.error;
|
|
2844
|
+
return decision.events;
|
|
2845
|
+
});
|
|
2846
|
+
claim = Effect.fn("WorkStore.claim")(function* (claim) {
|
|
2847
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkLaneClaim)(claim).pipe(Effect.mapError(storeError("claim.decode")));
|
|
2848
|
+
yield* this.secureFiles();
|
|
2849
|
+
const decision = yield* Effect.try({
|
|
2850
|
+
try: () => {
|
|
2851
|
+
let inTransaction = false;
|
|
2852
|
+
try {
|
|
2853
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
2854
|
+
inTransaction = true;
|
|
2855
|
+
const operationRaw = this.#database.prepare(`SELECT lane_id AS laneId, goal_id AS goalId, operation_id AS operationId,
|
|
2856
|
+
phase, revision, record
|
|
2857
|
+
FROM work_lane_operations WHERE operation_id = ?`).get(decoded.operationId);
|
|
2858
|
+
if (operationRaw !== undefined) {
|
|
2859
|
+
const prior = Schema.decodeUnknownSync(LaneRow)(operationRaw);
|
|
2860
|
+
const priorClaim = Schema.decodeUnknownSync(WorkLaneClaimed)(JSON.parse(prior.record));
|
|
2861
|
+
if (priorClaim.goalId !== prior.goalId ||
|
|
2862
|
+
priorClaim.laneId !== prior.laneId ||
|
|
2863
|
+
priorClaim.operationId !== prior.operationId ||
|
|
2864
|
+
priorClaim.phase !== prior.phase ||
|
|
2865
|
+
priorClaim.revision !== prior.revision) {
|
|
2866
|
+
this.#database.exec("ROLLBACK");
|
|
2867
|
+
inTransaction = false;
|
|
2868
|
+
return {
|
|
2869
|
+
_tag: "rejected",
|
|
2870
|
+
error: new WorkStoreError({
|
|
2871
|
+
cause: { claim: priorClaim, row: prior },
|
|
2872
|
+
operation: "claim.operation.identity-mismatch"
|
|
2873
|
+
})
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
this.#database.exec("ROLLBACK");
|
|
2877
|
+
inTransaction = false;
|
|
2878
|
+
return Equal.equals(claimInputFromClaimed(priorClaim), decoded)
|
|
2879
|
+
? { _tag: "claimed", value: priorClaim }
|
|
2880
|
+
: {
|
|
2881
|
+
_tag: "operation-conflict",
|
|
2882
|
+
error: new WorkLaneOperationConflictError({ operationId: decoded.operationId })
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
const laneLedger = readValidatedLaneLedger(this.#database, "claim.write");
|
|
2886
|
+
if (laneLedger._tag === "invalid") {
|
|
2887
|
+
this.#database.exec("ROLLBACK");
|
|
2888
|
+
inTransaction = false;
|
|
2889
|
+
return {
|
|
2890
|
+
_tag: "rejected",
|
|
2891
|
+
error: laneLedger.error
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
const existingEntry = laneLedger.entries.find(({ claim }) => claim.laneId === decoded.laneId);
|
|
2895
|
+
const existing = existingEntry?.row;
|
|
2896
|
+
const existingClaim = existingEntry?.claim;
|
|
2897
|
+
const actualRevision = existingClaim?.revision ?? 0;
|
|
2898
|
+
const reconciliation = decoded.reconciliation;
|
|
2899
|
+
if (reconciliation !== undefined) {
|
|
2900
|
+
const rejectLink = (reason) => {
|
|
2901
|
+
this.#database.exec("ROLLBACK");
|
|
2902
|
+
inTransaction = false;
|
|
2903
|
+
return {
|
|
2904
|
+
_tag: "rejected",
|
|
2905
|
+
error: new WorkPullRequestLinkError({
|
|
2906
|
+
goalId: decoded.goalId,
|
|
2907
|
+
laneId: decoded.laneId,
|
|
2908
|
+
reason
|
|
2909
|
+
})
|
|
2910
|
+
};
|
|
2911
|
+
};
|
|
2912
|
+
if (existingClaim === undefined || existingClaim.goalId !== decoded.goalId) {
|
|
2913
|
+
return rejectLink("missing_lane");
|
|
2914
|
+
}
|
|
2915
|
+
if (existingClaim.revision !== decoded.expectedRevision)
|
|
2916
|
+
return rejectLink("stale_revision");
|
|
2917
|
+
if (existingClaim.head !== reconciliation.expectedHead)
|
|
2918
|
+
return rejectLink("head_mismatch");
|
|
2919
|
+
if (!Equal.equals(existingClaim.owner, reconciliation.expectedOwner) ||
|
|
2920
|
+
!Equal.equals(decoded.owner, existingClaim.owner))
|
|
2921
|
+
return rejectLink("owner_mismatch");
|
|
2922
|
+
if (existingClaim.worktree !== decoded.worktree ||
|
|
2923
|
+
existingClaim.branch !== decoded.branch ||
|
|
2924
|
+
existingClaim.parent !== decoded.parent ||
|
|
2925
|
+
existingClaim.phase !== decoded.phase ||
|
|
2926
|
+
existingClaim.phase === "shipped")
|
|
2927
|
+
return rejectLink("identity_mismatch");
|
|
2928
|
+
const eventRaw = this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
2929
|
+
FROM work_goal_events WHERE goal_id = ? ORDER BY occurred_at DESC, event_id DESC LIMIT 1`).get(decoded.goalId);
|
|
2930
|
+
if (eventRaw === undefined)
|
|
2931
|
+
return rejectLink("missing_provenance");
|
|
2932
|
+
const event = Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(Schema.decodeUnknownSync(AgentBindingGoalEventRow)(eventRaw).record));
|
|
2933
|
+
const goal = event.goal;
|
|
2934
|
+
if (event.eventId !== reconciliation.expectedGoalEventId ||
|
|
2935
|
+
goal.review?.url !==
|
|
2936
|
+
`https://github.com/${reconciliation.repository}/pull/${reconciliation.pullRequest}` ||
|
|
2937
|
+
goal.goalFamily?.role !== "canonical" ||
|
|
2938
|
+
goal.goalFamily.canonicalGoalId !== goal.id)
|
|
2939
|
+
return rejectLink("missing_provenance");
|
|
2940
|
+
if (goal.state === "completed" || goal.state === "deployed")
|
|
2941
|
+
return rejectLink("terminal_goal");
|
|
2942
|
+
if (!Equal.equals(goal.owner, reconciliation.expectedOwner))
|
|
2943
|
+
return rejectLink("owner_mismatch");
|
|
2944
|
+
const bindingRows = Schema.decodeUnknownSync(Schema.Array(AgentBindingRow))(this.#database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
2945
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
2946
|
+
FROM work_agent_bindings WHERE lane_id = ? ORDER BY revision DESC`).all(decoded.laneId));
|
|
2947
|
+
const latest = bindingRows[0];
|
|
2948
|
+
if (latest === undefined)
|
|
2949
|
+
return rejectLink("missing_binding");
|
|
2950
|
+
if (latest.dispatchRequestId !== reconciliation.bindingDispatchRequestId) {
|
|
2951
|
+
return rejectLink("ambiguous_binding");
|
|
2952
|
+
}
|
|
2953
|
+
const binding = Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(latest.record));
|
|
2954
|
+
if (binding.request.laneId !== decoded.laneId ||
|
|
2955
|
+
binding.lane.goalId !== decoded.goalId ||
|
|
2956
|
+
!Equal.equals(binding.request.worker, reconciliation.worker) ||
|
|
2957
|
+
!Equal.equals(goal.agentHierarchy?.agent, reconciliation.worker) ||
|
|
2958
|
+
goal.connectTarget?.agentId !== reconciliation.worker.agentId ||
|
|
2959
|
+
goal.connectTarget.host.toLowerCase() !== reconciliation.worker.host.toLowerCase()) {
|
|
2960
|
+
return rejectLink("identity_mismatch");
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
if (actualRevision !== decoded.expectedRevision) {
|
|
2964
|
+
this.#database.exec("ROLLBACK");
|
|
2965
|
+
inTransaction = false;
|
|
2966
|
+
return {
|
|
2967
|
+
_tag: "conflict",
|
|
2968
|
+
error: new WorkLaneClaimConflictError({
|
|
2969
|
+
actualRevision,
|
|
2970
|
+
expectedRevision: decoded.expectedRevision,
|
|
2971
|
+
laneId: decoded.laneId
|
|
2972
|
+
})
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
if (existingClaim !== undefined && existingClaim.goalId !== decoded.goalId) {
|
|
2976
|
+
this.#database.exec("ROLLBACK");
|
|
2977
|
+
inTransaction = false;
|
|
2978
|
+
return {
|
|
2979
|
+
_tag: "goal-conflict",
|
|
2980
|
+
error: new WorkLaneGoalConflictError({
|
|
2981
|
+
activeLaneId: existingClaim.laneId,
|
|
2982
|
+
goalId: decoded.goalId,
|
|
2983
|
+
laneId: decoded.laneId
|
|
2984
|
+
})
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
const activeGoal = laneLedger.entries.find(({ claim }) => claim.goalId === decoded.goalId && claim.phase !== "shipped" && claim.laneId !== decoded.laneId);
|
|
2988
|
+
if (decoded.phase !== "shipped" && activeGoal !== undefined) {
|
|
2989
|
+
this.#database.exec("ROLLBACK");
|
|
2990
|
+
inTransaction = false;
|
|
2991
|
+
return {
|
|
2992
|
+
_tag: "goal-conflict",
|
|
2993
|
+
error: new WorkLaneGoalConflictError({
|
|
2994
|
+
activeLaneId: activeGoal.claim.laneId,
|
|
2995
|
+
goalId: decoded.goalId,
|
|
2996
|
+
laneId: decoded.laneId
|
|
2997
|
+
})
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
if (actualRevision >= Number.MAX_SAFE_INTEGER) {
|
|
3001
|
+
this.#database.exec("ROLLBACK");
|
|
3002
|
+
inTransaction = false;
|
|
3003
|
+
return {
|
|
3004
|
+
_tag: "rejected",
|
|
3005
|
+
error: new WorkProjectionError({
|
|
3006
|
+
cause: decoded,
|
|
3007
|
+
detail: "work lane claim revision cannot exceed Number.MAX_SAFE_INTEGER",
|
|
3008
|
+
reason: "capacity_exceeded"
|
|
3009
|
+
})
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
const revision = actualRevision + 1;
|
|
3013
|
+
const result = { ...decoded, revision };
|
|
3014
|
+
const encodedRecord = JSON.stringify(result);
|
|
3015
|
+
const operationEntryBytes = utf8.encode(decoded.operationId).byteLength +
|
|
3016
|
+
utf8.encode(encodedRecord).byteLength;
|
|
3017
|
+
const operationTotals = readLaneOperationLedgerTotals(this.#database);
|
|
3018
|
+
if (operationTotals.operationCount >= workLaneOperationMaxRecords) {
|
|
3019
|
+
this.#database.exec("ROLLBACK");
|
|
3020
|
+
inTransaction = false;
|
|
3021
|
+
return {
|
|
3022
|
+
_tag: "rejected",
|
|
3023
|
+
error: new WorkProjectionError({
|
|
3024
|
+
cause: decoded,
|
|
3025
|
+
detail: `work lane operation history cannot exceed ${workLaneOperationMaxRecords} operation IDs`,
|
|
3026
|
+
reason: "capacity_exceeded"
|
|
3027
|
+
})
|
|
3028
|
+
};
|
|
3029
|
+
}
|
|
3030
|
+
if (operationTotals.operationBytes + operationEntryBytes > workLaneOperationMaxBytes) {
|
|
3031
|
+
this.#database.exec("ROLLBACK");
|
|
3032
|
+
inTransaction = false;
|
|
3033
|
+
return {
|
|
3034
|
+
_tag: "rejected",
|
|
3035
|
+
error: new WorkProjectionError({
|
|
3036
|
+
cause: decoded,
|
|
3037
|
+
detail: `work lane operation history cannot exceed ${workLaneOperationMaxBytes} encoded bytes`,
|
|
3038
|
+
reason: "capacity_exceeded"
|
|
3039
|
+
})
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
const entryBytes = utf8.encode(decoded.laneId).byteLength + utf8.encode(encodedRecord).byteLength;
|
|
3043
|
+
const existingEntryBytes = existing === undefined
|
|
3044
|
+
? 0
|
|
3045
|
+
: utf8.encode(decoded.laneId).byteLength + utf8.encode(existing.record).byteLength;
|
|
3046
|
+
const claimCount = laneLedger.entries.length;
|
|
3047
|
+
if (existing === undefined && claimCount >= workLaneMaxRecords) {
|
|
3048
|
+
this.#database.exec("ROLLBACK");
|
|
3049
|
+
inTransaction = false;
|
|
3050
|
+
return {
|
|
3051
|
+
_tag: "rejected",
|
|
3052
|
+
error: new WorkProjectionError({
|
|
3053
|
+
cause: decoded,
|
|
3054
|
+
detail: `work lane claims cannot exceed ${workLaneMaxRecords} lanes`,
|
|
3055
|
+
reason: "capacity_exceeded"
|
|
3056
|
+
})
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
const claimBytes = Schema.decodeUnknownSync(LedgerBytesRow)(this.#database.prepare(`SELECT COALESCE(SUM(length(CAST(lane_id AS BLOB)) + length(CAST(record AS BLOB))), 0) AS bytes
|
|
3060
|
+
FROM work_lane_claims`).get()).bytes;
|
|
3061
|
+
if (claimBytes - existingEntryBytes + entryBytes > workLaneMaxBytes) {
|
|
3062
|
+
this.#database.exec("ROLLBACK");
|
|
3063
|
+
inTransaction = false;
|
|
3064
|
+
return {
|
|
3065
|
+
_tag: "rejected",
|
|
3066
|
+
error: new WorkProjectionError({
|
|
3067
|
+
cause: decoded,
|
|
3068
|
+
detail: `work lane claims cannot exceed ${workLaneMaxBytes} encoded bytes`,
|
|
3069
|
+
reason: "capacity_exceeded"
|
|
3070
|
+
})
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
const changes = existing === undefined
|
|
3074
|
+
? this.#database.prepare(`INSERT INTO work_lane_claims
|
|
3075
|
+
(lane_id, goal_id, operation_id, phase, revision, record)
|
|
3076
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(decoded.laneId, decoded.goalId, decoded.operationId, decoded.phase, revision, encodedRecord).changes
|
|
3077
|
+
: this.#database.prepare(`UPDATE work_lane_claims
|
|
3078
|
+
SET goal_id = ?, operation_id = ?, phase = ?, revision = ?, record = ?
|
|
3079
|
+
WHERE lane_id = ? AND revision = ?`).run(decoded.goalId, decoded.operationId, decoded.phase, revision, encodedRecord, decoded.laneId, decoded.expectedRevision).changes;
|
|
3080
|
+
if (changes !== 1 && changes !== 1n) {
|
|
3081
|
+
this.#database.exec("ROLLBACK");
|
|
3082
|
+
inTransaction = false;
|
|
3083
|
+
return {
|
|
3084
|
+
_tag: "conflict",
|
|
3085
|
+
error: new WorkLaneClaimConflictError({
|
|
3086
|
+
actualRevision,
|
|
3087
|
+
expectedRevision: decoded.expectedRevision,
|
|
3088
|
+
laneId: decoded.laneId
|
|
3089
|
+
})
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
const operationChanges = this.#database.prepare(`INSERT INTO work_lane_operations
|
|
3093
|
+
(operation_id, lane_id, goal_id, phase, revision, record)
|
|
3094
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(decoded.operationId, decoded.laneId, decoded.goalId, decoded.phase, revision, encodedRecord).changes;
|
|
3095
|
+
if (operationChanges !== 1 && operationChanges !== 1n) {
|
|
3096
|
+
this.#database.exec("ROLLBACK");
|
|
3097
|
+
inTransaction = false;
|
|
3098
|
+
return {
|
|
3099
|
+
_tag: "rejected",
|
|
3100
|
+
error: new WorkStoreError({ cause: decoded, operation: "claim.operation.insert" })
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
this.#database.exec("COMMIT");
|
|
3104
|
+
inTransaction = false;
|
|
3105
|
+
return { _tag: "claimed", value: result };
|
|
3106
|
+
}
|
|
3107
|
+
catch (error) {
|
|
3108
|
+
if (inTransaction)
|
|
3109
|
+
this.#database.exec("ROLLBACK");
|
|
3110
|
+
throw error;
|
|
3111
|
+
}
|
|
3112
|
+
},
|
|
3113
|
+
catch: storeError("claim.write")
|
|
3114
|
+
});
|
|
3115
|
+
if (decision._tag !== "claimed")
|
|
3116
|
+
return yield* decision.error;
|
|
3117
|
+
return decision.value;
|
|
3118
|
+
});
|
|
3119
|
+
decision = Effect.fn("WorkStore.decision")(function* (handoff) {
|
|
3120
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkDecisionHandoff)(handoff).pipe(Effect.mapError(storeError("decision.decode")));
|
|
3121
|
+
yield* this.secureFiles();
|
|
3122
|
+
const encodedHandoff = JSON.stringify(decoded);
|
|
3123
|
+
const handoffEntryBytes = utf8.encode(decoded.id).byteLength + utf8.encode(encodedHandoff).byteLength;
|
|
3124
|
+
const result = yield* Effect.try({
|
|
3125
|
+
try: () => {
|
|
3126
|
+
let inTransaction = false;
|
|
3127
|
+
try {
|
|
3128
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
3129
|
+
inTransaction = true;
|
|
3130
|
+
const sessionRaw = this.#database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
3131
|
+
occurred_at AS occurredAt, record
|
|
3132
|
+
FROM work_decision_handoffs WHERE session_id = ?`).get(decoded.sessionId);
|
|
3133
|
+
if (sessionRaw !== undefined) {
|
|
3134
|
+
const previous = Schema.decodeUnknownSync(DecisionRow)(sessionRaw);
|
|
3135
|
+
const prior = Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(previous.record));
|
|
3136
|
+
this.#database.exec("ROLLBACK");
|
|
3137
|
+
inTransaction = false;
|
|
3138
|
+
if (previous.handoffId !== prior.id ||
|
|
3139
|
+
previous.sessionId !== prior.sessionId ||
|
|
3140
|
+
previous.laneId !== prior.laneId ||
|
|
3141
|
+
previous.occurredAt !== prior.occurredAt) {
|
|
3142
|
+
return {
|
|
3143
|
+
_tag: "rejected",
|
|
3144
|
+
error: new WorkStoreError({
|
|
3145
|
+
cause: { row: previous, record: prior },
|
|
3146
|
+
operation: "decision.decode.identity-mismatch"
|
|
3147
|
+
})
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
if (Equal.equals(prior, decoded))
|
|
3151
|
+
return { _tag: "replayed", value: decoded };
|
|
3152
|
+
return {
|
|
3153
|
+
_tag: "coordinator-conflict",
|
|
3154
|
+
error: new WorkCoordinatorHandoffConflictError({ sessionId: decoded.sessionId })
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
const handoffRaw = this.#database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
3158
|
+
occurred_at AS occurredAt, record
|
|
3159
|
+
FROM work_decision_handoffs WHERE handoff_id = ?`).get(decoded.id);
|
|
3160
|
+
if (handoffRaw !== undefined) {
|
|
3161
|
+
this.#database.exec("ROLLBACK");
|
|
3162
|
+
inTransaction = false;
|
|
3163
|
+
return {
|
|
3164
|
+
_tag: "conflict",
|
|
3165
|
+
error: new WorkDecisionHandoffConflictError({ handoffId: decoded.id })
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
const laneLedger = readValidatedLaneLedger(this.#database, "decision.claim");
|
|
3169
|
+
if (laneLedger._tag === "invalid") {
|
|
3170
|
+
this.#database.exec("ROLLBACK");
|
|
3171
|
+
inTransaction = false;
|
|
3172
|
+
return { _tag: "rejected", error: laneLedger.error };
|
|
3173
|
+
}
|
|
3174
|
+
const activeGoalClaims = laneLedger.entries.filter(({ claim }) => claim.goalId === decoded.goalId && claim.phase !== "shipped");
|
|
3175
|
+
if (activeGoalClaims.length !== 1 || activeGoalClaims[0]?.claim.laneId !== decoded.laneId) {
|
|
3176
|
+
this.#database.exec("ROLLBACK");
|
|
3177
|
+
inTransaction = false;
|
|
3178
|
+
return {
|
|
3179
|
+
_tag: "rejected",
|
|
3180
|
+
error: new WorkDecisionAuthorityConflictError({ goalId: decoded.goalId, laneId: decoded.laneId })
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
const activeClaim = activeGoalClaims[0].claim;
|
|
3184
|
+
if (activeClaim.revision !== decoded.expectedRevision) {
|
|
3185
|
+
this.#database.exec("ROLLBACK");
|
|
3186
|
+
inTransaction = false;
|
|
3187
|
+
return {
|
|
3188
|
+
_tag: "rejected",
|
|
3189
|
+
error: new WorkDecisionRevisionConflictError({
|
|
3190
|
+
actualRevision: activeClaim.revision,
|
|
3191
|
+
expectedRevision: decoded.expectedRevision,
|
|
3192
|
+
laneId: decoded.laneId
|
|
3193
|
+
})
|
|
3194
|
+
};
|
|
3195
|
+
}
|
|
3196
|
+
const decisionLedgerTotals = readDecisionLedgerTotals(this.#database);
|
|
3197
|
+
if (decisionLedgerTotals.decisionCount >= workDecisionMaxRecords) {
|
|
3198
|
+
this.#database.exec("ROLLBACK");
|
|
3199
|
+
inTransaction = false;
|
|
3200
|
+
return {
|
|
3201
|
+
_tag: "rejected",
|
|
3202
|
+
error: new WorkProjectionError({
|
|
3203
|
+
cause: decoded,
|
|
3204
|
+
detail: `work decision history cannot exceed ${workDecisionMaxRecords} handoffs`,
|
|
3205
|
+
reason: "capacity_exceeded"
|
|
3206
|
+
})
|
|
3207
|
+
};
|
|
3208
|
+
}
|
|
3209
|
+
if (decisionLedgerTotals.decisionBytes + handoffEntryBytes > workDecisionMaxBytes) {
|
|
3210
|
+
this.#database.exec("ROLLBACK");
|
|
3211
|
+
inTransaction = false;
|
|
3212
|
+
return {
|
|
3213
|
+
_tag: "rejected",
|
|
3214
|
+
error: new WorkProjectionError({
|
|
3215
|
+
cause: decoded,
|
|
3216
|
+
detail: `work decision history cannot exceed ${workDecisionMaxBytes} encoded bytes`,
|
|
3217
|
+
reason: "capacity_exceeded"
|
|
3218
|
+
})
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
this.#database.prepare(`INSERT INTO work_decision_handoffs
|
|
3222
|
+
(handoff_id, session_id, lane_id, occurred_at, record)
|
|
3223
|
+
VALUES (?, ?, ?, ?, ?)`).run(decoded.id, decoded.sessionId, decoded.laneId, decoded.occurredAt, encodedHandoff);
|
|
3224
|
+
this.#database.exec("COMMIT");
|
|
3225
|
+
inTransaction = false;
|
|
3226
|
+
return { _tag: "inserted", value: decoded };
|
|
3227
|
+
}
|
|
3228
|
+
catch (error) {
|
|
3229
|
+
if (inTransaction)
|
|
3230
|
+
this.#database.exec("ROLLBACK");
|
|
3231
|
+
throw error;
|
|
3232
|
+
}
|
|
3233
|
+
},
|
|
3234
|
+
catch: storeError("decision.write")
|
|
3235
|
+
});
|
|
3236
|
+
if (result._tag !== "inserted" && result._tag !== "replayed")
|
|
3237
|
+
return yield* result.error;
|
|
3238
|
+
return result.value;
|
|
3239
|
+
});
|
|
3240
|
+
currentClaim = Effect.fn("WorkStore.currentClaim")(function* (laneId) {
|
|
3241
|
+
const decodedLaneId = yield* Schema.decodeUnknownEffect(WorkGoalId)(laneId).pipe(Effect.mapError(storeError("claim.read.decode-lane-id")));
|
|
3242
|
+
const ledger = yield* Effect.sync(() => readValidatedLaneLedger(this.#database, "claim.read"));
|
|
3243
|
+
if (ledger._tag === "invalid")
|
|
3244
|
+
return yield* ledger.error;
|
|
3245
|
+
const entry = ledger.entries.find(({ claim }) => claim.laneId === decodedLaneId);
|
|
3246
|
+
return entry === undefined ? Option.none() : Option.some(entry.claim);
|
|
3247
|
+
});
|
|
3248
|
+
activeGoalClaim = Effect.fn("WorkStore.activeGoalClaim")(function* (goalId) {
|
|
3249
|
+
const decodedGoalId = yield* Schema.decodeUnknownEffect(WorkGoalId)(goalId).pipe(Effect.mapError(storeError("claim.goal.decode-goal-id")));
|
|
3250
|
+
const ledger = yield* Effect.sync(() => readValidatedLaneLedger(this.#database, "claim.goal"));
|
|
3251
|
+
if (ledger._tag === "invalid")
|
|
3252
|
+
return yield* ledger.error;
|
|
3253
|
+
const active = ledger.entries.filter(({ claim }) => claim.goalId === decodedGoalId && claim.phase !== "shipped");
|
|
3254
|
+
if (active.length === 0)
|
|
3255
|
+
return Option.none();
|
|
3256
|
+
if (active.length !== 1) {
|
|
3257
|
+
return yield* new WorkStoreError({
|
|
3258
|
+
cause: { goalId: decodedGoalId, lanes: active.map(({ claim }) => claim.laneId) },
|
|
3259
|
+
operation: "claim.goal.exclusivity-violation"
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
const entry = active[0];
|
|
3263
|
+
if (entry === undefined) {
|
|
3264
|
+
return yield* new WorkStoreError({ cause: decodedGoalId, operation: "claim.goal.missing-row" });
|
|
3265
|
+
}
|
|
3266
|
+
return Option.some(entry.claim);
|
|
3267
|
+
});
|
|
3268
|
+
coordinatorHandoff = Effect.fn("WorkStore.coordinatorHandoff")(function* (sessionId) {
|
|
3269
|
+
const decodedSessionId = yield* Schema.decodeUnknownEffect(WorkCoordinatorSessionId)(sessionId).pipe(Effect.mapError(storeError("decision.session.decode-session-id")));
|
|
3270
|
+
const raw = yield* Effect.try({
|
|
3271
|
+
try: () => this.#database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
3272
|
+
occurred_at AS occurredAt, record
|
|
3273
|
+
FROM work_decision_handoffs WHERE session_id = ?`).get(decodedSessionId),
|
|
3274
|
+
catch: storeError("decision.session.read")
|
|
3275
|
+
});
|
|
3276
|
+
if (raw === undefined)
|
|
3277
|
+
return Option.none();
|
|
3278
|
+
const row = yield* Schema.decodeUnknownEffect(DecisionRow)(raw).pipe(Effect.mapError(storeError("decision.session.decode-row")));
|
|
3279
|
+
const handoff = yield* Effect.try({
|
|
3280
|
+
try: () => JSON.parse(row.record),
|
|
3281
|
+
catch: storeError("decision.session.parse")
|
|
3282
|
+
}).pipe(Effect.flatMap((value) => Schema.decodeUnknownEffect(WorkDecisionHandoff)(value)), Effect.mapError(storeError("decision.session.decode")));
|
|
3283
|
+
if (handoff.id !== row.handoffId ||
|
|
3284
|
+
handoff.sessionId !== row.sessionId ||
|
|
3285
|
+
handoff.sessionId !== decodedSessionId ||
|
|
3286
|
+
handoff.laneId !== row.laneId ||
|
|
3287
|
+
handoff.occurredAt !== row.occurredAt) {
|
|
3288
|
+
return yield* new WorkStoreError({
|
|
3289
|
+
cause: { handoff, row },
|
|
3290
|
+
operation: "decision.session.identity-mismatch"
|
|
3291
|
+
});
|
|
3292
|
+
}
|
|
3293
|
+
return Option.some(handoff);
|
|
3294
|
+
});
|
|
3295
|
+
decisions = Effect.fn("WorkStore.decisions")(function* (laneId) {
|
|
3296
|
+
const decodedLaneId = yield* Schema.decodeUnknownEffect(WorkGoalId)(laneId).pipe(Effect.mapError(storeError("decisions.list.decode-lane-id")));
|
|
3297
|
+
const rows = yield* Effect.try({
|
|
3298
|
+
try: () => this.#database.prepare(`SELECT handoff_id AS handoffId, session_id AS sessionId, lane_id AS laneId,
|
|
3299
|
+
occurred_at AS occurredAt, record
|
|
3300
|
+
FROM work_decision_handoffs
|
|
3301
|
+
WHERE lane_id = ? ORDER BY occurred_at ASC, handoff_id ASC`).all(decodedLaneId),
|
|
3302
|
+
catch: storeError("decisions.list")
|
|
3303
|
+
});
|
|
3304
|
+
return yield* Effect.forEach(rows, (row) => Schema.decodeUnknownEffect(DecisionRow)(row).pipe(Effect.mapError(storeError("decisions.decode-row")), Effect.flatMap((row) => Effect.try({
|
|
3305
|
+
try: () => Schema.decodeUnknownSync(WorkDecisionHandoff)(JSON.parse(row.record)),
|
|
3306
|
+
catch: storeError("decisions.decode")
|
|
3307
|
+
})
|
|
3308
|
+
.pipe(Effect.flatMap((handoff) => handoff.laneId !== decodedLaneId
|
|
3309
|
+
? Effect.fail(new WorkStoreError({
|
|
3310
|
+
cause: { requestedLaneId: decodedLaneId, recordLaneId: handoff.laneId },
|
|
3311
|
+
operation: "decisions.decode.lane-mismatch"
|
|
3312
|
+
}))
|
|
3313
|
+
: handoff.laneId !== row.laneId ||
|
|
3314
|
+
handoff.sessionId !== row.sessionId ||
|
|
3315
|
+
handoff.id !== row.handoffId ||
|
|
3316
|
+
handoff.occurredAt !== row.occurredAt
|
|
3317
|
+
? Effect.fail(new WorkStoreError({
|
|
3318
|
+
cause: {
|
|
3319
|
+
record: handoff,
|
|
3320
|
+
row: {
|
|
3321
|
+
handoffId: row.handoffId,
|
|
3322
|
+
sessionId: row.sessionId,
|
|
3323
|
+
laneId: row.laneId,
|
|
3324
|
+
occurredAt: row.occurredAt
|
|
3325
|
+
}
|
|
3326
|
+
},
|
|
3327
|
+
operation: "decisions.decode.identity-mismatch"
|
|
3328
|
+
}))
|
|
3329
|
+
: Effect.succeed(handoff))))));
|
|
3330
|
+
});
|
|
172
3331
|
list = Effect.fn("WorkStore.list")(function* () {
|
|
173
3332
|
const rows = yield* Effect.try({
|
|
174
3333
|
try: () => this.#database.prepare("SELECT record FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC").all(),
|
|
@@ -176,11 +3335,76 @@ export class WorkStore {
|
|
|
176
3335
|
});
|
|
177
3336
|
return yield* Effect.forEach(rows, decodeRow);
|
|
178
3337
|
});
|
|
3338
|
+
snapshotInput = Effect.fn("WorkStore.snapshotInput")(function* () {
|
|
3339
|
+
const source = yield* Effect.try({
|
|
3340
|
+
try: () => {
|
|
3341
|
+
let inTransaction = false;
|
|
3342
|
+
try {
|
|
3343
|
+
this.#database.exec("BEGIN");
|
|
3344
|
+
inTransaction = true;
|
|
3345
|
+
const events = this.#database.prepare(`SELECT event_id AS eventId, goal_id AS goalId, occurred_at AS occurredAt, record
|
|
3346
|
+
FROM work_goal_events ORDER BY occurred_at ASC, event_id ASC
|
|
3347
|
+
LIMIT ?`).all(workHistoryMaxEvents + 1);
|
|
3348
|
+
const bindings = this.#database.prepare(`SELECT dispatch_request_id AS dispatchRequestId, lane_id AS laneId,
|
|
3349
|
+
expected_revision AS expectedRevision, revision, agent_id AS agentId, host, record
|
|
3350
|
+
FROM work_agent_bindings ORDER BY dispatch_request_id ASC
|
|
3351
|
+
LIMIT ?`).all(workLaneOperationMaxRecords + 1);
|
|
3352
|
+
const laneOperations = this.#database.prepare(`SELECT operation_id AS operationId, lane_id AS laneId, goal_id AS goalId,
|
|
3353
|
+
phase, revision, record
|
|
3354
|
+
FROM work_lane_operations ORDER BY operation_id ASC
|
|
3355
|
+
LIMIT ?`).all(workLaneOperationMaxRecords + 1);
|
|
3356
|
+
this.#database.exec("COMMIT");
|
|
3357
|
+
inTransaction = false;
|
|
3358
|
+
return { bindings, events, laneOperations };
|
|
3359
|
+
}
|
|
3360
|
+
catch (cause) {
|
|
3361
|
+
if (inTransaction)
|
|
3362
|
+
this.#database.exec("ROLLBACK");
|
|
3363
|
+
throw cause;
|
|
3364
|
+
}
|
|
3365
|
+
},
|
|
3366
|
+
catch: storeError("snapshot-input.read")
|
|
3367
|
+
});
|
|
3368
|
+
const eventRows = yield* Schema.decodeUnknownEffect(Schema.Array(AgentBindingGoalEventRow))(source.events).pipe(Effect.mapError(storeError("snapshot-input.decode-events")));
|
|
3369
|
+
const bindingRows = yield* Schema.decodeUnknownEffect(AgentBindingRows)(source.bindings).pipe(Effect.mapError(storeError("snapshot-input.decode-bindings")));
|
|
3370
|
+
const laneRows = yield* Schema.decodeUnknownEffect(Schema.Array(AgentBindingLaneOperationRow))(source.laneOperations).pipe(Effect.mapError(storeError("snapshot-input.decode-lane-operations")));
|
|
3371
|
+
if (eventRows.length > workHistoryMaxEvents ||
|
|
3372
|
+
bindingRows.length > workLaneOperationMaxRecords ||
|
|
3373
|
+
laneRows.length > workLaneOperationMaxRecords) {
|
|
3374
|
+
return yield* new WorkStoreError({ cause: source, operation: "snapshot-input.capacity" });
|
|
3375
|
+
}
|
|
3376
|
+
const events = yield* Effect.forEach(eventRows, (row) => {
|
|
3377
|
+
const decision = decodeAgentBindingGoalEvent(row, "snapshot-input.event");
|
|
3378
|
+
return decision._tag === "valid" ? Effect.succeed(decision.checkpoint) : Effect.fail(decision.error);
|
|
3379
|
+
});
|
|
3380
|
+
const eventById = new Map(eventRows.map((row) => [row.eventId, row]));
|
|
3381
|
+
const laneByOperation = new Map(laneRows.map((row) => [row.operationId, row]));
|
|
3382
|
+
let logicalObservedAt = null;
|
|
3383
|
+
for (const row of bindingRows) {
|
|
3384
|
+
const binding = yield* Effect.try({
|
|
3385
|
+
try: () => Schema.decodeUnknownSync(WorkAgentBinding)(JSON.parse(row.record)),
|
|
3386
|
+
catch: storeError("snapshot-input.decode-binding")
|
|
3387
|
+
});
|
|
3388
|
+
if (row.dispatchRequestId !== binding.request.dispatchRequestId ||
|
|
3389
|
+
row.laneId !== binding.request.laneId ||
|
|
3390
|
+
row.expectedRevision !== binding.request.expectedRevision ||
|
|
3391
|
+
row.revision !== binding.lane.revision ||
|
|
3392
|
+
row.agentId !== binding.request.worker.agentId ||
|
|
3393
|
+
row.host.toLowerCase() !== binding.request.worker.host.toLowerCase()) {
|
|
3394
|
+
return yield* new WorkStoreError({
|
|
3395
|
+
cause: { binding, row },
|
|
3396
|
+
operation: "snapshot-input.binding-identity-mismatch"
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
const readbackError = agentBindingReadbackError(binding, laneByOperation.get(binding.lane.operationId), eventById.get(binding.checkpoint.eventId), "snapshot-input.binding");
|
|
3400
|
+
if (readbackError !== undefined)
|
|
3401
|
+
return yield* readbackError;
|
|
3402
|
+
logicalObservedAt = Math.max(logicalObservedAt ?? 0, binding.checkpoint.occurredAt);
|
|
3403
|
+
}
|
|
3404
|
+
return { events, logicalObservedAt };
|
|
3405
|
+
});
|
|
179
3406
|
secureFiles() {
|
|
180
|
-
|
|
181
|
-
return Effect.forEach(files, (path) => this.#fileSystem.exists(path).pipe(Effect.mapError(storeError("secure.exists")), Effect.flatMap((exists) => exists
|
|
182
|
-
? this.#fileSystem.chmod(path, 0o600).pipe(Effect.mapError(storeError("secure.chmod")))
|
|
183
|
-
: Effect.void)), { discard: true });
|
|
3407
|
+
return this.#secureFiles;
|
|
184
3408
|
}
|
|
185
3409
|
close() {
|
|
186
3410
|
this.#database.close();
|