@ccdd/project 5.1.2 → 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 +238 -299
- package/dist/src/broker/index.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,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))
|
|
@@ -688,9 +585,10 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
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 }) {
|
|
692
589
|
ensureOpen();
|
|
693
590
|
requireExecutors();
|
|
591
|
+
sweepSubmissionDeadlines();
|
|
694
592
|
positiveConcurrency(submissionIdentityConcurrency, 'identityConcurrency');
|
|
695
593
|
if ('mode' in removed)
|
|
696
594
|
throw new Error('Workspace modes are no longer supported; supply an unchanged workspace.');
|
|
@@ -707,14 +605,14 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
707
605
|
templates.push(...await prepareReviewRequests({ repoPath: workspace.descriptor.path, repoId, snapshotHash: workspace.descriptor.hash, criticId: id, preparedConfig: config }));
|
|
708
606
|
const id = randomUUID(), createdAt = now();
|
|
709
607
|
const record = { id, repoId, coalescingGraceMs, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
|
|
710
|
-
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 };
|
|
711
609
|
await workspace.assertUnchanged();
|
|
712
610
|
workspace.signal.throwIfAborted();
|
|
713
611
|
transaction(() => {
|
|
714
|
-
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)));
|
|
715
613
|
appendEvent(id, null, 'run.submitted', 'Project validation requested against fixed input.', { selection, recursive, force });
|
|
716
|
-
|
|
717
|
-
|
|
614
|
+
brokerTestHooks.onPlan?.();
|
|
615
|
+
readiness.initialize(record);
|
|
718
616
|
});
|
|
719
617
|
changed();
|
|
720
618
|
return required(getRun(id), 'Run');
|
|
@@ -926,7 +824,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
926
824
|
ensureOpen();
|
|
927
825
|
transaction(() => {
|
|
928
826
|
reconcileWithin(runId);
|
|
929
|
-
if (!
|
|
827
|
+
if (!polling.runStatus(runId))
|
|
930
828
|
throw new Error('Unknown Run.');
|
|
931
829
|
if (ownerData(runId))
|
|
932
830
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
@@ -939,7 +837,7 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
939
837
|
ensureOpen();
|
|
940
838
|
const error = codedError('Review canceled by requester.', 'REVIEW_CANCELED');
|
|
941
839
|
transaction(() => {
|
|
942
|
-
if (!
|
|
840
|
+
if (!polling.runStatus(runId))
|
|
943
841
|
throw new Error('Unknown Run.');
|
|
944
842
|
failWithin(runId, error);
|
|
945
843
|
});
|
|
@@ -949,6 +847,46 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
949
847
|
changed();
|
|
950
848
|
return getRun(runId);
|
|
951
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
|
+
},
|
|
952
890
|
onChange(callback) { listeners.add(callback); return () => listeners.delete(callback); },
|
|
953
891
|
async close() {
|
|
954
892
|
if (closed)
|
|
@@ -975,8 +913,9 @@ export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coal
|
|
|
975
913
|
...broker,
|
|
976
914
|
submitProject: async (...args) => viewRun(await broker.submitProject(...args)),
|
|
977
915
|
run: async (...args) => viewRun(await broker.run(...args)),
|
|
978
|
-
getRun: (id) =>
|
|
979
|
-
|
|
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')); }); },
|
|
980
919
|
getRequest: (id) => viewRequest(broker.getRequest(id)),
|
|
981
920
|
claimHuman: async (...args) => viewRequest(await broker.claimHuman(...args)),
|
|
982
921
|
completeHuman: async (...args) => viewRequest(await broker.completeHuman(...args)),
|