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.
- package/dist/cli.js +40 -30
- package/dist/orchestrator/reviewPackets.d.ts +3 -0
- package/dist/orchestrator/reviewPackets.js +13 -2
- package/dist/orchestrator/selectiveDeepening.d.ts +2 -0
- package/dist/orchestrator/selectiveDeepening.js +10 -1
- package/dist/orchestrator/state.js +2 -17
- package/dist/providers/opencodeProvider.js +23 -3
- package/dist/providers/spawnLoggedCommand.js +0 -5
- package/dist/quota/compositeQuotaSource.d.ts +7 -0
- package/dist/quota/compositeQuotaSource.js +20 -0
- package/dist/quota/errorParsers/claudeCodeErrorParser.d.ts +6 -0
- package/dist/quota/errorParsers/claudeCodeErrorParser.js +39 -0
- package/dist/quota/errorParsers/genericErrorParser.d.ts +9 -0
- package/dist/quota/errorParsers/genericErrorParser.js +7 -0
- package/dist/quota/errorParsers/index.d.ts +5 -0
- package/dist/quota/errorParsers/index.js +12 -0
- package/dist/quota/errorParsing.d.ts +7 -0
- package/dist/quota/errorParsing.js +69 -0
- package/dist/quota/fileLock.d.ts +6 -0
- package/dist/quota/fileLock.js +64 -0
- package/dist/quota/index.d.ts +11 -1
- package/dist/quota/index.js +7 -1
- package/dist/quota/learnedQuotaSource.d.ts +7 -0
- package/dist/quota/learnedQuotaSource.js +25 -0
- package/dist/quota/probe.d.ts +1 -4
- package/dist/quota/probe.js +1 -4
- package/dist/quota/quotaSource.d.ts +12 -0
- package/dist/quota/quotaSource.js +1 -0
- package/dist/quota/scheduler.d.ts +5 -1
- package/dist/quota/scheduler.js +51 -9
- package/dist/quota/slidingWindow.d.ts +4 -0
- package/dist/quota/slidingWindow.js +28 -0
- package/dist/quota/state.d.ts +3 -0
- package/dist/quota/state.js +57 -14
- package/dist/quota/types.d.ts +11 -2
- package/dist/reporting/mergeFindings.js +115 -23
- package/dist/types/sessionConfig.d.ts +2 -0
- package/package.json +1 -1
- 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) ??
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
1608
|
-
outcome:
|
|
1609
|
-
cooldown_until:
|
|
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
|
|
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:
|
|
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
|
-
|
|
2478
|
+
estimatedSlotTokens: perPacketTokens,
|
|
2475
2479
|
quotaStateEntry,
|
|
2476
2480
|
hostConcurrencyLimit,
|
|
2477
2481
|
});
|
|
2478
2482
|
const dispatchQuota = {
|
|
2479
|
-
contract_version: "audit-code-dispatch-quota/
|
|
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:
|
|
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 >=
|
|
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 (
|
|
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
|
-
? "
|
|
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
|
|
12
|
-
const
|
|
13
|
-
|
|
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,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
|
+
}
|
package/dist/quota/index.d.ts
CHANGED
|
@@ -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";
|