@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.
Files changed (91) hide show
  1. package/dist/monitor-ui/assets/{GraphView-6yzqBOLv.js → GraphView-BILt_jGR.js} +3 -3
  2. package/dist/monitor-ui/assets/ValidationView-D18mwExY.css +1 -0
  3. package/dist/monitor-ui/assets/ValidationView-L-a3hI_A.js +1 -0
  4. package/dist/monitor-ui/assets/{elk-api-BwZQovT5.js → elk-api-CW6DDhyE.js} +1 -1
  5. package/dist/monitor-ui/assets/{elk.bundled-CQowIhI8.js → elk.bundled-WR-av3nY.js} +1 -1
  6. package/dist/monitor-ui/assets/{index-D0yfhUlt.css → index-CRKnM9pk.css} +1 -1
  7. package/dist/monitor-ui/assets/index-Drtw0yLv.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/coalescing.d.ts +18 -0
  12. package/dist/src/broker/coalescing.js +43 -0
  13. package/dist/src/broker/coalescing.js.map +1 -0
  14. package/dist/src/broker/config.js +5 -1
  15. package/dist/src/broker/config.js.map +1 -1
  16. package/dist/src/broker/index.d.ts +44 -84
  17. package/dist/src/broker/index.js +155 -77
  18. package/dist/src/broker/index.js.map +1 -1
  19. package/dist/src/broker/ownership.d.ts +9 -0
  20. package/dist/src/broker/ownership.js +29 -0
  21. package/dist/src/broker/ownership.js.map +1 -0
  22. package/dist/src/broker/polling.d.ts +11 -0
  23. package/dist/src/broker/polling.js +27 -0
  24. package/dist/src/broker/polling.js.map +1 -0
  25. package/dist/src/contracts.d.ts +4 -4
  26. package/dist/src/definitions.d.ts +3 -1
  27. package/dist/src/executors/index.js +6 -21
  28. package/dist/src/executors/index.js.map +1 -1
  29. package/dist/src/executors/pi.d.ts +3 -1
  30. package/dist/src/executors/pi.js +2 -0
  31. package/dist/src/executors/pi.js.map +1 -1
  32. package/dist/src/executors/telemetry.d.ts +2 -0
  33. package/dist/src/executors/telemetry.js +12 -0
  34. package/dist/src/executors/telemetry.js.map +1 -1
  35. package/dist/src/monitor/actions.d.ts +1 -1
  36. package/dist/src/monitor/actions.js +3 -3
  37. package/dist/src/monitor/actions.js.map +1 -1
  38. package/dist/src/monitor/server.js +10 -5
  39. package/dist/src/monitor/server.js.map +1 -1
  40. package/dist/src/monitor/store.js +10 -6
  41. package/dist/src/monitor/store.js.map +1 -1
  42. package/dist/src/monitor/types.d.ts +5 -7
  43. package/dist/src/project/cli.js +47 -24
  44. package/dist/src/project/cli.js.map +1 -1
  45. package/dist/src/project/identity.d.ts +6 -1
  46. package/dist/src/project/identity.js +42 -13
  47. package/dist/src/project/identity.js.map +1 -1
  48. package/dist/src/project/index.d.ts +21 -6
  49. package/dist/src/project/index.js +19 -9
  50. package/dist/src/project/index.js.map +1 -1
  51. package/dist/src/project/owner-identity.d.ts +0 -4
  52. package/dist/src/project/owner-identity.js +5 -3
  53. package/dist/src/project/owner-identity.js.map +1 -1
  54. package/dist/src/project/prune.d.ts +12 -0
  55. package/dist/src/project/prune.js +132 -0
  56. package/dist/src/project/prune.js.map +1 -0
  57. package/dist/src/project/query.d.ts +5 -2
  58. package/dist/src/project/query.js +14 -12
  59. package/dist/src/project/query.js.map +1 -1
  60. package/dist/src/project/store.d.ts +7 -3
  61. package/dist/src/project/store.js +20 -7
  62. package/dist/src/project/store.js.map +1 -1
  63. package/dist/src/project/types.d.ts +11 -7
  64. package/dist/src/requester/index.js +1 -1
  65. package/dist/src/requester/index.js.map +1 -1
  66. package/dist/src/response-schema.d.ts +13 -0
  67. package/dist/src/response-schema.js +33 -0
  68. package/dist/src/response-schema.js.map +1 -0
  69. package/dist/src/result-view.d.ts +70 -0
  70. package/dist/src/result-view.js +44 -0
  71. package/dist/src/result-view.js.map +1 -0
  72. package/dist/src/review/local-claim.d.ts +1 -1
  73. package/dist/src/review/local-claim.js.map +1 -1
  74. package/dist/src/review-result.d.ts +2 -1
  75. package/dist/src/review-result.js +9 -8
  76. package/dist/src/review-result.js.map +1 -1
  77. package/dist/src/state-format.d.ts +8 -0
  78. package/dist/src/state-format.js +18 -0
  79. package/dist/src/state-format.js.map +1 -0
  80. package/dist/src/tools/runner.d.ts +2 -0
  81. package/dist/src/tools/runner.js +41 -11
  82. package/dist/src/tools/runner.js.map +1 -1
  83. package/dist/src/worker-client.d.ts +3 -2
  84. package/dist/src/worker-client.js +7 -4
  85. package/dist/src/worker-client.js.map +1 -1
  86. package/dist/src/worker.js +1 -1
  87. package/dist/src/worker.js.map +1 -1
  88. package/package.json +2 -2
  89. package/dist/monitor-ui/assets/ValidationView-BMqI2XQ5.css +0 -1
  90. package/dist/monitor-ui/assets/ValidationView-BwHWjMUI.js +0 -1
  91. package/dist/monitor-ui/assets/index-CoJ9_gC3.js +0 -7
@@ -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 { execFileSync } from 'node:child_process';
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 { tokenUsage } from '../executors/telemetry.js';
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(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
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) => db.prepare('UPDATE runs SET status = ?, data = ? WHERE id = ?').run(run.status, JSON.stringify(run), run.id);
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('Historical reviews are available for result lookup only.');
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 === 2) {
220
+ if (run.project?.version === 3) {
209
221
  if (terminal.has(run.status))
210
222
  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 });
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('Historical Runs cannot be resumed; submit a new validation request.');
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
- if (run.project?.version === 2) {
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
- const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
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('Historical Runs are available for result lookup only.');
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').summary, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
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 === 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) });
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 !== 2)
450
- throw new Error('Historical Runs cannot be resumed; submit a new validation request.');
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('This legacy Run has no workspace descriptor; submit a new review.');
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(runData(runId), 'Run').status))
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(runData(runId), 'Run');
484
- if (terminal.has(current.status)) {
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
- transaction(() => { refreshReadinessWithin(runId); updateRunStatus(runId); });
491
- const requests = runRequests(runId);
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 queued of requests.filter(request => request.status === 'QUEUED')) {
494
- if (queued.profile.kind !== 'human' && executing >= MAX_CONCURRENT_EXECUTORS)
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 (queued.profile.kind !== 'human')
547
+ if (kind !== 'human')
497
548
  executing++;
498
- const promise = executeOne(queued.id, workspace, token, reviewSignal).catch(error => {
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(queued.id); });
503
- inFlight.set(queued.id, { kind: queued.profile.kind, promise });
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
- if (waiting.some(request => !request.notifiedAt))
513
- throw new Error('Human notification did not complete; submit a new Run to retry.');
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(runData(runId), 'Run').status))
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
- return {
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: 2, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
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 { ...record, events: undefined, requests: record.requests.map(({ result, ...request }) => ({ ...request, result: result ? { verdict: result.verdict, summary: result.summary } : null })) };
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('Historical reviews are available for result lookup only.');
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('Historical reviews are available for result lookup only.');
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 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.');
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('Historical reviews are available for result lookup only.');
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