@ccdd/project 5.1.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +28 -3
- package/dist/src/broker/index.js +248 -249
- package/dist/src/broker/index.js.map +1 -1
- package/dist/src/broker/polling.d.ts +1 -0
- package/dist/src/broker/polling.js +2 -0
- package/dist/src/broker/polling.js.map +1 -1
- package/dist/src/broker/readiness.d.ts +17 -0
- package/dist/src/broker/readiness.js +230 -0
- package/dist/src/broker/readiness.js.map +1 -0
- package/dist/src/broker/storage.d.ts +65 -0
- package/dist/src/broker/storage.js +178 -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/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
|
@@ -10,6 +10,10 @@ import { setTimeout as delay } 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,53 +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
|
};
|
|
159
|
+
const store = records(db);
|
|
160
|
+
const clearReadCaches = () => store.clear();
|
|
153
161
|
const transaction = (callback) => {
|
|
154
162
|
db.exec('BEGIN IMMEDIATE');
|
|
155
163
|
try {
|
|
156
|
-
const
|
|
164
|
+
const value = callback();
|
|
157
165
|
db.exec('COMMIT');
|
|
158
|
-
return
|
|
166
|
+
return value;
|
|
159
167
|
}
|
|
160
168
|
catch (error) {
|
|
161
|
-
|
|
169
|
+
try {
|
|
170
|
+
db.exec('ROLLBACK');
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
store.clear();
|
|
174
|
+
}
|
|
162
175
|
throw error;
|
|
163
176
|
}
|
|
164
177
|
};
|
|
165
|
-
const requestData = (id) =>
|
|
166
|
-
|
|
167
|
-
return row ? parseStored(row.data) : null;
|
|
168
|
-
};
|
|
169
|
-
// Run definitions never change after submission. Keep one parsed definition,
|
|
170
|
-
// while re-reading mutable lifecycle/claim state on every operation (including
|
|
171
|
-
// writes from other Broker processes). Do not hydrate a whole project per Critic.
|
|
172
|
-
let runDefinition;
|
|
173
|
-
const runData = (id) => {
|
|
174
|
-
if (runDefinition?.id !== id) {
|
|
175
|
-
const row = db.prepare('SELECT data FROM runs WHERE id = ?').get(id);
|
|
176
|
-
if (!row)
|
|
177
|
-
return null;
|
|
178
|
-
const run = parseStored(row.data);
|
|
179
|
-
if (!run.project)
|
|
180
|
-
return run;
|
|
181
|
-
runDefinition = { id, graph: run.graph, snapshot: run.project.snapshot, templates: run.project.templates };
|
|
182
|
-
return run;
|
|
183
|
-
}
|
|
184
|
-
const row = db.prepare("SELECT json_remove(data, '$.graph', '$.project.snapshot', '$.project.templates') AS data FROM runs WHERE id = ?").get(id);
|
|
185
|
-
if (!row)
|
|
186
|
-
return null;
|
|
187
|
-
const run = parseStored(row.data);
|
|
188
|
-
return { ...run, graph: runDefinition.graph, ...(run.project ? { project: { ...run.project, snapshot: runDefinition.snapshot, templates: runDefinition.templates } } : {}) };
|
|
189
|
-
};
|
|
178
|
+
const requestData = (id) => store.request(id);
|
|
179
|
+
const runData = (id) => store.run(id);
|
|
190
180
|
const ownerData = (id) => db.prepare('SELECT * FROM run_owners WHERE run_id = ?').get(id);
|
|
191
|
-
const runRequests = (id) => db.prepare('SELECT
|
|
192
|
-
const sharedRequests = (run) => (
|
|
193
|
-
const attemptsFor = (run) => [...runRequests(run.id), ...sharedRequests(run)];
|
|
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));
|
|
194
183
|
const submissionDeadlines = new Map();
|
|
195
184
|
function sweepSubmissionDeadlines() {
|
|
196
185
|
for (const id of submissionDeadlines.keys()) {
|
|
@@ -203,12 +192,22 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
203
192
|
}
|
|
204
193
|
}
|
|
205
194
|
const coalescing = { reconcile: reconcileWithin, deadlines: submissionDeadlines };
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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);
|
|
199
|
+
};
|
|
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);
|
|
212
211
|
};
|
|
213
212
|
const appendEvent = (runId, requestId, type, message, data = null) => {
|
|
214
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));
|
|
@@ -231,112 +230,47 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
231
230
|
throw new Error('An in-place review requires its monitoring worker to remain alive.');
|
|
232
231
|
},
|
|
233
232
|
});
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (run.project?.version === 3) {
|
|
238
|
-
if (terminal.has(run.status))
|
|
239
|
-
return;
|
|
240
|
-
for (const shared of sharedRequests(run))
|
|
241
|
-
reconcileWithin(shared.runId);
|
|
242
|
-
// Called in a write transaction: expire only unowned queued sources, then
|
|
243
|
-
// recheck all matching owned/leased requests before creating our own ticket.
|
|
244
|
-
const retained = sharedRequests(run).filter(request => terminal.has(request.status) || shareable(request));
|
|
245
|
-
if (retained.length !== (run.project.coalescedRequestIds ?? []).length) {
|
|
246
|
-
run.project.coalescedRequestIds = retained.map(request => request.id);
|
|
247
|
-
saveRun(run);
|
|
248
|
-
appendEvent(runId, null, 'request.uncoalesced', 'Unowned source submission lease expired; replanning this Run.');
|
|
249
|
-
}
|
|
250
|
-
brokerTestHooks.onPlan?.();
|
|
251
|
-
const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, coalescedRequestIds: run.project.coalescedRequestIds, attempts: [...requests, ...sharedRequests(run)] });
|
|
252
|
-
for (const item of plan.items.filter(item => item.action === 'EXECUTE')) {
|
|
253
|
-
if (!run.project.force) {
|
|
254
|
-
const shared = findCoalescibleRequest(db, item.id, item.input.key, coalescing)?.request;
|
|
255
|
-
if (shared) {
|
|
256
|
-
run.project.coalescedRequestIds = [...new Set([...(run.project.coalescedRequestIds ?? []), shared.id])];
|
|
257
|
-
saveRun(run);
|
|
258
|
-
appendEvent(runId, shared.id, 'request.coalesced', 'Waiting for an identical active review in another Run.', { sourceRunId: shared.runId });
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const envelope = run.project.templates.find(template => template.criticId === item.id);
|
|
263
|
-
if (!envelope)
|
|
264
|
-
throw codedError(`Missing prepared Critic ${item.id}.`, 'REVIEW_GRAPH_INVALID');
|
|
265
|
-
const request = { ...copy(envelope), validationInput: copy(item.input), id: randomUUID(), runId,
|
|
266
|
-
workspace: run.workspace, worktreePath: run.workspace.path, status: 'QUEUED', createdAt: now(),
|
|
267
|
-
startedAt: null, completedAt: null, claimedBy: null, claimedAt: null, notifiedAt: null, result: null, error: null, blockedReason: null };
|
|
268
|
-
db.prepare('INSERT INTO requests(id,run_id,ordinal,status,data) VALUES (?,?,?,?,?)').run(request.id, runId, requests.length, request.status, JSON.stringify(request));
|
|
269
|
-
requests.push(request);
|
|
270
|
-
appendEvent(runId, request.id, 'request.queued', 'Pull validation found an executable Critic requiring an actual review.');
|
|
271
|
-
}
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
throw new Error('Invalid stored Run format; submit a new validation request.');
|
|
275
|
-
}
|
|
276
|
-
const updateRunStatus = (runId) => {
|
|
277
|
-
const run = required(runData(runId), 'Run');
|
|
278
|
-
const states = attemptsFor(run).map(request => request.status);
|
|
279
|
-
if (run.project?.version === 3) {
|
|
280
|
-
if (terminal.has(run.status))
|
|
281
|
-
return;
|
|
282
|
-
brokerTestHooks.onPlan?.();
|
|
283
|
-
const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, coalescedRequestIds: run.project.coalescedRequestIds, attempts: attemptsFor(run) });
|
|
284
|
-
const status = states.includes('RUNNING') ? 'RUNNING' : states.includes('QUEUED') ? 'QUEUED' : states.includes('WAITING_HUMAN') ? 'WAITING_HUMAN' :
|
|
285
|
-
states.includes('ERROR') ? 'ERROR' : states.includes('RED') || plan.critics.some(critic => critic.status === 'RED') ? 'RED' : plan.satisfied ? 'GREEN' : 'INCOMPLETE';
|
|
286
|
-
if (run.status !== status) {
|
|
287
|
-
run.status = status;
|
|
288
|
-
if (terminal.has(status)) {
|
|
289
|
-
run.completedAt = now();
|
|
290
|
-
run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
|
|
291
|
-
}
|
|
292
|
-
saveRun(run);
|
|
293
|
-
appendEvent(runId, null, 'run.status', `Run ${status}`, { status });
|
|
294
|
-
}
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
throw new Error('Invalid stored Run format.');
|
|
298
|
-
};
|
|
233
|
+
const readiness = createReadiness(db, store, coalescing, appendEvent);
|
|
234
|
+
const refreshReadinessWithin = (id) => readiness.refresh(id);
|
|
235
|
+
const updateRunStatus = (id) => readiness.updateRun(id);
|
|
299
236
|
function finishWithin(requestId, outcome, expectedStates = ['RUNNING', 'WAITING_HUMAN']) {
|
|
300
237
|
const { result, error } = outcome, hasError = Object.hasOwn(outcome, 'error');
|
|
301
|
-
const request =
|
|
302
|
-
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')))
|
|
303
240
|
return false;
|
|
304
241
|
request.status = hasError ? 'ERROR' : required(result, 'review result').verdict;
|
|
305
|
-
request.
|
|
242
|
+
request.resultRef = result ? store.put(result) : null;
|
|
243
|
+
request.semanticRef = result ? store.put(semanticResult(result)) : null;
|
|
306
244
|
request.error = hasError ? errorText(error) : null;
|
|
307
245
|
request.errorCode = errorCode(error) ?? null;
|
|
308
246
|
request.completedAt = now();
|
|
309
|
-
|
|
247
|
+
saveHeader(request);
|
|
310
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 } : {}) });
|
|
311
|
-
refreshReadinessWithin(request.runId);
|
|
312
|
-
updateRunStatus(request.runId);
|
|
313
249
|
return true;
|
|
314
250
|
}
|
|
315
251
|
function failWithin(runId, error) {
|
|
316
|
-
const run =
|
|
252
|
+
const run = readiness.header(runId);
|
|
317
253
|
if (!run || terminal.has(run.status))
|
|
318
254
|
return;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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);
|
|
322
263
|
request.status = 'ERROR';
|
|
323
|
-
request.
|
|
264
|
+
request.resultRef = null;
|
|
265
|
+
request.semanticRef = null;
|
|
324
266
|
request.error = errorText(error);
|
|
325
267
|
request.errorCode = errorCode(error) ?? null;
|
|
326
268
|
request.completedAt = now();
|
|
327
269
|
request.blockedReason = null;
|
|
328
|
-
|
|
270
|
+
db.prepare('UPDATE requests SET status=?,data=? WHERE id=?').run('ERROR', JSON.stringify(request), request.id);
|
|
271
|
+
readiness.transition(request);
|
|
329
272
|
appendEvent(runId, request.id, 'request.error', request.error, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
|
|
330
273
|
}
|
|
331
|
-
if (run.project?.version === 3) {
|
|
332
|
-
brokerTestHooks.onPlan?.();
|
|
333
|
-
const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, coalescedRequestIds: run.project.coalescedRequestIds, attempts: attemptsFor(run) });
|
|
334
|
-
run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
|
|
335
|
-
}
|
|
336
|
-
run.status = 'ERROR';
|
|
337
|
-
run.error = errorText(error);
|
|
338
|
-
run.completedAt = now();
|
|
339
|
-
saveRun(run);
|
|
340
274
|
appendEvent(runId, null, 'run.error', run.error);
|
|
341
275
|
}
|
|
342
276
|
function reconcileWithin(runId) {
|
|
@@ -359,14 +293,17 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
359
293
|
}
|
|
360
294
|
function getRun(id) {
|
|
361
295
|
ensureOpen();
|
|
362
|
-
if (!
|
|
296
|
+
if (!polling.runStatus(id))
|
|
363
297
|
return null;
|
|
364
298
|
if (ownerData(id))
|
|
365
299
|
reconcile(id);
|
|
366
300
|
const run = required(runData(id), 'Run');
|
|
367
301
|
const owner = ownerData(id);
|
|
302
|
+
if (owner || terminal.has(run.status))
|
|
303
|
+
submissionDeadlines.delete(id);
|
|
368
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) } : {}) }));
|
|
369
|
-
|
|
305
|
+
const view = copy(run);
|
|
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 };
|
|
370
307
|
}
|
|
371
308
|
function failOwned(runId, token, error) {
|
|
372
309
|
transaction(() => { if (ownerData(runId)?.token === token)
|
|
@@ -374,120 +311,140 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
374
311
|
changed();
|
|
375
312
|
}
|
|
376
313
|
async function executeOne(requestId, workspace, token, signal) {
|
|
377
|
-
const initial = required(
|
|
314
|
+
const initial = required(requestHeader(requestId), 'Request');
|
|
378
315
|
const runId = initial.runId;
|
|
379
|
-
|
|
380
|
-
const request = required(requestData(requestId), 'Request');
|
|
381
|
-
if (ownerData(runId)?.token !== token || request.status !== 'QUEUED')
|
|
382
|
-
throw codedError('Review ownership changed before execution.', 'RUN_OWNERSHIP_LOST');
|
|
383
|
-
request.status = 'RUNNING';
|
|
384
|
-
request.startedAt = now();
|
|
385
|
-
request.blockedReason = null;
|
|
386
|
-
saveRequest(request);
|
|
387
|
-
appendEvent(runId, request.id, 'request.started', `${request.title}: reviewing the supplied workspace.`, { snapshotHash: workspace.descriptor.hash });
|
|
388
|
-
updateRunStatus(runId);
|
|
389
|
-
});
|
|
390
|
-
changed();
|
|
316
|
+
let lease;
|
|
391
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
|
+
} });
|
|
392
324
|
signal.throwIfAborted();
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const current = required(requestData(requestId), 'Request');
|
|
407
|
-
if (current.status !== 'RUNNING')
|
|
408
|
-
throw codedError('Review was canceled before Human notification.', 'REVIEW_CANCELED');
|
|
409
|
-
current.status = 'WAITING_HUMAN';
|
|
410
|
-
saveRequest(current);
|
|
411
|
-
appendEvent(runId, requestId, 'human.waiting', 'Human review is ready; registered alarm delivery is pending.');
|
|
412
|
-
updateRunStatus(runId);
|
|
413
|
-
});
|
|
414
|
-
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 {
|
|
415
338
|
signal.throwIfAborted();
|
|
416
339
|
await workspace.assertUnchanged();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (typeof event[key] === 'string')
|
|
444
|
-
safe[key] = event[key].slice(0, 1000);
|
|
445
|
-
if (event.type === 'artifact.tool.completed') {
|
|
446
|
-
Object.assign(safe, toolResponseBytes(event));
|
|
447
|
-
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)))
|
|
448
|
-
safe.startedAt = event.startedAt;
|
|
449
|
-
if (typeof event.durationMs === 'number' && Number.isFinite(event.durationMs) && event.durationMs >= 0)
|
|
450
|
-
safe.durationMs = event.durationMs;
|
|
451
|
-
if (event.outcome === 'success' || event.outcome === 'error')
|
|
452
|
-
safe.outcome = event.outcome;
|
|
453
|
-
if (typeof event.operation === 'string')
|
|
454
|
-
safe.operation = event.operation.slice(0, 1000);
|
|
455
|
-
}
|
|
456
|
-
if (event.type === 'executor.usage') {
|
|
457
|
-
const usage = tokenUsage(event.usage);
|
|
458
|
-
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')
|
|
459
366
|
return;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (object(event.observation)) {
|
|
465
|
-
// Persist the same bounded metadata as final results, even if the Provider later fails.
|
|
466
|
-
const observed = storedObservation(event.observation, event.isError === true);
|
|
467
|
-
if (observed)
|
|
468
|
-
safe.observation = observed;
|
|
469
|
-
}
|
|
470
|
-
if (Array.isArray(event.tools))
|
|
471
|
-
safe.tools = event.tools.slice(0, 32).map(tool => typeof tool === 'string' ? tool : tool?.name).filter(value => typeof value === 'string');
|
|
472
|
-
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
|
+
});
|
|
473
371
|
changed();
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
+
}
|
|
482
437
|
}
|
|
483
438
|
catch (error) {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
throw reason;
|
|
439
|
+
if (signal.aborted || fatalExecutionError(error))
|
|
440
|
+
throw error;
|
|
487
441
|
transaction(() => { if (ownerData(runId)?.token === token)
|
|
488
|
-
finishWithin(requestId, { error
|
|
442
|
+
finishWithin(requestId, { error }, ['QUEUED']); });
|
|
489
443
|
changed();
|
|
490
444
|
}
|
|
445
|
+
finally {
|
|
446
|
+
await lease?.release();
|
|
447
|
+
}
|
|
491
448
|
}
|
|
492
449
|
async function run(runId, { signal, onStarted } = {}) {
|
|
493
450
|
ensureOpen();
|
|
@@ -496,7 +453,8 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
496
453
|
let stored = null;
|
|
497
454
|
const acquired = transaction(() => {
|
|
498
455
|
reconcileWithin(runId);
|
|
499
|
-
|
|
456
|
+
const header = readiness.header(runId);
|
|
457
|
+
stored = header ? { ...header, workspace: store.get(header.workspaceRef) } : null;
|
|
500
458
|
if (!stored)
|
|
501
459
|
throw new Error('Unknown Run.');
|
|
502
460
|
if (stored.project?.version !== 3)
|
|
@@ -507,6 +465,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
507
465
|
throw new Error('Invalid Run: no workspace descriptor.');
|
|
508
466
|
if (ownerData(runId))
|
|
509
467
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
468
|
+
submissionDeadlines.delete(runId);
|
|
510
469
|
db.prepare('INSERT INTO run_owners(run_id,pid,process_identity,token,claimed_at) VALUES (?,?,?,?,?)').run(runId, process.pid, ownProcessIdentity, token, now());
|
|
511
470
|
appendEvent(runId, null, 'worker.started', 'A request-scoped worker owns this Run.', { pid: process.pid });
|
|
512
471
|
return true;
|
|
@@ -525,10 +484,9 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
525
484
|
let plannedRevision = -1, leaseDeadline = Infinity;
|
|
526
485
|
let pendingSources = [];
|
|
527
486
|
const notified = new Set();
|
|
528
|
-
const kinds = new Map();
|
|
529
487
|
try {
|
|
530
488
|
onStarted?.({ runId, pid: process.pid });
|
|
531
|
-
workspace = await workspaceAdapter.reopenWorkspace(ownedRun.workspace, { signal: executionSignal });
|
|
489
|
+
workspace = await workspaceAdapter.reopenWorkspace(copy(ownedRun.workspace), { signal: executionSignal });
|
|
532
490
|
const reviewSignal = AbortSignal.any([executionSignal, workspace.signal]);
|
|
533
491
|
poll = setInterval(() => {
|
|
534
492
|
if (ownerData(runId)?.token !== token)
|
|
@@ -549,16 +507,15 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
549
507
|
transaction(() => {
|
|
550
508
|
refreshReadinessWithin(runId);
|
|
551
509
|
updateRunStatus(runId);
|
|
552
|
-
const pending = sharedRequests(
|
|
510
|
+
const pending = sharedRequests({ id: runId }).filter(request => !terminal.has(request.status));
|
|
553
511
|
pendingSources = [...new Set(pending.map(request => request.runId))];
|
|
554
512
|
leaseDeadline = Math.min(Infinity, ...pendingSources.filter(id => !ownerData(id)).map(id => submissionDeadlines.get(id) ?? performance.now()));
|
|
555
513
|
plannedRevision = polling.revision();
|
|
556
514
|
});
|
|
557
|
-
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);
|
|
558
516
|
let executing = [...inFlight.values()].filter(item => item.kind !== 'human').length;
|
|
559
|
-
for (const row of requests.filter(request => request.status === 'QUEUED')) {
|
|
560
|
-
const kind =
|
|
561
|
-
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;
|
|
562
519
|
if (kind !== 'human' && executing >= maxConcurrentExecutors)
|
|
563
520
|
continue;
|
|
564
521
|
if (kind !== 'human')
|
|
@@ -580,7 +537,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
580
537
|
await delay(Math.max(1, Math.min(100, leaseDeadline - performance.now())), undefined, { signal: reviewSignal });
|
|
581
538
|
continue;
|
|
582
539
|
}
|
|
583
|
-
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);
|
|
584
541
|
if (waiting.length) {
|
|
585
542
|
for (const request of waiting) {
|
|
586
543
|
if (notified.has(request.id))
|
|
@@ -628,9 +585,10 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
628
585
|
return entry.promise;
|
|
629
586
|
}
|
|
630
587
|
const broker = {
|
|
631
|
-
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 }) {
|
|
632
589
|
ensureOpen();
|
|
633
590
|
requireExecutors();
|
|
591
|
+
sweepSubmissionDeadlines();
|
|
634
592
|
positiveConcurrency(submissionIdentityConcurrency, 'identityConcurrency');
|
|
635
593
|
if ('mode' in removed)
|
|
636
594
|
throw new Error('Workspace modes are no longer supported; supply an unchanged workspace.');
|
|
@@ -647,14 +605,14 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
647
605
|
templates.push(...await prepareReviewRequests({ repoPath: workspace.descriptor.path, repoId, snapshotHash: workspace.descriptor.hash, criticId: id, preparedConfig: config }));
|
|
648
606
|
const id = randomUUID(), createdAt = now();
|
|
649
607
|
const record = { id, repoId, coalescingGraceMs, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
|
|
650
|
-
scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 3, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
|
|
608
|
+
scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 3, snapshot, selection, recursive, force, ignoreGates, templates }, status: 'QUEUED', createdAt };
|
|
651
609
|
await workspace.assertUnchanged();
|
|
652
610
|
workspace.signal.throwIfAborted();
|
|
653
611
|
transaction(() => {
|
|
654
|
-
db.prepare('INSERT INTO runs(id,created_at,status,data) VALUES (?,?,?,?)').run(id, createdAt, record.status, JSON.stringify(record));
|
|
612
|
+
db.prepare('INSERT INTO runs(id,created_at,status,data) VALUES (?,?,?,?)').run(id, createdAt, record.status, JSON.stringify(store.packRun(record)));
|
|
655
613
|
appendEvent(id, null, 'run.submitted', 'Project validation requested against fixed input.', { selection, recursive, force });
|
|
656
|
-
|
|
657
|
-
|
|
614
|
+
brokerTestHooks.onPlan?.();
|
|
615
|
+
readiness.initialize(record);
|
|
658
616
|
});
|
|
659
617
|
changed();
|
|
660
618
|
return required(getRun(id), 'Run');
|
|
@@ -866,7 +824,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
866
824
|
ensureOpen();
|
|
867
825
|
transaction(() => {
|
|
868
826
|
reconcileWithin(runId);
|
|
869
|
-
if (!
|
|
827
|
+
if (!polling.runStatus(runId))
|
|
870
828
|
throw new Error('Unknown Run.');
|
|
871
829
|
if (ownerData(runId))
|
|
872
830
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
@@ -879,7 +837,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
879
837
|
ensureOpen();
|
|
880
838
|
const error = codedError('Review canceled by requester.', 'REVIEW_CANCELED');
|
|
881
839
|
transaction(() => {
|
|
882
|
-
if (!
|
|
840
|
+
if (!polling.runStatus(runId))
|
|
883
841
|
throw new Error('Unknown Run.');
|
|
884
842
|
failWithin(runId, error);
|
|
885
843
|
});
|
|
@@ -889,6 +847,46 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
889
847
|
changed();
|
|
890
848
|
return getRun(runId);
|
|
891
849
|
},
|
|
850
|
+
retryRequest(requestId) {
|
|
851
|
+
ensureOpen();
|
|
852
|
+
transaction(() => {
|
|
853
|
+
const request = required(requestHeader(requestId), 'Request');
|
|
854
|
+
if (request.status !== 'ERROR')
|
|
855
|
+
throw new Error('Only an operationally failed request can be retried. Submit changed input as a new Run.');
|
|
856
|
+
const run = required(readiness.header(request.runId), 'Run');
|
|
857
|
+
if (ownerData(run.id))
|
|
858
|
+
throw codedError('Wait for the current worker to settle before retrying.', 'RUN_ALREADY_OWNED');
|
|
859
|
+
run.status = 'QUEUED';
|
|
860
|
+
delete run.completedAt;
|
|
861
|
+
delete run.error;
|
|
862
|
+
db.prepare('UPDATE runs SET status=?,data=? WHERE id=?').run(run.status, JSON.stringify(run), run.id);
|
|
863
|
+
const gate = db.prepare('SELECT unmet,red FROM gate_counts WHERE run_id=? AND critic_id=?').get(request.runId, request.criticId);
|
|
864
|
+
request.status = Number(gate?.unmet ?? 0) ? Number(gate?.red ?? 0) ? 'BLOCKED' : 'WAIT_DEPENDENCY' : 'QUEUED';
|
|
865
|
+
request.error = null;
|
|
866
|
+
request.errorCode = null;
|
|
867
|
+
request.resultRef = null;
|
|
868
|
+
request.semanticRef = null;
|
|
869
|
+
request.startedAt = null;
|
|
870
|
+
request.completedAt = null;
|
|
871
|
+
saveHeader(request);
|
|
872
|
+
appendEvent(run.id, request.id, 'request.retried', 'Retry requested against the same immutable input.');
|
|
873
|
+
});
|
|
874
|
+
changed();
|
|
875
|
+
return viewRequest(requestData(requestId));
|
|
876
|
+
},
|
|
877
|
+
changes(runId, options) {
|
|
878
|
+
ensureOpen();
|
|
879
|
+
db.exec('BEGIN');
|
|
880
|
+
try {
|
|
881
|
+
const page = readChanges(db, runId, stateDir, options);
|
|
882
|
+
db.exec('COMMIT');
|
|
883
|
+
return page;
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
db.exec('ROLLBACK');
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
},
|
|
892
890
|
onChange(callback) { listeners.add(callback); return () => listeners.delete(callback); },
|
|
893
891
|
async close() {
|
|
894
892
|
if (closed)
|
|
@@ -902,7 +900,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
902
900
|
for (const entry of owned)
|
|
903
901
|
entry.abort.abort(codedError('The review worker stopped before completion.', 'WORKER_STOPPED'));
|
|
904
902
|
await Promise.allSettled(owned.map(entry => entry.promise));
|
|
905
|
-
|
|
903
|
+
clearReadCaches();
|
|
906
904
|
submissionDeadlines.clear();
|
|
907
905
|
listeners.clear();
|
|
908
906
|
db.close();
|
|
@@ -915,8 +913,9 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
915
913
|
...broker,
|
|
916
914
|
submitProject: async (...args) => viewRun(await broker.submitProject(...args)),
|
|
917
915
|
run: async (...args) => viewRun(await broker.run(...args)),
|
|
918
|
-
getRun: (id) =>
|
|
919
|
-
|
|
916
|
+
getRun: (id) => { ensureOpen(); if (ownerData(id))
|
|
917
|
+
reconcile(id); return resultView({ detail }, detail === 'full' ? broker.getRun(id) : null, () => storedRequesterRun(db, id, stateDir)); },
|
|
918
|
+
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')); }); },
|
|
920
919
|
getRequest: (id) => viewRequest(broker.getRequest(id)),
|
|
921
920
|
claimHuman: async (...args) => viewRequest(await broker.claimHuman(...args)),
|
|
922
921
|
completeHuman: async (...args) => viewRequest(await broker.completeHuman(...args)),
|