@ccdd/project 5.1.2 → 6.0.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/dist/monitor-ui/assets/{GraphView-BILt_jGR.js → GraphView-Dvqux-3b.js} +3 -3
- package/dist/monitor-ui/assets/ValidationView-ksCr_lB7.css +1 -0
- package/dist/monitor-ui/assets/ValidationView-qhnNfj00.js +1 -0
- package/dist/monitor-ui/assets/{elk-api-CW6DDhyE.js → elk-api-D1k8qsLm.js} +1 -1
- package/dist/monitor-ui/assets/{elk.bundled-WR-av3nY.js → elk.bundled-DgeQWR_s.js} +1 -1
- package/dist/monitor-ui/assets/{index-Drtw0yLv.js → index-beXagC6U.js} +3 -3
- package/dist/monitor-ui/index.html +1 -1
- package/dist/src/artifacts/scope.d.ts +1 -1
- package/dist/src/artifacts/scope.js +2 -2
- package/dist/src/artifacts/scope.js.map +1 -1
- package/dist/src/broker/admission.d.ts +19 -0
- package/dist/src/broker/admission.js +33 -0
- package/dist/src/broker/admission.js.map +1 -0
- package/dist/src/broker/changes.d.ts +25 -0
- package/dist/src/broker/changes.js +27 -0
- package/dist/src/broker/changes.js.map +1 -0
- package/dist/src/broker/coalescing.d.ts +1 -0
- package/dist/src/broker/coalescing.js +5 -3
- package/dist/src/broker/coalescing.js.map +1 -1
- package/dist/src/broker/graph.d.ts +1 -1
- package/dist/src/broker/graph.js +1 -1
- package/dist/src/broker/graph.js.map +1 -1
- package/dist/src/broker/index.d.ts +29 -3
- package/dist/src/broker/index.js +263 -307
- package/dist/src/broker/index.js.map +1 -1
- package/dist/src/broker/readiness.d.ts +17 -0
- package/dist/src/broker/readiness.js +252 -0
- package/dist/src/broker/readiness.js.map +1 -0
- package/dist/src/broker/storage.d.ts +101 -0
- package/dist/src/broker/storage.js +315 -0
- package/dist/src/broker/storage.js.map +1 -0
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/monitor/store.js +6 -4
- package/dist/src/monitor/store.js.map +1 -1
- package/dist/src/monitor/ui/format.js +1 -1
- package/dist/src/monitor/ui/format.js.map +1 -1
- package/dist/src/project/cli.js +8 -7
- package/dist/src/project/cli.js.map +1 -1
- package/dist/src/project/gates.d.ts +6 -0
- package/dist/src/project/gates.js +33 -0
- package/dist/src/project/gates.js.map +1 -0
- package/dist/src/project/index.d.ts +2 -1
- package/dist/src/project/index.js +2 -2
- package/dist/src/project/index.js.map +1 -1
- package/dist/src/project/query.js +35 -8
- package/dist/src/project/query.js.map +1 -1
- package/dist/src/project/store.d.ts +1 -1
- package/dist/src/project/store.js +29 -17
- package/dist/src/project/store.js.map +1 -1
- package/dist/src/project/types.d.ts +7 -3
- package/dist/src/requester/index.d.ts +3 -2
- package/dist/src/requester/index.js +6 -5
- package/dist/src/requester/index.js.map +1 -1
- package/dist/src/state-format.js +2 -2
- package/package.json +2 -2
- package/dist/monitor-ui/assets/ValidationView-D18mwExY.css +0 -1
- package/dist/monitor-ui/assets/ValidationView-L-a3hI_A.js +0 -1
package/dist/src/broker/index.js
CHANGED
|
@@ -6,10 +6,14 @@ import { randomUUID } from 'node:crypto';
|
|
|
6
6
|
import { performance } from 'node:perf_hooks';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
9
|
+
import { setTimeout as delay, setImmediate as yieldTurn } from 'node:timers/promises';
|
|
10
10
|
import { prepareReviewRequests } from '../requester/index.js';
|
|
11
11
|
import { readWorkspaceConfig } from './config.js';
|
|
12
12
|
import { createGraphDefinition } from './graph.js';
|
|
13
|
+
import { localAdmission } from './admission.js';
|
|
14
|
+
import { readChanges } from './changes.js';
|
|
15
|
+
import { initializeRecords, records } from './storage.js';
|
|
16
|
+
import { createReadiness } from './readiness.js';
|
|
13
17
|
import { createStatusPolling } from './polling.js';
|
|
14
18
|
import { ownerAlive, ownProcessIdentity } from './ownership.js';
|
|
15
19
|
import { coalescingEligibility, findCoalescibleRequest } from './coalescing.js';
|
|
@@ -86,7 +90,7 @@ function prepareStateDirectory(repoPath, stateDir) {
|
|
|
86
90
|
return fs.realpathSync(canonical);
|
|
87
91
|
}
|
|
88
92
|
/** Durable broker operations. Merely opening the store never starts a worker. */
|
|
89
|
-
export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coalescingGraceMs = 15_000, maxConcurrentExecutors = 4, identityConcurrency = DEFAULT_IDENTITY_CONCURRENCY, executors, workspaceIntegrity = 'content', workspaceAdapter = { prepareWorkspace, reopenWorkspace } }) {
|
|
93
|
+
export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coalescingGraceMs = 15_000, maxConcurrentExecutors = 4, admission = localAdmission(maxConcurrentExecutors), identityConcurrency = DEFAULT_IDENTITY_CONCURRENCY, executors, workspaceIntegrity = 'content', workspaceAdapter = { prepareWorkspace, reopenWorkspace } }) {
|
|
90
94
|
positiveConcurrency(maxConcurrentExecutors, 'maxConcurrentExecutors');
|
|
91
95
|
positiveConcurrency(identityConcurrency, 'identityConcurrency');
|
|
92
96
|
if (!Number.isSafeInteger(coalescingGraceMs) || coalescingGraceMs < 0 || coalescingGraceMs > 300_000)
|
|
@@ -121,10 +125,12 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
121
125
|
CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, created_at TEXT NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
|
|
122
126
|
CREATE TABLE IF NOT EXISTS requests (id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), ordinal INTEGER NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
|
|
123
127
|
CREATE INDEX IF NOT EXISTS requests_run ON requests(run_id, ordinal);
|
|
124
|
-
CREATE INDEX IF NOT EXISTS requests_validation_input ON requests(json_extract(data, '$.
|
|
128
|
+
CREATE INDEX IF NOT EXISTS requests_validation_input ON requests(json_extract(data, '$.criticId'),json_extract(data, '$.inputKey'));
|
|
129
|
+
CREATE INDEX IF NOT EXISTS requests_ready ON requests(run_id,status,ordinal);
|
|
125
130
|
CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL REFERENCES runs(id), request_id TEXT, created_at TEXT NOT NULL, type TEXT NOT NULL, message TEXT NOT NULL, data TEXT);
|
|
126
131
|
CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
127
132
|
CREATE TABLE IF NOT EXISTS run_owners (run_id TEXT PRIMARY KEY REFERENCES runs(id), pid INTEGER NOT NULL, process_identity TEXT, token TEXT NOT NULL, claimed_at TEXT NOT NULL);`);
|
|
133
|
+
initializeRecords(db);
|
|
128
134
|
const previousRepo = db.prepare('SELECT value FROM metadata WHERE key = ?').get('registered-repo');
|
|
129
135
|
const identity = JSON.stringify({ repoPath, repoId });
|
|
130
136
|
if (previousRepo && previousRepo.value !== identity)
|
|
@@ -144,102 +150,36 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
144
150
|
const active = new Map();
|
|
145
151
|
const listeners = new Set();
|
|
146
152
|
const ensureOpen = () => { if (closed || closing)
|
|
147
|
-
throw new Error('Broker is closed.');
|
|
153
|
+
throw new Error('Broker is closed.'); };
|
|
148
154
|
const requireExecutors = () => {
|
|
149
155
|
if (!executors || typeof executors.canExecute !== 'function' || typeof executors.execute !== 'function')
|
|
150
156
|
throw new Error('Review submission and execution require an executor registry.');
|
|
151
157
|
return executors;
|
|
152
158
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// locally published cache entry. These are bounded read caches, never evidence.
|
|
156
|
-
const versionStatement = db.prepare('PRAGMA data_version');
|
|
157
|
-
let dataVersion = Number(versionStatement.get().data_version);
|
|
158
|
-
const runCache = new Map();
|
|
159
|
-
const attemptCache = new Map();
|
|
160
|
-
let evidenceCache;
|
|
161
|
-
const clearReadCaches = () => { runCache.clear(); attemptCache.clear(); evidenceCache = undefined; };
|
|
162
|
-
const syncReadCaches = () => {
|
|
163
|
-
const version = Number(versionStatement.get().data_version);
|
|
164
|
-
if (version !== dataVersion) {
|
|
165
|
-
clearReadCaches();
|
|
166
|
-
dataVersion = version;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const currentEvidence = () => {
|
|
170
|
-
syncReadCaches();
|
|
171
|
-
if (evidenceCache)
|
|
172
|
-
return evidenceCache;
|
|
173
|
-
const evidence = readEvidence(db);
|
|
174
|
-
// Large histories remain authoritative reads rather than unbounded retention.
|
|
175
|
-
if (Buffer.byteLength(JSON.stringify(evidence)) <= 16 * 1024 ** 2)
|
|
176
|
-
evidenceCache = evidence;
|
|
177
|
-
return evidence;
|
|
178
|
-
};
|
|
179
|
-
const cacheRun = (run, bytes) => {
|
|
180
|
-
runCache.delete(run.id);
|
|
181
|
-
while (runCache.size && ([...runCache.values()].reduce((sum, item) => sum + item.bytes, 0) + bytes > 96 * 1024 ** 2 || runCache.size >= 4))
|
|
182
|
-
runCache.delete(runCache.keys().next().value);
|
|
183
|
-
// Keep at most four Runs / 96 MiB of serialized definitions, or one
|
|
184
|
-
// oversized current Run. Lifecycle fields are copied for each caller.
|
|
185
|
-
runCache.set(run.id, { value: { ...run, ...(run.project ? { project: { ...run.project } } : {}) }, bytes });
|
|
186
|
-
};
|
|
187
|
-
const cacheAttempt = ({ id, criticId, status, error }) => {
|
|
188
|
-
attemptCache.delete(id);
|
|
189
|
-
if (attemptCache.size >= 4096)
|
|
190
|
-
attemptCache.delete(attemptCache.keys().next().value);
|
|
191
|
-
const attempt = { id, criticId, status, error };
|
|
192
|
-
attemptCache.set(id, attempt);
|
|
193
|
-
return attempt;
|
|
194
|
-
};
|
|
159
|
+
const store = records(db);
|
|
160
|
+
const clearReadCaches = () => store.clear();
|
|
195
161
|
const transaction = (callback) => {
|
|
196
162
|
db.exec('BEGIN IMMEDIATE');
|
|
197
163
|
try {
|
|
198
|
-
|
|
199
|
-
const result = callback();
|
|
164
|
+
const value = callback();
|
|
200
165
|
db.exec('COMMIT');
|
|
201
|
-
return
|
|
166
|
+
return value;
|
|
202
167
|
}
|
|
203
168
|
catch (error) {
|
|
204
169
|
try {
|
|
205
170
|
db.exec('ROLLBACK');
|
|
206
171
|
}
|
|
207
172
|
finally {
|
|
208
|
-
|
|
173
|
+
store.clear();
|
|
209
174
|
}
|
|
210
175
|
throw error;
|
|
211
176
|
}
|
|
212
177
|
};
|
|
213
|
-
const requestData = (id) =>
|
|
214
|
-
|
|
215
|
-
return row ? parseStored(row.data) : null;
|
|
216
|
-
};
|
|
217
|
-
const runData = (id) => {
|
|
218
|
-
syncReadCaches();
|
|
219
|
-
let cached = runCache.get(id);
|
|
220
|
-
if (!cached) {
|
|
221
|
-
const row = db.prepare('SELECT data FROM runs WHERE id = ?').get(id);
|
|
222
|
-
if (!row)
|
|
223
|
-
return null;
|
|
224
|
-
cacheRun(parseStored(row.data), Buffer.byteLength(String(row.data)));
|
|
225
|
-
cached = runCache.get(id);
|
|
226
|
-
}
|
|
227
|
-
const run = cached.value;
|
|
228
|
-
return { ...run, ...(run.project ? { project: { ...run.project } } : {}) };
|
|
229
|
-
};
|
|
178
|
+
const requestData = (id) => store.request(id);
|
|
179
|
+
const runData = (id) => store.run(id);
|
|
230
180
|
const ownerData = (id) => db.prepare('SELECT * FROM run_owners WHERE run_id = ?').get(id);
|
|
231
|
-
const runRequests = (id) => db.prepare('SELECT
|
|
232
|
-
const sharedRequests = (run) => (
|
|
233
|
-
const attemptData = (id) => {
|
|
234
|
-
syncReadCaches();
|
|
235
|
-
const cached = attemptCache.get(id);
|
|
236
|
-
if (cached)
|
|
237
|
-
return cached;
|
|
238
|
-
const row = required(db.prepare("SELECT json_object('id',id,'criticId',json_extract(data,'$.criticId'),'status',status,'error',json_extract(data,'$.error')) AS data FROM requests WHERE id = ?").get(id), 'Request');
|
|
239
|
-
return cacheAttempt(parseStored(row.data));
|
|
240
|
-
};
|
|
241
|
-
const runAttempts = (id) => polling.requests(id).map(row => attemptData(row.id));
|
|
242
|
-
const attemptsFor = (run) => [...runAttempts(run.id), ...(run.project?.coalescedRequestIds ?? []).map(attemptData)];
|
|
181
|
+
const runRequests = (id) => db.prepare('SELECT id FROM requests WHERE run_id=? ORDER BY ordinal').all(id).map(row => required(requestData(String(row.id)), 'Request'));
|
|
182
|
+
const sharedRequests = (run) => db.prepare("SELECT q.data FROM shared_members m JOIN requests q ON q.id=m.request_id WHERE m.run_id=? AND m.source_run_id!=?").all(run.id, run.id).map(row => parseStored(row.data));
|
|
243
183
|
const submissionDeadlines = new Map();
|
|
244
184
|
function sweepSubmissionDeadlines() {
|
|
245
185
|
for (const id of submissionDeadlines.keys()) {
|
|
@@ -252,22 +192,22 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
252
192
|
}
|
|
253
193
|
}
|
|
254
194
|
const coalescing = { reconcile: reconcileWithin, deadlines: submissionDeadlines };
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
db.prepare('UPDATE requests SET status = ?, data = ? WHERE id = ?').run(request.status, JSON.stringify(request), request.id);
|
|
260
|
-
if (previous === 'GREEN' || previous === 'RED' || request.status === 'GREEN' || request.status === 'RED')
|
|
261
|
-
evidenceCache = undefined;
|
|
262
|
-
cacheAttempt(request);
|
|
195
|
+
const requestHeader = (id) => { const row = db.prepare('SELECT data FROM requests WHERE id=?').get(id); return row ? parseStored(row.data) : null; };
|
|
196
|
+
const saveHeader = (packed) => {
|
|
197
|
+
db.prepare('UPDATE requests SET status=?,data=? WHERE id=?').run(packed.status, JSON.stringify(packed), packed.id);
|
|
198
|
+
readiness.transition(packed);
|
|
263
199
|
};
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
200
|
+
const saveRequest = (request) => {
|
|
201
|
+
const packed = required(requestHeader(request.id), 'Request');
|
|
202
|
+
for (const key of ['status', 'startedAt', 'completedAt', 'claimedBy', 'claimedAt', 'tryClaim', 'preparationAttempt', 'claimAttemptId', 'notifiedAt', 'error', 'errorCode', 'blockedReason']) {
|
|
203
|
+
if (request[key] === undefined)
|
|
204
|
+
delete packed[key];
|
|
205
|
+
else
|
|
206
|
+
packed[key] = request[key];
|
|
207
|
+
}
|
|
208
|
+
packed.resultRef = request.result ? store.put(request.result) : null;
|
|
209
|
+
packed.semanticRef = request.result ? store.put(semanticResult(request.result)) : null;
|
|
210
|
+
saveHeader(packed);
|
|
271
211
|
};
|
|
272
212
|
const appendEvent = (runId, requestId, type, message, data = null) => {
|
|
273
213
|
db.prepare('INSERT INTO events(run_id,request_id,created_at,type,message,data) VALUES (?,?,?,?,?,?)').run(runId, requestId, now(), type, message.slice(0, 4000), data === null ? null : JSON.stringify(data));
|
|
@@ -290,112 +230,47 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
290
230
|
throw new Error('An in-place review requires its monitoring worker to remain alive.');
|
|
291
231
|
},
|
|
292
232
|
});
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (run.project?.version === 3) {
|
|
297
|
-
if (terminal.has(run.status))
|
|
298
|
-
return;
|
|
299
|
-
for (const shared of sharedRequests(run))
|
|
300
|
-
reconcileWithin(shared.runId);
|
|
301
|
-
// Called in a write transaction: expire only unowned queued sources, then
|
|
302
|
-
// recheck all matching owned/leased requests before creating our own ticket.
|
|
303
|
-
const retained = sharedRequests(run).filter(request => terminal.has(request.status) || shareable(request));
|
|
304
|
-
if (retained.length !== (run.project.coalescedRequestIds ?? []).length) {
|
|
305
|
-
run.project.coalescedRequestIds = retained.map(request => request.id);
|
|
306
|
-
saveRun(run);
|
|
307
|
-
appendEvent(runId, null, 'request.uncoalesced', 'Unowned source submission lease expired; replanning this Run.');
|
|
308
|
-
}
|
|
309
|
-
brokerTestHooks.onPlan?.();
|
|
310
|
-
const plan = planProject(run.project.snapshot, currentEvidence(), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, coalescedRequestIds: run.project.coalescedRequestIds, attempts: [...requests, ...(run.project.coalescedRequestIds ?? []).map(attemptData)] });
|
|
311
|
-
for (const item of plan.items.filter(item => item.action === 'EXECUTE')) {
|
|
312
|
-
if (!run.project.force) {
|
|
313
|
-
const shared = findCoalescibleRequest(db, item.id, item.input.key, coalescing)?.request;
|
|
314
|
-
if (shared) {
|
|
315
|
-
run.project.coalescedRequestIds = [...new Set([...(run.project.coalescedRequestIds ?? []), shared.id])];
|
|
316
|
-
saveRun(run);
|
|
317
|
-
appendEvent(runId, shared.id, 'request.coalesced', 'Waiting for an identical active review in another Run.', { sourceRunId: shared.runId });
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
const envelope = run.project.templates.find(template => template.criticId === item.id);
|
|
322
|
-
if (!envelope)
|
|
323
|
-
throw codedError(`Missing prepared Critic ${item.id}.`, 'REVIEW_GRAPH_INVALID');
|
|
324
|
-
const request = { ...copy(envelope), validationInput: copy(item.input), id: randomUUID(), runId,
|
|
325
|
-
workspace: run.workspace, worktreePath: run.workspace.path, status: 'QUEUED', createdAt: now(),
|
|
326
|
-
startedAt: null, completedAt: null, claimedBy: null, claimedAt: null, notifiedAt: null, result: null, error: null, blockedReason: null };
|
|
327
|
-
db.prepare('INSERT INTO requests(id,run_id,ordinal,status,data) VALUES (?,?,?,?,?)').run(request.id, runId, requests.length, request.status, JSON.stringify(request));
|
|
328
|
-
requests.push(cacheAttempt(request));
|
|
329
|
-
appendEvent(runId, request.id, 'request.queued', 'Pull validation found an executable Critic requiring an actual review.');
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
throw new Error('Invalid stored Run format; submit a new validation request.');
|
|
334
|
-
}
|
|
335
|
-
const updateRunStatus = (runId) => {
|
|
336
|
-
const run = required(runData(runId), 'Run');
|
|
337
|
-
const attempts = attemptsFor(run), states = attempts.map(request => request.status);
|
|
338
|
-
if (run.project?.version === 3) {
|
|
339
|
-
if (terminal.has(run.status))
|
|
340
|
-
return;
|
|
341
|
-
brokerTestHooks.onPlan?.();
|
|
342
|
-
const plan = planProject(run.project.snapshot, currentEvidence(), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, coalescedRequestIds: run.project.coalescedRequestIds, attempts });
|
|
343
|
-
const status = states.includes('RUNNING') ? 'RUNNING' : states.includes('QUEUED') ? 'QUEUED' : states.includes('WAITING_HUMAN') ? 'WAITING_HUMAN' :
|
|
344
|
-
states.includes('ERROR') ? 'ERROR' : states.includes('RED') || plan.critics.some(critic => critic.status === 'RED') ? 'RED' : plan.satisfied ? 'GREEN' : 'INCOMPLETE';
|
|
345
|
-
if (run.status !== status) {
|
|
346
|
-
run.status = status;
|
|
347
|
-
if (terminal.has(status)) {
|
|
348
|
-
run.completedAt = now();
|
|
349
|
-
run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
|
|
350
|
-
}
|
|
351
|
-
saveRun(run);
|
|
352
|
-
appendEvent(runId, null, 'run.status', `Run ${status}`, { status });
|
|
353
|
-
}
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
throw new Error('Invalid stored Run format.');
|
|
357
|
-
};
|
|
233
|
+
const readiness = createReadiness(db, store, coalescing, appendEvent);
|
|
234
|
+
const refreshReadinessWithin = (id) => readiness.refresh(id);
|
|
235
|
+
const updateRunStatus = (id) => readiness.updateRun(id);
|
|
358
236
|
function finishWithin(requestId, outcome, expectedStates = ['RUNNING', 'WAITING_HUMAN']) {
|
|
359
237
|
const { result, error } = outcome, hasError = Object.hasOwn(outcome, 'error');
|
|
360
|
-
const request =
|
|
361
|
-
if (!request || !expectedStates.includes(request.status) || terminal.has(required(
|
|
238
|
+
const request = requestHeader(requestId);
|
|
239
|
+
if (!request || !expectedStates.includes(request.status) || terminal.has(required(polling.runStatus(request.runId), 'Run')))
|
|
362
240
|
return false;
|
|
363
241
|
request.status = hasError ? 'ERROR' : required(result, 'review result').verdict;
|
|
364
|
-
request.
|
|
242
|
+
request.resultRef = result ? store.put(result) : null;
|
|
243
|
+
request.semanticRef = result ? store.put(semanticResult(result)) : null;
|
|
365
244
|
request.error = hasError ? errorText(error) : null;
|
|
366
245
|
request.errorCode = errorCode(error) ?? null;
|
|
367
246
|
request.completedAt = now();
|
|
368
|
-
|
|
247
|
+
saveHeader(request);
|
|
369
248
|
appendEvent(request.runId, request.id, hasError ? 'request.error' : 'request.completed', hasError ? required(request.error, 'error message') : `Review ${required(result, 'review result').verdict}.`, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
|
|
370
|
-
refreshReadinessWithin(request.runId);
|
|
371
|
-
updateRunStatus(request.runId);
|
|
372
249
|
return true;
|
|
373
250
|
}
|
|
374
251
|
function failWithin(runId, error) {
|
|
375
|
-
const run =
|
|
252
|
+
const run = readiness.header(runId);
|
|
376
253
|
if (!run || terminal.has(run.status))
|
|
377
254
|
return;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
255
|
+
// Cancellation is proportional to affected requests, never definitions.
|
|
256
|
+
submissionDeadlines.delete(runId);
|
|
257
|
+
run.status = 'ERROR';
|
|
258
|
+
run.error = errorText(error);
|
|
259
|
+
run.completedAt = now();
|
|
260
|
+
db.prepare('UPDATE runs SET status=?,data=? WHERE id=?').run(run.status, JSON.stringify(run), runId);
|
|
261
|
+
for (const row of db.prepare("SELECT data FROM requests WHERE run_id=? AND status IN ('QUEUED','RUNNING','WAITING_HUMAN','WAIT_DEPENDENCY','BLOCKED')").all(runId)) {
|
|
262
|
+
const request = parseStored(row.data);
|
|
381
263
|
request.status = 'ERROR';
|
|
382
|
-
request.
|
|
264
|
+
request.resultRef = null;
|
|
265
|
+
request.semanticRef = null;
|
|
383
266
|
request.error = errorText(error);
|
|
384
267
|
request.errorCode = errorCode(error) ?? null;
|
|
385
268
|
request.completedAt = now();
|
|
386
269
|
request.blockedReason = null;
|
|
387
|
-
|
|
270
|
+
db.prepare('UPDATE requests SET status=?,data=? WHERE id=?').run('ERROR', JSON.stringify(request), request.id);
|
|
271
|
+
readiness.transition(request);
|
|
388
272
|
appendEvent(runId, request.id, 'request.error', request.error, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
|
|
389
273
|
}
|
|
390
|
-
if (run.project?.version === 3) {
|
|
391
|
-
brokerTestHooks.onPlan?.();
|
|
392
|
-
const plan = planProject(run.project.snapshot, currentEvidence(), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, coalescedRequestIds: run.project.coalescedRequestIds, attempts: attemptsFor(run) });
|
|
393
|
-
run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
|
|
394
|
-
}
|
|
395
|
-
run.status = 'ERROR';
|
|
396
|
-
run.error = errorText(error);
|
|
397
|
-
run.completedAt = now();
|
|
398
|
-
saveRun(run);
|
|
399
274
|
appendEvent(runId, null, 'run.error', run.error);
|
|
400
275
|
}
|
|
401
276
|
function reconcileWithin(runId) {
|
|
@@ -418,12 +293,14 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
418
293
|
}
|
|
419
294
|
function getRun(id) {
|
|
420
295
|
ensureOpen();
|
|
421
|
-
if (!
|
|
296
|
+
if (!polling.runStatus(id))
|
|
422
297
|
return null;
|
|
423
298
|
if (ownerData(id))
|
|
424
299
|
reconcile(id);
|
|
425
300
|
const run = required(runData(id), 'Run');
|
|
426
301
|
const owner = ownerData(id);
|
|
302
|
+
if (owner || terminal.has(run.status))
|
|
303
|
+
submissionDeadlines.delete(id);
|
|
427
304
|
const events = db.prepare('SELECT * FROM (SELECT * FROM events WHERE run_id = ? ORDER BY id DESC LIMIT 500) ORDER BY id').all(id).map(event => ({ id: Number(event.id), runId: String(event.run_id), requestId: event.request_id === null ? null : String(event.request_id), createdAt: String(event.created_at), type: String(event.type), message: String(event.message), ...(event.data ? { data: parseStored(event.data) } : {}) }));
|
|
428
305
|
const view = copy(run);
|
|
429
306
|
return { ...view, scope: view.scope ?? { kind: view.graph ? 'graph' : 'chain' }, owner: owner ? { pid: owner.pid, claimedAt: owner.claimed_at } : null, requests: runRequests(id), events };
|
|
@@ -434,120 +311,140 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
434
311
|
changed();
|
|
435
312
|
}
|
|
436
313
|
async function executeOne(requestId, workspace, token, signal) {
|
|
437
|
-
const initial = required(
|
|
314
|
+
const initial = required(requestHeader(requestId), 'Request');
|
|
438
315
|
const runId = initial.runId;
|
|
439
|
-
|
|
440
|
-
const request = required(requestData(requestId), 'Request');
|
|
441
|
-
if (ownerData(runId)?.token !== token || request.status !== 'QUEUED')
|
|
442
|
-
throw codedError('Review ownership changed before execution.', 'RUN_OWNERSHIP_LOST');
|
|
443
|
-
request.status = 'RUNNING';
|
|
444
|
-
request.startedAt = now();
|
|
445
|
-
request.blockedReason = null;
|
|
446
|
-
saveRequest(request);
|
|
447
|
-
appendEvent(runId, request.id, 'request.started', `${request.title}: reviewing the supplied workspace.`, { snapshotHash: workspace.descriptor.hash });
|
|
448
|
-
updateRunStatus(runId);
|
|
449
|
-
});
|
|
450
|
-
changed();
|
|
316
|
+
let lease;
|
|
451
317
|
try {
|
|
318
|
+
if (initial.profile.kind !== 'human')
|
|
319
|
+
lease = await admission.acquire({ requestId, runId, kind: initial.profile.kind, provider: initial.profile.provider, model: initial.profile.model }, { signal, waiting(reason) {
|
|
320
|
+
transaction(() => { const current = required(requestHeader(requestId), 'Request'); if (current.status !== 'QUEUED')
|
|
321
|
+
return; current.blockedReason = reason.slice(0, 2000); saveHeader(current); });
|
|
322
|
+
changed();
|
|
323
|
+
} });
|
|
452
324
|
signal.throwIfAborted();
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const current = required(requestData(requestId), 'Request');
|
|
467
|
-
if (current.status !== 'RUNNING')
|
|
468
|
-
throw codedError('Review was canceled before Human notification.', 'REVIEW_CANCELED');
|
|
469
|
-
current.status = 'WAITING_HUMAN';
|
|
470
|
-
saveRequest(current);
|
|
471
|
-
appendEvent(runId, requestId, 'human.waiting', 'Human review is ready; registered alarm delivery is pending.');
|
|
472
|
-
updateRunStatus(runId);
|
|
473
|
-
});
|
|
474
|
-
await requireExecutors().notifyHuman(copy(required(requestData(requestId), 'Request')), { signal });
|
|
325
|
+
transaction(() => {
|
|
326
|
+
const request = required(requestHeader(requestId), 'Request');
|
|
327
|
+
if (ownerData(runId)?.token !== token || request.status !== 'QUEUED')
|
|
328
|
+
throw codedError('Review ownership changed before execution.', 'RUN_OWNERSHIP_LOST');
|
|
329
|
+
request.status = 'RUNNING';
|
|
330
|
+
request.startedAt = now();
|
|
331
|
+
request.blockedReason = null;
|
|
332
|
+
saveHeader(request);
|
|
333
|
+
appendEvent(runId, request.id, 'request.started', `${request.title}: reviewing the supplied workspace.`, { snapshotHash: workspace.descriptor.hash });
|
|
334
|
+
updateRunStatus(runId);
|
|
335
|
+
});
|
|
336
|
+
changed();
|
|
337
|
+
try {
|
|
475
338
|
signal.throwIfAborted();
|
|
476
339
|
await workspace.assertUnchanged();
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (typeof event[key] === 'string')
|
|
504
|
-
safe[key] = event[key].slice(0, 1000);
|
|
505
|
-
if (event.type === 'artifact.tool.completed') {
|
|
506
|
-
Object.assign(safe, toolResponseBytes(event));
|
|
507
|
-
if (typeof event.startedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.startedAt) && Number.isFinite(Date.parse(event.startedAt)))
|
|
508
|
-
safe.startedAt = event.startedAt;
|
|
509
|
-
if (typeof event.durationMs === 'number' && Number.isFinite(event.durationMs) && event.durationMs >= 0)
|
|
510
|
-
safe.durationMs = event.durationMs;
|
|
511
|
-
if (event.outcome === 'success' || event.outcome === 'error')
|
|
512
|
-
safe.outcome = event.outcome;
|
|
513
|
-
if (typeof event.operation === 'string')
|
|
514
|
-
safe.operation = event.operation.slice(0, 1000);
|
|
515
|
-
}
|
|
516
|
-
if (event.type === 'executor.usage') {
|
|
517
|
-
const usage = tokenUsage(event.usage);
|
|
518
|
-
if (!usage)
|
|
340
|
+
const request = required(requestData(requestId), 'Request');
|
|
341
|
+
const runDir = path.join(stateDir, 'runs', runId, request.id);
|
|
342
|
+
fs.mkdirSync(runDir, { recursive: true, mode: 0o700 });
|
|
343
|
+
if (required(readiness.header(runId), 'Run').project) {
|
|
344
|
+
const capability = await requireExecutors().canExecute(copy(request));
|
|
345
|
+
if (!capability?.ok)
|
|
346
|
+
throw codedError(capability?.reason ?? 'No compatible executor.', capability?.code ?? 'EXECUTOR_UNAVAILABLE');
|
|
347
|
+
}
|
|
348
|
+
if (request.profile.kind === 'human') {
|
|
349
|
+
if (typeof requireExecutors().notifyHuman !== 'function')
|
|
350
|
+
throw new Error('Human execution requires a registered alarm method.');
|
|
351
|
+
transaction(() => {
|
|
352
|
+
const current = required(requestData(requestId), 'Request');
|
|
353
|
+
if (current.status !== 'RUNNING')
|
|
354
|
+
throw codedError('Review was canceled before Human notification.', 'REVIEW_CANCELED');
|
|
355
|
+
current.status = 'WAITING_HUMAN';
|
|
356
|
+
saveRequest(current);
|
|
357
|
+
appendEvent(runId, requestId, 'human.waiting', 'Human review is ready; registered alarm delivery is pending.');
|
|
358
|
+
updateRunStatus(runId);
|
|
359
|
+
});
|
|
360
|
+
await requireExecutors().notifyHuman(copy(required(requestData(requestId), 'Request')), { signal });
|
|
361
|
+
signal.throwIfAborted();
|
|
362
|
+
await workspace.assertUnchanged();
|
|
363
|
+
transaction(() => {
|
|
364
|
+
const current = required(requestData(requestId), 'Request');
|
|
365
|
+
if (current.status !== 'WAITING_HUMAN')
|
|
519
366
|
return;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (object(event.observation)) {
|
|
525
|
-
// Persist the same bounded metadata as final results, even if the Provider later fails.
|
|
526
|
-
const observed = storedObservation(event.observation, event.isError === true);
|
|
527
|
-
if (observed)
|
|
528
|
-
safe.observation = observed;
|
|
529
|
-
}
|
|
530
|
-
if (Array.isArray(event.tools))
|
|
531
|
-
safe.tools = event.tools.slice(0, 32).map(tool => typeof tool === 'string' ? tool : tool?.name).filter(value => typeof value === 'string');
|
|
532
|
-
appendEvent(runId, requestId, event.type, event.type === 'executor.telemetry.failed' ? 'Some optional execution diagnostics could not be recorded.' : String(event.message ?? event.type).slice(0, 2000), safe);
|
|
367
|
+
current.notifiedAt = now();
|
|
368
|
+
saveRequest(current);
|
|
369
|
+
appendEvent(runId, requestId, 'human.notified', 'Registered human alarm methods confirmed delivery.');
|
|
370
|
+
});
|
|
533
371
|
changed();
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const result = validateResult(await requireExecutors().execute(copy(request), {
|
|
375
|
+
worktreePath: workspace.descriptor.path, workspacePath: workspace.descriptor.path, runDir, signal,
|
|
376
|
+
onEvent(event) {
|
|
377
|
+
if (closed || closing || signal.aborted || !event || polling.requestStatus(requestId) !== 'RUNNING' || !['executor.started', 'artifact.tools.ready', 'artifact.tool.called', 'artifact.tool.completed', 'executor.usage', 'executor.final.invalid', 'executor.final.repair', 'executor.telemetry.failed', 'executor.completed'].includes(event.type))
|
|
378
|
+
return;
|
|
379
|
+
if (event.type === 'executor.final.invalid' || event.type === 'executor.final.repair') {
|
|
380
|
+
const safe = finalResultEventData(event);
|
|
381
|
+
if (safe) {
|
|
382
|
+
appendEvent(runId, requestId, event.type, event.type, safe);
|
|
383
|
+
changed();
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const safe = {};
|
|
388
|
+
for (const key of ['name', 'provider', 'model', 'kind', 'artifactId', 'path'])
|
|
389
|
+
if (typeof event[key] === 'string')
|
|
390
|
+
safe[key] = event[key].slice(0, 1000);
|
|
391
|
+
if (event.type === 'artifact.tool.completed') {
|
|
392
|
+
Object.assign(safe, toolResponseBytes(event));
|
|
393
|
+
if (typeof event.startedAt === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.startedAt) && Number.isFinite(Date.parse(event.startedAt)))
|
|
394
|
+
safe.startedAt = event.startedAt;
|
|
395
|
+
if (typeof event.durationMs === 'number' && Number.isFinite(event.durationMs) && event.durationMs >= 0)
|
|
396
|
+
safe.durationMs = event.durationMs;
|
|
397
|
+
if (event.outcome === 'success' || event.outcome === 'error')
|
|
398
|
+
safe.outcome = event.outcome;
|
|
399
|
+
if (typeof event.operation === 'string')
|
|
400
|
+
safe.operation = event.operation.slice(0, 1000);
|
|
401
|
+
}
|
|
402
|
+
if (event.type === 'executor.usage') {
|
|
403
|
+
const usage = tokenUsage(event.usage);
|
|
404
|
+
if (!usage)
|
|
405
|
+
return;
|
|
406
|
+
safe.usage = usage;
|
|
407
|
+
}
|
|
408
|
+
if (event.isError === true)
|
|
409
|
+
safe.isError = true;
|
|
410
|
+
if (object(event.observation)) {
|
|
411
|
+
// Persist the same bounded metadata as final results, even if the Provider later fails.
|
|
412
|
+
const observed = storedObservation(event.observation, event.isError === true);
|
|
413
|
+
if (observed)
|
|
414
|
+
safe.observation = observed;
|
|
415
|
+
}
|
|
416
|
+
if (Array.isArray(event.tools))
|
|
417
|
+
safe.tools = event.tools.slice(0, 32).map(tool => typeof tool === 'string' ? tool : tool?.name).filter(value => typeof value === 'string');
|
|
418
|
+
appendEvent(runId, requestId, event.type, event.type === 'executor.telemetry.failed' ? 'Some optional execution diagnostics could not be recorded.' : String(event.message ?? event.type).slice(0, 2000), safe);
|
|
419
|
+
changed();
|
|
420
|
+
},
|
|
421
|
+
}));
|
|
422
|
+
validateFinalResult(semanticResult(result), request);
|
|
423
|
+
await workspace.assertUnchanged();
|
|
424
|
+
signal.throwIfAborted();
|
|
425
|
+
transaction(() => { if (ownerData(runId)?.token === token)
|
|
426
|
+
finishWithin(requestId, { result }); });
|
|
427
|
+
changed();
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
const reason = signal.aborted ? signal.reason : error;
|
|
431
|
+
if (signal.aborted || fatalExecutionError(reason))
|
|
432
|
+
throw reason;
|
|
433
|
+
transaction(() => { if (ownerData(runId)?.token === token)
|
|
434
|
+
finishWithin(requestId, { error: reason }); });
|
|
435
|
+
changed();
|
|
436
|
+
}
|
|
542
437
|
}
|
|
543
438
|
catch (error) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
throw reason;
|
|
439
|
+
if (signal.aborted || fatalExecutionError(error))
|
|
440
|
+
throw error;
|
|
547
441
|
transaction(() => { if (ownerData(runId)?.token === token)
|
|
548
|
-
finishWithin(requestId, { error
|
|
442
|
+
finishWithin(requestId, { error }, ['QUEUED']); });
|
|
549
443
|
changed();
|
|
550
444
|
}
|
|
445
|
+
finally {
|
|
446
|
+
await lease?.release();
|
|
447
|
+
}
|
|
551
448
|
}
|
|
552
449
|
async function run(runId, { signal, onStarted } = {}) {
|
|
553
450
|
ensureOpen();
|
|
@@ -556,7 +453,8 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
556
453
|
let stored = null;
|
|
557
454
|
const acquired = transaction(() => {
|
|
558
455
|
reconcileWithin(runId);
|
|
559
|
-
|
|
456
|
+
const header = readiness.header(runId);
|
|
457
|
+
stored = header ? { ...header, workspace: store.get(header.workspaceRef) } : null;
|
|
560
458
|
if (!stored)
|
|
561
459
|
throw new Error('Unknown Run.');
|
|
562
460
|
if (stored.project?.version !== 3)
|
|
@@ -567,6 +465,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
567
465
|
throw new Error('Invalid Run: no workspace descriptor.');
|
|
568
466
|
if (ownerData(runId))
|
|
569
467
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
468
|
+
submissionDeadlines.delete(runId);
|
|
570
469
|
db.prepare('INSERT INTO run_owners(run_id,pid,process_identity,token,claimed_at) VALUES (?,?,?,?,?)').run(runId, process.pid, ownProcessIdentity, token, now());
|
|
571
470
|
appendEvent(runId, null, 'worker.started', 'A request-scoped worker owns this Run.', { pid: process.pid });
|
|
572
471
|
return true;
|
|
@@ -585,7 +484,6 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
585
484
|
let plannedRevision = -1, leaseDeadline = Infinity;
|
|
586
485
|
let pendingSources = [];
|
|
587
486
|
const notified = new Set();
|
|
588
|
-
const kinds = new Map();
|
|
589
487
|
try {
|
|
590
488
|
onStarted?.({ runId, pid: process.pid });
|
|
591
489
|
workspace = await workspaceAdapter.reopenWorkspace(copy(ownedRun.workspace), { signal: executionSignal });
|
|
@@ -609,16 +507,15 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
609
507
|
transaction(() => {
|
|
610
508
|
refreshReadinessWithin(runId);
|
|
611
509
|
updateRunStatus(runId);
|
|
612
|
-
const pending = sharedRequests(
|
|
510
|
+
const pending = sharedRequests({ id: runId }).filter(request => !terminal.has(request.status));
|
|
613
511
|
pendingSources = [...new Set(pending.map(request => request.runId))];
|
|
614
512
|
leaseDeadline = Math.min(Infinity, ...pendingSources.filter(id => !ownerData(id)).map(id => submissionDeadlines.get(id) ?? performance.now()));
|
|
615
513
|
plannedRevision = polling.revision();
|
|
616
514
|
});
|
|
617
|
-
const requests =
|
|
515
|
+
const requests = db.prepare("SELECT id,status,json_extract(data,'$.profile.kind') AS kind FROM requests WHERE run_id=? AND status='QUEUED' ORDER BY ordinal LIMIT ?").all(runId, maxConcurrentExecutors + inFlight.size + 1);
|
|
618
516
|
let executing = [...inFlight.values()].filter(item => item.kind !== 'human').length;
|
|
619
|
-
for (const row of requests.filter(request => request.status === 'QUEUED')) {
|
|
620
|
-
const kind =
|
|
621
|
-
kinds.set(row.id, kind);
|
|
517
|
+
for (const row of requests.filter(request => request.status === 'QUEUED' && !inFlight.has(request.id))) {
|
|
518
|
+
const kind = row.kind;
|
|
622
519
|
if (kind !== 'human' && executing >= maxConcurrentExecutors)
|
|
623
520
|
continue;
|
|
624
521
|
if (kind !== 'human')
|
|
@@ -640,7 +537,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
640
537
|
await delay(Math.max(1, Math.min(100, leaseDeadline - performance.now())), undefined, { signal: reviewSignal });
|
|
641
538
|
continue;
|
|
642
539
|
}
|
|
643
|
-
const waiting =
|
|
540
|
+
const waiting = db.prepare("SELECT id FROM requests WHERE run_id=? AND status='WAITING_HUMAN' ORDER BY ordinal LIMIT 1").all(runId);
|
|
644
541
|
if (waiting.length) {
|
|
645
542
|
for (const request of waiting) {
|
|
646
543
|
if (notified.has(request.id))
|
|
@@ -683,14 +580,18 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
683
580
|
}
|
|
684
581
|
}
|
|
685
582
|
// close() can be awaiting this promise; read directly before it closes the DB.
|
|
686
|
-
return { ...copy(required(runData(runId), 'Run')), requests: runRequests(runId) };
|
|
583
|
+
return detail === 'full' ? { ...copy(required(runData(runId), 'Run')), requests: runRequests(runId) } : { ...ownedRun, requests: [] };
|
|
687
584
|
})();
|
|
688
585
|
return entry.promise;
|
|
689
586
|
}
|
|
690
587
|
const broker = {
|
|
691
|
-
async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, signal, identityConcurrency: submissionIdentityConcurrency = identityConcurrency, ...removed }) {
|
|
588
|
+
async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, ignoreGates = false, signal, identityConcurrency: submissionIdentityConcurrency = identityConcurrency, ...removed }) {
|
|
589
|
+
// Options are destructured before entry; selection is the sole mutable data
|
|
590
|
+
// input. Keep signal live for cancellation rather than cloning its state.
|
|
591
|
+
selection = structuredClone(selection);
|
|
692
592
|
ensureOpen();
|
|
693
593
|
requireExecutors();
|
|
594
|
+
sweepSubmissionDeadlines();
|
|
694
595
|
positiveConcurrency(submissionIdentityConcurrency, 'identityConcurrency');
|
|
695
596
|
if ('mode' in removed)
|
|
696
597
|
throw new Error('Workspace modes are no longer supported; supply an unchanged workspace.');
|
|
@@ -699,25 +600,39 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
699
600
|
await requireExecutors().validateWorkspace?.(repoPath);
|
|
700
601
|
const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, integrity: workspaceIntegrity, signal });
|
|
701
602
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
603
|
+
// A custom adapter may retain a mutable descriptor. Snapshot it once,
|
|
604
|
+
// before any later await, and never store its externally owned object.
|
|
605
|
+
const descriptor = structuredClone(workspace.descriptor);
|
|
606
|
+
const { config } = await readWorkspaceConfig(descriptor.path, workspace.signal);
|
|
607
|
+
const snapshot = await createProjectSnapshot(config, descriptor.path, descriptor.hash, workspace.signal, descriptor.integrity, selection, { identityConcurrency: submissionIdentityConcurrency });
|
|
704
608
|
const ids = includedCritics(snapshot, selection, recursive);
|
|
705
609
|
const templates = [];
|
|
706
|
-
|
|
707
|
-
|
|
610
|
+
const critics = new Map(config.critics.map(critic => [critic.id, critic]));
|
|
611
|
+
for (const id of ids) {
|
|
612
|
+
templates.push(...await prepareReviewRequests({ repoPath: descriptor.path, repoId, snapshotHash: descriptor.hash, criticId: id, preparedConfig: { ...config, critics: [critics.get(id)] }, copy: false }));
|
|
613
|
+
if (templates.length % 16 === 0) {
|
|
614
|
+
workspace.signal.throwIfAborted();
|
|
615
|
+
await yieldTurn();
|
|
616
|
+
}
|
|
617
|
+
}
|
|
708
618
|
const id = randomUUID(), createdAt = now();
|
|
709
|
-
const record = { id, repoId, coalescingGraceMs, snapshotHash:
|
|
710
|
-
scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 3, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
|
|
619
|
+
const record = { id, repoId, coalescingGraceMs, snapshotHash: descriptor.hash, workspace: descriptor, requesterId,
|
|
620
|
+
scope: { kind: 'project' }, graph: createGraphDefinition(config, false), project: { version: 3, snapshot, selection, recursive, force, ignoreGates, templates }, status: 'QUEUED', createdAt };
|
|
621
|
+
const prepared = await store.prepareRun(record, workspace.signal);
|
|
711
622
|
await workspace.assertUnchanged();
|
|
712
623
|
workspace.signal.throwIfAborted();
|
|
624
|
+
ensureOpen();
|
|
625
|
+
const commitStarted = performance.now();
|
|
713
626
|
transaction(() => {
|
|
714
|
-
|
|
627
|
+
prepared.verify();
|
|
628
|
+
db.prepare('INSERT INTO runs(id,created_at,status,data) VALUES (?,?,?,?)').run(id, createdAt, record.status, JSON.stringify(prepared.header));
|
|
715
629
|
appendEvent(id, null, 'run.submitted', 'Project validation requested against fixed input.', { selection, recursive, force });
|
|
716
|
-
|
|
717
|
-
|
|
630
|
+
brokerTestHooks.onPlan?.();
|
|
631
|
+
readiness.initialize(record, prepared);
|
|
718
632
|
});
|
|
633
|
+
brokerTestHooks.onSubmissionCommit?.(performance.now() - commitStarted);
|
|
719
634
|
changed();
|
|
720
|
-
return required(getRun(id), 'Run');
|
|
635
|
+
return detail === 'full' ? required(getRun(id), 'Run') : { ...record, scope: record.scope, requests: [], events: [], owner: null };
|
|
721
636
|
}
|
|
722
637
|
finally {
|
|
723
638
|
await workspace.close();
|
|
@@ -926,7 +841,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
926
841
|
ensureOpen();
|
|
927
842
|
transaction(() => {
|
|
928
843
|
reconcileWithin(runId);
|
|
929
|
-
if (!
|
|
844
|
+
if (!polling.runStatus(runId))
|
|
930
845
|
throw new Error('Unknown Run.');
|
|
931
846
|
if (ownerData(runId))
|
|
932
847
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
@@ -939,7 +854,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
939
854
|
ensureOpen();
|
|
940
855
|
const error = codedError('Review canceled by requester.', 'REVIEW_CANCELED');
|
|
941
856
|
transaction(() => {
|
|
942
|
-
if (!
|
|
857
|
+
if (!polling.runStatus(runId))
|
|
943
858
|
throw new Error('Unknown Run.');
|
|
944
859
|
failWithin(runId, error);
|
|
945
860
|
});
|
|
@@ -949,6 +864,46 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
949
864
|
changed();
|
|
950
865
|
return getRun(runId);
|
|
951
866
|
},
|
|
867
|
+
retryRequest(requestId) {
|
|
868
|
+
ensureOpen();
|
|
869
|
+
transaction(() => {
|
|
870
|
+
const request = required(requestHeader(requestId), 'Request');
|
|
871
|
+
if (request.status !== 'ERROR')
|
|
872
|
+
throw new Error('Only an operationally failed request can be retried. Submit changed input as a new Run.');
|
|
873
|
+
const run = required(readiness.header(request.runId), 'Run');
|
|
874
|
+
if (ownerData(run.id))
|
|
875
|
+
throw codedError('Wait for the current worker to settle before retrying.', 'RUN_ALREADY_OWNED');
|
|
876
|
+
run.status = 'QUEUED';
|
|
877
|
+
delete run.completedAt;
|
|
878
|
+
delete run.error;
|
|
879
|
+
db.prepare('UPDATE runs SET status=?,data=? WHERE id=?').run(run.status, JSON.stringify(run), run.id);
|
|
880
|
+
const gate = db.prepare('SELECT unmet,red FROM gate_counts WHERE run_id=? AND critic_id=?').get(request.runId, request.criticId);
|
|
881
|
+
request.status = Number(gate?.unmet ?? 0) ? Number(gate?.red ?? 0) ? 'BLOCKED' : 'WAIT_DEPENDENCY' : 'QUEUED';
|
|
882
|
+
request.error = null;
|
|
883
|
+
request.errorCode = null;
|
|
884
|
+
request.resultRef = null;
|
|
885
|
+
request.semanticRef = null;
|
|
886
|
+
request.startedAt = null;
|
|
887
|
+
request.completedAt = null;
|
|
888
|
+
saveHeader(request);
|
|
889
|
+
appendEvent(run.id, request.id, 'request.retried', 'Retry requested against the same immutable input.');
|
|
890
|
+
});
|
|
891
|
+
changed();
|
|
892
|
+
return viewRequest(requestData(requestId));
|
|
893
|
+
},
|
|
894
|
+
changes(runId, options) {
|
|
895
|
+
ensureOpen();
|
|
896
|
+
db.exec('BEGIN');
|
|
897
|
+
try {
|
|
898
|
+
const page = readChanges(db, runId, stateDir, options);
|
|
899
|
+
db.exec('COMMIT');
|
|
900
|
+
return page;
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
db.exec('ROLLBACK');
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
906
|
+
},
|
|
952
907
|
onChange(callback) { listeners.add(callback); return () => listeners.delete(callback); },
|
|
953
908
|
async close() {
|
|
954
909
|
if (closed)
|
|
@@ -975,8 +930,9 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
975
930
|
...broker,
|
|
976
931
|
submitProject: async (...args) => viewRun(await broker.submitProject(...args)),
|
|
977
932
|
run: async (...args) => viewRun(await broker.run(...args)),
|
|
978
|
-
getRun: (id) =>
|
|
979
|
-
|
|
933
|
+
getRun: (id) => { ensureOpen(); if (ownerData(id))
|
|
934
|
+
reconcile(id); return resultView({ detail }, detail === 'full' ? broker.getRun(id) : null, () => storedRequesterRun(db, id, stateDir)); },
|
|
935
|
+
listRuns: () => { ensureOpen(); return db.prepare('SELECT id FROM runs ORDER BY created_at DESC,rowid DESC').all().map(row => { const id = String(row.id); return resultView({ detail }, detail === 'full' ? required(broker.getRun(id), 'Run') : null, () => required(storedRequesterRun(db, id, stateDir), 'Run')); }); },
|
|
980
936
|
getRequest: (id) => viewRequest(broker.getRequest(id)),
|
|
981
937
|
claimHuman: async (...args) => viewRequest(await broker.claimHuman(...args)),
|
|
982
938
|
completeHuman: async (...args) => viewRequest(await broker.completeHuman(...args)),
|