@ccdd/project 4.3.0 → 5.1.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-BILt_jGR.js} +3 -3
- package/dist/monitor-ui/assets/ValidationView-D18mwExY.css +1 -0
- package/dist/monitor-ui/assets/ValidationView-L-a3hI_A.js +1 -0
- package/dist/monitor-ui/assets/{elk-api-BwZQovT5.js → elk-api-CW6DDhyE.js} +1 -1
- package/dist/monitor-ui/assets/{elk.bundled-CQowIhI8.js → elk.bundled-WR-av3nY.js} +1 -1
- package/dist/monitor-ui/assets/{index-D0yfhUlt.css → index-CRKnM9pk.css} +1 -1
- package/dist/monitor-ui/assets/index-Drtw0yLv.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/coalescing.d.ts +18 -0
- package/dist/src/broker/coalescing.js +43 -0
- package/dist/src/broker/coalescing.js.map +1 -0
- 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 +155 -77
- package/dist/src/broker/index.js.map +1 -1
- package/dist/src/broker/ownership.d.ts +9 -0
- package/dist/src/broker/ownership.js +29 -0
- package/dist/src/broker/ownership.js.map +1 -0
- 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 -9
- 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.d.ts +5 -2
- package/dist/src/project/query.js +14 -12
- package/dist/src/project/query.js.map +1 -1
- package/dist/src/project/store.d.ts +7 -3
- package/dist/src/project/store.js +20 -7
- package/dist/src/project/store.js.map +1 -1
- package/dist/src/project/types.d.ts +11 -7
- 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 +70 -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,26 +1,34 @@
|
|
|
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';
|
|
3
|
-
import {
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
4
7
|
import fs from 'node:fs';
|
|
5
8
|
import path from 'node:path';
|
|
6
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
10
|
import { prepareReviewRequests } from '../requester/index.js';
|
|
8
11
|
import { readWorkspaceConfig } from './config.js';
|
|
9
12
|
import { createGraphDefinition } from './graph.js';
|
|
10
|
-
import {
|
|
13
|
+
import { createStatusPolling } from './polling.js';
|
|
14
|
+
import { ownerAlive, ownProcessIdentity } from './ownership.js';
|
|
15
|
+
import { coalescingEligibility, findCoalescibleRequest } from './coalescing.js';
|
|
16
|
+
import { tokenUsage, toolResponseBytes } from '../executors/telemetry.js';
|
|
11
17
|
import { finalResultEventData } from '../executors/final-result.js';
|
|
12
18
|
import { normalizeReviewResult as validateResult, storedObservation } from '../review-result.js';
|
|
13
19
|
import { createReviewTools } from '../tools/runner.js';
|
|
14
20
|
import { createHumanClaims, HUMAN_PREPARATION_LEASE_MS } from './human-claims.js';
|
|
15
21
|
import { prepareHumanReview } from '../executors/human-preparation.js';
|
|
16
22
|
import { prepareWorkspace, reopenWorkspace } from '../workspaces/index.js';
|
|
17
|
-
import { createProjectSnapshot } from '../project/identity.js';
|
|
23
|
+
import { createProjectSnapshot, DEFAULT_IDENTITY_CONCURRENCY, positiveConcurrency } from '../project/identity.js';
|
|
18
24
|
import { includedCritics, planProject } from '../project/query.js';
|
|
19
|
-
import { readEvidence } from '../project/store.js';
|
|
25
|
+
import { readEvidence, storedRun } from '../project/store.js';
|
|
26
|
+
/** Internal instrumentation for deterministic scheduler regression tests. */
|
|
27
|
+
export const brokerTestHooks = {};
|
|
20
28
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
29
|
const errorCode = (error) => object(error) && typeof error.code === 'string' ? error.code : undefined;
|
|
22
30
|
const parseStored = (value) => { if (typeof value !== 'string')
|
|
23
|
-
throw new Error('Broker store contains a non-text JSON record.'); return JSON.parse(value); };
|
|
31
|
+
throw new Error('Broker store contains a non-text JSON record.'); brokerTestHooks.onHydrate?.(Buffer.byteLength(value)); return JSON.parse(value); };
|
|
24
32
|
const required = (value, label) => { if (value == null)
|
|
25
33
|
throw new Error(`Unknown ${label}.`); return value; };
|
|
26
34
|
const terminal = new Set(['GREEN', 'RED', 'ERROR', 'INCOMPLETE']);
|
|
@@ -28,37 +36,10 @@ const now = () => new Date().toISOString();
|
|
|
28
36
|
const errorText = (error) => String(object(error) && typeof error.message === 'string' ? error.message : error).slice(0, 2000);
|
|
29
37
|
const copy = (value) => JSON.parse(JSON.stringify(value));
|
|
30
38
|
const codedError = (message, code) => Object.assign(new Error(message), { code });
|
|
31
|
-
const MAX_CONCURRENT_EXECUTORS = 4;
|
|
32
39
|
const fatalExecutionError = (error) => {
|
|
33
40
|
const code = errorCode(error);
|
|
34
41
|
return Boolean(code?.startsWith('WORKSPACE_') || ['RUN_OWNERSHIP_LOST', 'REVIEW_CANCELED', 'WORKER_STOPPED', 'WORKER_EXITED', 'REVIEW_GRAPH_INVALID'].includes(code ?? ''));
|
|
35
42
|
};
|
|
36
|
-
function processIdentity(pid) {
|
|
37
|
-
try {
|
|
38
|
-
if (process.platform === 'linux') {
|
|
39
|
-
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
40
|
-
return `linux:${stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]}`;
|
|
41
|
-
}
|
|
42
|
-
return `ps:${execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).trim()}`;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const ownProcessIdentity = processIdentity(process.pid);
|
|
49
|
-
function ownerAlive(owner) {
|
|
50
|
-
if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0)
|
|
51
|
-
return false;
|
|
52
|
-
try {
|
|
53
|
-
process.kill(owner.pid, 0);
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
if (errorCode(error) === 'ESRCH')
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
const identity = owner.pid === process.pid ? ownProcessIdentity : processIdentity(owner.pid);
|
|
60
|
-
return !owner.process_identity || !identity || owner.process_identity === identity;
|
|
61
|
-
}
|
|
62
43
|
function canonicalFuturePath(value) {
|
|
63
44
|
let existing = path.resolve(value);
|
|
64
45
|
const missing = [];
|
|
@@ -82,6 +63,7 @@ export function readStateContext(stateDir) {
|
|
|
82
63
|
// A worker closing the last WAL connection can briefly lock even read-only queries.
|
|
83
64
|
const database = new DatabaseSync(filename, { readOnly: true, timeout: 5000 });
|
|
84
65
|
try {
|
|
66
|
+
assertStateFormat(database);
|
|
85
67
|
const row = database.prepare('SELECT value FROM metadata WHERE key = ?').get('registered-repo');
|
|
86
68
|
const identity = row ? parseStored(row.value) : null;
|
|
87
69
|
if (!object(identity) || typeof identity.repoPath !== 'string' || !path.isAbsolute(identity.repoPath) || typeof identity.repoId !== 'string')
|
|
@@ -104,7 +86,13 @@ function prepareStateDirectory(repoPath, stateDir) {
|
|
|
104
86
|
return fs.realpathSync(canonical);
|
|
105
87
|
}
|
|
106
88
|
/** 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 } }) {
|
|
89
|
+
export function createBroker({ detail, repoPath, stateDir, repoId = 'demo', coalescingGraceMs = 15_000, maxConcurrentExecutors = 4, identityConcurrency = DEFAULT_IDENTITY_CONCURRENCY, executors, workspaceIntegrity = 'content', workspaceAdapter = { prepareWorkspace, reopenWorkspace } }) {
|
|
90
|
+
positiveConcurrency(maxConcurrentExecutors, 'maxConcurrentExecutors');
|
|
91
|
+
positiveConcurrency(identityConcurrency, 'identityConcurrency');
|
|
92
|
+
if (!Number.isSafeInteger(coalescingGraceMs) || coalescingGraceMs < 0 || coalescingGraceMs > 300_000)
|
|
93
|
+
throw new Error('coalescingGraceMs must be an integer from 0 to 300000.');
|
|
94
|
+
if (detail !== undefined && detail !== 'compact' && detail !== 'full')
|
|
95
|
+
throw new Error('detail must be compact or full.');
|
|
108
96
|
if (!['content', 'metadata'].includes(workspaceIntegrity))
|
|
109
97
|
throw new Error('Workspace integrity must be content or metadata.');
|
|
110
98
|
try {
|
|
@@ -126,7 +114,10 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
126
114
|
let db;
|
|
127
115
|
try {
|
|
128
116
|
db = new DatabaseSync(path.join(stateDir, 'broker.sqlite'));
|
|
129
|
-
db.exec(
|
|
117
|
+
db.exec('PRAGMA busy_timeout=5000; BEGIN IMMEDIATE');
|
|
118
|
+
initializeStateFormat(db);
|
|
119
|
+
db.exec('COMMIT');
|
|
120
|
+
db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
|
|
130
121
|
CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, created_at TEXT NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
|
|
131
122
|
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
123
|
CREATE INDEX IF NOT EXISTS requests_run ON requests(run_id, ordinal);
|
|
@@ -147,12 +138,13 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
147
138
|
db?.close();
|
|
148
139
|
throw error;
|
|
149
140
|
}
|
|
141
|
+
const polling = createStatusPolling(db);
|
|
150
142
|
let closed = false;
|
|
151
143
|
let closing = false;
|
|
152
144
|
const active = new Map();
|
|
153
145
|
const listeners = new Set();
|
|
154
146
|
const ensureOpen = () => { if (closed || closing)
|
|
155
|
-
throw new Error('Broker is closed.'); };
|
|
147
|
+
throw new Error('Broker is closed.'); sweepSubmissionDeadlines(); };
|
|
156
148
|
const requireExecutors = () => {
|
|
157
149
|
if (!executors || typeof executors.canExecute !== 'function' || typeof executors.execute !== 'function')
|
|
158
150
|
throw new Error('Review submission and execution require an executor registry.');
|
|
@@ -180,8 +172,27 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
180
172
|
};
|
|
181
173
|
const ownerData = (id) => db.prepare('SELECT * FROM run_owners WHERE run_id = ?').get(id);
|
|
182
174
|
const runRequests = (id) => db.prepare('SELECT data FROM requests WHERE run_id = ? ORDER BY ordinal').all(id).map(row => parseStored(row.data));
|
|
175
|
+
const sharedRequests = (run) => (run.project?.coalescedRequestIds ?? []).map(id => required(requestData(id), 'Shared Request'));
|
|
176
|
+
const attemptsFor = (run) => [...runRequests(run.id), ...sharedRequests(run)];
|
|
177
|
+
const submissionDeadlines = new Map();
|
|
178
|
+
function sweepSubmissionDeadlines() {
|
|
179
|
+
for (const id of submissionDeadlines.keys()) {
|
|
180
|
+
const status = polling.runStatus(id);
|
|
181
|
+
// Retain expired unowned sources: dropping their monotonic tombstone could
|
|
182
|
+
// renew eligibility after a wall-clock rollback. Owned Runs cannot revert
|
|
183
|
+
// to an abandoned submission; worker exit is reconciled as terminal.
|
|
184
|
+
if (!status || terminal.has(status) || ownerData(id))
|
|
185
|
+
submissionDeadlines.delete(id);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const coalescing = { reconcile: reconcileWithin, deadlines: submissionDeadlines };
|
|
189
|
+
const shareable = (request) => coalescingEligibility(db, request, coalescing) !== null;
|
|
183
190
|
const saveRequest = (request) => db.prepare('UPDATE requests SET status = ?, data = ? WHERE id = ?').run(request.status, JSON.stringify(request), request.id);
|
|
184
|
-
const saveRun = (run) =>
|
|
191
|
+
const saveRun = (run) => {
|
|
192
|
+
db.prepare('UPDATE runs SET status = ?, data = ? WHERE id = ?').run(run.status, JSON.stringify(run), run.id);
|
|
193
|
+
if (terminal.has(run.status))
|
|
194
|
+
submissionDeadlines.delete(run.id);
|
|
195
|
+
};
|
|
185
196
|
const appendEvent = (runId, requestId, type, message, data = null) => {
|
|
186
197
|
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
198
|
};
|
|
@@ -196,7 +207,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
196
207
|
assertWaiting: request => {
|
|
197
208
|
ensureOpen();
|
|
198
209
|
if (request.configManifest?.version !== 2)
|
|
199
|
-
throw new Error('
|
|
210
|
+
throw new Error('Invalid stored review configuration.');
|
|
200
211
|
if (!request.notifiedAt)
|
|
201
212
|
throw new Error('Human alarm delivery is still pending.');
|
|
202
213
|
if (!ownerAlive(ownerData(request.runId)))
|
|
@@ -204,12 +215,33 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
204
215
|
},
|
|
205
216
|
});
|
|
206
217
|
function refreshReadinessWithin(runId) {
|
|
218
|
+
sweepSubmissionDeadlines();
|
|
207
219
|
const run = required(runData(runId), 'Run'), requests = runRequests(runId);
|
|
208
|
-
if (run.project?.version ===
|
|
220
|
+
if (run.project?.version === 3) {
|
|
209
221
|
if (terminal.has(run.status))
|
|
210
222
|
return;
|
|
211
|
-
const
|
|
223
|
+
for (const shared of sharedRequests(run))
|
|
224
|
+
reconcileWithin(shared.runId);
|
|
225
|
+
// Called in a write transaction: expire only unowned queued sources, then
|
|
226
|
+
// recheck all matching owned/leased requests before creating our own ticket.
|
|
227
|
+
const retained = sharedRequests(run).filter(request => terminal.has(request.status) || shareable(request));
|
|
228
|
+
if (retained.length !== (run.project.coalescedRequestIds ?? []).length) {
|
|
229
|
+
run.project.coalescedRequestIds = retained.map(request => request.id);
|
|
230
|
+
saveRun(run);
|
|
231
|
+
appendEvent(runId, null, 'request.uncoalesced', 'Unowned source submission lease expired; replanning this Run.');
|
|
232
|
+
}
|
|
233
|
+
brokerTestHooks.onPlan?.();
|
|
234
|
+
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
235
|
for (const item of plan.items.filter(item => item.action === 'EXECUTE')) {
|
|
236
|
+
if (!run.project.force) {
|
|
237
|
+
const shared = findCoalescibleRequest(db, item.id, item.input.key, coalescing)?.request;
|
|
238
|
+
if (shared) {
|
|
239
|
+
run.project.coalescedRequestIds = [...new Set([...(run.project.coalescedRequestIds ?? []), shared.id])];
|
|
240
|
+
saveRun(run);
|
|
241
|
+
appendEvent(runId, shared.id, 'request.coalesced', 'Waiting for an identical active review in another Run.', { sourceRunId: shared.runId });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
213
245
|
const envelope = run.project.templates.find(template => template.criticId === item.id);
|
|
214
246
|
if (!envelope)
|
|
215
247
|
throw codedError(`Missing prepared Critic ${item.id}.`, 'REVIEW_GRAPH_INVALID');
|
|
@@ -222,17 +254,18 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
222
254
|
}
|
|
223
255
|
return;
|
|
224
256
|
}
|
|
225
|
-
throw new Error('
|
|
257
|
+
throw new Error('Invalid stored Run format; submit a new validation request.');
|
|
226
258
|
}
|
|
227
259
|
const updateRunStatus = (runId) => {
|
|
228
|
-
const states = runRequests(runId).map(request => request.status);
|
|
229
260
|
const run = required(runData(runId), 'Run');
|
|
230
|
-
|
|
261
|
+
const states = attemptsFor(run).map(request => request.status);
|
|
262
|
+
if (run.project?.version === 3) {
|
|
231
263
|
if (terminal.has(run.status))
|
|
232
264
|
return;
|
|
233
|
-
|
|
265
|
+
brokerTestHooks.onPlan?.();
|
|
266
|
+
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
267
|
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';
|
|
268
|
+
states.includes('ERROR') ? 'ERROR' : states.includes('RED') || plan.critics.some(critic => critic.status === 'RED') ? 'RED' : plan.satisfied ? 'GREEN' : 'INCOMPLETE';
|
|
236
269
|
if (run.status !== status) {
|
|
237
270
|
run.status = status;
|
|
238
271
|
if (terminal.has(status)) {
|
|
@@ -244,7 +277,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
244
277
|
}
|
|
245
278
|
return;
|
|
246
279
|
}
|
|
247
|
-
throw new Error('
|
|
280
|
+
throw new Error('Invalid stored Run format.');
|
|
248
281
|
};
|
|
249
282
|
function finishWithin(requestId, outcome, expectedStates = ['RUNNING', 'WAITING_HUMAN']) {
|
|
250
283
|
const { result, error } = outcome, hasError = Object.hasOwn(outcome, 'error');
|
|
@@ -257,7 +290,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
257
290
|
request.errorCode = errorCode(error) ?? null;
|
|
258
291
|
request.completedAt = now();
|
|
259
292
|
saveRequest(request);
|
|
260
|
-
appendEvent(request.runId, request.id, hasError ? 'request.error' : 'request.completed', hasError ? required(request.error, 'error message') : required(result, 'review result').
|
|
293
|
+
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
294
|
refreshReadinessWithin(request.runId);
|
|
262
295
|
updateRunStatus(request.runId);
|
|
263
296
|
return true;
|
|
@@ -278,8 +311,9 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
278
311
|
saveRequest(request);
|
|
279
312
|
appendEvent(runId, request.id, 'request.error', request.error, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
|
|
280
313
|
}
|
|
281
|
-
if (run.project?.version ===
|
|
282
|
-
|
|
314
|
+
if (run.project?.version === 3) {
|
|
315
|
+
brokerTestHooks.onPlan?.();
|
|
316
|
+
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
317
|
run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
|
|
284
318
|
}
|
|
285
319
|
run.status = 'ERROR';
|
|
@@ -392,6 +426,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
392
426
|
if (typeof event[key] === 'string')
|
|
393
427
|
safe[key] = event[key].slice(0, 1000);
|
|
394
428
|
if (event.type === 'artifact.tool.completed') {
|
|
429
|
+
Object.assign(safe, toolResponseBytes(event));
|
|
395
430
|
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
431
|
safe.startedAt = event.startedAt;
|
|
397
432
|
if (typeof event.durationMs === 'number' && Number.isFinite(event.durationMs) && event.durationMs >= 0)
|
|
@@ -421,6 +456,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
421
456
|
changed();
|
|
422
457
|
},
|
|
423
458
|
}));
|
|
459
|
+
validateFinalResult(semanticResult(result), request);
|
|
424
460
|
await workspace.assertUnchanged();
|
|
425
461
|
signal.throwIfAborted();
|
|
426
462
|
transaction(() => { if (ownerData(runId)?.token === token)
|
|
@@ -446,12 +482,12 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
446
482
|
stored = runData(runId);
|
|
447
483
|
if (!stored)
|
|
448
484
|
throw new Error('Unknown Run.');
|
|
449
|
-
if (stored.project?.version !==
|
|
450
|
-
throw new Error('
|
|
485
|
+
if (stored.project?.version !== 3)
|
|
486
|
+
throw new Error('Invalid stored Run format; submit a new validation request.');
|
|
451
487
|
if (terminal.has(stored.status))
|
|
452
488
|
return false;
|
|
453
489
|
if (!stored.workspace)
|
|
454
|
-
throw new Error('
|
|
490
|
+
throw new Error('Invalid Run: no workspace descriptor.');
|
|
455
491
|
if (ownerData(runId))
|
|
456
492
|
throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
|
|
457
493
|
db.prepare('INSERT INTO run_owners(run_id,pid,process_identity,token,claimed_at) VALUES (?,?,?,?,?)').run(runId, process.pid, ownProcessIdentity, token, now());
|
|
@@ -469,6 +505,10 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
469
505
|
let workspace;
|
|
470
506
|
let poll;
|
|
471
507
|
const inFlight = new Map();
|
|
508
|
+
let plannedRevision = -1, leaseDeadline = Infinity;
|
|
509
|
+
let pendingSources = [];
|
|
510
|
+
const notified = new Set();
|
|
511
|
+
const kinds = new Map();
|
|
472
512
|
try {
|
|
473
513
|
onStarted?.({ runId, pid: process.pid });
|
|
474
514
|
workspace = await workspaceAdapter.reopenWorkspace(ownedRun.workspace, { signal: executionSignal });
|
|
@@ -476,48 +516,70 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
476
516
|
poll = setInterval(() => {
|
|
477
517
|
if (ownerData(runId)?.token !== token)
|
|
478
518
|
abort.abort(codedError('Review ownership was lost.', 'RUN_OWNERSHIP_LOST'));
|
|
479
|
-
else if (terminal.has(required(
|
|
519
|
+
else if (terminal.has(required(polling.runStatus(runId), 'Run')))
|
|
480
520
|
abort.abort(codedError('Review is already complete or canceled.', 'REVIEW_CANCELED'));
|
|
481
521
|
}, 100);
|
|
482
522
|
while (true) {
|
|
483
|
-
const current = required(
|
|
484
|
-
if (terminal.has(current
|
|
523
|
+
const current = required(polling.runStatus(runId), 'Run');
|
|
524
|
+
if (terminal.has(current)) {
|
|
485
525
|
if (inFlight.size)
|
|
486
526
|
abort.abort(codedError('Review is already complete or canceled.', 'REVIEW_CANCELED'));
|
|
487
527
|
break;
|
|
488
528
|
}
|
|
489
529
|
reviewSignal.throwIfAborted();
|
|
490
|
-
|
|
491
|
-
|
|
530
|
+
const sourceExited = pendingSources.some(id => { const owner = ownerData(id); return owner !== undefined && !ownerAlive(owner); });
|
|
531
|
+
if (polling.revision() !== plannedRevision || performance.now() >= leaseDeadline || sourceExited)
|
|
532
|
+
transaction(() => {
|
|
533
|
+
refreshReadinessWithin(runId);
|
|
534
|
+
updateRunStatus(runId);
|
|
535
|
+
const pending = sharedRequests(required(runData(runId), 'Run')).filter(request => !terminal.has(request.status));
|
|
536
|
+
pendingSources = [...new Set(pending.map(request => request.runId))];
|
|
537
|
+
leaseDeadline = Math.min(Infinity, ...pendingSources.filter(id => !ownerData(id)).map(id => submissionDeadlines.get(id) ?? performance.now()));
|
|
538
|
+
plannedRevision = polling.revision();
|
|
539
|
+
});
|
|
540
|
+
const requests = polling.requests(runId);
|
|
492
541
|
let executing = [...inFlight.values()].filter(item => item.kind !== 'human').length;
|
|
493
|
-
for (const
|
|
494
|
-
|
|
542
|
+
for (const row of requests.filter(request => request.status === 'QUEUED')) {
|
|
543
|
+
const kind = kinds.get(row.id) ?? required(requestData(row.id), 'Request').profile.kind;
|
|
544
|
+
kinds.set(row.id, kind);
|
|
545
|
+
if (kind !== 'human' && executing >= maxConcurrentExecutors)
|
|
495
546
|
continue;
|
|
496
|
-
if (
|
|
547
|
+
if (kind !== 'human')
|
|
497
548
|
executing++;
|
|
498
|
-
const promise = executeOne(
|
|
549
|
+
const promise = executeOne(row.id, workspace, token, reviewSignal).catch(error => {
|
|
499
550
|
// A different task may win Promise.race before this rejection. Preserve fatal failure independently of that race.
|
|
500
551
|
abort.abort(error);
|
|
501
552
|
throw error;
|
|
502
|
-
}).finally(() => { inFlight.delete(
|
|
503
|
-
inFlight.set(
|
|
553
|
+
}).finally(() => { inFlight.delete(row.id); });
|
|
554
|
+
inFlight.set(row.id, { kind, promise });
|
|
504
555
|
}
|
|
505
556
|
if (inFlight.size) {
|
|
506
557
|
// Human alarms and independent evaluations progress together.
|
|
507
558
|
await Promise.race([...inFlight.values()].map(item => item.promise).concat(delay(50, undefined, { signal: reviewSignal })));
|
|
508
559
|
continue;
|
|
509
560
|
}
|
|
561
|
+
if (pendingSources.length) {
|
|
562
|
+
brokerTestHooks.onIdleTick?.();
|
|
563
|
+
await delay(Math.max(1, Math.min(100, leaseDeadline - performance.now())), undefined, { signal: reviewSignal });
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
510
566
|
const waiting = requests.filter(request => request.status === 'WAITING_HUMAN');
|
|
511
567
|
if (waiting.length) {
|
|
512
|
-
|
|
513
|
-
|
|
568
|
+
for (const request of waiting) {
|
|
569
|
+
if (notified.has(request.id))
|
|
570
|
+
continue;
|
|
571
|
+
if (!required(requestData(request.id), 'Request').notifiedAt)
|
|
572
|
+
throw new Error('Human notification did not complete; submit a new Run to retry.');
|
|
573
|
+
notified.add(request.id);
|
|
574
|
+
}
|
|
514
575
|
// The workspace observer stays alive with filesystem events and metadata polls.
|
|
515
576
|
// Notification already crossed its final content boundary; idle waiting
|
|
516
577
|
// must not rehash the entire workspace on every scheduling iteration.
|
|
578
|
+
brokerTestHooks.onIdleTick?.();
|
|
517
579
|
await delay(100, undefined, { signal: reviewSignal });
|
|
518
580
|
continue;
|
|
519
581
|
}
|
|
520
|
-
if (terminal.has(required(
|
|
582
|
+
if (terminal.has(required(polling.runStatus(runId), 'Run')))
|
|
521
583
|
break;
|
|
522
584
|
throw codedError('Run has no executable request or pending Human review.', 'REVIEW_GRAPH_INVALID');
|
|
523
585
|
}
|
|
@@ -548,10 +610,11 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
548
610
|
})();
|
|
549
611
|
return entry.promise;
|
|
550
612
|
}
|
|
551
|
-
|
|
552
|
-
async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, signal, ...removed }) {
|
|
613
|
+
const broker = {
|
|
614
|
+
async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, signal, identityConcurrency: submissionIdentityConcurrency = identityConcurrency, ...removed }) {
|
|
553
615
|
ensureOpen();
|
|
554
616
|
requireExecutors();
|
|
617
|
+
positiveConcurrency(submissionIdentityConcurrency, 'identityConcurrency');
|
|
555
618
|
if ('mode' in removed)
|
|
556
619
|
throw new Error('Workspace modes are no longer supported; supply an unchanged workspace.');
|
|
557
620
|
if (!requesterId.trim() || requesterId.length > 200)
|
|
@@ -560,14 +623,14 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
560
623
|
const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, integrity: workspaceIntegrity, signal });
|
|
561
624
|
try {
|
|
562
625
|
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);
|
|
626
|
+
const snapshot = await createProjectSnapshot(config, workspace.descriptor.path, workspace.descriptor.hash, workspace.signal, workspace.descriptor.integrity, selection, { identityConcurrency: submissionIdentityConcurrency });
|
|
564
627
|
const ids = includedCritics(snapshot, selection, recursive);
|
|
565
628
|
const templates = [];
|
|
566
629
|
for (const id of ids)
|
|
567
630
|
templates.push(...await prepareReviewRequests({ repoPath: workspace.descriptor.path, repoId, snapshotHash: workspace.descriptor.hash, criticId: id, preparedConfig: config }));
|
|
568
631
|
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:
|
|
632
|
+
const record = { id, repoId, coalescingGraceMs, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
|
|
633
|
+
scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 3, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
|
|
571
634
|
await workspace.assertUnchanged();
|
|
572
635
|
workspace.signal.throwIfAborted();
|
|
573
636
|
transaction(() => {
|
|
@@ -589,7 +652,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
589
652
|
ensureOpen();
|
|
590
653
|
return db.prepare('SELECT id FROM runs ORDER BY created_at DESC, rowid DESC').all().map(row => {
|
|
591
654
|
const record = required(getRun(String(row.id)), 'Run');
|
|
592
|
-
return
|
|
655
|
+
return record;
|
|
593
656
|
});
|
|
594
657
|
},
|
|
595
658
|
getRun,
|
|
@@ -618,7 +681,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
618
681
|
reconcile(existing.runId);
|
|
619
682
|
const request = required(requestData(requestId), 'request');
|
|
620
683
|
if (request.configManifest?.version !== 2)
|
|
621
|
-
throw new Error('
|
|
684
|
+
throw new Error('Invalid stored review configuration.');
|
|
622
685
|
if (request.status === 'WAITING_HUMAN' && request.claimedBy === reviewerId)
|
|
623
686
|
return request;
|
|
624
687
|
if (request.claimedBy)
|
|
@@ -661,7 +724,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
661
724
|
if (!current || current.profile.kind !== 'human' || current.status !== 'WAITING_HUMAN')
|
|
662
725
|
throw new Error('Request is not waiting for a human review.');
|
|
663
726
|
if (current.configManifest?.version !== 2)
|
|
664
|
-
throw new Error('
|
|
727
|
+
throw new Error('Invalid stored review configuration.');
|
|
665
728
|
if (!reviewerId || current.claimedBy !== reviewerId)
|
|
666
729
|
throw new Error('Only the reviewer who claimed this request can use its tools.');
|
|
667
730
|
if (!ownerAlive(ownerData(current.runId)))
|
|
@@ -731,9 +794,9 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
731
794
|
},
|
|
732
795
|
async completeHuman(requestId, { reviewerId, result }) {
|
|
733
796
|
ensureOpen();
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
797
|
+
const definition = required(requestData(requestId), 'Request');
|
|
798
|
+
validateFinalResult(result, definition);
|
|
799
|
+
const validated = validateResult(result, definition);
|
|
737
800
|
const first = requestData(requestId);
|
|
738
801
|
if (first)
|
|
739
802
|
reconcile(first.runId);
|
|
@@ -741,7 +804,7 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
741
804
|
if (!request || request.profile.kind !== 'human' || request.status !== 'WAITING_HUMAN')
|
|
742
805
|
throw new Error('Request is not waiting for a human review.');
|
|
743
806
|
if (request.configManifest?.version !== 2)
|
|
744
|
-
throw new Error('
|
|
807
|
+
throw new Error('Invalid stored review configuration.');
|
|
745
808
|
if (!reviewerId || request.claimedBy !== reviewerId)
|
|
746
809
|
throw new Error('Only the reviewer who claimed this request can submit its result.');
|
|
747
810
|
if (!request.notifiedAt)
|
|
@@ -822,10 +885,25 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
|
|
|
822
885
|
for (const entry of owned)
|
|
823
886
|
entry.abort.abort(codedError('The review worker stopped before completion.', 'WORKER_STOPPED'));
|
|
824
887
|
await Promise.allSettled(owned.map(entry => entry.promise));
|
|
888
|
+
submissionDeadlines.clear();
|
|
825
889
|
listeners.clear();
|
|
826
890
|
db.close();
|
|
827
891
|
closed = true;
|
|
828
892
|
},
|
|
829
893
|
};
|
|
894
|
+
const viewRun = (value) => resultView({ detail }, value, () => value ? requesterRun(storedRun(db, value.id) ?? value, stateDir) : null);
|
|
895
|
+
const viewRequest = (value) => resultView({ detail }, value, () => value ? requesterRequest(value, stateDir) : null);
|
|
896
|
+
return {
|
|
897
|
+
...broker,
|
|
898
|
+
submitProject: async (...args) => viewRun(await broker.submitProject(...args)),
|
|
899
|
+
run: async (...args) => viewRun(await broker.run(...args)),
|
|
900
|
+
getRun: (id) => viewRun(broker.getRun(id)),
|
|
901
|
+
listRuns: () => broker.listRuns().map(value => viewRun(value)),
|
|
902
|
+
getRequest: (id) => viewRequest(broker.getRequest(id)),
|
|
903
|
+
claimHuman: async (...args) => viewRequest(await broker.claimHuman(...args)),
|
|
904
|
+
completeHuman: async (...args) => viewRequest(await broker.completeHuman(...args)),
|
|
905
|
+
failRun: (...args) => viewRun(broker.failRun(...args)),
|
|
906
|
+
cancel: (...args) => viewRun(broker.cancel(...args)),
|
|
907
|
+
};
|
|
830
908
|
}
|
|
831
909
|
//# sourceMappingURL=index.js.map
|