auditor-lambda 0.3.33 → 0.3.36

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 (39) hide show
  1. package/dist/cli.js +40 -30
  2. package/dist/orchestrator/reviewPackets.d.ts +3 -0
  3. package/dist/orchestrator/reviewPackets.js +13 -2
  4. package/dist/orchestrator/selectiveDeepening.d.ts +2 -0
  5. package/dist/orchestrator/selectiveDeepening.js +10 -1
  6. package/dist/orchestrator/state.js +2 -17
  7. package/dist/providers/opencodeProvider.js +23 -3
  8. package/dist/providers/spawnLoggedCommand.js +0 -5
  9. package/dist/quota/compositeQuotaSource.d.ts +7 -0
  10. package/dist/quota/compositeQuotaSource.js +20 -0
  11. package/dist/quota/errorParsers/claudeCodeErrorParser.d.ts +6 -0
  12. package/dist/quota/errorParsers/claudeCodeErrorParser.js +39 -0
  13. package/dist/quota/errorParsers/genericErrorParser.d.ts +9 -0
  14. package/dist/quota/errorParsers/genericErrorParser.js +7 -0
  15. package/dist/quota/errorParsers/index.d.ts +5 -0
  16. package/dist/quota/errorParsers/index.js +12 -0
  17. package/dist/quota/errorParsing.d.ts +7 -0
  18. package/dist/quota/errorParsing.js +69 -0
  19. package/dist/quota/fileLock.d.ts +6 -0
  20. package/dist/quota/fileLock.js +64 -0
  21. package/dist/quota/index.d.ts +11 -1
  22. package/dist/quota/index.js +7 -1
  23. package/dist/quota/learnedQuotaSource.d.ts +7 -0
  24. package/dist/quota/learnedQuotaSource.js +25 -0
  25. package/dist/quota/probe.d.ts +1 -4
  26. package/dist/quota/probe.js +1 -4
  27. package/dist/quota/quotaSource.d.ts +12 -0
  28. package/dist/quota/quotaSource.js +1 -0
  29. package/dist/quota/scheduler.d.ts +5 -1
  30. package/dist/quota/scheduler.js +51 -9
  31. package/dist/quota/slidingWindow.d.ts +4 -0
  32. package/dist/quota/slidingWindow.js +28 -0
  33. package/dist/quota/state.d.ts +3 -0
  34. package/dist/quota/state.js +57 -14
  35. package/dist/quota/types.d.ts +11 -2
  36. package/dist/reporting/mergeFindings.js +115 -23
  37. package/dist/types/sessionConfig.d.ts +2 -0
  38. package/package.json +1 -1
  39. package/schemas/dispatch_quota.schema.json +23 -2
package/dist/cli.js CHANGED
@@ -28,11 +28,11 @@ import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./superv
28
28
  import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
29
29
  import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
30
30
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
31
- import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/reviewPackets.js";
31
+ import { buildReviewPackets, orderTasksForPacketReview, estimateTaskGroupTokens, } from "./orchestrator/reviewPackets.js";
32
32
  import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
33
33
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
34
34
  import { runAuditCodeMcpServer } from "./mcp/server.js";
35
- import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, } from "./quota/index.js";
35
+ import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, } from "./quota/index.js";
36
36
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
37
37
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
38
38
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
@@ -101,7 +101,7 @@ export function resolveHostDispatchCapability(options) {
101
101
  if (options.sessionConfig.host_can_dispatch_subagents !== undefined) {
102
102
  return options.sessionConfig.host_can_dispatch_subagents;
103
103
  }
104
- return optionalBooleanEnv((options.env ?? process.env).AUDIT_CODE_HOST_CAN_DISPATCH) ?? false;
104
+ return optionalBooleanEnv((options.env ?? process.env).AUDIT_CODE_HOST_CAN_DISPATCH) ?? true;
105
105
  }
106
106
  function toBase64Url(value) {
107
107
  return Buffer.from(value, "utf8").toString("base64url");
@@ -228,18 +228,6 @@ function getQuotaProbeMode(argv, sessionConfig) {
228
228
  return raw;
229
229
  return "auto";
230
230
  }
231
- function detectRateLimitError(errorText) {
232
- const lower = errorText.toLowerCase();
233
- return lower.includes("429") || lower.includes("rate limit") || lower.includes("rate_limit");
234
- }
235
- function defaultCooldownUntil(resetAtHeader) {
236
- if (resetAtHeader) {
237
- const t = new Date(resetAtHeader).getTime();
238
- if (!Number.isNaN(t))
239
- return new Date(t).toISOString();
240
- }
241
- return new Date(Date.now() + 60_000).toISOString();
242
- }
243
231
  function resolveRunProviderName(argv, sessionConfig) {
244
232
  return resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig);
245
233
  }
@@ -1294,12 +1282,25 @@ async function cmdRunToCompletion(argv) {
1294
1282
  let pendingRuntimeUpdatesPath = getFlag(argv, "--updates");
1295
1283
  let pendingExternalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
1296
1284
  let runCount = 0;
1285
+ let deepeningCycles = 0;
1286
+ const MAX_DEEPENING_CYCLES = 3;
1297
1287
  let anyProgress = false;
1298
1288
  let lastResult = null;
1299
1289
  const artifactsWritten = new Set();
1300
1290
  while (runCount < maxRuns) {
1301
1291
  const bundle = await loadArtifactBundle(artifactsDir);
1302
1292
  const decision = decideNextStep(bundle);
1293
+ if (decision.selected_executor === "agent" &&
1294
+ bundle.audit_tasks?.some((t) => t.tags?.includes("selective_deepening") &&
1295
+ t.status !== "complete") &&
1296
+ !bundle.audit_tasks?.some((t) => !t.tags?.includes("selective_deepening") &&
1297
+ t.status !== "complete")) {
1298
+ deepeningCycles++;
1299
+ if (deepeningCycles > MAX_DEEPENING_CYCLES) {
1300
+ process.stderr.write(`[audit-code] Reached max deepening cycles (${MAX_DEEPENING_CYCLES}). Stopping to prevent churn.\n`);
1301
+ break;
1302
+ }
1303
+ }
1303
1304
  let preferredExecutor = decision.selected_executor;
1304
1305
  let obligationId = decision.selected_obligation;
1305
1306
  let auditResultsPath;
@@ -1422,11 +1423,15 @@ async function cmdRunToCompletion(argv) {
1422
1423
  const quotaState = await readQuotaState();
1423
1424
  const providerModelKey = buildProviderModelKey(provider.name, hostModel);
1424
1425
  const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
1426
+ const allCandidateTasks = buildPendingAuditTasks(bundle);
1427
+ const candidateGroups = chunkArray(allCandidateTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
1428
+ const slotTokenEstimates = candidateGroups.map((g) => estimateTaskGroupTokens(g));
1425
1429
  const waveSchedule = scheduleWave({
1426
1430
  providerName: resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig),
1427
1431
  sessionConfig,
1428
1432
  hostModel,
1429
1433
  requestedConcurrency: parallelWorkers,
1434
+ estimatedSlotTokens: slotTokenEstimates,
1430
1435
  quotaStateEntry,
1431
1436
  });
1432
1437
  const waveSize = waveSchedule.wave_size;
@@ -1438,8 +1443,7 @@ async function cmdRunToCompletion(argv) {
1438
1443
  await new Promise((r) => setTimeout(r, cappedWait));
1439
1444
  }
1440
1445
  }
1441
- const allPendingTasks = buildPendingAuditTasks(bundle);
1442
- const taskGroups = chunkArray(allPendingTasks.slice(0, waveSize * agentBatchSize), agentBatchSize);
1446
+ const taskGroups = candidateGroups.slice(0, waveSize);
1443
1447
  const workerSlots = [];
1444
1448
  for (const rawGroup of taskGroups) {
1445
1449
  const group = await addFileLineCountHints(root, rawGroup);
@@ -1478,7 +1482,7 @@ async function cmdRunToCompletion(argv) {
1478
1482
  pending_audit_tasks_path: slot.pendingTasksPath,
1479
1483
  })), workerSlots.flatMap((slot) => slot.group));
1480
1484
  const parallelStartedAt = new Date().toISOString();
1481
- const launchResults = await Promise.allSettled(workerSlots.map((slot) => provider.launch({
1485
+ const { results: launchResults } = await runSlidingWindow(workerSlots.map((slot) => () => provider.launch({
1482
1486
  repoRoot: root,
1483
1487
  runId: slot.runId,
1484
1488
  obligationId,
@@ -1489,7 +1493,7 @@ async function cmdRunToCompletion(argv) {
1489
1493
  stderrPath: slot.paths.stderrPath,
1490
1494
  uiMode,
1491
1495
  timeoutMs,
1492
- })));
1496
+ })), waveSize);
1493
1497
  const launchErrorsByRunId = new Map();
1494
1498
  for (let index = 0; index < launchResults.length; index++) {
1495
1499
  const outcome = launchResults[index];
@@ -1601,12 +1605,14 @@ async function cmdRunToCompletion(argv) {
1601
1605
  }
1602
1606
  // Record outcome for adaptive learning (best-effort — never blocks dispatch)
1603
1607
  {
1604
- const hasRateLimit = batchErrors.some(detectRateLimitError);
1608
+ const rateLimitResults = batchErrors.map((e) => detectRateLimitError(e));
1609
+ const rateLimitHit = rateLimitResults.find((r) => r.isRateLimited);
1610
+ const retryAfterMs = rateLimitHit?.retryAfterMs ?? null;
1605
1611
  await recordWaveOutcome(providerModelKey, {
1606
1612
  concurrency: workerSlots.length,
1607
- estimated_tokens: waveSize * agentBatchSize * 900,
1608
- outcome: hasRateLimit ? "rate_limited" : batchErrors.length > 0 ? "timeout" : "success",
1609
- cooldown_until: hasRateLimit ? defaultCooldownUntil(null) : null,
1613
+ estimated_tokens: slotTokenEstimates.slice(0, workerSlots.length).reduce((a, b) => a + b, 0),
1614
+ outcome: rateLimitHit ? "rate_limited" : batchErrors.length > 0 ? "timeout" : "success",
1615
+ cooldown_until: rateLimitHit ? computeCooldownUntil(retryAfterMs) : null,
1610
1616
  }, sessionConfig.quota?.empirical_half_life_hours ?? 24).catch(() => undefined);
1611
1617
  }
1612
1618
  if (batchErrors.length > 0) {
@@ -2455,12 +2461,10 @@ async function prepareDispatchArtifacts(params) {
2455
2461
  });
2456
2462
  // Compute and write dispatch-quota.json
2457
2463
  const hostModel = params.hostModel ?? null;
2458
- const avgPacketTokens = plan.length > 0
2459
- ? Math.floor(plan.reduce((s, p) => s + p.complexity.estimated_tokens, 0) / plan.length)
2460
- : 0;
2464
+ const perPacketTokens = plan.map((p) => p.complexity.estimated_tokens);
2461
2465
  const quotaProviderName = resolveFreshSessionProviderName(undefined, sessionConfig);
2462
2466
  const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
2463
- const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
2467
+ const quotaState = await readQuotaState().catch(() => ({ version: 2, entries: {} }));
2464
2468
  const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
2465
2469
  const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
2466
2470
  explicitLimit: params.hostActiveSubagentLimit,
@@ -2471,12 +2475,12 @@ async function prepareDispatchArtifacts(params) {
2471
2475
  sessionConfig,
2472
2476
  hostModel,
2473
2477
  requestedConcurrency: sessionConfig.parallel_workers ?? plan.length,
2474
- estimatedPacketTokens: avgPacketTokens,
2478
+ estimatedSlotTokens: perPacketTokens,
2475
2479
  quotaStateEntry,
2476
2480
  hostConcurrencyLimit,
2477
2481
  });
2478
2482
  const dispatchQuota = {
2479
- contract_version: "audit-code-dispatch-quota/v1alpha1",
2483
+ contract_version: "audit-code-dispatch-quota/v1alpha2",
2480
2484
  run_id: runId,
2481
2485
  model: hostModel,
2482
2486
  resolved_limits: waveSchedule.resolved_limits,
@@ -2486,6 +2490,8 @@ async function prepareDispatchArtifacts(params) {
2486
2490
  wave_size: waveSchedule.wave_size,
2487
2491
  estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
2488
2492
  cooldown_until: waveSchedule.cooldown_until,
2493
+ quota_source_snapshot: waveSchedule.quota_source_snapshot ?? null,
2494
+ backoff_state: null,
2489
2495
  };
2490
2496
  const dispatchQuotaPath = join(runDir, "dispatch-quota.json");
2491
2497
  await writeJsonFile(dispatchQuotaPath, dispatchQuota);
@@ -3212,13 +3218,15 @@ async function cmdQuota(argv) {
3212
3218
  const providerModelKey = buildProviderModelKey(providerName, hostModel);
3213
3219
  const { limits, source, confidence } = resolveLimits({ providerName, sessionConfig, hostModel });
3214
3220
  const probeResult = await probeProvider(providerName, probeMode);
3215
- const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
3221
+ const quotaState = await readQuotaState().catch(() => ({ version: 2, entries: {} }));
3216
3222
  const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
3217
3223
  const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ?? 24;
3218
3224
  const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
3219
3225
  explicitLimit: getHostMaxActiveSubagents(argv),
3220
3226
  sessionConfig,
3221
3227
  });
3228
+ const quotaSource = new CompositeQuotaSource([new LearnedQuotaSource(halfLifeHours)]);
3229
+ const quotaSourceSnapshot = await quotaSource.queryCurrentUsage(providerModelKey).catch(() => null);
3222
3230
  const waveSchedule = scheduleWave({
3223
3231
  providerName,
3224
3232
  sessionConfig,
@@ -3226,6 +3234,7 @@ async function cmdQuota(argv) {
3226
3234
  requestedConcurrency: sessionConfig.parallel_workers ?? 1,
3227
3235
  quotaStateEntry,
3228
3236
  hostConcurrencyLimit,
3237
+ quotaSourceSnapshot,
3229
3238
  });
3230
3239
  console.log(JSON.stringify({
3231
3240
  provider: providerName,
@@ -3243,6 +3252,7 @@ async function cmdQuota(argv) {
3243
3252
  last_429_at: quotaStateEntry.last_429_at,
3244
3253
  }
3245
3254
  : null,
3255
+ quota_source_snapshot: quotaSourceSnapshot,
3246
3256
  wave_schedule: waveSchedule,
3247
3257
  quota_state_path: getQuotaStatePath(),
3248
3258
  }, null, 2));
@@ -1,6 +1,9 @@
1
1
  import type { AuditTask } from "../types.js";
2
2
  import type { AuditPlanMetrics, ReviewPacket } from "../types/reviewPlanning.js";
3
3
  import type { GraphBundle } from "../types/graph.js";
4
+ export declare const ESTIMATED_TOKENS_PER_LINE = 4;
5
+ export declare const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
6
+ export declare function estimateTaskGroupTokens(tasks: AuditTask[]): number;
4
7
  export interface BuildReviewPacketOptions {
5
8
  graphBundle?: GraphBundle;
6
9
  lineIndex?: Record<string, number>;
@@ -2,8 +2,19 @@ import { createHash } from "node:crypto";
2
2
  import { LENS_ORDER } from "./unitBuilder.js";
3
3
  const DEFAULT_MAX_TASKS_PER_PACKET = 0;
4
4
  const DEFAULT_TARGET_PACKET_LINES = 8000;
5
- const ESTIMATED_TOKENS_PER_LINE = 4;
6
- const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
5
+ export const ESTIMATED_TOKENS_PER_LINE = 4;
6
+ export const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
7
+ export function estimateTaskGroupTokens(tasks) {
8
+ let totalLines = 0;
9
+ for (const task of tasks) {
10
+ if (task.file_line_counts) {
11
+ for (const count of Object.values(task.file_line_counts)) {
12
+ totalLines += count;
13
+ }
14
+ }
15
+ }
16
+ return ESTIMATED_PACKET_PROMPT_TOKENS + totalLines * ESTIMATED_TOKENS_PER_LINE;
17
+ }
7
18
  const PACKET_EXPANSION_MIN_CONFIDENCE = 0.65;
8
19
  const HIGH_FAN_DEGREE_THRESHOLD = 12;
9
20
  const HIGH_FAN_EXPANSION_CONFIDENCE = 0.99;
@@ -9,10 +9,12 @@ export interface BuildSelectiveDeepeningTaskOptions {
9
9
  runtimeValidationReport?: RuntimeValidationReport;
10
10
  externalAnalyzerResults?: ExternalAnalyzerResults;
11
11
  maxTasks?: number;
12
+ maxTotalDeepeningTasks?: number;
12
13
  }
13
14
  export declare function buildSelectiveDeepeningTasks(options: BuildSelectiveDeepeningTaskOptions): AuditTask[];
14
15
  export declare const selectiveDeepeningTestUtils: {
15
16
  DEEPENING_TAG: string;
16
17
  LENS_VERIFICATION_TAG: string;
17
18
  LENS_VERIFICATION_FOLLOWUP_TAG: string;
19
+ DEFAULT_MAX_TOTAL_DEEPENING_TASKS: number;
18
20
  };
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  const DEFAULT_MAX_DEEPENING_TASKS = 6;
3
+ const DEFAULT_MAX_TOTAL_DEEPENING_TASKS = 24;
3
4
  const DEEPENING_TAG = "selective_deepening";
4
5
  const LENS_VERIFICATION_TAG = "lens_verification";
5
6
  const LENS_VERIFICATION_FOLLOWUP_TAG = "lens_verification_followup";
@@ -649,9 +650,16 @@ export function buildSelectiveDeepeningTasks(options) {
649
650
  const existingTasks = options.existingTasks ?? [];
650
651
  const existingIds = new Set(taskById.keys());
651
652
  const maxTasks = options.maxTasks ?? DEFAULT_MAX_DEEPENING_TASKS;
653
+ const maxTotalDeepeningTasks = options.maxTotalDeepeningTasks ?? DEFAULT_MAX_TOTAL_DEEPENING_TASKS;
654
+ const existingDeepeningCount = existingTasks.filter((task) => isDeepeningTask(task)).length;
655
+ if (existingDeepeningCount >= maxTotalDeepeningTasks) {
656
+ return [];
657
+ }
658
+ const remainingBudget = maxTotalDeepeningTasks - existingDeepeningCount;
659
+ const effectiveMax = Math.min(maxTasks, remainingBudget);
652
660
  const created = [];
653
661
  function pushIfNew(task) {
654
- if (created.length >= maxTasks || existingIds.has(task.task_id)) {
662
+ if (created.length >= effectiveMax || existingIds.has(task.task_id)) {
655
663
  return;
656
664
  }
657
665
  existingIds.add(task.task_id);
@@ -748,4 +756,5 @@ export const selectiveDeepeningTestUtils = {
748
756
  DEEPENING_TAG,
749
757
  LENS_VERIFICATION_TAG,
750
758
  LENS_VERIFICATION_FOLLOWUP_TAG,
759
+ DEFAULT_MAX_TOTAL_DEEPENING_TASKS,
751
760
  };
@@ -42,31 +42,16 @@ export function deriveAuditState(bundle) {
42
42
  "audit_tasks.json",
43
43
  "requeue_tasks.json",
44
44
  ], planningReady)));
45
- const hasRequiredCoverage = bundle.coverage_matrix?.files.every((f) => f.required_lenses.every((req) => f.completed_lenses.includes(req))) ?? true;
46
45
  const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
47
46
  const hasPendingAuditTasks = bundle.audit_tasks?.some((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id)) ?? false;
48
- const hasCompletedTaskStatuses = bundle.audit_tasks?.length
49
- ? bundle.audit_tasks.every((task) => task.status === "complete")
50
- : false;
51
- const hasResultForEveryTask = bundle.audit_tasks?.length && bundle.audit_results
52
- ? bundle.audit_tasks.every((task) => bundle.audit_results?.some((result) => result.task_id === task.task_id))
53
- : false;
54
47
  if (hasPendingAuditTasks) {
55
48
  obligations.push(obligation("audit_tasks_completed", "missing"));
56
49
  }
57
- else if (!hasRequiredCoverage &&
58
- !hasCompletedTaskStatuses &&
59
- !hasResultForEveryTask &&
60
- has(bundle.audit_tasks) &&
61
- (bundle.audit_tasks?.length ?? 0) > 0) {
62
- obligations.push(obligation("audit_tasks_completed", "missing"));
63
- }
64
- else if ((hasRequiredCoverage || hasCompletedTaskStatuses || hasResultForEveryTask) &&
65
- has(bundle.audit_tasks)) {
50
+ else if (has(bundle.audit_tasks)) {
66
51
  obligations.push(obligation("audit_tasks_completed", "satisfied"));
67
52
  }
68
53
  obligations.push(obligation("audit_results_ingested", (bundle.audit_tasks?.length ?? 0) === 0 || has(bundle.audit_results)
69
- ? "present"
54
+ ? "satisfied"
70
55
  : "missing"));
71
56
  const runtimeTasks = bundle.runtime_validation_tasks?.tasks ?? [];
72
57
  const runtimeResults = bundle.runtime_validation_report?.results ?? [];
@@ -1,5 +1,24 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { spawnLoggedCommand } from "./spawnLoggedCommand.js";
3
+ function resolveOpenCodeSpawnCommand(command, args, platform = process.platform, shellCommand = process.env.ComSpec ?? "cmd.exe") {
4
+ if (platform !== "win32") {
5
+ return { command, args };
6
+ }
7
+ const base = command.replace(/\.(cmd|bat|exe)$/i, "").toLowerCase();
8
+ if (base === "opencode" || base === "npx" || command.endsWith(".cmd")) {
9
+ return {
10
+ command: shellCommand,
11
+ args: ["/d", "/s", "/c", [command, ...args].map(quoteCmdArg).join(" ")],
12
+ };
13
+ }
14
+ return { command, args };
15
+ }
16
+ function quoteCmdArg(value) {
17
+ if (/^[A-Za-z0-9_./:=+-]+$/.test(value)) {
18
+ return value;
19
+ }
20
+ return `"${value.replace(/(["^&|<>%])/g, "^$1")}"`;
21
+ }
3
22
  export class OpenCodeProvider {
4
23
  name = "opencode";
5
24
  config;
@@ -8,8 +27,9 @@ export class OpenCodeProvider {
8
27
  }
9
28
  async launch(input) {
10
29
  const prompt = await readFile(input.promptPath, "utf8");
11
- const command = this.config.command ?? "opencode";
12
- const args = ["run", prompt, ...(this.config.extra_args ?? [])];
13
- return await spawnLoggedCommand(command, args, input);
30
+ const baseCommand = this.config.command ?? "opencode";
31
+ const baseArgs = ["run", prompt, ...(this.config.extra_args ?? [])];
32
+ const resolved = resolveOpenCodeSpawnCommand(baseCommand, baseArgs);
33
+ return await spawnLoggedCommand(resolved.command, resolved.args, input);
14
34
  }
15
35
  }
@@ -152,13 +152,8 @@ export async function spawnLoggedCommand(command, args, input, env, options = {}
152
152
  });
153
153
  spawnedChild.on("error", fail);
154
154
  spawnedChild.on("exit", (code, signal) => {
155
- if (!timedOut) {
156
- return;
157
- }
158
- childClosed = true;
159
155
  closeCode = code;
160
156
  closeSignal = signal;
161
- maybeSettleFromClose();
162
157
  });
163
158
  spawnedChild.on("close", (code, signal) => {
164
159
  childClosed = true;
@@ -0,0 +1,7 @@
1
+ import type { QuotaSource, QuotaUsageSnapshot } from "./quotaSource.js";
2
+ export declare class CompositeQuotaSource implements QuotaSource {
3
+ readonly name = "composite";
4
+ private sources;
5
+ constructor(sources: QuotaSource[]);
6
+ queryCurrentUsage(providerModelKey: string): Promise<QuotaUsageSnapshot | null>;
7
+ }
@@ -0,0 +1,20 @@
1
+ export class CompositeQuotaSource {
2
+ name = "composite";
3
+ sources;
4
+ constructor(sources) {
5
+ this.sources = sources;
6
+ }
7
+ async queryCurrentUsage(providerModelKey) {
8
+ for (const source of this.sources) {
9
+ try {
10
+ const snapshot = await source.queryCurrentUsage(providerModelKey);
11
+ if (snapshot)
12
+ return snapshot;
13
+ }
14
+ catch {
15
+ // Skip failing sources, try next
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ import type { RateLimitDetectionResult } from "../errorParsing.js";
2
+ import type { ErrorParser } from "./genericErrorParser.js";
3
+ export declare class ClaudeCodeErrorParser implements ErrorParser {
4
+ readonly name = "claude-code";
5
+ parse(text: string): RateLimitDetectionResult;
6
+ }
@@ -0,0 +1,39 @@
1
+ export class ClaudeCodeErrorParser {
2
+ name = "claude-code";
3
+ parse(text) {
4
+ for (const line of text.split("\n")) {
5
+ const trimmed = line.trim();
6
+ if (!trimmed.startsWith("{"))
7
+ continue;
8
+ try {
9
+ const obj = JSON.parse(trimmed);
10
+ const level = obj["level"];
11
+ const type = obj["type"];
12
+ const message = obj["message"] ?? "";
13
+ const statusCode = obj["status_code"];
14
+ if (statusCode === 429 ||
15
+ type === "rate_limit_error" ||
16
+ (level === "error" && /\brate.?limit/i.test(message))) {
17
+ const retryAfter = obj["retry_after"];
18
+ const retryAfterMs = obj["retry_after_ms"];
19
+ let extractedMs = null;
20
+ if (retryAfterMs != null && retryAfterMs > 0) {
21
+ extractedMs = retryAfterMs;
22
+ }
23
+ else if (retryAfter != null && retryAfter > 0) {
24
+ extractedMs = retryAfter < 600 ? retryAfter * 1000 : retryAfter;
25
+ }
26
+ return {
27
+ isRateLimited: true,
28
+ retryAfterMs: extractedMs,
29
+ rawMatch: `claude-code-stderr:${statusCode ?? type ?? "rate_limit"}`,
30
+ };
31
+ }
32
+ }
33
+ catch {
34
+ // Not valid JSON, skip
35
+ }
36
+ }
37
+ return { isRateLimited: false, retryAfterMs: null, rawMatch: null };
38
+ }
39
+ }
@@ -0,0 +1,9 @@
1
+ import type { RateLimitDetectionResult } from "../errorParsing.js";
2
+ export interface ErrorParser {
3
+ readonly name: string;
4
+ parse(text: string): RateLimitDetectionResult;
5
+ }
6
+ export declare class GenericErrorParser implements ErrorParser {
7
+ readonly name = "generic";
8
+ parse(text: string): RateLimitDetectionResult;
9
+ }
@@ -0,0 +1,7 @@
1
+ import { detectRateLimitError } from "../errorParsing.js";
2
+ export class GenericErrorParser {
3
+ name = "generic";
4
+ parse(text) {
5
+ return detectRateLimitError(text);
6
+ }
7
+ }
@@ -0,0 +1,5 @@
1
+ export type { ErrorParser } from "./genericErrorParser.js";
2
+ export { GenericErrorParser } from "./genericErrorParser.js";
3
+ export { ClaudeCodeErrorParser } from "./claudeCodeErrorParser.js";
4
+ import type { ErrorParser } from "./genericErrorParser.js";
5
+ export declare function getErrorParserForProvider(providerName: string): ErrorParser;
@@ -0,0 +1,12 @@
1
+ export { GenericErrorParser } from "./genericErrorParser.js";
2
+ export { ClaudeCodeErrorParser } from "./claudeCodeErrorParser.js";
3
+ import { GenericErrorParser } from "./genericErrorParser.js";
4
+ import { ClaudeCodeErrorParser } from "./claudeCodeErrorParser.js";
5
+ const PROVIDER_PARSERS = {
6
+ "claude-code": () => new ClaudeCodeErrorParser(),
7
+ };
8
+ const genericParser = new GenericErrorParser();
9
+ export function getErrorParserForProvider(providerName) {
10
+ const factory = PROVIDER_PARSERS[providerName];
11
+ return factory ? factory() : genericParser;
12
+ }
@@ -0,0 +1,7 @@
1
+ export interface RateLimitDetectionResult {
2
+ isRateLimited: boolean;
3
+ retryAfterMs: number | null;
4
+ rawMatch: string | null;
5
+ }
6
+ export declare function detectRateLimitError(text: string): RateLimitDetectionResult;
7
+ export declare function computeCooldownUntil(retryAfterMs: number | null, defaultMs?: number): string;
@@ -0,0 +1,69 @@
1
+ const RATE_LIMIT_PATTERNS = [
2
+ /\b429\b/i,
3
+ /\btoo many requests\b/i,
4
+ /\brate.?limit/i,
5
+ /\boverloaded\b/i,
6
+ /\bresource.?exhausted\b/i,
7
+ /\bquota.?exceeded\b/i,
8
+ ];
9
+ function tryParseJson(text) {
10
+ const jsonStart = text.indexOf("{");
11
+ if (jsonStart === -1)
12
+ return null;
13
+ try {
14
+ return JSON.parse(text.slice(jsonStart));
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ function extractRetryAfterMs(obj) {
21
+ const headers = obj["headers"];
22
+ const retryAfter = headers?.["retry-after"] ??
23
+ headers?.["Retry-After"] ??
24
+ obj["retry_after"] ??
25
+ obj["retry_after_ms"];
26
+ if (retryAfter == null)
27
+ return null;
28
+ const val = typeof retryAfter === "string" ? Number(retryAfter) : retryAfter;
29
+ if (!Number.isFinite(val) || val <= 0)
30
+ return null;
31
+ // If the value looks like seconds (< 600), convert to ms
32
+ return val < 600 ? val * 1000 : val;
33
+ }
34
+ function detectFromJson(text) {
35
+ const obj = tryParseJson(text);
36
+ if (!obj)
37
+ return null;
38
+ const status = obj["status"];
39
+ const type = obj["type"];
40
+ const errorObj = obj["error"];
41
+ const errorType = errorObj?.["type"];
42
+ const isRateLimited = status === 429 ||
43
+ type === "rate_limit_error" ||
44
+ errorType === "rate_limit_error";
45
+ if (!isRateLimited)
46
+ return null;
47
+ return {
48
+ isRateLimited: true,
49
+ retryAfterMs: extractRetryAfterMs(obj),
50
+ rawMatch: `json:${status === 429 ? "status=429" : `type=${type ?? errorType}`}`,
51
+ };
52
+ }
53
+ export function detectRateLimitError(text) {
54
+ const jsonResult = detectFromJson(text);
55
+ if (jsonResult)
56
+ return jsonResult;
57
+ for (const pattern of RATE_LIMIT_PATTERNS) {
58
+ const match = pattern.exec(text);
59
+ if (match) {
60
+ return { isRateLimited: true, retryAfterMs: null, rawMatch: match[0] };
61
+ }
62
+ }
63
+ return { isRateLimited: false, retryAfterMs: null, rawMatch: null };
64
+ }
65
+ const DEFAULT_COOLDOWN_MS = 60_000;
66
+ export function computeCooldownUntil(retryAfterMs, defaultMs = DEFAULT_COOLDOWN_MS) {
67
+ const ms = retryAfterMs != null && retryAfterMs > 0 ? retryAfterMs : defaultMs;
68
+ return new Date(Date.now() + ms).toISOString();
69
+ }
@@ -0,0 +1,6 @@
1
+ export declare class FileLockTimeoutError extends Error {
2
+ constructor(lockPath: string);
3
+ }
4
+ export declare function acquireLock(lockPath: string, timeoutMs?: number): Promise<void>;
5
+ export declare function releaseLock(lockPath: string): Promise<void>;
6
+ export declare function withFileLock<T>(lockPath: string, fn: () => Promise<T>, timeoutMs?: number): Promise<T>;
@@ -0,0 +1,64 @@
1
+ import { open, unlink, stat } from "node:fs/promises";
2
+ const STALE_LOCK_MS = 30_000;
3
+ const RETRY_INTERVAL_MS = 50;
4
+ const DEFAULT_TIMEOUT_MS = 10_000;
5
+ export class FileLockTimeoutError extends Error {
6
+ constructor(lockPath) {
7
+ super(`Timed out acquiring lock: ${lockPath}`);
8
+ this.name = "FileLockTimeoutError";
9
+ }
10
+ }
11
+ async function isLockStale(lockPath) {
12
+ try {
13
+ const info = await stat(lockPath);
14
+ return Date.now() - info.mtimeMs > STALE_LOCK_MS;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export async function acquireLock(lockPath, timeoutMs = DEFAULT_TIMEOUT_MS) {
21
+ const deadline = Date.now() + timeoutMs;
22
+ while (true) {
23
+ try {
24
+ const fd = await open(lockPath, "wx");
25
+ await fd.close();
26
+ return;
27
+ }
28
+ catch (err) {
29
+ if (err.code !== "EEXIST")
30
+ throw err;
31
+ }
32
+ if (await isLockStale(lockPath)) {
33
+ try {
34
+ await unlink(lockPath);
35
+ continue;
36
+ }
37
+ catch {
38
+ // Another process may have already cleaned it up
39
+ }
40
+ }
41
+ if (Date.now() >= deadline) {
42
+ throw new FileLockTimeoutError(lockPath);
43
+ }
44
+ await new Promise((r) => setTimeout(r, RETRY_INTERVAL_MS));
45
+ }
46
+ }
47
+ export async function releaseLock(lockPath) {
48
+ try {
49
+ await unlink(lockPath);
50
+ }
51
+ catch (err) {
52
+ if (err.code !== "ENOENT")
53
+ throw err;
54
+ }
55
+ }
56
+ export async function withFileLock(lockPath, fn, timeoutMs) {
57
+ await acquireLock(lockPath, timeoutMs);
58
+ try {
59
+ return await fn();
60
+ }
61
+ finally {
62
+ await releaseLock(lockPath);
63
+ }
64
+ }
@@ -1,9 +1,19 @@
1
1
  export { resolveLimits, lookupKnownModel, classifyProvider } from "./limits.js";
2
2
  export type { LimitResolutionResult, ResolveLimitsOptions, ProviderType } from "./limits.js";
3
3
  export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
4
- export { readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, } from "./state.js";
4
+ export { readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, computeBackoffCooldownMs, computeBackoffFailureWeight, computeRampUpConcurrency, } from "./state.js";
5
5
  export { scheduleWave, buildProviderModelKey } from "./scheduler.js";
6
6
  export type { ScheduleWaveOptions } from "./scheduler.js";
7
+ export { detectRateLimitError, computeCooldownUntil } from "./errorParsing.js";
8
+ export { acquireLock, releaseLock, withFileLock, FileLockTimeoutError } from "./fileLock.js";
9
+ export { runSlidingWindow } from "./slidingWindow.js";
10
+ export type { SlidingWindowResult } from "./slidingWindow.js";
11
+ export type { RateLimitDetectionResult } from "./errorParsing.js";
7
12
  export { probeProvider } from "./probe.js";
8
13
  export type { ProbeResult } from "./probe.js";
14
+ export type { QuotaSource, QuotaUsageSnapshot } from "./quotaSource.js";
15
+ export type { ErrorParser } from "./errorParsers/index.js";
16
+ export { GenericErrorParser, ClaudeCodeErrorParser, getErrorParserForProvider } from "./errorParsers/index.js";
17
+ export { LearnedQuotaSource } from "./learnedQuotaSource.js";
18
+ export { CompositeQuotaSource } from "./compositeQuotaSource.js";
9
19
  export type { ResolvedLimits, LimitSource, LimitConfidence, HostConcurrencyLimit, HostConcurrencyLimitSource, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, DispatchQuota, ObservedWaveOutcome, } from "./types.js";