auditor-lambda 0.3.32 → 0.3.34

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 (40) hide show
  1. package/README.md +2 -1
  2. package/audit-code-wrapper-lib.mjs +30 -28
  3. package/dist/cli.d.ts +5 -0
  4. package/dist/cli.js +55 -123
  5. package/dist/mcp/server.js +11 -11
  6. package/dist/orchestrator/reviewPackets.d.ts +3 -0
  7. package/dist/orchestrator/reviewPackets.js +13 -2
  8. package/dist/quota/compositeQuotaSource.d.ts +7 -0
  9. package/dist/quota/compositeQuotaSource.js +20 -0
  10. package/dist/quota/errorParsers/claudeCodeErrorParser.d.ts +6 -0
  11. package/dist/quota/errorParsers/claudeCodeErrorParser.js +39 -0
  12. package/dist/quota/errorParsers/genericErrorParser.d.ts +9 -0
  13. package/dist/quota/errorParsers/genericErrorParser.js +7 -0
  14. package/dist/quota/errorParsers/index.d.ts +5 -0
  15. package/dist/quota/errorParsers/index.js +12 -0
  16. package/dist/quota/errorParsing.d.ts +7 -0
  17. package/dist/quota/errorParsing.js +69 -0
  18. package/dist/quota/fileLock.d.ts +6 -0
  19. package/dist/quota/fileLock.js +64 -0
  20. package/dist/quota/index.d.ts +11 -1
  21. package/dist/quota/index.js +7 -1
  22. package/dist/quota/learnedQuotaSource.d.ts +7 -0
  23. package/dist/quota/learnedQuotaSource.js +25 -0
  24. package/dist/quota/probe.d.ts +1 -4
  25. package/dist/quota/probe.js +1 -4
  26. package/dist/quota/quotaSource.d.ts +12 -0
  27. package/dist/quota/quotaSource.js +1 -0
  28. package/dist/quota/scheduler.d.ts +5 -1
  29. package/dist/quota/scheduler.js +51 -9
  30. package/dist/quota/slidingWindow.d.ts +4 -0
  31. package/dist/quota/slidingWindow.js +28 -0
  32. package/dist/quota/state.d.ts +3 -0
  33. package/dist/quota/state.js +57 -14
  34. package/dist/quota/types.d.ts +11 -2
  35. package/dist/supervisor/operatorHandoff.js +1 -1
  36. package/dist/types/sessionConfig.d.ts +3 -0
  37. package/dist/validation/sessionConfig.js +4 -0
  38. package/package.json +1 -1
  39. package/schemas/dispatch_quota.schema.json +23 -2
  40. package/skills/audit-code/audit-code.prompt.md +5 -0
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { withFileLock } from "./fileLock.js";
4
5
  const STATE_DIR = join(homedir(), ".audit-code");
5
6
  const STATE_PATH = join(STATE_DIR, "quota-state.json");
6
7
  // A bucket needs at least this much success weight before we trust it.
@@ -27,31 +28,38 @@ export function applyDecayToEntry(entry, halfLifeHours) {
27
28
  return { ...entry, buckets: decayed };
28
29
  }
29
30
  function isQuotaState(value) {
30
- return (value !== null &&
31
- typeof value === "object" &&
32
- !Array.isArray(value) &&
33
- value["version"] === 1 &&
34
- typeof value["entries"] === "object");
31
+ if (value === null || typeof value !== "object" || Array.isArray(value))
32
+ return false;
33
+ const obj = value;
34
+ const version = obj["version"];
35
+ return (version === 1 || version === 2) && typeof obj["entries"] === "object";
35
36
  }
36
37
  export async function readQuotaState() {
37
38
  try {
38
39
  const raw = await readFile(STATE_PATH, "utf8");
39
40
  const parsed = JSON.parse(raw);
40
- if (isQuotaState(parsed))
41
+ if (isQuotaState(parsed)) {
42
+ if (parsed.version === 1) {
43
+ for (const entry of Object.values(parsed.entries)) {
44
+ entry.consecutive_429_count ??= 0;
45
+ }
46
+ }
41
47
  return parsed;
42
- process.stderr.write(`[quota] ignoring invalid quota state at ${STATE_PATH}: expected { version: 1, entries: object }\n`);
48
+ }
49
+ process.stderr.write(`[quota] ignoring invalid quota state at ${STATE_PATH}: expected { version: 1|2, entries: object }\n`);
43
50
  }
44
51
  catch (error) {
45
52
  if (error.code === "ENOENT") {
46
- return { version: 1, entries: {} };
53
+ return { version: 2, entries: {} };
47
54
  }
48
55
  process.stderr.write(`[quota] ignoring unreadable quota state at ${STATE_PATH}: ${error instanceof Error ? error.message : String(error)}\n`);
49
56
  }
50
- return { version: 1, entries: {} };
57
+ return { version: 2, entries: {} };
51
58
  }
52
59
  export async function writeQuotaState(state) {
53
60
  await mkdir(STATE_DIR, { recursive: true });
54
- await writeFile(STATE_PATH, JSON.stringify(state, null, 2) + "\n", "utf8");
61
+ const normalized = { ...state, version: 2 };
62
+ await writeFile(STATE_PATH, JSON.stringify(normalized, null, 2) + "\n", "utf8");
55
63
  }
56
64
  /**
57
65
  * Returns the highest concurrency level for which decayed success evidence
@@ -74,14 +82,39 @@ export function computeMaxSafeConcurrency(entry, halfLifeHours, maxToCheck = 32)
74
82
  }
75
83
  return maxSafe;
76
84
  }
85
+ const RAMP_UP_MIN_SUCCESSES = 2;
86
+ export function computeRampUpConcurrency(entry, halfLifeHours, maxToCheck = 32) {
87
+ const maxSafe = computeMaxSafeConcurrency(entry, halfLifeHours, maxToCheck);
88
+ const decayed = applyDecayToEntry(entry, halfLifeHours);
89
+ const bucket = decayed.buckets[String(maxSafe)];
90
+ if (bucket &&
91
+ bucket.success_weight >= RAMP_UP_MIN_SUCCESSES &&
92
+ bucket.failure_weight === 0) {
93
+ return maxSafe + 1;
94
+ }
95
+ return maxSafe;
96
+ }
77
97
  function blankEntry() {
78
98
  return { updated_at: new Date().toISOString(), buckets: {}, cooldown_until: null, last_429_at: null };
79
99
  }
100
+ const BASE_COOLDOWN_MS = 60_000;
101
+ const MAX_COOLDOWN_MS = 15 * 60_000;
102
+ export function computeBackoffCooldownMs(consecutive429Count) {
103
+ const ms = BASE_COOLDOWN_MS * Math.pow(2, Math.max(0, consecutive429Count - 1));
104
+ return Math.min(ms, MAX_COOLDOWN_MS);
105
+ }
106
+ export function computeBackoffFailureWeight(consecutive429Count) {
107
+ return 1.0 + 0.5 * Math.max(0, consecutive429Count - 1);
108
+ }
109
+ const LOCK_PATH = STATE_PATH + ".lock";
80
110
  export async function recordWaveOutcome(providerModelKey, outcome, halfLifeHours) {
111
+ await withFileLock(LOCK_PATH, () => recordWaveOutcomeUnsafe(providerModelKey, outcome, halfLifeHours));
112
+ }
113
+ async function recordWaveOutcomeUnsafe(providerModelKey, outcome, halfLifeHours) {
81
114
  const state = await readQuotaState();
82
115
  const entry = applyDecayToEntry(state.entries[providerModelKey] ?? blankEntry(), halfLifeHours);
83
116
  if (outcome.outcome === "success") {
84
- // Success at N proves 1..N are all safe
117
+ entry.consecutive_429_count = 0;
85
118
  for (let n = 1; n <= outcome.concurrency; n++) {
86
119
  const bucket = entry.buckets[String(n)] ?? { success_weight: 0, failure_weight: 0 };
87
120
  bucket.success_weight += 1.0;
@@ -89,13 +122,23 @@ export async function recordWaveOutcome(providerModelKey, outcome, halfLifeHours
89
122
  }
90
123
  }
91
124
  else {
125
+ const prev429Count = entry.consecutive_429_count ?? 0;
126
+ const new429Count = outcome.outcome === "rate_limited" ? prev429Count + 1 : prev429Count;
127
+ entry.consecutive_429_count = new429Count;
92
128
  entry.last_429_at = new Date().toISOString();
93
- if (outcome.cooldown_until)
129
+ if (outcome.outcome === "rate_limited" && new429Count > 0) {
130
+ const backoffMs = computeBackoffCooldownMs(new429Count);
131
+ entry.cooldown_until = new Date(Date.now() + backoffMs).toISOString();
132
+ }
133
+ else if (outcome.cooldown_until) {
94
134
  entry.cooldown_until = outcome.cooldown_until;
95
- // Failure at N marks N and above as unsafe
135
+ }
136
+ const failureWeight = outcome.outcome === "rate_limited"
137
+ ? computeBackoffFailureWeight(new429Count)
138
+ : 1.0;
96
139
  for (let n = outcome.concurrency; n <= outcome.concurrency + 4; n++) {
97
140
  const bucket = entry.buckets[String(n)] ?? { success_weight: 0, failure_weight: 0 };
98
- bucket.failure_weight += 1.0;
141
+ bucket.failure_weight += failureWeight;
99
142
  entry.buckets[String(n)] = bucket;
100
143
  }
101
144
  }
@@ -22,9 +22,10 @@ export interface QuotaStateEntry {
22
22
  buckets: Record<string, ConcurrencyBucket>;
23
23
  cooldown_until: string | null;
24
24
  last_429_at: string | null;
25
+ consecutive_429_count?: number;
25
26
  }
26
27
  export interface QuotaState {
27
- version: 1;
28
+ version: 1 | 2;
28
29
  entries: Record<string, QuotaStateEntry>;
29
30
  }
30
31
  export interface WaveSchedule {
@@ -36,9 +37,15 @@ export interface WaveSchedule {
36
37
  resolved_limits: ResolvedLimits;
37
38
  host_concurrency_limit: HostConcurrencyLimit | null;
38
39
  model: string | null;
40
+ quota_source_snapshot?: import("./quotaSource.js").QuotaUsageSnapshot | null;
41
+ }
42
+ export interface BackoffState {
43
+ consecutive_429_count: number;
44
+ current_cooldown_ms: number;
45
+ current_failure_weight: number;
39
46
  }
40
47
  export interface DispatchQuota {
41
- contract_version: "audit-code-dispatch-quota/v1alpha1";
48
+ contract_version: "audit-code-dispatch-quota/v1alpha1" | "audit-code-dispatch-quota/v1alpha2";
42
49
  run_id: string;
43
50
  model: string | null;
44
51
  resolved_limits: ResolvedLimits;
@@ -48,6 +55,8 @@ export interface DispatchQuota {
48
55
  wave_size: number;
49
56
  estimated_wave_tokens: number;
50
57
  cooldown_until: string | null;
58
+ quota_source_snapshot?: import("./quotaSource.js").QuotaUsageSnapshot | null;
59
+ backoff_state?: BackoffState | null;
51
60
  }
52
61
  export interface ObservedWaveOutcome {
53
62
  concurrency: number;
@@ -168,7 +168,7 @@ function renderMarkdown(handoff) {
168
168
  lines.push(`- ${command}`);
169
169
  }
170
170
  if (handoff.active_review_run) {
171
- lines.push("- Use next-step so the backend renders either packet dispatch or single-task fallback after the host reports capabilities.");
171
+ lines.push("- Use next-step so the backend renders either packet dispatch or single-task fallback from CLI flags, session config, environment, or the default single-task path.");
172
172
  }
173
173
  }
174
174
  if (handoff.active_review_run) {
@@ -44,6 +44,8 @@ export interface QuotaConfig {
44
44
  reserved_output_tokens?: number;
45
45
  /** Half-life of empirical success/failure evidence in hours (default: 24). */
46
46
  empirical_half_life_hours?: number;
47
+ /** Allow the scheduler to try concurrency maxSafe+1 after consecutive successes (default: true). */
48
+ ramp_up_enabled?: boolean;
47
49
  /** Hard host ceiling for simultaneously active conversation subagents. */
48
50
  host_active_subagent_limit?: number;
49
51
  /** Per-model overrides keyed by "provider/model". */
@@ -63,6 +65,7 @@ export interface SessionConfig {
63
65
  provider?: ProviderName;
64
66
  timeout_ms?: number;
65
67
  ui_mode?: SessionUiMode;
68
+ host_can_dispatch_subagents?: boolean;
66
69
  subprocess_template?: SubprocessTemplateConfig;
67
70
  claude_code?: ClaudeCodeConfig;
68
71
  opencode?: OpenCodeConfig;
@@ -151,6 +151,10 @@ export function validateSessionConfig(value) {
151
151
  pushIssue(issues, "ui_mode", `ui_mode must be one of: ${Array.from(VALID_UI_MODES).join(", ")}.`);
152
152
  }
153
153
  }
154
+ if (value.host_can_dispatch_subagents !== undefined &&
155
+ typeof value.host_can_dispatch_subagents !== "boolean") {
156
+ pushIssue(issues, "host_can_dispatch_subagents", "host_can_dispatch_subagents must be a boolean when provided.");
157
+ }
154
158
  validateTemplateProviderSection(value.subprocess_template, "subprocess_template", issues, provider === "subprocess-template");
155
159
  validateTemplateProviderSection(value.vscode_task, "vscode_task", issues, provider === "vscode-task");
156
160
  validateAgentProviderSection(value.claude_code, "claude_code", issues);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "audit-code-dispatch-quota/v1alpha1",
3
+ "$id": "audit-code-dispatch-quota/v1alpha2",
4
4
  "title": "DispatchQuota",
5
5
  "description": "Quota schedule for a prepare-dispatch run. Written beside dispatch-plan.json. Hosts must launch at most wave_size packets per wave, then re-read this file before the next wave to pick up any updated limits.",
6
6
  "type": "object",
@@ -20,7 +20,7 @@
20
20
  "properties": {
21
21
  "contract_version": {
22
22
  "type": "string",
23
- "const": "audit-code-dispatch-quota/v1alpha1"
23
+ "enum": ["audit-code-dispatch-quota/v1alpha1", "audit-code-dispatch-quota/v1alpha2"]
24
24
  },
25
25
  "run_id": {
26
26
  "type": "string",
@@ -97,6 +97,27 @@
97
97
  "type": ["string", "null"],
98
98
  "format": "date-time",
99
99
  "description": "If non-null, the host should wait until this timestamp before launching the next wave."
100
+ },
101
+ "quota_source_snapshot": {
102
+ "type": ["object", "null"],
103
+ "description": "Real-time usage snapshot from a QuotaSource, if available.",
104
+ "properties": {
105
+ "remaining_pct": { "type": ["number", "null"] },
106
+ "reset_at": { "type": ["string", "null"], "format": "date-time" },
107
+ "requests_remaining": { "type": ["integer", "null"] },
108
+ "tokens_remaining": { "type": ["integer", "null"] },
109
+ "captured_at": { "type": "string", "format": "date-time" },
110
+ "source": { "type": "string" }
111
+ }
112
+ },
113
+ "backoff_state": {
114
+ "type": ["object", "null"],
115
+ "description": "Exponential backoff state for repeated rate-limit errors.",
116
+ "properties": {
117
+ "consecutive_429_count": { "type": "integer", "minimum": 0 },
118
+ "current_cooldown_ms": { "type": "integer", "minimum": 0 },
119
+ "current_failure_weight": { "type": "number", "minimum": 0 }
120
+ }
100
121
  }
101
122
  }
102
123
  }
@@ -40,6 +40,11 @@ follow only that prompt. Do not read packet prompts, schemas, command catalogs,
40
40
  or handoff files unless the current step prompt explicitly instructs you to do
41
41
  so.
42
42
 
43
+ Use MCP tools only as a compatibility adapter when direct shell access to
44
+ `audit-code next-step` is unavailable. The MCP `start_audit` and
45
+ `continue_audit` tools return the same one-step contract; they are not a
46
+ separate orchestration path.
47
+
43
48
  When a step prompt tells you to continue, run `audit-code next-step` again and
44
49
  follow only the newly returned `prompt_path`.
45
50