@ccdd/project 4.2.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.
Files changed (94) hide show
  1. package/dist/monitor-ui/assets/{GraphView-6yzqBOLv.js → GraphView-BDrRFY2b.js} +3 -3
  2. package/dist/monitor-ui/assets/ValidationView-CznEpoWz.js +1 -0
  3. package/dist/monitor-ui/assets/ValidationView-DBRoun_B.css +1 -0
  4. package/dist/monitor-ui/assets/{elk-api-BwZQovT5.js → elk-api-BJkhNJwW.js} +1 -1
  5. package/dist/monitor-ui/assets/{elk.bundled-CQowIhI8.js → elk.bundled-uROSxMms.js} +1 -1
  6. package/dist/monitor-ui/assets/{index-D0yfhUlt.css → index-CRKnM9pk.css} +1 -1
  7. package/dist/monitor-ui/assets/index-CTW1fvAj.js +6 -0
  8. package/dist/monitor-ui/index.html +2 -2
  9. package/dist/src/artifacts/tool-check.js +1 -3
  10. package/dist/src/artifacts/tool-check.js.map +1 -1
  11. package/dist/src/broker/config.js +22 -3
  12. package/dist/src/broker/config.js.map +1 -1
  13. package/dist/src/broker/index.d.ts +45 -84
  14. package/dist/src/broker/index.js +176 -51
  15. package/dist/src/broker/index.js.map +1 -1
  16. package/dist/src/broker/polling.d.ts +11 -0
  17. package/dist/src/broker/polling.js +27 -0
  18. package/dist/src/broker/polling.js.map +1 -0
  19. package/dist/src/cli-cancellation.d.ts +2 -0
  20. package/dist/src/cli-cancellation.js +15 -0
  21. package/dist/src/cli-cancellation.js.map +1 -0
  22. package/dist/src/contracts.d.ts +4 -4
  23. package/dist/src/definitions.d.ts +8 -1
  24. package/dist/src/executors/index.js +6 -21
  25. package/dist/src/executors/index.js.map +1 -1
  26. package/dist/src/executors/pi.d.ts +3 -1
  27. package/dist/src/executors/pi.js +2 -0
  28. package/dist/src/executors/pi.js.map +1 -1
  29. package/dist/src/executors/telemetry.d.ts +2 -0
  30. package/dist/src/executors/telemetry.js +12 -0
  31. package/dist/src/executors/telemetry.js.map +1 -1
  32. package/dist/src/monitor/actions.d.ts +1 -1
  33. package/dist/src/monitor/actions.js +3 -3
  34. package/dist/src/monitor/actions.js.map +1 -1
  35. package/dist/src/monitor/server.js +10 -5
  36. package/dist/src/monitor/server.js.map +1 -1
  37. package/dist/src/monitor/store.js +10 -6
  38. package/dist/src/monitor/store.js.map +1 -1
  39. package/dist/src/monitor/types.d.ts +5 -7
  40. package/dist/src/project/cli.js +76 -38
  41. package/dist/src/project/cli.js.map +1 -1
  42. package/dist/src/project/identity.d.ts +7 -2
  43. package/dist/src/project/identity.js +57 -8
  44. package/dist/src/project/identity.js.map +1 -1
  45. package/dist/src/project/index.d.ts +21 -6
  46. package/dist/src/project/index.js +19 -8
  47. package/dist/src/project/index.js.map +1 -1
  48. package/dist/src/project/owner-identity.d.ts +7 -0
  49. package/dist/src/project/owner-identity.js +43 -0
  50. package/dist/src/project/owner-identity.js.map +1 -0
  51. package/dist/src/project/prune.d.ts +12 -0
  52. package/dist/src/project/prune.js +132 -0
  53. package/dist/src/project/prune.js.map +1 -0
  54. package/dist/src/project/query.d.ts +3 -3
  55. package/dist/src/project/query.js +14 -13
  56. package/dist/src/project/query.js.map +1 -1
  57. package/dist/src/project/store.d.ts +4 -3
  58. package/dist/src/project/store.js +12 -7
  59. package/dist/src/project/store.js.map +1 -1
  60. package/dist/src/project/types.d.ts +16 -6
  61. package/dist/src/requester/index.js +1 -1
  62. package/dist/src/requester/index.js.map +1 -1
  63. package/dist/src/response-schema.d.ts +13 -0
  64. package/dist/src/response-schema.js +33 -0
  65. package/dist/src/response-schema.js.map +1 -0
  66. package/dist/src/result-view.d.ts +72 -0
  67. package/dist/src/result-view.js +44 -0
  68. package/dist/src/result-view.js.map +1 -0
  69. package/dist/src/review/local-claim.d.ts +1 -1
  70. package/dist/src/review/local-claim.js +4 -11
  71. package/dist/src/review/local-claim.js.map +1 -1
  72. package/dist/src/review-result.d.ts +2 -1
  73. package/dist/src/review-result.js +9 -8
  74. package/dist/src/review-result.js.map +1 -1
  75. package/dist/src/state-format.d.ts +8 -0
  76. package/dist/src/state-format.js +18 -0
  77. package/dist/src/state-format.js.map +1 -0
  78. package/dist/src/tools/environment-host.js +1 -1
  79. package/dist/src/tools/environment-host.js.map +1 -1
  80. package/dist/src/tools/environment.d.ts +17 -0
  81. package/dist/src/tools/environment.js +20 -17
  82. package/dist/src/tools/environment.js.map +1 -1
  83. package/dist/src/tools/runner.d.ts +2 -0
  84. package/dist/src/tools/runner.js +41 -11
  85. package/dist/src/tools/runner.js.map +1 -1
  86. package/dist/src/worker-client.d.ts +3 -2
  87. package/dist/src/worker-client.js +7 -4
  88. package/dist/src/worker-client.js.map +1 -1
  89. package/dist/src/worker.js +1 -1
  90. package/dist/src/worker.js.map +1 -1
  91. package/package.json +2 -2
  92. package/dist/monitor-ui/assets/ValidationView-BMqI2XQ5.css +0 -1
  93. package/dist/monitor-ui/assets/ValidationView-BwHWjMUI.js +0 -1
  94. package/dist/monitor-ui/assets/index-CoJ9_gC3.js +0 -7
@@ -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 { tokenUsage } from '../executors/telemetry.js';
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(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
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) => db.prepare('UPDATE runs SET status = ?, data = ? WHERE id = ?').run(run.status, JSON.stringify(run), run.id);
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('Historical reviews are available for result lookup only.');
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 === 2) {
266
+ if (run.project?.version === 3) {
209
267
  if (terminal.has(run.status))
210
268
  return;
211
- const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: requests });
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('Historical Runs cannot be resumed; submit a new validation request.');
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
- if (run.project?.version === 2) {
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
- const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
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('Historical Runs are available for result lookup only.');
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').summary, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
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 === 2) {
282
- const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
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 !== 2)
450
- throw new Error('Historical Runs cannot be resumed; submit a new validation request.');
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('This legacy Run has no workspace descriptor; submit a new review.');
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(runData(runId), 'Run').status))
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(runData(runId), 'Run');
484
- if (terminal.has(current.status)) {
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
- transaction(() => { refreshReadinessWithin(runId); updateRunStatus(runId); });
491
- const requests = runRequests(runId);
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 queued of requests.filter(request => request.status === 'QUEUED')) {
494
- if (queued.profile.kind !== 'human' && executing >= MAX_CONCURRENT_EXECUTORS)
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 (queued.profile.kind !== 'human')
594
+ if (kind !== 'human')
497
595
  executing++;
498
- const promise = executeOne(queued.id, workspace, token, reviewSignal).catch(error => {
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(queued.id); });
503
- inFlight.set(queued.id, { kind: queued.profile.kind, promise });
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
- if (waiting.some(request => !request.notifiedAt))
513
- throw new Error('Human notification did not complete; submit a new Run to retry.');
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(runData(runId), 'Run').status))
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,26 +657,27 @@ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, w
548
657
  })();
549
658
  return entry.promise;
550
659
  }
551
- return {
552
- async submitProject({ requesterId = 'cli', selection, recursive = false, force = false, ...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)
558
668
  throw new Error('requesterId is required (maximum 200 characters).');
559
669
  await requireExecutors().validateWorkspace?.(repoPath);
560
- const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, integrity: workspaceIntegrity });
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);
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: 2, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
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 { ...record, events: undefined, requests: record.requests.map(({ result, ...request }) => ({ ...request, result: result ? { verdict: result.verdict, summary: result.summary } : null })) };
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('Historical reviews are available for result lookup only.');
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('Historical reviews are available for result lookup only.');
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 validated = validateResult(result);
735
- if (!validated.evidence.length || validated.evidence.some(item => !item.trim()))
736
- throw new Error('Human review requires at least one nonempty evidence entry.');
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('Historical reviews are available for result lookup only.');
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