auditor-lambda 0.6.8 → 0.6.10
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/auditStep.d.ts +63 -0
- package/dist/cli/auditStep.js +133 -0
- package/dist/cli/envelope.d.ts +47 -0
- package/dist/cli/envelope.js +64 -0
- package/dist/cli/lineIndex.d.ts +4 -0
- package/dist/cli/lineIndex.js +44 -0
- package/dist/cli/reviewRun.d.ts +29 -0
- package/dist/cli/reviewRun.js +143 -0
- package/dist/cli.js +7 -358
- package/dist/extractors/fileInventory.js +1 -1452
- package/dist/extractors/graph.js +5 -825
- package/dist/extractors/graphPathUtils.d.ts +12 -0
- package/dist/extractors/graphPathUtils.js +58 -0
- package/dist/extractors/graphRoutes.d.ts +12 -0
- package/dist/extractors/graphRoutes.js +435 -0
- package/dist/extractors/graphSuites.d.ts +4 -0
- package/dist/extractors/graphSuites.js +246 -0
- package/dist/extractors/graphTestSources.d.ts +2 -0
- package/dist/extractors/graphTestSources.js +102 -0
- package/dist/extractors/languageMap.generated.d.ts +1 -0
- package/dist/extractors/languageMap.generated.js +1455 -0
- package/dist/providers/claudeCodeProvider.d.ts +1 -1
- package/dist/providers/claudeCodeProvider.js +3 -2
- package/dist/providers/localSubprocessProvider.d.ts +1 -1
- package/dist/providers/localSubprocessProvider.js +3 -3
- package/dist/providers/opencodeProvider.js +6 -22
- package/dist/providers/subprocessTemplateProvider.js +22 -9
- package/dist/quota/index.d.ts +2 -2
- package/dist/quota/index.js +5 -2
- package/package.json +5 -4
- package/dist/providers/spawnLoggedCommand.d.ts +0 -12
- package/dist/providers/spawnLoggedCommand.js +0 -186
- package/dist/quota/scheduler.d.ts +0 -20
- package/dist/quota/scheduler.js +0 -151
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FreshSessionProvider, LaunchFreshSessionInput, ClaudeCodeConfig, OpenTokenConfig } from "@audit-tools/shared";
|
|
2
|
-
import { spawnLoggedCommand } from "
|
|
2
|
+
import { spawnLoggedCommand } from "@audit-tools/shared";
|
|
3
3
|
export declare const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE: string;
|
|
4
4
|
export declare class ClaudeCodeProvider implements FreshSessionProvider {
|
|
5
5
|
name: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { spawnLoggedCommand } from "
|
|
2
|
+
import { readJsonFile, spawnLoggedCommand, applyWorkerTaskLaunchSettings } from "@audit-tools/shared";
|
|
3
3
|
export const ACTIVE_CLAUDE_CODE_SESSION_MESSAGE = "claude-code provider cannot be used inside an active Claude Code session. " +
|
|
4
4
|
'Set provider to "local-subprocess" in .audit-artifacts/session-config.json, ' +
|
|
5
5
|
"then run /audit-code conversationally and follow the dispatch prompts manually.";
|
|
@@ -18,6 +18,7 @@ export class ClaudeCodeProvider {
|
|
|
18
18
|
throw new Error(ACTIVE_CLAUDE_CODE_SESSION_MESSAGE);
|
|
19
19
|
}
|
|
20
20
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
21
|
+
const task = await readJsonFile(input.taskPath);
|
|
21
22
|
const command = this.config.command ?? "claude";
|
|
22
23
|
const promptFlag = this.config.prompt_flag ?? "-p";
|
|
23
24
|
const args = [
|
|
@@ -28,7 +29,7 @@ export class ClaudeCodeProvider {
|
|
|
28
29
|
? ["--dangerously-skip-permissions"]
|
|
29
30
|
: []),
|
|
30
31
|
];
|
|
31
|
-
return await this.launchCommand(command, args, input, undefined, {
|
|
32
|
+
return await this.launchCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
|
|
32
33
|
opentoken: this.opentoken.enabled,
|
|
33
34
|
opentokenCommand: this.opentoken.command,
|
|
34
35
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FreshSessionProvider, LaunchFreshSessionInput } from "@audit-tools/shared";
|
|
2
|
-
import { spawnLoggedCommand } from "
|
|
2
|
+
import { spawnLoggedCommand } from "@audit-tools/shared";
|
|
3
3
|
export declare const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
4
4
|
export declare class LocalSubprocessProvider implements FreshSessionProvider {
|
|
5
5
|
name: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readJsonFile } from "@audit-tools/shared";
|
|
2
|
-
import { spawnLoggedCommand } from "
|
|
2
|
+
import { spawnLoggedCommand, applyWorkerTaskLaunchSettings } from "@audit-tools/shared";
|
|
3
3
|
export const MISSING_WORKER_COMMAND_MESSAGE = "local-subprocess provider requires task.worker_command.";
|
|
4
4
|
export class LocalSubprocessProvider {
|
|
5
5
|
name = "local-subprocess";
|
|
@@ -9,10 +9,10 @@ export class LocalSubprocessProvider {
|
|
|
9
9
|
}
|
|
10
10
|
async launch(input) {
|
|
11
11
|
const task = await readJsonFile(input.taskPath);
|
|
12
|
-
if (!task.worker_command
|
|
12
|
+
if (!task.worker_command?.length) {
|
|
13
13
|
throw new Error(MISSING_WORKER_COMMAND_MESSAGE);
|
|
14
14
|
}
|
|
15
15
|
const [command, ...args] = task.worker_command;
|
|
16
|
-
return await this.launchCommand(command, args, input);
|
|
16
|
+
return await this.launchCommand(command, args, applyWorkerTaskLaunchSettings(input, task));
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { spawnLoggedCommand } from "
|
|
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
|
-
}
|
|
2
|
+
import { readJsonFile, spawnLoggedCommand, resolveOpenCodeSpawnCommand, applyWorkerTaskLaunchSettings, } from "@audit-tools/shared";
|
|
22
3
|
export class OpenCodeProvider {
|
|
23
4
|
name = "opencode";
|
|
24
5
|
config;
|
|
@@ -29,10 +10,13 @@ export class OpenCodeProvider {
|
|
|
29
10
|
}
|
|
30
11
|
async launch(input) {
|
|
31
12
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
13
|
+
const task = await readJsonFile(input.taskPath);
|
|
32
14
|
const baseCommand = this.config.command ?? "opencode";
|
|
33
15
|
const baseArgs = ["run", prompt, ...(this.config.extra_args ?? [])];
|
|
34
|
-
|
|
35
|
-
|
|
16
|
+
// On Windows the `opencode` launcher is a `.cmd` shim that `spawn` cannot
|
|
17
|
+
// run without a shell; resolve it through cmd.exe (no-op on other OSes).
|
|
18
|
+
const { command, args } = resolveOpenCodeSpawnCommand(baseCommand, baseArgs);
|
|
19
|
+
return await spawnLoggedCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
|
|
36
20
|
opentoken: this.opentoken.enabled,
|
|
37
21
|
opentokenCommand: this.opentoken.command,
|
|
38
22
|
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { readJsonFile } from "@audit-tools/shared";
|
|
2
|
-
import { spawnLoggedCommand } from "
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const workerCommandShell = task.worker_command.map(shellQuote).join(" ");
|
|
2
|
+
import { spawnLoggedCommand, shellQuote, applyWorkerTaskLaunchSettings, } from "@audit-tools/shared";
|
|
3
|
+
function applyTemplate(template, input, task, context) {
|
|
4
|
+
const workerCommandShell = task.worker_command
|
|
5
|
+
.map((arg) => shellQuote(arg))
|
|
6
|
+
.join(" ");
|
|
8
7
|
const workerCommandJson = JSON.stringify(task.worker_command);
|
|
9
8
|
const values = {
|
|
10
9
|
repoRoot: input.repoRoot,
|
|
@@ -20,7 +19,17 @@ function applyTemplate(template, input, task) {
|
|
|
20
19
|
uiMode: input.uiMode,
|
|
21
20
|
timeoutMs: String(input.timeoutMs),
|
|
22
21
|
};
|
|
23
|
-
|
|
22
|
+
const wholePlaceholder = template.match(/^\{([A-Za-z0-9_]+)\}$/);
|
|
23
|
+
return template.replace(/\{([A-Za-z0-9_]+)\}/g, (match, key) => {
|
|
24
|
+
if (!(key in values)) {
|
|
25
|
+
console.warn(`applyTemplate: unknown placeholder ${match} ` +
|
|
26
|
+
`provider=${context.providerName} runId=${input.runId} ` +
|
|
27
|
+
`obligationId=${input.obligationId ?? ""} taskPath=${input.taskPath} ` +
|
|
28
|
+
`entryIndex=${context.entryIndex}`);
|
|
29
|
+
}
|
|
30
|
+
const value = values[key] ?? "";
|
|
31
|
+
return wholePlaceholder || key.endsWith("Shell") ? value : shellQuote(value);
|
|
32
|
+
});
|
|
24
33
|
}
|
|
25
34
|
export class SubprocessTemplateProvider {
|
|
26
35
|
name;
|
|
@@ -36,9 +45,13 @@ export class SubprocessTemplateProvider {
|
|
|
36
45
|
if (!this.config.command_template.length) {
|
|
37
46
|
throw new Error(`${this.name} provider requires a non-empty command_template.`);
|
|
38
47
|
}
|
|
39
|
-
const
|
|
48
|
+
const launchInput = applyWorkerTaskLaunchSettings(input, task);
|
|
49
|
+
const rendered = this.config.command_template.map((entry, entryIndex) => applyTemplate(entry, launchInput, task, {
|
|
50
|
+
providerName: this.name,
|
|
51
|
+
entryIndex,
|
|
52
|
+
}));
|
|
40
53
|
const [command, ...args] = rendered;
|
|
41
|
-
return await spawnLoggedCommand(command, args,
|
|
54
|
+
return await spawnLoggedCommand(command, args, launchInput, this.config.env, {
|
|
42
55
|
opentoken: this.opentoken.enabled,
|
|
43
56
|
opentokenCommand: this.opentoken.command,
|
|
44
57
|
});
|
package/dist/quota/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ResolvedLimits as _ResolvedLimits, LimitConfidence as _LimitConfidence, LimitSource as _LimitSource, HostConcurrencyLimit as _HostConcurrencyLimit, QuotaUsageSnapshot as _QuotaUsageSnapshot, BackoffState as _BackoffState } from "@audit-tools/shared";
|
|
2
2
|
export { resolveLimits, lookupKnownModel, classifyProvider, readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, computeBackoffCooldownMs, computeBackoffFailureWeight, computeRampUpConcurrency, setQuotaStateDir, detectRateLimitError, computeCooldownUntil, acquireLock, releaseLock, withFileLock, FileLockTimeoutError, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, GenericErrorParser, ClaudeCodeErrorParser, getErrorParserForProvider, } from "@audit-tools/shared";
|
|
3
3
|
export type { LimitResolutionResult, ResolveLimitsOptions, ProviderType, ResolvedLimits, LimitSource, LimitConfidence, HostConcurrencyLimit, HostConcurrencyLimitSource, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, BackoffState, ObservedWaveOutcome, RateLimitDetectionResult, SlidingWindowResult, QuotaSource, QuotaUsageSnapshot, ErrorParser, } from "@audit-tools/shared";
|
|
4
|
-
export { scheduleWave, buildProviderModelKey } from "
|
|
5
|
-
export type { ScheduleWaveOptions } from "
|
|
4
|
+
export { scheduleWave, buildProviderModelKey } from "@audit-tools/shared";
|
|
5
|
+
export type { ScheduleWaveOptions } from "@audit-tools/shared";
|
|
6
6
|
export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
|
|
7
7
|
export { probeProvider } from "./probe.js";
|
|
8
8
|
export type { ProbeResult } from "./probe.js";
|
package/dist/quota/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Re-exported from @audit-tools/shared
|
|
2
2
|
export { resolveLimits, lookupKnownModel, classifyProvider, readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, computeBackoffCooldownMs, computeBackoffFailureWeight, computeRampUpConcurrency, setQuotaStateDir, detectRateLimitError, computeCooldownUntil, acquireLock, releaseLock, withFileLock, FileLockTimeoutError, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, GenericErrorParser, ClaudeCodeErrorParser, getErrorParserForProvider, } from "@audit-tools/shared";
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// Wave scheduler now lives in @audit-tools/shared (single source of truth for
|
|
4
|
+
// both orchestrators). Auditor passes its discovered-limits via the structural
|
|
5
|
+
// DiscoveredRateLimitsInput the shared scheduler accepts.
|
|
6
|
+
export { scheduleWave, buildProviderModelKey } from "@audit-tools/shared";
|
|
7
|
+
// Auditor-specific: probe, discovered limits, header extraction
|
|
5
8
|
export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
|
|
6
9
|
export { probeProvider } from "./probe.js";
|
|
7
10
|
export { lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, readDiscoveredLimitsCache, writeDiscoveredLimitsCache, } from "./discoveredLimits.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auditor-lambda",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Portable hybrid code-auditing framework for arbitrary repositories.",
|
|
6
6
|
"type": "module",
|
|
@@ -47,14 +47,15 @@
|
|
|
47
47
|
},
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|
|
50
|
-
"url": "git+https://github.com/OhOkThisIsFine/
|
|
50
|
+
"url": "git+https://github.com/OhOkThisIsFine/audit-tools.git",
|
|
51
|
+
"directory": "packages/audit-code"
|
|
51
52
|
},
|
|
52
53
|
"publishConfig": {
|
|
53
54
|
"access": "public"
|
|
54
55
|
},
|
|
55
|
-
"homepage": "https://github.com/OhOkThisIsFine/
|
|
56
|
+
"homepage": "https://github.com/OhOkThisIsFine/audit-tools/tree/master/packages/audit-code#readme",
|
|
56
57
|
"bugs": {
|
|
57
|
-
"url": "https://github.com/OhOkThisIsFine/
|
|
58
|
+
"url": "https://github.com/OhOkThisIsFine/audit-tools/issues"
|
|
58
59
|
},
|
|
59
60
|
"keywords": [
|
|
60
61
|
"audit",
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { createWriteStream } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import type { LaunchFreshSessionInput, LaunchFreshSessionResult } from "@audit-tools/shared";
|
|
4
|
-
interface SpawnLoggedCommandOptions {
|
|
5
|
-
createWriteStream?: typeof createWriteStream;
|
|
6
|
-
spawn?: typeof spawn;
|
|
7
|
-
killGraceMs?: number;
|
|
8
|
-
opentoken?: boolean;
|
|
9
|
-
opentokenCommand?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare function spawnLoggedCommand(command: string, args: string[], input: LaunchFreshSessionInput, env?: Record<string, string>, options?: SpawnLoggedCommandOptions): Promise<LaunchFreshSessionResult>;
|
|
12
|
-
export {};
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { createWriteStream } from "node:fs";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
const TERMINATION_SIGNAL = "SIGTERM";
|
|
4
|
-
const FORCE_KILL_SIGNAL = "SIGKILL";
|
|
5
|
-
const FORCE_KILL_GRACE_MS = 1_000;
|
|
6
|
-
function formatCommand(command, args) {
|
|
7
|
-
return [command, ...args].join(" ");
|
|
8
|
-
}
|
|
9
|
-
function quoteCmdArg(value) {
|
|
10
|
-
if (/^[A-Za-z0-9_./:=@+-]+$/.test(value))
|
|
11
|
-
return value;
|
|
12
|
-
return `"${value.replace(/(["^&|<>%])/g, "^$1")}"`;
|
|
13
|
-
}
|
|
14
|
-
function applyOpenTokenWrap(command, args, opentokenCommand, platform = process.platform) {
|
|
15
|
-
if (platform === "win32") {
|
|
16
|
-
const shell = process.env.ComSpec ?? "cmd.exe";
|
|
17
|
-
const inner = [command, ...args].map(quoteCmdArg).join(" ");
|
|
18
|
-
return {
|
|
19
|
-
command: shell,
|
|
20
|
-
args: ["/d", "/s", "/c", `${opentokenCommand} wrap ${inner}`],
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
return { command: opentokenCommand, args: ["wrap", command, ...args] };
|
|
24
|
-
}
|
|
25
|
-
// On Windows `command` must be the resolved .cmd / .exe path because `spawn`
|
|
26
|
-
// does not consult PATH for executables without a shell. Callers should use
|
|
27
|
-
// `platformCommand()` (scripts/smoke-packaged-audit-code.mjs) or similar to
|
|
28
|
-
// supply the correct command form for the host OS.
|
|
29
|
-
export async function spawnLoggedCommand(command, args, input, env, options = {}) {
|
|
30
|
-
const openWriteStream = options.createWriteStream ?? createWriteStream;
|
|
31
|
-
const spawnProcess = options.spawn ?? spawn;
|
|
32
|
-
const killGraceMs = options.killGraceMs ?? FORCE_KILL_GRACE_MS;
|
|
33
|
-
if (options.opentoken) {
|
|
34
|
-
const wrapped = applyOpenTokenWrap(command, args, options.opentokenCommand ?? "opentoken");
|
|
35
|
-
command = wrapped.command;
|
|
36
|
-
args = wrapped.args;
|
|
37
|
-
}
|
|
38
|
-
return await new Promise((resolve, reject) => {
|
|
39
|
-
const stdoutLog = openWriteStream(input.stdoutPath, { flags: "a" });
|
|
40
|
-
const stderrLog = openWriteStream(input.stderrPath, { flags: "a" });
|
|
41
|
-
const startedAt = Date.now();
|
|
42
|
-
let timedOut = false;
|
|
43
|
-
let settled = false;
|
|
44
|
-
let child = null;
|
|
45
|
-
let timer;
|
|
46
|
-
let heartbeat;
|
|
47
|
-
let forceKillTimer;
|
|
48
|
-
let pendingLogWrites = 0;
|
|
49
|
-
let childClosed = false;
|
|
50
|
-
let closeCode = null;
|
|
51
|
-
let closeSignal = null;
|
|
52
|
-
let logsEnded = false;
|
|
53
|
-
const clearTimers = () => {
|
|
54
|
-
if (timer) {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
}
|
|
57
|
-
if (heartbeat) {
|
|
58
|
-
clearInterval(heartbeat);
|
|
59
|
-
}
|
|
60
|
-
if (forceKillTimer) {
|
|
61
|
-
clearTimeout(forceKillTimer);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
const endLogs = (callback) => {
|
|
65
|
-
if (logsEnded) {
|
|
66
|
-
callback();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
logsEnded = true;
|
|
70
|
-
let remaining = 2;
|
|
71
|
-
const done = () => {
|
|
72
|
-
remaining -= 1;
|
|
73
|
-
if (remaining === 0) {
|
|
74
|
-
callback();
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
stdoutLog.end(done);
|
|
78
|
-
stderrLog.end(done);
|
|
79
|
-
};
|
|
80
|
-
const settle = (callback) => {
|
|
81
|
-
if (settled) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
settled = true;
|
|
85
|
-
clearTimers();
|
|
86
|
-
endLogs(callback);
|
|
87
|
-
};
|
|
88
|
-
const fail = (error) => {
|
|
89
|
-
if (child && !child.killed) {
|
|
90
|
-
child.kill(FORCE_KILL_SIGNAL);
|
|
91
|
-
}
|
|
92
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
93
|
-
settle(() => reject(normalized));
|
|
94
|
-
};
|
|
95
|
-
const writeLog = (write, chunk) => {
|
|
96
|
-
pendingLogWrites += 1;
|
|
97
|
-
write.write(chunk, () => {
|
|
98
|
-
pendingLogWrites -= 1;
|
|
99
|
-
maybeSettleFromClose();
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
const maybeSettleFromClose = () => {
|
|
103
|
-
if (!childClosed || pendingLogWrites > 0 || settled) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (timedOut) {
|
|
107
|
-
settle(() => reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`)));
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
settle(() => resolve({
|
|
111
|
-
accepted: closeCode === 0 && closeSignal === null,
|
|
112
|
-
processId: spawnedChild.pid,
|
|
113
|
-
exitCode: closeCode,
|
|
114
|
-
signal: closeSignal,
|
|
115
|
-
command: formatCommand(command, args),
|
|
116
|
-
args,
|
|
117
|
-
stdoutPath: input.stdoutPath,
|
|
118
|
-
stderrPath: input.stderrPath,
|
|
119
|
-
error: closeCode === 0 && closeSignal === null
|
|
120
|
-
? undefined
|
|
121
|
-
: closeSignal
|
|
122
|
-
? `Provider command exited with signal ${closeSignal}.`
|
|
123
|
-
: `Provider command exited with code ${closeCode}.`,
|
|
124
|
-
}));
|
|
125
|
-
};
|
|
126
|
-
stdoutLog.on("error", fail);
|
|
127
|
-
stderrLog.on("error", fail);
|
|
128
|
-
let spawnedChild;
|
|
129
|
-
try {
|
|
130
|
-
spawnedChild = spawnProcess(command, args, {
|
|
131
|
-
cwd: input.repoRoot,
|
|
132
|
-
env: { ...process.env, ...env },
|
|
133
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
134
|
-
});
|
|
135
|
-
child = spawnedChild;
|
|
136
|
-
}
|
|
137
|
-
catch (error) {
|
|
138
|
-
fail(error);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (!spawnedChild.stdout || !spawnedChild.stderr) {
|
|
142
|
-
fail(new Error(`Fresh session spawn for run ${input.runId} did not provide pipe-backed stdout/stderr streams.`));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
timer = setTimeout(() => {
|
|
146
|
-
timedOut = true;
|
|
147
|
-
spawnedChild.kill(TERMINATION_SIGNAL);
|
|
148
|
-
forceKillTimer = setTimeout(() => {
|
|
149
|
-
if (!settled) {
|
|
150
|
-
spawnedChild.kill(FORCE_KILL_SIGNAL);
|
|
151
|
-
}
|
|
152
|
-
}, killGraceMs);
|
|
153
|
-
}, input.timeoutMs);
|
|
154
|
-
heartbeat = setInterval(() => {
|
|
155
|
-
const elapsedMs = Date.now() - startedAt;
|
|
156
|
-
const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
|
|
157
|
-
writeLog(stderrLog, message);
|
|
158
|
-
if (input.uiMode === "visible") {
|
|
159
|
-
process.stderr.write(message);
|
|
160
|
-
}
|
|
161
|
-
}, 30_000);
|
|
162
|
-
spawnedChild.stdout.on("data", (chunk) => {
|
|
163
|
-
writeLog(stdoutLog, chunk);
|
|
164
|
-
if (input.uiMode === "visible") {
|
|
165
|
-
process.stdout.write(chunk);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
spawnedChild.stderr.on("data", (chunk) => {
|
|
169
|
-
writeLog(stderrLog, chunk);
|
|
170
|
-
if (input.uiMode === "visible") {
|
|
171
|
-
process.stderr.write(chunk);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
spawnedChild.on("error", fail);
|
|
175
|
-
spawnedChild.on("exit", (code, signal) => {
|
|
176
|
-
closeCode = code;
|
|
177
|
-
closeSignal = signal;
|
|
178
|
-
});
|
|
179
|
-
spawnedChild.on("close", (code, signal) => {
|
|
180
|
-
childClosed = true;
|
|
181
|
-
closeCode = code;
|
|
182
|
-
closeSignal = signal;
|
|
183
|
-
maybeSettleFromClose();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { ResolvedProviderName, SessionConfig, HostConcurrencyLimit, QuotaStateEntry, WaveSchedule, QuotaUsageSnapshot } from "@audit-tools/shared";
|
|
2
|
-
import type { DiscoveredRateLimits } from "./discoveredLimits.js";
|
|
3
|
-
export interface ScheduleWaveOptions {
|
|
4
|
-
providerName: ResolvedProviderName;
|
|
5
|
-
sessionConfig: SessionConfig;
|
|
6
|
-
hostModel: string | null;
|
|
7
|
-
requestedConcurrency: number;
|
|
8
|
-
/** Per-slot estimated tokens (one entry per worker slot). Used for TPM budget. */
|
|
9
|
-
estimatedSlotTokens?: number[];
|
|
10
|
-
/** @deprecated Use estimatedSlotTokens instead. Average tokens per slot — used as fallback. */
|
|
11
|
-
estimatedPacketTokens?: number;
|
|
12
|
-
quotaStateEntry?: QuotaStateEntry | null;
|
|
13
|
-
hostConcurrencyLimit?: HostConcurrencyLimit | null;
|
|
14
|
-
quotaSourceSnapshot?: QuotaUsageSnapshot | null;
|
|
15
|
-
/** RPM/TPM discovered from provider queries or response header extraction. */
|
|
16
|
-
discoveredLimits?: DiscoveredRateLimits | null;
|
|
17
|
-
}
|
|
18
|
-
export declare function scheduleWave(options: ScheduleWaveOptions): WaveSchedule;
|
|
19
|
-
/** Build the state key used for indexing quota-state.json entries. */
|
|
20
|
-
export declare function buildProviderModelKey(providerName: string, hostModel: string | null | undefined): string;
|
package/dist/quota/scheduler.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { classifyProvider, resolveLimits, computeMaxSafeConcurrency, computeRampUpConcurrency } from "@audit-tools/shared";
|
|
2
|
-
function sumTopN(sorted, n) {
|
|
3
|
-
let sum = 0;
|
|
4
|
-
for (let i = 0; i < Math.min(n, sorted.length); i++)
|
|
5
|
-
sum += sorted[i];
|
|
6
|
-
return sum;
|
|
7
|
-
}
|
|
8
|
-
export function scheduleWave(options) {
|
|
9
|
-
const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedSlotTokens, estimatedPacketTokens = 0, quotaStateEntry = null, hostConcurrencyLimit = null, quotaSourceSnapshot = null, discoveredLimits = null, } = options;
|
|
10
|
-
// Descending sort so sumTopN picks the largest slots
|
|
11
|
-
const slotsSorted = estimatedSlotTokens
|
|
12
|
-
? [...estimatedSlotTokens].sort((a, b) => b - a)
|
|
13
|
-
: null;
|
|
14
|
-
const avgTokens = slotsSorted && slotsSorted.length > 0
|
|
15
|
-
? Math.floor(slotsSorted.reduce((a, b) => a + b, 0) / slotsSorted.length)
|
|
16
|
-
: estimatedPacketTokens;
|
|
17
|
-
const quota = sessionConfig.quota ?? {};
|
|
18
|
-
const applyHostConcurrencyLimit = (waveSize) => {
|
|
19
|
-
if (hostConcurrencyLimit === null)
|
|
20
|
-
return waveSize;
|
|
21
|
-
return Math.min(waveSize, hostConcurrencyLimit.active_subagents);
|
|
22
|
-
};
|
|
23
|
-
if (quota.enabled === false) {
|
|
24
|
-
const waveSize = Math.max(1, applyHostConcurrencyLimit(requestedConcurrency));
|
|
25
|
-
const limits = {
|
|
26
|
-
context_tokens: quota.default_context_tokens ?? 32_000,
|
|
27
|
-
output_tokens: quota.reserved_output_tokens ?? 4_096,
|
|
28
|
-
requests_per_minute: null,
|
|
29
|
-
input_tokens_per_minute: null,
|
|
30
|
-
output_tokens_per_minute: null,
|
|
31
|
-
};
|
|
32
|
-
return {
|
|
33
|
-
wave_size: waveSize,
|
|
34
|
-
estimated_wave_tokens: slotsSorted ? sumTopN(slotsSorted, waveSize) : waveSize * avgTokens,
|
|
35
|
-
cooldown_until: null,
|
|
36
|
-
confidence: "high",
|
|
37
|
-
source: "default",
|
|
38
|
-
resolved_limits: limits,
|
|
39
|
-
host_concurrency_limit: hostConcurrencyLimit,
|
|
40
|
-
model: hostModel,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
const safetyMargin = quota.safety_margin ?? 0.8;
|
|
44
|
-
const halfLifeHours = quota.empirical_half_life_hours ?? 24;
|
|
45
|
-
const { limits, source, confidence } = resolveLimits({ providerName, sessionConfig, hostModel });
|
|
46
|
-
// Fill null RPM/TPM from discovered limits (provider query or header extraction)
|
|
47
|
-
if (discoveredLimits) {
|
|
48
|
-
limits.requests_per_minute ??= discoveredLimits.requests_per_minute ?? null;
|
|
49
|
-
limits.input_tokens_per_minute ??= discoveredLimits.input_tokens_per_minute ?? null;
|
|
50
|
-
limits.output_tokens_per_minute ??= discoveredLimits.output_tokens_per_minute ?? null;
|
|
51
|
-
}
|
|
52
|
-
let waveSize = requestedConcurrency;
|
|
53
|
-
let cooldownUntil = null;
|
|
54
|
-
// Respect an active cooldown period
|
|
55
|
-
if (quotaStateEntry?.cooldown_until) {
|
|
56
|
-
const cooldownExpiry = new Date(quotaStateEntry.cooldown_until).getTime();
|
|
57
|
-
if (cooldownExpiry > Date.now()) {
|
|
58
|
-
cooldownUntil = quotaStateEntry.cooldown_until;
|
|
59
|
-
waveSize = 1;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (!cooldownUntil) {
|
|
63
|
-
// Cap by requests-per-minute
|
|
64
|
-
if (limits.requests_per_minute != null) {
|
|
65
|
-
const rpmCap = Math.max(1, Math.floor(limits.requests_per_minute * safetyMargin));
|
|
66
|
-
waveSize = Math.min(waveSize, rpmCap);
|
|
67
|
-
}
|
|
68
|
-
// Cap by input tokens-per-minute
|
|
69
|
-
if (limits.input_tokens_per_minute != null && avgTokens > 0) {
|
|
70
|
-
const tpmBudget = limits.input_tokens_per_minute * safetyMargin;
|
|
71
|
-
if (slotsSorted && slotsSorted.length > 0) {
|
|
72
|
-
let candidateSize = waveSize;
|
|
73
|
-
while (candidateSize > 1 && sumTopN(slotsSorted, candidateSize) > tpmBudget) {
|
|
74
|
-
candidateSize--;
|
|
75
|
-
}
|
|
76
|
-
waveSize = Math.max(1, candidateSize);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
const tpmCap = Math.max(1, Math.floor(tpmBudget / avgTokens));
|
|
80
|
-
waveSize = Math.min(waveSize, tpmCap);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (quotaStateEntry) {
|
|
84
|
-
const rampUp = quota.ramp_up_enabled !== false;
|
|
85
|
-
const learnedCap = rampUp
|
|
86
|
-
? computeRampUpConcurrency(quotaStateEntry, halfLifeHours)
|
|
87
|
-
: computeMaxSafeConcurrency(quotaStateEntry, halfLifeHours);
|
|
88
|
-
waveSize = Math.min(waveSize, learnedCap);
|
|
89
|
-
}
|
|
90
|
-
else if (hostConcurrencyLimit !== null) {
|
|
91
|
-
// The host explicitly reported its active-subagent capacity. That is a
|
|
92
|
-
// real concurrency signal, so it supersedes the conservative
|
|
93
|
-
// unknown-provider fallback (which exists only when we have no signal at
|
|
94
|
-
// all). Leaving waveSize untouched here lets applyHostConcurrencyLimit()
|
|
95
|
-
// below enforce the reported limit as the hard ceiling, while any RPM/TPM
|
|
96
|
-
// caps applied above still bind.
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
const providerType = classifyProvider(providerName);
|
|
100
|
-
const fallbackCap = providerType === "local"
|
|
101
|
-
? quota.unknown_local_concurrency
|
|
102
|
-
: (quota.unknown_hosted_concurrency ?? 1);
|
|
103
|
-
if (fallbackCap === "unlimited") {
|
|
104
|
-
// no cap — "unlimited" intentionally skips clamping
|
|
105
|
-
}
|
|
106
|
-
else if (typeof fallbackCap === "number" && Number.isFinite(fallbackCap)) {
|
|
107
|
-
waveSize = Math.min(waveSize, Math.max(1, Math.floor(fallbackCap)));
|
|
108
|
-
}
|
|
109
|
-
// First-contact cap: when no learned history, no configured fallback, AND
|
|
110
|
-
// no RPM/TPM limits from any source, apply a conservative ceiling.
|
|
111
|
-
// This triggers only for unconfigured local providers (fallbackCap is
|
|
112
|
-
// undefined). Hosted providers default to 1 via unknown_hosted_concurrency,
|
|
113
|
-
// and "unlimited" is an explicit opt-out.
|
|
114
|
-
if (fallbackCap == null &&
|
|
115
|
-
limits.requests_per_minute == null &&
|
|
116
|
-
limits.input_tokens_per_minute == null) {
|
|
117
|
-
const firstContactCap = quota.first_contact_concurrency ?? 3;
|
|
118
|
-
waveSize = Math.min(waveSize, Math.max(1, firstContactCap));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
// Apply real-time quota source data if available
|
|
123
|
-
if (quotaSourceSnapshot && !cooldownUntil) {
|
|
124
|
-
if (quotaSourceSnapshot.remaining_pct != null && quotaSourceSnapshot.remaining_pct < 0.1) {
|
|
125
|
-
waveSize = 1;
|
|
126
|
-
if (quotaSourceSnapshot.reset_at) {
|
|
127
|
-
cooldownUntil = quotaSourceSnapshot.reset_at;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else if (quotaSourceSnapshot.remaining_pct != null && quotaSourceSnapshot.remaining_pct < 0.3) {
|
|
131
|
-
waveSize = Math.min(waveSize, Math.max(1, Math.floor(waveSize * 0.5)));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
waveSize = applyHostConcurrencyLimit(waveSize);
|
|
135
|
-
waveSize = Math.max(1, waveSize);
|
|
136
|
-
return {
|
|
137
|
-
wave_size: waveSize,
|
|
138
|
-
estimated_wave_tokens: slotsSorted ? sumTopN(slotsSorted, waveSize) : waveSize * avgTokens,
|
|
139
|
-
cooldown_until: cooldownUntil,
|
|
140
|
-
confidence,
|
|
141
|
-
source,
|
|
142
|
-
resolved_limits: limits,
|
|
143
|
-
host_concurrency_limit: hostConcurrencyLimit,
|
|
144
|
-
model: hostModel,
|
|
145
|
-
quota_source_snapshot: quotaSourceSnapshot,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
/** Build the state key used for indexing quota-state.json entries. */
|
|
149
|
-
export function buildProviderModelKey(providerName, hostModel) {
|
|
150
|
-
return hostModel ? `${providerName}/${hostModel}` : `${providerName}/*`;
|
|
151
|
-
}
|