@ccdd/project 4.3.0 → 5.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-6yzqBOLv.js → GraphView-BDrRFY2b.js} +3 -3
- package/dist/monitor-ui/assets/ValidationView-CznEpoWz.js +1 -0
- package/dist/monitor-ui/assets/ValidationView-DBRoun_B.css +1 -0
- package/dist/monitor-ui/assets/{elk-api-BwZQovT5.js → elk-api-BJkhNJwW.js} +1 -1
- package/dist/monitor-ui/assets/{elk.bundled-CQowIhI8.js → elk.bundled-uROSxMms.js} +1 -1
- package/dist/monitor-ui/assets/{index-D0yfhUlt.css → index-CRKnM9pk.css} +1 -1
- package/dist/monitor-ui/assets/index-CTW1fvAj.js +6 -0
- package/dist/monitor-ui/index.html +2 -2
- package/dist/src/artifacts/tool-check.js +1 -3
- package/dist/src/artifacts/tool-check.js.map +1 -1
- package/dist/src/broker/config.js +5 -1
- package/dist/src/broker/config.js.map +1 -1
- package/dist/src/broker/index.d.ts +44 -84
- package/dist/src/broker/index.js +175 -50
- package/dist/src/broker/index.js.map +1 -1
- package/dist/src/broker/polling.d.ts +11 -0
- package/dist/src/broker/polling.js +27 -0
- package/dist/src/broker/polling.js.map +1 -0
- package/dist/src/contracts.d.ts +4 -4
- package/dist/src/definitions.d.ts +3 -1
- package/dist/src/executors/index.js +6 -21
- package/dist/src/executors/index.js.map +1 -1
- package/dist/src/executors/pi.d.ts +3 -1
- package/dist/src/executors/pi.js +2 -0
- package/dist/src/executors/pi.js.map +1 -1
- package/dist/src/executors/telemetry.d.ts +2 -0
- package/dist/src/executors/telemetry.js +12 -0
- package/dist/src/executors/telemetry.js.map +1 -1
- package/dist/src/monitor/actions.d.ts +1 -1
- package/dist/src/monitor/actions.js +3 -3
- package/dist/src/monitor/actions.js.map +1 -1
- package/dist/src/monitor/server.js +10 -5
- package/dist/src/monitor/server.js.map +1 -1
- package/dist/src/monitor/store.js +10 -6
- package/dist/src/monitor/store.js.map +1 -1
- package/dist/src/monitor/types.d.ts +5 -7
- package/dist/src/project/cli.js +47 -24
- package/dist/src/project/cli.js.map +1 -1
- package/dist/src/project/identity.d.ts +6 -1
- package/dist/src/project/identity.js +42 -13
- package/dist/src/project/identity.js.map +1 -1
- package/dist/src/project/index.d.ts +21 -6
- package/dist/src/project/index.js +19 -8
- package/dist/src/project/index.js.map +1 -1
- package/dist/src/project/owner-identity.d.ts +0 -4
- package/dist/src/project/owner-identity.js +5 -3
- package/dist/src/project/owner-identity.js.map +1 -1
- package/dist/src/project/prune.d.ts +12 -0
- package/dist/src/project/prune.js +132 -0
- package/dist/src/project/prune.js.map +1 -0
- package/dist/src/project/query.js +9 -9
- package/dist/src/project/query.js.map +1 -1
- package/dist/src/project/store.d.ts +4 -3
- package/dist/src/project/store.js +12 -7
- package/dist/src/project/store.js.map +1 -1
- package/dist/src/project/types.d.ts +8 -6
- package/dist/src/requester/index.js +1 -1
- package/dist/src/requester/index.js.map +1 -1
- package/dist/src/response-schema.d.ts +13 -0
- package/dist/src/response-schema.js +33 -0
- package/dist/src/response-schema.js.map +1 -0
- package/dist/src/result-view.d.ts +72 -0
- package/dist/src/result-view.js +44 -0
- package/dist/src/result-view.js.map +1 -0
- package/dist/src/review/local-claim.d.ts +1 -1
- package/dist/src/review/local-claim.js.map +1 -1
- package/dist/src/review-result.d.ts +2 -1
- package/dist/src/review-result.js +9 -8
- package/dist/src/review-result.js.map +1 -1
- package/dist/src/state-format.d.ts +8 -0
- package/dist/src/state-format.js +18 -0
- package/dist/src/state-format.js.map +1 -0
- package/dist/src/tools/runner.d.ts +2 -0
- package/dist/src/tools/runner.js +41 -11
- package/dist/src/tools/runner.js.map +1 -1
- package/dist/src/worker-client.d.ts +3 -2
- package/dist/src/worker-client.js +7 -4
- package/dist/src/worker-client.js.map +1 -1
- package/dist/src/worker.js +1 -1
- package/dist/src/worker.js.map +1 -1
- package/package.json +2 -2
- package/dist/monitor-ui/assets/ValidationView-BMqI2XQ5.css +0 -1
- package/dist/monitor-ui/assets/ValidationView-BwHWjMUI.js +0 -1
- package/dist/monitor-ui/assets/index-CoJ9_gC3.js +0 -7
package/dist/src/broker/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { assertStateFormat, initializeStateFormat } from '../state-format.js';
|
|
2
|
+
import { semanticResult, validateFinalResult } from '../response-schema.js';
|
|
3
|
+
import { requesterRun, requesterRequest, resultView } from '../result-view.js';
|
|
1
4
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
3
7
|
import { execFileSync } from 'node:child_process';
|
|
4
8
|
import fs from 'node:fs';
|
|
5
9
|
import path from 'node:path';
|
|
@@ -7,20 +11,23 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
7
11
|
import { prepareReviewRequests } from '../requester/index.js';
|
|
8
12
|
import { readWorkspaceConfig } from './config.js';
|
|
9
13
|
import { createGraphDefinition } from './graph.js';
|
|
10
|
-
import {
|
|
14
|
+
import { createStatusPolling } from './polling.js';
|
|
15
|
+
import { tokenUsage, toolResponseBytes } from '../executors/telemetry.js';
|
|
11
16
|
import { finalResultEventData } from '../executors/final-result.js';
|
|
12
17
|
import { normalizeReviewResult as validateResult, storedObservation } from '../review-result.js';
|
|
13
18
|
import { createReviewTools } from '../tools/runner.js';
|
|
14
19
|
import { createHumanClaims, HUMAN_PREPARATION_LEASE_MS } from './human-claims.js';
|
|
15
20
|
import { prepareHumanReview } from '../executors/human-preparation.js';
|
|
16
21
|
import { prepareWorkspace, reopenWorkspace } from '../workspaces/index.js';
|
|
17
|
-
import { createProjectSnapshot } from '../project/identity.js';
|
|
22
|
+
import { createProjectSnapshot, DEFAULT_IDENTITY_CONCURRENCY, positiveConcurrency } from '../project/identity.js';
|
|
18
23
|
import { includedCritics, planProject } from '../project/query.js';
|
|
19
|
-
import { readEvidence } from '../project/store.js';
|
|
24
|
+
import { readEvidence, storedRun } from '../project/store.js';
|
|
25
|
+
/** Internal instrumentation for deterministic scheduler regression tests. */
|
|
26
|
+
export const brokerTestHooks = {};
|
|
20
27
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
28
|
const errorCode = (error) => object(error) && typeof error.code === 'string' ? error.code : undefined;
|
|
22
29
|
const parseStored = (value) => { if (typeof value !== 'string')
|
|
23
|
-
throw new Error('Broker store contains a non-text JSON record.'); return JSON.parse(value); };
|
|
30
|
+
throw new Error('Broker store contains a non-text JSON record.'); brokerTestHooks.onHydrate?.(Buffer.byteLength(value)); return JSON.parse(value); };
|
|
24
31
|
const required = (value, label) => { if (value == null)
|
|
25
32
|
throw new Error(`Unknown ${label}.`); return value; };
|
|
26
33
|
const terminal = new Set(['GREEN', 'RED', 'ERROR', 'INCOMPLETE']);
|
|
@@ -28,7 +35,6 @@ const now = () => new Date().toISOString();
|
|
|
28
35
|
const errorText = (error) => String(object(error) && typeof error.message === 'string' ? error.message : error).slice(0, 2000);
|
|
29
36
|
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
30
37
|
const codedError = (message, code) => Object.assign(new Error(message), { code });
|
|
31
|
-
const MAX_CONCURRENT_EXECUTORS = 4;
|
|
32
38
|
const fatalExecutionError = (error) => {
|
|
33
39
|
const code = errorCode(error);
|
|
34
40
|
return Boolean(code?.startsWith('WORKSPACE_') || ['RUN_OWNERSHIP_LOST', 'REVIEW_CANCELED', 'WORKER_STOPPED', 'WORKER_EXITED', 'REVIEW_GRAPH_INVALID'].includes(code ?? ''));
|
|
@@ -82,6 +88,7 @@ export function readStateContext(stateDir) {
|
|
|
82
88
|
// A worker closing the last WAL connection can briefly lock even read-only queries.
|
|
83
89
|
const database = new DatabaseSync(filename, { readOnly: true, timeout: 5000 });
|
|
84
90
|
try {
|
|
91
|
+
assertStateFormat(database);
|
|
85
92
|
const row = database.prepare('SELECT value FROM metadata WHERE key = ?').get('registered-repo');
|
|
86
93
|
const identity = row ? parseStored(row.value) : null;
|
|
87
94
|
if (!object(identity) || typeof identity.repoPath !== 'string' || !path.isAbsolute(identity.repoPath) || typeof identity.repoId !== 'string')
|
|
@@ -104,7 +111,13 @@ function prepareStateDirectory(repoPath, stateDir) {
|
|
|
104
111
|
return fs.realpathSync(canonical);
|
|
105
112
|
}
|
|
106
113
|
/** Durable broker operations. Merely opening the store never starts a worker. */
|
|
107
|
-
export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, workspaceIntegrity = 'content', workspaceAdapter = { prepareWorkspace, reopenWorkspace } }) {
|
|
114
|
+
export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coalescingGraceMs = 15_000, maxConcurrentExecutors = 4, identityConcurrency = DEFAULT_IDENTITY_CONCURRENCY, executors, workspaceIntegrity = 'content', workspaceAdapter = { prepareWorkspace, reopenWorkspace } }) {
|
|
115
|
+
positiveConcurrency(maxConcurrentExecutors, 'maxConcurrentExecutors');
|
|
116
|
+
positiveConcurrency(identityConcurrency, 'identityConcurrency');
|
|
117
|
+
if (!Number.isSafeInteger(coalescingGraceMs) || coalescingGraceMs < 0 || coalescingGraceMs > 300_000)
|
|
118
|
+
throw new Error('coalescingGraceMs must be an integer from 0 to 300000.');
|
|
119
|
+
if (detail !== undefined && detail !== 'compact' && detail !== 'full')
|
|
120
|
+
throw new Error('detail must be compact or full.');
|
|
108
121
|
if (!['content', 'metadata'].includes(workspaceIntegrity))
|
|
109
122
|
throw new Error('Workspace integrity must be content or metadata.');
|
|
110
123
|
try {
|
|
@@ -126,7 +139,10 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
126
139
|
let db;
|
|
127
140
|
try {
|
|
128
141
|
db = new DatabaseSync(path.join(stateDir, 'broker.sqlite'));
|
|
129
|
-
db.exec(
|
|
142
|
+
db.exec('PRAGMA busy_timeout=5000; BEGIN IMMEDIATE');
|
|
143
|
+
initializeStateFormat(db);
|
|
144
|
+
db.exec('COMMIT');
|
|
145
|
+
db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
|
|
130
146
|
CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, created_at TEXT NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
|
|
131
147
|
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);
|
|
132
148
|
CREATE INDEX IF NOT EXISTS requests_run ON requests(run_id, ordinal);
|
|
@@ -147,12 +163,13 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
147
163
|
db?.close();
|
|
148
164
|
throw error;
|
|
149
165
|
}
|
|
166
|
+
const polling = createStatusPolling(db);
|
|
150
167
|
let closed = false;
|
|
151
168
|
let closing = false;
|
|
152
169
|
const active = new Map();
|
|
153
170
|
const listeners = new Set();
|
|
154
171
|
const ensureOpen = () => { if (closed || closing)
|
|
155
|
-
throw new Error('Broker is closed.'); };
|
|
172
|
+
throw new Error('Broker is closed.'); sweepSubmissionDeadlines(); };
|
|
156
173
|
const requireExecutors = () => {
|
|
157
174
|
if (!executors || typeof executors.canExecute !== 'function' || typeof executors.execute !== 'function')
|
|
158
175
|
throw new Error('Review submission and execution require an executor registry.');
|
|
@@ -180,8 +197,48 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
180
197
|
};
|
|
181
198
|
const ownerData = (id) => db.prepare('SELECT * FROM run_owners WHERE run_id = ?').get(id);
|
|
182
199
|
const runRequests = (id) => db.prepare('SELECT data FROM requests WHERE run_id = ? ORDER BY ordinal').all(id).map(row => parseStored(row.data));
|
|
200
|
+
const sharedRequests = (run) => (run.project?.coalescedRequestIds ?? []).map(id => required(requestData(id), 'Shared Request'));
|
|
201
|
+
const attemptsFor = (run) => [...runRequests(run.id), ...sharedRequests(run)];
|
|
202
|
+
const submissionDeadlines = new Map();
|
|
203
|
+
function sweepSubmissionDeadlines() {
|
|
204
|
+
for (const id of submissionDeadlines.keys()) {
|
|
205
|
+
const status = polling.runStatus(id);
|
|
206
|
+
// Retain expired unowned sources: dropping their monotonic tombstone could
|
|
207
|
+
// renew eligibility after a wall-clock rollback. Owned Runs cannot revert
|
|
208
|
+
// to an abandoned submission; worker exit is reconciled as terminal.
|
|
209
|
+
if (!status || terminal.has(status) || ownerData(id))
|
|
210
|
+
submissionDeadlines.delete(id);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const shareable = (request) => {
|
|
214
|
+
// Reconcile PID/token ownership at the adoption boundary, before even an
|
|
215
|
+
// unowned submission lease can make a dead worker's ticket eligible.
|
|
216
|
+
reconcileWithin(request.runId);
|
|
217
|
+
const source = required(runData(request.runId), 'Source Run');
|
|
218
|
+
if (terminal.has(source.status))
|
|
219
|
+
return false;
|
|
220
|
+
const owner = ownerData(source.id);
|
|
221
|
+
if (owner)
|
|
222
|
+
return ownerAlive(owner); // Never fall back to grace for an owned source.
|
|
223
|
+
const grace = source.coalescingGraceMs;
|
|
224
|
+
if (request.status !== 'QUEUED' || typeof grace !== 'number' || !Number.isSafeInteger(grace) || grace <= 0 || grace > 300_000)
|
|
225
|
+
return false;
|
|
226
|
+
const elapsed = typeof source.createdAt === 'string' ? Date.now() - Date.parse(source.createdAt) : NaN;
|
|
227
|
+
if (!Number.isFinite(elapsed) || elapsed < 0 || elapsed >= grace)
|
|
228
|
+
return false;
|
|
229
|
+
// A wall-clock rollback must not extend a lease already observed here.
|
|
230
|
+
// Keep this process-local bound without changing the source Run.
|
|
231
|
+
const tick = performance.now();
|
|
232
|
+
const deadline = Math.min(submissionDeadlines.get(source.id) ?? Infinity, tick + grace - elapsed);
|
|
233
|
+
submissionDeadlines.set(source.id, deadline);
|
|
234
|
+
return tick < deadline;
|
|
235
|
+
};
|
|
183
236
|
const saveRequest = (request) => db.prepare('UPDATE requests SET status = ?, data = ? WHERE id = ?').run(request.status, JSON.stringify(request), request.id);
|
|
184
|
-
const saveRun = (run) =>
|
|
237
|
+
const saveRun = (run) => {
|
|
238
|
+
db.prepare('UPDATE runs SET status = ?, data = ? WHERE id = ?').run(run.status, JSON.stringify(run), run.id);
|
|
239
|
+
if (terminal.has(run.status))
|
|
240
|
+
submissionDeadlines.delete(run.id);
|
|
241
|
+
};
|
|
185
242
|
const appendEvent = (runId, requestId, type, message, data = null) => {
|
|
186
243
|
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));
|
|
187
244
|
};
|
|
@@ -196,7 +253,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
196
253
|
assertWaiting: request => {
|
|
197
254
|
ensureOpen();
|
|
198
255
|
if (request.configManifest?.version !== 2)
|
|
199
|
-
throw new Error('
|
|
256
|
+
throw new Error('Invalid stored review configuration.');
|
|
200
257
|
if (!request.notifiedAt)
|
|
201
258
|
throw new Error('Human alarm delivery is still pending.');
|
|
202
259
|
if (!ownerAlive(ownerData(request.runId)))
|
|
@@ -204,12 +261,34 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
204
261
|
},
|
|
205
262
|
});
|
|
206
263
|
function refreshReadinessWithin(runId) {
|
|
264
|
+
sweepSubmissionDeadlines();
|
|
207
265
|
const run = required(runData(runId), 'Run'), requests = runRequests(runId);
|
|
208
|
-
if (run.project?.version ===
|
|
266
|
+
if (run.project?.version === 3) {
|
|
209
267
|
if (terminal.has(run.status))
|
|
210
268
|
return;
|
|
211
|
-
const
|
|
269
|
+
for (const shared of sharedRequests(run))
|
|
270
|
+
reconcileWithin(shared.runId);
|
|
271
|
+
// Called in a write transaction: expire only unowned queued sources, then
|
|
272
|
+
// recheck all matching owned/leased requests before creating our own ticket.
|
|
273
|
+
const retained = sharedRequests(run).filter(request => terminal.has(request.status) || shareable(request));
|
|
274
|
+
if (retained.length !== (run.project.coalescedRequestIds ?? []).length) {
|
|
275
|
+
run.project.coalescedRequestIds = retained.map(request => request.id);
|
|
276
|
+
saveRun(run);
|
|
277
|
+
appendEvent(runId, null, 'request.uncoalesced', 'Unowned source submission lease expired; replanning this Run.');
|
|
278
|
+
}
|
|
279
|
+
brokerTestHooks.onPlan?.();
|
|
280
|
+
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)] });
|
|
212
281
|
for (const item of plan.items.filter(item => item.action === 'EXECUTE')) {
|
|
282
|
+
if (!run.project.force) {
|
|
283
|
+
const candidates = db.prepare("SELECT data FROM requests WHERE status IN ('QUEUED','RUNNING','WAITING_HUMAN') AND json_extract(data, '$.criticId') = ? AND json_extract(data, '$.validationInput.key') = ? AND json_extract(data, '$.validationInput.version') = 3 ORDER BY rowid").all(item.id, item.input.key).map(row => parseStored(row.data));
|
|
284
|
+
const shared = candidates.find(candidate => shareable(candidate));
|
|
285
|
+
if (shared) {
|
|
286
|
+
run.project.coalescedRequestIds = [...new Set([...(run.project.coalescedRequestIds ?? []), shared.id])];
|
|
287
|
+
saveRun(run);
|
|
288
|
+
appendEvent(runId, shared.id, 'request.coalesced', 'Waiting for an identical active review in another Run.', { sourceRunId: shared.runId });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
213
292
|
const envelope = run.project.templates.find(template => template.criticId === item.id);
|
|
214
293
|
if (!envelope)
|
|
215
294
|
throw codedError(`Missing prepared Critic ${item.id}.`, 'REVIEW_GRAPH_INVALID');
|
|
@@ -222,17 +301,18 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
222
301
|
}
|
|
223
302
|
return;
|
|
224
303
|
}
|
|
225
|
-
throw new Error('
|
|
304
|
+
throw new Error('Invalid stored Run format; submit a new validation request.');
|
|
226
305
|
}
|
|
227
306
|
const updateRunStatus = (runId) => {
|
|
228
|
-
const states = runRequests(runId).map(request => request.status);
|
|
229
307
|
const run = required(runData(runId), 'Run');
|
|
230
|
-
|
|
308
|
+
const states = attemptsFor(run).map(request => request.status);
|
|
309
|
+
if (run.project?.version === 3) {
|
|
231
310
|
if (terminal.has(run.status))
|
|
232
311
|
return;
|
|
233
|
-
|
|
312
|
+
brokerTestHooks.onPlan?.();
|
|
313
|
+
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) });
|
|
234
314
|
const status = states.includes('RUNNING') ? 'RUNNING' : states.includes('QUEUED') ? 'QUEUED' : states.includes('WAITING_HUMAN') ? 'WAITING_HUMAN' :
|
|
235
|
-
states.includes('ERROR') ? 'ERROR' : states.includes('RED') ? 'RED' : plan.satisfied ? 'GREEN' : 'INCOMPLETE';
|
|
315
|
+
states.includes('ERROR') ? 'ERROR' : states.includes('RED') || plan.critics.some(critic => critic.status === 'RED') ? 'RED' : plan.satisfied ? 'GREEN' : 'INCOMPLETE';
|
|
236
316
|
if (run.status !== status) {
|
|
237
317
|
run.status = status;
|
|
238
318
|
if (terminal.has(status)) {
|
|
@@ -244,7 +324,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
244
324
|
}
|
|
245
325
|
return;
|
|
246
326
|
}
|
|
247
|
-
throw new Error('
|
|
327
|
+
throw new Error('Invalid stored Run format.');
|
|
248
328
|
};
|
|
249
329
|
function finishWithin(requestId, outcome, expectedStates = ['RUNNING', 'WAITING_HUMAN']) {
|
|
250
330
|
const { result, error } = outcome, hasError = Object.hasOwn(outcome, 'error');
|
|
@@ -257,7 +337,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
257
337
|
request.errorCode = errorCode(error) ?? null;
|
|
258
338
|
request.completedAt = now();
|
|
259
339
|
saveRequest(request);
|
|
260
|
-
appendEvent(request.runId, request.id, hasError ? 'request.error' : 'request.completed', hasError ? required(request.error, 'error message') : required(result, 'review result').
|
|
340
|
+
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 } : {}) });
|
|
261
341
|
refreshReadinessWithin(request.runId);
|
|
262
342
|
updateRunStatus(request.runId);
|
|
263
343
|
return true;
|
|
@@ -278,8 +358,9 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
278
358
|
saveRequest(request);
|
|
279
359
|
appendEvent(runId, request.id, 'request.error', request.error, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
|
|
280
360
|
}
|
|
281
|
-
if (run.project?.version ===
|
|
282
|
-
|
|
361
|
+
if (run.project?.version === 3) {
|
|
362
|
+
brokerTestHooks.onPlan?.();
|
|
363
|
+
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) });
|
|
283
364
|
run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
|
|
284
365
|
}
|
|
285
366
|
run.status = 'ERROR';
|
|
@@ -392,6 +473,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
392
473
|
if (typeof event[key] === 'string')
|
|
393
474
|
safe[key] = event[key].slice(0, 1000);
|
|
394
475
|
if (event.type === 'artifact.tool.completed') {
|
|
476
|
+
Object.assign(safe, toolResponseBytes(event));
|
|
395
477
|
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)))
|
|
396
478
|
safe.startedAt = event.startedAt;
|
|
397
479
|
if (typeof event.durationMs === 'number' && Number.isFinite(event.durationMs) && event.durationMs >= 0)
|
|
@@ -421,6 +503,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
421
503
|
changed();
|
|
422
504
|
},
|
|
423
505
|
}));
|
|
506
|
+
validateFinalResult(semanticResult(result), request);
|
|
424
507
|
await workspace.assertUnchanged();
|
|
425
508
|
signal.throwIfAborted();
|
|
426
509
|
transaction(() => { if (ownerData(runId)?.token === token)
|
|
@@ -446,12 +529,12 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
446
529
|
stored = runData(runId);
|
|
447
530
|
if (!stored)
|
|
448
531
|
throw new Error('Unknown Run.');
|
|
449
|
-
if (stored.project?.version !==
|
|
450
|
-
throw new Error('
|
|
532
|
+
if (stored.project?.version !== 3)
|
|
533
|
+
throw new Error('Invalid stored Run format; submit a new validation request.');
|
|
451
534
|
if (terminal.has(stored.status))
|
|
452
535
|
return false;
|
|
453
536
|
if (!stored.workspace)
|
|
454
|
-
throw new Error('
|
|
537
|
+
throw new Error('Invalid Run: no workspace descriptor.');
|
|
455
538
|
if (ownerData(runId))
|
|
456
539
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
457
540
|
db.prepare('INSERT INTO run_owners(run_id,pid,process_identity,token,claimed_at) VALUES (?,?,?,?,?)').run(runId, process.pid, ownProcessIdentity, token, now());
|
|
@@ -469,6 +552,10 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
469
552
|
let workspace;
|
|
470
553
|
let poll;
|
|
471
554
|
const inFlight = new Map();
|
|
555
|
+
let plannedRevision = -1, leaseDeadline = Infinity;
|
|
556
|
+
let pendingSources = [];
|
|
557
|
+
const notified = new Set();
|
|
558
|
+
const kinds = new Map();
|
|
472
559
|
try {
|
|
473
560
|
onStarted?.({ runId, pid: process.pid });
|
|
474
561
|
workspace = await workspaceAdapter.reopenWorkspace(ownedRun.workspace, { signal: executionSignal });
|
|
@@ -476,48 +563,70 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
476
563
|
poll = setInterval(() => {
|
|
477
564
|
if (ownerData(runId)?.token !== token)
|
|
478
565
|
abort.abort(codedError('Review ownership was lost.', 'RUN_OWNERSHIP_LOST'));
|
|
479
|
-
else if (terminal.has(required(
|
|
566
|
+
else if (terminal.has(required(polling.runStatus(runId), 'Run')))
|
|
480
567
|
abort.abort(codedError('Review is already complete or canceled.', 'REVIEW_CANCELED'));
|
|
481
568
|
}, 100);
|
|
482
569
|
while (true) {
|
|
483
|
-
const current = required(
|
|
484
|
-
if (terminal.has(current
|
|
570
|
+
const current = required(polling.runStatus(runId), 'Run');
|
|
571
|
+
if (terminal.has(current)) {
|
|
485
572
|
if (inFlight.size)
|
|
486
573
|
abort.abort(codedError('Review is already complete or canceled.', 'REVIEW_CANCELED'));
|
|
487
574
|
break;
|
|
488
575
|
}
|
|
489
576
|
reviewSignal.throwIfAborted();
|
|
490
|
-
|
|
491
|
-
|
|
577
|
+
const sourceExited = pendingSources.some(id => { const owner = ownerData(id); return owner !== undefined && !ownerAlive(owner); });
|
|
578
|
+
if (polling.revision() !== plannedRevision || performance.now() >= leaseDeadline || sourceExited)
|
|
579
|
+
transaction(() => {
|
|
580
|
+
refreshReadinessWithin(runId);
|
|
581
|
+
updateRunStatus(runId);
|
|
582
|
+
const pending = sharedRequests(required(runData(runId), 'Run')).filter(request => !terminal.has(request.status));
|
|
583
|
+
pendingSources = [...new Set(pending.map(request => request.runId))];
|
|
584
|
+
leaseDeadline = Math.min(Infinity, ...pendingSources.filter(id => !ownerData(id)).map(id => submissionDeadlines.get(id) ?? performance.now()));
|
|
585
|
+
plannedRevision = polling.revision();
|
|
586
|
+
});
|
|
587
|
+
const requests = polling.requests(runId);
|
|
492
588
|
let executing = [...inFlight.values()].filter(item => item.kind !== 'human').length;
|
|
493
|
-
for (const
|
|
494
|
-
|
|
589
|
+
for (const row of requests.filter(request => request.status === 'QUEUED')) {
|
|
590
|
+
const kind = kinds.get(row.id) ?? required(requestData(row.id), 'Request').profile.kind;
|
|
591
|
+
kinds.set(row.id, kind);
|
|
592
|
+
if (kind !== 'human' && executing >= maxConcurrentExecutors)
|
|
495
593
|
continue;
|
|
496
|
-
if (
|
|
594
|
+
if (kind !== 'human')
|
|
497
595
|
executing++;
|
|
498
|
-
const promise = executeOne(
|
|
596
|
+
const promise = executeOne(row.id, workspace, token, reviewSignal).catch(error => {
|
|
499
597
|
// A different task may win Promise.race before this rejection. Preserve fatal failure independently of that race.
|
|
500
598
|
abort.abort(error);
|
|
501
599
|
throw error;
|
|
502
|
-
}).finally(() => { inFlight.delete(
|
|
503
|
-
inFlight.set(
|
|
600
|
+
}).finally(() => { inFlight.delete(row.id); });
|
|
601
|
+
inFlight.set(row.id, { kind, promise });
|
|
504
602
|
}
|
|
505
603
|
if (inFlight.size) {
|
|
506
604
|
// Human alarms and independent evaluations progress together.
|
|
507
605
|
await Promise.race([...inFlight.values()].map(item => item.promise).concat(delay(50, undefined, { signal: reviewSignal })));
|
|
508
606
|
continue;
|
|
509
607
|
}
|
|
608
|
+
if (pendingSources.length) {
|
|
609
|
+
brokerTestHooks.onIdleTick?.();
|
|
610
|
+
await delay(Math.max(1, Math.min(100, leaseDeadline - performance.now())), undefined, { signal: reviewSignal });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
510
613
|
const waiting = requests.filter(request => request.status === 'WAITING_HUMAN');
|
|
511
614
|
if (waiting.length) {
|
|
512
|
-
|
|
513
|
-
|
|
615
|
+
for (const request of waiting) {
|
|
616
|
+
if (notified.has(request.id))
|
|
617
|
+
continue;
|
|
618
|
+
if (!required(requestData(request.id), 'Request').notifiedAt)
|
|
619
|
+
throw new Error('Human notification did not complete; submit a new Run to retry.');
|
|
620
|
+
notified.add(request.id);
|
|
621
|
+
}
|
|
514
622
|
// The workspace observer stays alive with filesystem events and metadata polls.
|
|
515
623
|
// Notification already crossed its final content boundary; idle waiting
|
|
516
624
|
// must not rehash the entire workspace on every scheduling iteration.
|
|
625
|
+
brokerTestHooks.onIdleTick?.();
|
|
517
626
|
await delay(100, undefined, { signal: reviewSignal });
|
|
518
627
|
continue;
|
|
519
628
|
}
|
|
520
|
-
if (terminal.has(required(
|
|
629
|
+
if (terminal.has(required(polling.runStatus(runId), 'Run')))
|
|
521
630
|
break;
|
|
522
631
|
throw codedError('Run has no executable request or pending Human review.', 'REVIEW_GRAPH_INVALID');
|
|
523
632
|
}
|
|
@@ -548,10 +657,11 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
548
657
|
})();
|
|
549
658
|
return entry.promise;
|
|
550
659
|
}
|
|
551
|
-
|
|
552
|
-
async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, signal, ...removed }) {
|
|
660
|
+
const broker = {
|
|
661
|
+
async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, signal, identityConcurrency: submissionIdentityConcurrency = identityConcurrency, ...removed }) {
|
|
553
662
|
ensureOpen();
|
|
554
663
|
requireExecutors();
|
|
664
|
+
positiveConcurrency(submissionIdentityConcurrency, 'identityConcurrency');
|
|
555
665
|
if ('mode' in removed)
|
|
556
666
|
throw new Error('Workspace modes are no longer supported; supply an unchanged workspace.');
|
|
557
667
|
if (!requesterId.trim() || requesterId.length > 200)
|
|
@@ -560,14 +670,14 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
560
670
|
const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, integrity: workspaceIntegrity, signal });
|
|
561
671
|
try {
|
|
562
672
|
const { config } = await readWorkspaceConfig(workspace.descriptor.path, workspace.signal);
|
|
563
|
-
const snapshot = await createProjectSnapshot(config, workspace.descriptor.path, workspace.descriptor.hash, workspace.signal, workspace.descriptor.integrity, selection);
|
|
673
|
+
const snapshot = await createProjectSnapshot(config, workspace.descriptor.path, workspace.descriptor.hash, workspace.signal, workspace.descriptor.integrity, selection, { identityConcurrency: submissionIdentityConcurrency });
|
|
564
674
|
const ids = includedCritics(snapshot, selection, recursive);
|
|
565
675
|
const templates = [];
|
|
566
676
|
for (const id of ids)
|
|
567
677
|
templates.push(...await prepareReviewRequests({ repoPath: workspace.descriptor.path, repoId, snapshotHash: workspace.descriptor.hash, criticId: id, preparedConfig: config }));
|
|
568
678
|
const id = randomUUID(), createdAt = now();
|
|
569
|
-
const record = { id, repoId, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
|
|
570
|
-
scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version:
|
|
679
|
+
const record = { id, repoId, coalescingGraceMs, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
|
|
680
|
+
scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 3, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
|
|
571
681
|
await workspace.assertUnchanged();
|
|
572
682
|
workspace.signal.throwIfAborted();
|
|
573
683
|
transaction(() => {
|
|
@@ -589,7 +699,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
589
699
|
ensureOpen();
|
|
590
700
|
return db.prepare('SELECT id FROM runs ORDER BY created_at DESC, rowid DESC').all().map(row => {
|
|
591
701
|
const record = required(getRun(String(row.id)), 'Run');
|
|
592
|
-
return
|
|
702
|
+
return record;
|
|
593
703
|
});
|
|
594
704
|
},
|
|
595
705
|
getRun,
|
|
@@ -618,7 +728,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
618
728
|
reconcile(existing.runId);
|
|
619
729
|
const request = required(requestData(requestId), 'request');
|
|
620
730
|
if (request.configManifest?.version !== 2)
|
|
621
|
-
throw new Error('
|
|
731
|
+
throw new Error('Invalid stored review configuration.');
|
|
622
732
|
if (request.status === 'WAITING_HUMAN' && request.claimedBy === reviewerId)
|
|
623
733
|
return request;
|
|
624
734
|
if (request.claimedBy)
|
|
@@ -661,7 +771,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
661
771
|
if (!current || current.profile.kind !== 'human' || current.status !== 'WAITING_HUMAN')
|
|
662
772
|
throw new Error('Request is not waiting for a human review.');
|
|
663
773
|
if (current.configManifest?.version !== 2)
|
|
664
|
-
throw new Error('
|
|
774
|
+
throw new Error('Invalid stored review configuration.');
|
|
665
775
|
if (!reviewerId || current.claimedBy !== reviewerId)
|
|
666
776
|
throw new Error('Only the reviewer who claimed this request can use its tools.');
|
|
667
777
|
if (!ownerAlive(ownerData(current.runId)))
|
|
@@ -731,9 +841,9 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
731
841
|
},
|
|
732
842
|
async completeHuman(requestId, { reviewerId, result }) {
|
|
733
843
|
ensureOpen();
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
844
|
+
const definition = required(requestData(requestId), 'Request');
|
|
845
|
+
validateFinalResult(result, definition);
|
|
846
|
+
const validated = validateResult(result, definition);
|
|
737
847
|
const first = requestData(requestId);
|
|
738
848
|
if (first)
|
|
739
849
|
reconcile(first.runId);
|
|
@@ -741,7 +851,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
741
851
|
if (!request || request.profile.kind !== 'human' || request.status !== 'WAITING_HUMAN')
|
|
742
852
|
throw new Error('Request is not waiting for a human review.');
|
|
743
853
|
if (request.configManifest?.version !== 2)
|
|
744
|
-
throw new Error('
|
|
854
|
+
throw new Error('Invalid stored review configuration.');
|
|
745
855
|
if (!reviewerId || request.claimedBy !== reviewerId)
|
|
746
856
|
throw new Error('Only the reviewer who claimed this request can submit its result.');
|
|
747
857
|
if (!request.notifiedAt)
|
|
@@ -822,10 +932,25 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
822
932
|
for (const entry of owned)
|
|
823
933
|
entry.abort.abort(codedError('The review worker stopped before completion.', 'WORKER_STOPPED'));
|
|
824
934
|
await Promise.allSettled(owned.map(entry => entry.promise));
|
|
935
|
+
submissionDeadlines.clear();
|
|
825
936
|
listeners.clear();
|
|
826
937
|
db.close();
|
|
827
938
|
closed = true;
|
|
828
939
|
},
|
|
829
940
|
};
|
|
941
|
+
const viewRun = (value) => resultView({ detail }, value, () => value ? requesterRun(storedRun(db, value.id) ?? value, stateDir) : null);
|
|
942
|
+
const viewRequest = (value) => resultView({ detail }, value, () => value ? requesterRequest(value, stateDir) : null);
|
|
943
|
+
return {
|
|
944
|
+
...broker,
|
|
945
|
+
submitProject: async (...args) => viewRun(await broker.submitProject(...args)),
|
|
946
|
+
run: async (...args) => viewRun(await broker.run(...args)),
|
|
947
|
+
getRun: (id) => viewRun(broker.getRun(id)),
|
|
948
|
+
listRuns: () => broker.listRuns().map(value => viewRun(value)),
|
|
949
|
+
getRequest: (id) => viewRequest(broker.getRequest(id)),
|
|
950
|
+
claimHuman: async (...args) => viewRequest(await broker.claimHuman(...args)),
|
|
951
|
+
completeHuman: async (...args) => viewRequest(await broker.completeHuman(...args)),
|
|
952
|
+
failRun: (...args) => viewRun(broker.failRun(...args)),
|
|
953
|
+
cancel: (...args) => viewRun(broker.cancel(...args)),
|
|
954
|
+
};
|
|
830
955
|
}
|
|
831
956
|
//# sourceMappingURL=index.js.map
|