@allocator-one/rcl 4.5.13 → 4.5.15

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.
@@ -0,0 +1,158 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { normalizeGeneratedText } from '../telemetry/scrub.js';
3
+ import { blockingCheckpointReviewSchema } from './checkpoint.js';
4
+ import { hasSuccessfulQuorum, resolveQuorumPolicy } from './quorum.js';
5
+ import { reviewCallIdentity, runReviews } from './runner.js';
6
+ /**
7
+ * Ordinary guarded councils journal every call so an interrupted process can
8
+ * resume without repeating paid work (RCL-80). A call in flight when its
9
+ * process ended may already be billed: it is dispatched at most once more.
10
+ * Each dispatch keeps the runner's bounded in-call retries, so one intent
11
+ * covers one logical call, not one provider request.
12
+ */
13
+ export const MAX_DISPATCHES_PER_CELL = 2;
14
+ const INTERRUPTED = 'Previous process ended without a recorded outcome; the call remains possibly billed.';
15
+ const EXHAUSTED = 'Interrupted twice without a recorded outcome; possibly billed and not dispatched again.';
16
+ /**
17
+ * The report normalizes reviewer prose before hashing; the journal keeps that
18
+ * same normalized text, because its strict schema refuses raw control bytes.
19
+ * One journaled dispatch may include the runner's in-call retries, so the
20
+ * diagnostic adapter counter is not kept: the journal reserves it for the
21
+ * one-request-per-intent retained executor.
22
+ */
23
+ function journalable(review) {
24
+ const { adapterAttempts: _diagnostic, ...rest } = review;
25
+ return { ...rest, findings: review.findings.map(finding => ({ ...finding, id: normalizeGeneratedText(finding.id),
26
+ title: normalizeGeneratedText(finding.title), description: normalizeGeneratedText(finding.description),
27
+ ...(finding.suggestedFix === undefined ? {} : { suggestedFix: normalizeGeneratedText(finding.suggestedFix) }) })) };
28
+ }
29
+ /** Pure selection over a validated journal; matrix order is kept as dispatch priority. */
30
+ export function planJournaledCouncil(cells, state) {
31
+ const intents = new Map();
32
+ for (const record of state.records) {
33
+ if (record.type !== 'intent')
34
+ continue;
35
+ intents.set(record.cell, [...intents.get(record.cell) ?? [], record.paidAttempt]);
36
+ }
37
+ const outcomes = new Map(state.outcomes.map(outcome => [`${outcome.cell}\0${outcome.paidAttempt.id}`, outcome.result]));
38
+ const plan = { retained: new Map(), recorded: new Map(), exhausted: new Set(), eligible: [] };
39
+ cells.forEach((cell, index) => {
40
+ const attempts = intents.get(cell.id) ?? [];
41
+ const last = attempts.at(-1);
42
+ const outcome = last && outcomes.get(`${cell.id}\0${last.id}`);
43
+ if (outcome?.kind === 'success')
44
+ plan.retained.set(index, JSON.parse(outcome.reviewBytes));
45
+ else if (outcome)
46
+ plan.recorded.set(index, JSON.parse(outcome.reviewBytes));
47
+ else if (attempts.length >= MAX_DISPATCHES_PER_CELL)
48
+ plan.exhausted.add(index);
49
+ else
50
+ plan.eligible.push(index);
51
+ });
52
+ return plan;
53
+ }
54
+ /** Whether the ordinary runner would close this council at quorum; wait-for-all never closes early. */
55
+ function closesAtQuorum(assignments, reviews, quorum) {
56
+ if (!quorum)
57
+ return false;
58
+ const expected = new Map(), succeeded = new Map();
59
+ assignments.forEach((assignment, index) => {
60
+ if (quorum.blocking && !quorum.blocking[index])
61
+ return;
62
+ const seat = JSON.stringify([assignment.provider, assignment.model, assignment.role.name]);
63
+ expected.set(seat, (expected.get(seat) ?? 0) + 1);
64
+ if (reviews[index]?.status === 'success')
65
+ succeeded.set(seat, (succeeded.get(seat) ?? 0) + 1);
66
+ });
67
+ const complete = [...expected].filter(([seat, count]) => succeeded.get(seat) === count).length;
68
+ const policy = resolveQuorumPolicy(expected.size, quorum.fraction);
69
+ return policy.fraction < 1 && hasSuccessfulQuorum(policy, complete);
70
+ }
71
+ /**
72
+ * Run the ordinary council over its journal: reuse recorded successes, keep
73
+ * recorded failures, give an interrupted call one more dispatch, and run the
74
+ * rest with the unchanged ordinary scheduling, provider caps and quorum.
75
+ * The caller holds target ownership and binds the journal to its launch.
76
+ */
77
+ export async function runJournaledCouncil(input) {
78
+ const { journal, ownership, assignments, prompts } = input;
79
+ const cells = journal.getPlan().cells;
80
+ if (assignments.length !== cells.length || prompts.length !== cells.length)
81
+ throw new Error('journaled_council_matrix_mismatch');
82
+ assignments.forEach((assignment, index) => {
83
+ const cell = cells[index];
84
+ if (assignment.model !== cell.model || assignment.provider !== cell.route || assignment.role.name !== cell.role) {
85
+ throw new Error('journaled_council_matrix_mismatch');
86
+ }
87
+ });
88
+ let state = await journal.read();
89
+ if (!state.finalized) {
90
+ for (const pending of state.uncertain) {
91
+ if (!state.records.some(record => record.type === 'uncertain' && record.cell === pending.cell &&
92
+ record.paidAttempt?.id === pending.paidAttempt.id)) {
93
+ await journal.recordUncertain(pending.cell, pending.paidAttempt, INTERRUPTED, ownership);
94
+ }
95
+ }
96
+ state = await journal.read();
97
+ }
98
+ const plan = planJournaledCouncil(cells, state);
99
+ const placeholder = (index, error) => ({ model: assignments[index].model,
100
+ role: assignments[index].role.name, provider: assignments[index].provider, status: 'canceled',
101
+ findings: [], durationMs: 0, error });
102
+ const merge = (reviews) => reviews.map((review, index) => plan.recorded.get(index) ?? (plan.exhausted.has(index) ? placeholder(index, EXHAUSTED) : review));
103
+ if (state.finalized) {
104
+ const reviews = cells.map((_, index) => plan.retained.get(index) ?? plan.recorded.get(index) ??
105
+ placeholder(index, plan.exhausted.has(index) ? EXHAUSTED : INTERRUPTED));
106
+ return { reviews, interrupted: false, retained: plan.retained.size, dispatched: 0 };
107
+ }
108
+ const attempts = new Map();
109
+ const declined = new Set();
110
+ const reviews = merge(await runReviews(assignments, prompts, {
111
+ ...input.runner,
112
+ retainedReviews: [...plan.retained].map(([callIndex, review]) => ({ callIndex, review,
113
+ callIdentity: reviewCallIdentity(assignments[callIndex], prompts[callIndex], callIndex) })),
114
+ eligibleCallIndices: plan.eligible,
115
+ beforeReview: async (index, signal) => {
116
+ if (signal.aborted)
117
+ return false;
118
+ if (input.admit && !input.admit()) {
119
+ declined.add(index);
120
+ return false;
121
+ }
122
+ const attempt = { id: randomUUID(), kind: 'paid' };
123
+ await journal.recordIntent(cells[index].id, attempt, ownership);
124
+ attempts.set(index, attempt);
125
+ },
126
+ acceptReview: async (review, index) => {
127
+ const attempt = attempts.get(index), cell = cells[index];
128
+ if (!attempt)
129
+ throw new Error('journaled_council_missing_intent');
130
+ if (review.status === 'canceled') {
131
+ await journal.recordUncertain(cell.id, attempt, review.error ?? 'Canceled with an unknown provider outcome.', ownership);
132
+ return;
133
+ }
134
+ const stored = journalable(review);
135
+ try {
136
+ if (!blockingCheckpointReviewSchema.safeParse(stored).success)
137
+ throw new Error('checkpoint_invalid_result');
138
+ await journal.recordResult(cell.id, attempt, stored.status === 'success'
139
+ ? { kind: 'success', chunk: cell.chunk, reviewBytes: JSON.stringify(stored) }
140
+ : { kind: 'failure', chunk: cell.chunk, reviewBytes: JSON.stringify(stored), possiblyBilled: true }, ownership);
141
+ }
142
+ catch (error) {
143
+ // Validation refuses before any write. This run still reports the
144
+ // result; only a later resume would dispatch the call again.
145
+ if (!(error instanceof Error) || !['checkpoint_invalid_result', 'checkpoint_result_cell_mismatch'].includes(error.message))
146
+ throw error;
147
+ await journal.recordUncertain(cell.id, attempt, 'Reviewer result could not be journaled; a resume treats it as interrupted.', ownership);
148
+ }
149
+ },
150
+ }));
151
+ const stopped = declined.size > 0 || input.runner.signal?.aborted === true;
152
+ const interrupted = stopped && reviews.some(review => review.status === 'canceled') &&
153
+ !closesAtQuorum(assignments, reviews, input.runner.quorum);
154
+ if (!interrupted)
155
+ await journal.finalize(ownership);
156
+ return { reviews, interrupted, retained: plan.retained.size, dispatched: attempts.size };
157
+ }
158
+ //# sourceMappingURL=journaled-council.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journaled-council.js","sourceRoot":"","sources":["../../src/dispatch/journaled-council.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import { DEFAULT_CONVERGE_ATTEMPT_CAP, claimConvergeAttempt, ConvergeAttemptBudg
49
49
  import { DEFAULT_CONVERGE_ROUND_CAP, HARD_CONVERGE_ROUND_CAP, processRoundReport, recordVerdicts, findingGatingReason, ConvergeRoundCapError, ConvergeRunStateError, } from './converge/run-state.js';
50
50
  import { applyRoundGap, previewRoundGap } from './converge/round-gap.js';
51
51
  import { withNativeTarget } from './converge/target-ownership.js';
52
- import { guardReviewLaunch, ReviewLaunchRefused } from './converge/launch-guard.js';
52
+ import { guardReviewLaunch, ReviewCouncilInterrupted, ReviewLaunchRefused } from './converge/launch-guard.js';
53
53
  import { selectCurrentRecoveredProduction } from './converge/recovered-production.js';
54
54
  import { createBoundFixRecovery } from './converge/bound-fix-recovery.js';
55
55
  import { openReadSink } from './telemetry/read-sink.js';
@@ -87,7 +87,8 @@ import { exportOrdinaryPendingPackage, retainOrdinaryLaunchInputs } from './conv
87
87
  import { capturePreparedCouncil } from './dispatch/capture-council.js';
88
88
  import { executeCapturedOriginal } from './dispatch/original-execution.js';
89
89
  import { resolveFinalizedAsyncExecution } from './dispatch/checkpoint-async-store.js';
90
- import { exportCheckpointProof } from './dispatch/checkpoint.js';
90
+ import { CheckpointJournal, checkpointPath, exportCheckpointProof } from './dispatch/checkpoint.js';
91
+ import { runJournaledCouncil } from './dispatch/journaled-council.js';
91
92
  import { captureAggregationInputs, AGGREGATION_ALGORITHM } from './report/aggregation-inputs.js';
92
93
  import { decodeSupplementalAsync } from './report/supplemental-async.js';
93
94
  import { projectCheckpointReport } from './report/checkpoint-projection.js';
@@ -213,7 +214,7 @@ program
213
214
  prev.push(val);
214
215
  return prev;
215
216
  }, [])
216
- .option('--context <path>', 'Context file or directory to include (repeatable)', (val, prev) => {
217
+ .option('--context <path>', 'Context file to include (repeatable); a directory is not read', (val, prev) => {
217
218
  prev.push(val);
218
219
  return prev;
219
220
  }, [])
@@ -253,6 +254,7 @@ program
253
254
  .option('--resume-async-sha256 <hashes>', 'Comma-separated SHA-256 bindings for every legacy async result retained as historical evidence')
254
255
  .option('--retry-reason <reason>', 'Explicit bounded recovery decision for a previous failed or unknown launch; never resets caps')
255
256
  .option('--max-attempts <n>', 'Guarded convergence: explicitly authorized attempt cap (omitting preserves the cap)')
257
+ .option('--max-slice-minutes <n>', 'Guarded convergence: start no new reviewer call after N minutes; running calls finish, then exit 5. Rerunning the same command resumes without a new attempt')
256
258
  .option('--max-rounds <n>', 'Guarded convergence: explicitly authorized round cap (2–99; omitting preserves the cap)')
257
259
  .option('--no-telemetry', 'Do not deliver this review as evidence to Harness')
258
260
  .option('--evidence-required', 'Exit 4 unless Harness acknowledged the evidence (spools first; retry with rcl telemetry flush)')
@@ -272,7 +274,7 @@ program
272
274
  prev.push(val);
273
275
  return prev;
274
276
  }, [])
275
- .option('--context <path>', 'Context file or directory to include (repeatable)', (val, prev) => {
277
+ .option('--context <path>', 'Context file to include (repeatable); a directory is not read', (val, prev) => {
276
278
  prev.push(val);
277
279
  return prev;
278
280
  }, [])
@@ -1763,8 +1765,11 @@ async function runReview(target, opts) {
1763
1765
  catch { /* Startup delivery remains best-effort for legacy reviews. */ }
1764
1766
  }
1765
1767
  if (!opts.guardedConverge && (opts.launchIntent !== undefined || opts.retryReason !== undefined || opts.retryReport !== undefined ||
1766
- opts.maxAttempts !== undefined || opts.maxRounds !== undefined)) {
1767
- throw new ReviewLaunchRefused('guard_required', 'Launch intent, retry reason and launch caps require --guarded-converge.');
1768
+ opts.maxAttempts !== undefined || opts.maxRounds !== undefined || opts.maxSliceMinutes !== undefined)) {
1769
+ throw new ReviewLaunchRefused('guard_required', 'Launch intent, retry reason, launch caps and slice budgets require --guarded-converge.');
1770
+ }
1771
+ if (opts.maxSliceMinutes !== undefined && (!/^[1-9]\d{0,3}$/.test(opts.maxSliceMinutes) || Number(opts.maxSliceMinutes) > 1440)) {
1772
+ throw new ReviewLaunchRefused('invalid_slice_budget', '--max-slice-minutes must be a whole number of minutes from 1 to 1440.');
1768
1773
  }
1769
1774
  if (opts.guardedConverge) {
1770
1775
  const converge = resolveConvergeContext(opts, reviewConvergeEnvironment(opts));
@@ -1912,7 +1917,8 @@ async function runReview(target, opts) {
1912
1917
  console.error(err);
1913
1918
  }
1914
1919
  process.exitCode = opts.guardedConverge && (err instanceof ConvergeAttemptBudgetExceededError || err instanceof ConvergeRoundCapError)
1915
- ? 2 : opts.guardedConverge && (err instanceof ConvergeAttemptStateError || err instanceof ConvergeRunStateError) ? 3 : 1;
1920
+ ? 2 : opts.guardedConverge && (err instanceof ConvergeAttemptStateError || err instanceof ConvergeRunStateError) ? 3
1921
+ : opts.guardedConverge && err instanceof ReviewCouncilInterrupted ? 5 : 1;
1916
1922
  }
1917
1923
  }
1918
1924
  /**
@@ -2218,10 +2224,38 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2218
2224
  }
2219
2225
  return result.completion;
2220
2226
  }
2227
+ // Journal the ordinary council so an interrupted process resumes without
2228
+ // repeating successful calls (RCL-80). Without an exact head and base, or
2229
+ // inputs beyond the capture limits, dispatch stays unjournaled.
2230
+ let ordinaryPlan;
2231
+ if (/^[a-f0-9]{40}$/.test(extra.target.headSha ?? '') && /^[a-f0-9]{40}$/.test(extra.target.baseSha ?? '')) {
2232
+ try {
2233
+ ordinaryPlan = capturePreparedCouncil({ target: prepared.converge.target,
2234
+ headSha: extra.target.headSha, mergeBaseSha: extra.target.baseSha, diff, assignments,
2235
+ lanes: assignments.map(item => assignmentLane(item.model, prepared.coreModels, prepared.explicit)),
2236
+ chunks, prompts, config, specBytes: prepared.specContent ?? '', contextDocs,
2237
+ compatibility: { parser: { name: 'findings-json', version: 1 }, aggregation: AGGREGATION_ALGORITHM } }).plan;
2238
+ }
2239
+ catch (error) {
2240
+ process.stderr.write(`Council journal unavailable (${scrubText(String(error), 200)}); an interrupted run cannot resume.\n`);
2241
+ }
2242
+ }
2243
+ const journalPlan = ordinaryPlan;
2244
+ if (opts.maxSliceMinutes !== undefined && !journalPlan) {
2245
+ throw new ReviewLaunchRefused('slice_budget_unavailable', '--max-slice-minutes needs a journaled council (an exact head and base within the capture limits); rerun without it.');
2246
+ }
2221
2247
  const claim = await guardReviewLaunch({
2222
2248
  gitCommonDir: await resolveGitCommonDir(),
2223
2249
  target: prepared.converge.target,
2224
2250
  startOver: opts.startOver, cycleRemote,
2251
+ ...(journalPlan ? { ordinaryCheckpoint: { exists: async (namespace) => {
2252
+ try {
2253
+ return !!await CheckpointJournal.openRead(checkpointPath(common, prepared.converge.target, namespace), journalPlan);
2254
+ }
2255
+ catch {
2256
+ return false;
2257
+ }
2258
+ } } } : {}),
2225
2259
  boundFixRecovery,
2226
2260
  recoverySource: prepared.converge.recovery_source,
2227
2261
  headSha: extra.target.headSha ?? '',
@@ -2263,8 +2297,10 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2263
2297
  if (boundFixRecovery)
2264
2298
  process.stderr.write(`Bound fix recovery verified from run ${boundFixRecovery.runId}; attempt ${claim.attempt} retains the existing review history.\n`);
2265
2299
  },
2266
- run: async (converge) => {
2267
- const guardedCompletion = await executeCouncil(spinner, { ...prepared, converge }, diff, { ...opts, guardedConverge: false, exclusiveOutputs: true }, extra, work);
2300
+ run: async (converge, ownership, _original, checkpoint) => {
2301
+ if (checkpoint?.resumed)
2302
+ process.stderr.write(`Resuming the interrupted council of attempt ${converge.attempt}; journaled calls are not repeated.\n`);
2303
+ const guardedCompletion = await executeCouncil(spinner, { ...prepared, converge }, diff, { ...opts, guardedConverge: false, exclusiveOutputs: true }, { ...extra, ...(checkpoint && journalPlan ? { ordinaryCheckpoint: { ...checkpoint, ownership, plan: journalPlan } } : {}) }, work);
2268
2304
  if (!guardedCompletion)
2269
2305
  throw new Error('guarded_launch_missing_completion');
2270
2306
  completion = guardedCompletion;
@@ -2289,7 +2325,7 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2289
2325
  let asyncStoreDir;
2290
2326
  let asyncKey;
2291
2327
  let asyncLaunched = 0;
2292
- if (!extra.pendingResume &&
2328
+ if (!extra.pendingResume && !extra.ordinaryCheckpoint?.resumed &&
2293
2329
  asyncTargetLabel !== undefined &&
2294
2330
  (asyncAssignments.length > 0 || (config.asyncModels?.length ?? 0) > 0)) {
2295
2331
  try {
@@ -2396,8 +2432,8 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2396
2432
  policy: { version: 1, fraction: config.quorumFraction ?? DEFAULT_QUORUM_FRACTION } });
2397
2433
  chunkReviews = pendingProjection.seatReviews.map(seat => structuredClone(seat.review));
2398
2434
  }
2399
- else
2400
- chunkReviews = await runReviews(chunkAssignments.map((ca) => ca.assignment), prompts, {
2435
+ else {
2436
+ const ordinaryRunner = {
2401
2437
  // Fall back to the shared constants, never to inline literals:
2402
2438
  // duplicated defaults drift (this read 120_000 after the default
2403
2439
  // moved to 600_000).
@@ -2418,7 +2454,26 @@ async function executeCouncil(spinner, prepared, diff, opts, extra, preparedWork
2418
2454
  blocking: chunkAssignments.map((ca) => assignmentLane(ca.assignment.model, prepared.coreModels, prepared.explicit) === 'blocking'),
2419
2455
  },
2420
2456
  onReviewComplete: (review) => progress.complete(review),
2421
- });
2457
+ };
2458
+ const checkpoint = extra.ordinaryCheckpoint;
2459
+ if (checkpoint) {
2460
+ const open = { commonDir: await resolveGitCommonDir(), namespace: checkpoint.namespace,
2461
+ plan: checkpoint.plan, ownership: checkpoint.ownership };
2462
+ const journal = await (checkpoint.resumed ? CheckpointJournal.openWrite(open) : CheckpointJournal.create(open));
2463
+ const sliceEndsAt = opts.maxSliceMinutes === undefined ? undefined : Date.now() + Number(opts.maxSliceMinutes) * 60_000;
2464
+ const journaled = await runJournaledCouncil({ journal, ownership: checkpoint.ownership,
2465
+ assignments: chunkAssignments.map((ca) => ca.assignment), prompts, runner: ordinaryRunner,
2466
+ ...(sliceEndsAt === undefined ? {} : { admit: () => Date.now() < sliceEndsAt }) });
2467
+ if (journaled.retained > 0)
2468
+ process.stderr.write(`Reused ${journaled.retained} journaled call(s) from the interrupted council.\n`);
2469
+ if (journaled.interrupted) {
2470
+ throw new ReviewCouncilInterrupted(journaled.reviews.filter(review => review.status !== 'canceled').length, totalCalls);
2471
+ }
2472
+ chunkReviews = journaled.reviews;
2473
+ }
2474
+ else
2475
+ chunkReviews = await runReviews(chunkAssignments.map((ca) => ca.assignment), prompts, ordinaryRunner);
2476
+ }
2422
2477
  }
2423
2478
  finally {
2424
2479
  progress.stop();