@ccdd/project 5.1.1 → 6.0.0

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