auditor-lambda 0.3.27 → 0.3.28
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/audit-code-wrapper-lib.mjs +32 -6
- package/dist/cli.js +24 -1
- package/dist/mcp/server.js +9 -0
- package/dist/quota/hostLimits.d.ts +8 -0
- package/dist/quota/hostLimits.js +50 -0
- package/dist/quota/index.d.ts +2 -1
- package/dist/quota/index.js +1 -0
- package/dist/quota/scheduler.d.ts +2 -1
- package/dist/quota/scheduler.js +12 -3
- package/dist/quota/types.d.ts +8 -0
- package/dist/reporting/mergeFindings.js +86 -1
- package/dist/types/sessionConfig.d.ts +2 -0
- package/package.json +1 -1
- package/schemas/dispatch_quota.schema.json +25 -0
|
@@ -996,6 +996,21 @@ function renderAntigravityPlanningGuide(root) {
|
|
|
996
996
|
].join('\n');
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
+
function renderGeminiCommandToml(promptBody) {
|
|
1000
|
+
const escapedBody = promptBody.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1001
|
+
return [
|
|
1002
|
+
'# /audit-code \u2014 Autonomous local-loop code auditing',
|
|
1003
|
+
'# Registered as a Gemini/Antigravity slash command.',
|
|
1004
|
+
'',
|
|
1005
|
+
'description = "Autonomous local-loop code auditing \u2014 loads one backend-rendered audit step at a time"',
|
|
1006
|
+
'',
|
|
1007
|
+
'prompt = """',
|
|
1008
|
+
promptBody.trimEnd(),
|
|
1009
|
+
'"""',
|
|
1010
|
+
'',
|
|
1011
|
+
].join('\n');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
999
1014
|
function renderSharedMcpLauncher(sourcePackageRoot) {
|
|
1000
1015
|
return [
|
|
1001
1016
|
"import { access, readFile } from 'node:fs/promises';",
|
|
@@ -1428,19 +1443,20 @@ const INSTALL_HOST_DEFINITIONS = {
|
|
|
1428
1443
|
antigravity: {
|
|
1429
1444
|
host: 'antigravity',
|
|
1430
1445
|
label: 'Antigravity',
|
|
1431
|
-
support_level: '
|
|
1432
|
-
setup_kind: 'planning-guide+mcp-ready',
|
|
1446
|
+
support_level: 'supported',
|
|
1447
|
+
setup_kind: 'gemini-command+planning-guide+mcp-ready',
|
|
1433
1448
|
summary:
|
|
1434
|
-
'
|
|
1435
|
-
primary_path_key: '
|
|
1449
|
+
'Use the repo-local .gemini/commands/audit-code.toml slash command, the planning guide, and AGENTS instructions. The shared MCP launcher is available for structured tool calls.',
|
|
1450
|
+
primary_path_key: 'geminiCommandPath',
|
|
1436
1451
|
supporting_path_keys: [
|
|
1452
|
+
'antigravityPlanningGuidePath',
|
|
1437
1453
|
'agentsInstructionsPath',
|
|
1438
1454
|
'mcpLauncherPath',
|
|
1439
1455
|
'installedPromptPath',
|
|
1440
1456
|
],
|
|
1441
1457
|
steps: [
|
|
1442
|
-
'Open this repository in Antigravity
|
|
1443
|
-
'
|
|
1458
|
+
'Open this repository in Antigravity.',
|
|
1459
|
+
'The /audit-code slash command is automatically discovered from .gemini/commands/audit-code.toml.',
|
|
1444
1460
|
'Use the shared auditor MCP server when Antigravity needs structured audit state instead of free-form shell guesses.',
|
|
1445
1461
|
],
|
|
1446
1462
|
profile: {
|
|
@@ -2451,6 +2467,9 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2451
2467
|
antigravityPlanningGuidePath: profile.writeAntigravity
|
|
2452
2468
|
? join(root, '.audit-code', 'install', 'antigravity', 'PLANNING-MODE.md')
|
|
2453
2469
|
: null,
|
|
2470
|
+
geminiCommandPath: profile.writeAntigravity
|
|
2471
|
+
? join(root, '.gemini', 'commands', 'audit-code.toml')
|
|
2472
|
+
: null,
|
|
2454
2473
|
};
|
|
2455
2474
|
|
|
2456
2475
|
const results = [];
|
|
@@ -2578,6 +2597,12 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2578
2597
|
renderAntigravityPlanningGuide(root),
|
|
2579
2598
|
),
|
|
2580
2599
|
);
|
|
2600
|
+
results.push(
|
|
2601
|
+
await writeGeneratedMarkdown(
|
|
2602
|
+
assetPaths.geminiCommandPath,
|
|
2603
|
+
renderGeminiCommandToml(promptBody),
|
|
2604
|
+
),
|
|
2605
|
+
);
|
|
2581
2606
|
}
|
|
2582
2607
|
|
|
2583
2608
|
const hostGuidance = buildHostCatalog({
|
|
@@ -2639,6 +2664,7 @@ async function installBootstrap(argv, options = {}) {
|
|
|
2639
2664
|
slash_command_surfaces: {
|
|
2640
2665
|
vscode_prompt: assetPaths.vscodePromptPath,
|
|
2641
2666
|
opencode_config: assetPaths.opencodeConfigPath,
|
|
2667
|
+
gemini_command: assetPaths.geminiCommandPath,
|
|
2642
2668
|
},
|
|
2643
2669
|
instruction_surfaces: {
|
|
2644
2670
|
agents: assetPaths.agentsInstructionsPath,
|
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/r
|
|
|
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, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, } from "./quota/index.js";
|
|
35
|
+
import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, } 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";
|
|
@@ -188,6 +188,9 @@ function getExplicitProvider(argv) {
|
|
|
188
188
|
function getHostModel(argv) {
|
|
189
189
|
return getFlag(argv, "--host-model") ?? null;
|
|
190
190
|
}
|
|
191
|
+
function getHostMaxActiveSubagents(argv) {
|
|
192
|
+
return parsePositiveIntegerFlag(argv, "--host-max-active-subagents") ?? null;
|
|
193
|
+
}
|
|
191
194
|
function getQuotaProbeMode(argv, sessionConfig) {
|
|
192
195
|
const raw = getFlag(argv, "--quota-probe") ?? sessionConfig.quota?.probe ?? "auto";
|
|
193
196
|
if (raw === "auto" || raw === "never" || raw === "force")
|
|
@@ -539,6 +542,7 @@ function renderCapabilityCheckPrompt(params) {
|
|
|
539
542
|
"- `can_dispatch_subagents: true` if the `task` tool or equivalent subagent dispatch is available",
|
|
540
543
|
"- `can_dispatch_subagents: false` if not",
|
|
541
544
|
"- Optionally `can_restrict_subagent_tools: true` and/or `can_select_subagent_model: true`",
|
|
545
|
+
"- If the host documents or exposes a hard cap on simultaneously active subagents, include `max_active_subagents`.",
|
|
542
546
|
"",
|
|
543
547
|
"Read the `prompt_content` field in the tool response and follow it.",
|
|
544
548
|
"",
|
|
@@ -554,6 +558,8 @@ function renderCapabilityCheckPrompt(params) {
|
|
|
554
558
|
"",
|
|
555
559
|
"If the host can also restrict tools per subagent or select models per subagent, add the matching `--host-can-restrict-subagent-tools true` or `--host-can-select-subagent-model true` flags to the same command. Omit those flags when unsure.",
|
|
556
560
|
"",
|
|
561
|
+
"If the host has a known active-subagent ceiling, add `--host-max-active-subagents <n>` to the same command. For Codex Desktop, use 6.",
|
|
562
|
+
"",
|
|
557
563
|
"After the command writes the next step, read and follow only its `prompt_path`.",
|
|
558
564
|
"",
|
|
559
565
|
].join("\n");
|
|
@@ -576,6 +582,8 @@ function renderDispatchReviewPrompt(params) {
|
|
|
576
582
|
"",
|
|
577
583
|
"Use the `wave_size` from `dispatch_quota`. If `cooldown_until` is non-null, wait until that timestamp before starting the first wave.",
|
|
578
584
|
"",
|
|
585
|
+
"`dispatch_quota.host_concurrency_limit` records any detected hard host cap that contributed to `wave_size`.",
|
|
586
|
+
"",
|
|
579
587
|
"For each wave: use the `task` tool (or equivalent subagent dispatch) to launch up to `wave_size` subagents in parallel (one per entry), wait for all to finish, then start the next wave.",
|
|
580
588
|
"",
|
|
581
589
|
"**Fallback — if auditor MCP tools are not available:** Read both of these files:",
|
|
@@ -1134,6 +1142,7 @@ async function cmdNextStep(argv) {
|
|
|
1134
1142
|
const hostCanRestrictSubagentTools = getOptionalBooleanFlag(argv, "--host-can-restrict-subagent-tools") ??
|
|
1135
1143
|
false;
|
|
1136
1144
|
const hostCanSelectSubagentModel = getOptionalBooleanFlag(argv, "--host-can-select-subagent-model") ?? false;
|
|
1145
|
+
const hostMaxActiveSubagents = getHostMaxActiveSubagents(argv);
|
|
1137
1146
|
let sessionConfig;
|
|
1138
1147
|
try {
|
|
1139
1148
|
sessionConfig = await loadSessionConfig(artifactsDir);
|
|
@@ -1260,6 +1269,7 @@ async function cmdNextStep(argv) {
|
|
|
1260
1269
|
runId: result.activeReviewRun.run_id,
|
|
1261
1270
|
artifactsDir,
|
|
1262
1271
|
root,
|
|
1272
|
+
hostActiveSubagentLimit: hostMaxActiveSubagents,
|
|
1263
1273
|
});
|
|
1264
1274
|
const mergeCommand = mergeAndIngestCommand(artifactsDir, result.activeReviewRun.run_id);
|
|
1265
1275
|
const continueCommand = nextStepCommand(root, artifactsDir);
|
|
@@ -2488,6 +2498,10 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2488
2498
|
const quotaProviderKey = buildProviderModelKey(quotaProviderName, hostModel);
|
|
2489
2499
|
const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
|
|
2490
2500
|
const quotaStateEntry = quotaState.entries[quotaProviderKey] ?? null;
|
|
2501
|
+
const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
|
|
2502
|
+
explicitLimit: params.hostActiveSubagentLimit,
|
|
2503
|
+
sessionConfig,
|
|
2504
|
+
});
|
|
2491
2505
|
const waveSchedule = scheduleWave({
|
|
2492
2506
|
providerName: quotaProviderName,
|
|
2493
2507
|
sessionConfig,
|
|
@@ -2495,6 +2509,7 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2495
2509
|
requestedConcurrency: sessionConfig.parallel_workers ?? plan.length,
|
|
2496
2510
|
estimatedPacketTokens: avgPacketTokens,
|
|
2497
2511
|
quotaStateEntry,
|
|
2512
|
+
hostConcurrencyLimit,
|
|
2498
2513
|
});
|
|
2499
2514
|
const dispatchQuota = {
|
|
2500
2515
|
contract_version: "audit-code-dispatch-quota/v1alpha1",
|
|
@@ -2503,6 +2518,7 @@ async function prepareDispatchArtifacts(params) {
|
|
|
2503
2518
|
resolved_limits: waveSchedule.resolved_limits,
|
|
2504
2519
|
confidence: waveSchedule.confidence,
|
|
2505
2520
|
source: waveSchedule.source,
|
|
2521
|
+
host_concurrency_limit: waveSchedule.host_concurrency_limit,
|
|
2506
2522
|
wave_size: waveSchedule.wave_size,
|
|
2507
2523
|
estimated_wave_tokens: waveSchedule.estimated_wave_tokens,
|
|
2508
2524
|
cooldown_until: waveSchedule.cooldown_until,
|
|
@@ -2558,6 +2574,7 @@ async function cmdPrepareDispatch(argv) {
|
|
|
2558
2574
|
artifactsDir: getArtifactsDir(argv),
|
|
2559
2575
|
root: getFlag(argv, "--root") ? getRootDir(argv) : undefined,
|
|
2560
2576
|
hostModel: getHostModel(argv),
|
|
2577
|
+
hostActiveSubagentLimit: getHostMaxActiveSubagents(argv),
|
|
2561
2578
|
});
|
|
2562
2579
|
console.log(JSON.stringify(result, null, 2));
|
|
2563
2580
|
}
|
|
@@ -3234,12 +3251,17 @@ async function cmdQuota(argv) {
|
|
|
3234
3251
|
const quotaState = await readQuotaState().catch(() => ({ version: 1, entries: {} }));
|
|
3235
3252
|
const quotaStateEntry = quotaState.entries[providerModelKey] ?? null;
|
|
3236
3253
|
const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ?? 24;
|
|
3254
|
+
const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
|
|
3255
|
+
explicitLimit: getHostMaxActiveSubagents(argv),
|
|
3256
|
+
sessionConfig,
|
|
3257
|
+
});
|
|
3237
3258
|
const waveSchedule = scheduleWave({
|
|
3238
3259
|
providerName,
|
|
3239
3260
|
sessionConfig,
|
|
3240
3261
|
hostModel,
|
|
3241
3262
|
requestedConcurrency: sessionConfig.parallel_workers ?? 1,
|
|
3242
3263
|
quotaStateEntry,
|
|
3264
|
+
hostConcurrencyLimit,
|
|
3243
3265
|
});
|
|
3244
3266
|
console.log(JSON.stringify({
|
|
3245
3267
|
provider: providerName,
|
|
@@ -3248,6 +3270,7 @@ async function cmdQuota(argv) {
|
|
|
3248
3270
|
resolved_limits: limits,
|
|
3249
3271
|
confidence,
|
|
3250
3272
|
source,
|
|
3273
|
+
host_concurrency_limit: hostConcurrencyLimit,
|
|
3251
3274
|
probe: probeResult,
|
|
3252
3275
|
learned_caps: quotaStateEntry
|
|
3253
3276
|
? {
|
package/dist/mcp/server.js
CHANGED
|
@@ -364,6 +364,10 @@ async function handleToolCall(name, params, defaults) {
|
|
|
364
364
|
if (canSelect !== undefined) {
|
|
365
365
|
extraArgs.push("--host-can-select-subagent-model", String(Boolean(canSelect)));
|
|
366
366
|
}
|
|
367
|
+
const maxActiveSubagents = params?.max_active_subagents ?? params?.maxActiveSubagents;
|
|
368
|
+
if (maxActiveSubagents !== undefined) {
|
|
369
|
+
extraArgs.push("--host-max-active-subagents", String(maxActiveSubagents));
|
|
370
|
+
}
|
|
367
371
|
return toolResult(await runContinueAudit(context, ["next-step", ...extraArgs]));
|
|
368
372
|
}
|
|
369
373
|
default:
|
|
@@ -522,6 +526,11 @@ function toolDefinitions() {
|
|
|
522
526
|
type: "boolean",
|
|
523
527
|
description: "Whether this host can select a model per subagent.",
|
|
524
528
|
},
|
|
529
|
+
max_active_subagents: {
|
|
530
|
+
type: "integer",
|
|
531
|
+
minimum: 1,
|
|
532
|
+
description: "Known hard cap on simultaneously active subagents for this host, if available.",
|
|
533
|
+
},
|
|
525
534
|
root: { type: "string", description: "Repository root override." },
|
|
526
535
|
artifacts_dir: {
|
|
527
536
|
type: "string",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SessionConfig } from "../types/sessionConfig.js";
|
|
2
|
+
import type { HostConcurrencyLimit } from "./types.js";
|
|
3
|
+
export declare function detectHostActiveSubagentLimit(env?: NodeJS.ProcessEnv): HostConcurrencyLimit | null;
|
|
4
|
+
export declare function resolveHostActiveSubagentLimit(options: {
|
|
5
|
+
explicitLimit?: number | null;
|
|
6
|
+
sessionConfig: SessionConfig;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
}): HostConcurrencyLimit | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const CODEX_DESKTOP_ACTIVE_SUBAGENT_LIMIT = 6;
|
|
2
|
+
function parsePositiveInteger(value) {
|
|
3
|
+
if (typeof value === "number") {
|
|
4
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
5
|
+
}
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
return null;
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!/^\d+$/.test(trimmed))
|
|
10
|
+
return null;
|
|
11
|
+
const parsed = Number(trimmed);
|
|
12
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
13
|
+
}
|
|
14
|
+
export function detectHostActiveSubagentLimit(env = process.env) {
|
|
15
|
+
const explicitEnvLimit = parsePositiveInteger(env.AUDIT_CODE_HOST_MAX_ACTIVE_SUBAGENTS ??
|
|
16
|
+
env.CODEX_MAX_ACTIVE_SUBAGENTS);
|
|
17
|
+
if (explicitEnvLimit !== null) {
|
|
18
|
+
return {
|
|
19
|
+
active_subagents: explicitEnvLimit,
|
|
20
|
+
source: "environment",
|
|
21
|
+
description: "Host active subagent limit from environment.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE === "Codex Desktop") {
|
|
25
|
+
return {
|
|
26
|
+
active_subagents: CODEX_DESKTOP_ACTIVE_SUBAGENT_LIMIT,
|
|
27
|
+
source: "environment",
|
|
28
|
+
description: "Codex Desktop active subagent limit.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
export function resolveHostActiveSubagentLimit(options) {
|
|
34
|
+
if (options.explicitLimit !== undefined && options.explicitLimit !== null) {
|
|
35
|
+
return {
|
|
36
|
+
active_subagents: options.explicitLimit,
|
|
37
|
+
source: "cli_flags",
|
|
38
|
+
description: "Host active subagent limit reported by the conversation host.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const configuredLimit = parsePositiveInteger(options.sessionConfig.quota?.host_active_subagent_limit);
|
|
42
|
+
if (configuredLimit !== null) {
|
|
43
|
+
return {
|
|
44
|
+
active_subagents: configuredLimit,
|
|
45
|
+
source: "session_config",
|
|
46
|
+
description: "Host active subagent limit from session-config quota settings.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return detectHostActiveSubagentLimit(options.env);
|
|
50
|
+
}
|
package/dist/quota/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { resolveLimits, lookupKnownModel, classifyProvider } from "./limits.js";
|
|
2
2
|
export type { LimitResolutionResult, ResolveLimitsOptions, ProviderType } from "./limits.js";
|
|
3
|
+
export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
|
|
3
4
|
export { readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, } from "./state.js";
|
|
4
5
|
export { scheduleWave, buildProviderModelKey } from "./scheduler.js";
|
|
5
6
|
export type { ScheduleWaveOptions } from "./scheduler.js";
|
|
6
7
|
export { probeProvider } from "./probe.js";
|
|
7
8
|
export type { ProbeResult } from "./probe.js";
|
|
8
|
-
export type { ResolvedLimits, LimitSource, LimitConfidence, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, DispatchQuota, ObservedWaveOutcome, } from "./types.js";
|
|
9
|
+
export type { ResolvedLimits, LimitSource, LimitConfidence, HostConcurrencyLimit, HostConcurrencyLimitSource, QuotaState, QuotaStateEntry, ConcurrencyBucket, WaveSchedule, DispatchQuota, ObservedWaveOutcome, } from "./types.js";
|
package/dist/quota/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { resolveLimits, lookupKnownModel, classifyProvider } from "./limits.js";
|
|
2
|
+
export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "./hostLimits.js";
|
|
2
3
|
export { readQuotaState, writeQuotaState, computeMaxSafeConcurrency, recordWaveOutcome, getQuotaStatePath, decayWeight, applyDecayToEntry, } from "./state.js";
|
|
3
4
|
export { scheduleWave, buildProviderModelKey } from "./scheduler.js";
|
|
4
5
|
export { probeProvider } from "./probe.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResolvedProviderName, SessionConfig } from "../types/sessionConfig.js";
|
|
2
|
-
import type { QuotaStateEntry, WaveSchedule } from "./types.js";
|
|
2
|
+
import type { HostConcurrencyLimit, QuotaStateEntry, WaveSchedule } from "./types.js";
|
|
3
3
|
export interface ScheduleWaveOptions {
|
|
4
4
|
providerName: ResolvedProviderName;
|
|
5
5
|
sessionConfig: SessionConfig;
|
|
@@ -8,6 +8,7 @@ export interface ScheduleWaveOptions {
|
|
|
8
8
|
/** Average estimated tokens per packet/worker. Used for TPM budget. */
|
|
9
9
|
estimatedPacketTokens?: number;
|
|
10
10
|
quotaStateEntry?: QuotaStateEntry | null;
|
|
11
|
+
hostConcurrencyLimit?: HostConcurrencyLimit | null;
|
|
11
12
|
}
|
|
12
13
|
export declare function scheduleWave(options: ScheduleWaveOptions): WaveSchedule;
|
|
13
14
|
/** Build the state key used for indexing quota-state.json entries. */
|
package/dist/quota/scheduler.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { resolveLimits } from "./limits.js";
|
|
2
2
|
import { computeMaxSafeConcurrency } from "./state.js";
|
|
3
3
|
export function scheduleWave(options) {
|
|
4
|
-
const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedPacketTokens = 0, quotaStateEntry = null, } = options;
|
|
4
|
+
const { providerName, sessionConfig, hostModel, requestedConcurrency, estimatedPacketTokens = 0, quotaStateEntry = null, hostConcurrencyLimit = null, } = options;
|
|
5
5
|
const quota = sessionConfig.quota ?? {};
|
|
6
|
+
const applyHostConcurrencyLimit = (waveSize) => {
|
|
7
|
+
if (hostConcurrencyLimit === null)
|
|
8
|
+
return waveSize;
|
|
9
|
+
return Math.min(waveSize, hostConcurrencyLimit.active_subagents);
|
|
10
|
+
};
|
|
6
11
|
if (quota.enabled === false) {
|
|
12
|
+
const waveSize = Math.max(1, applyHostConcurrencyLimit(requestedConcurrency));
|
|
7
13
|
const limits = {
|
|
8
14
|
context_tokens: quota.default_context_tokens ?? 32_000,
|
|
9
15
|
output_tokens: quota.reserved_output_tokens ?? 4_096,
|
|
@@ -12,12 +18,13 @@ export function scheduleWave(options) {
|
|
|
12
18
|
output_tokens_per_minute: null,
|
|
13
19
|
};
|
|
14
20
|
return {
|
|
15
|
-
wave_size:
|
|
16
|
-
estimated_wave_tokens:
|
|
21
|
+
wave_size: waveSize,
|
|
22
|
+
estimated_wave_tokens: waveSize * estimatedPacketTokens,
|
|
17
23
|
cooldown_until: null,
|
|
18
24
|
confidence: "high",
|
|
19
25
|
source: "default",
|
|
20
26
|
resolved_limits: limits,
|
|
27
|
+
host_concurrency_limit: hostConcurrencyLimit,
|
|
21
28
|
model: hostModel,
|
|
22
29
|
};
|
|
23
30
|
}
|
|
@@ -51,6 +58,7 @@ export function scheduleWave(options) {
|
|
|
51
58
|
}
|
|
52
59
|
// No learned data: use requestedConcurrency and let 429 outcomes train the cap
|
|
53
60
|
}
|
|
61
|
+
waveSize = applyHostConcurrencyLimit(waveSize);
|
|
54
62
|
waveSize = Math.max(1, waveSize);
|
|
55
63
|
return {
|
|
56
64
|
wave_size: waveSize,
|
|
@@ -59,6 +67,7 @@ export function scheduleWave(options) {
|
|
|
59
67
|
confidence,
|
|
60
68
|
source,
|
|
61
69
|
resolved_limits: limits,
|
|
70
|
+
host_concurrency_limit: hostConcurrencyLimit,
|
|
62
71
|
model: hostModel,
|
|
63
72
|
};
|
|
64
73
|
}
|
package/dist/quota/types.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type LimitSource = "explicit_config" | "cli_flags" | "known_metadata" | "learned" | "default";
|
|
2
2
|
export type LimitConfidence = "high" | "medium" | "low";
|
|
3
|
+
export type HostConcurrencyLimitSource = "cli_flags" | "session_config" | "environment";
|
|
4
|
+
export interface HostConcurrencyLimit {
|
|
5
|
+
active_subagents: number;
|
|
6
|
+
source: HostConcurrencyLimitSource;
|
|
7
|
+
description: string;
|
|
8
|
+
}
|
|
3
9
|
export interface ResolvedLimits {
|
|
4
10
|
context_tokens: number;
|
|
5
11
|
output_tokens: number;
|
|
@@ -28,6 +34,7 @@ export interface WaveSchedule {
|
|
|
28
34
|
confidence: LimitConfidence;
|
|
29
35
|
source: LimitSource;
|
|
30
36
|
resolved_limits: ResolvedLimits;
|
|
37
|
+
host_concurrency_limit: HostConcurrencyLimit | null;
|
|
31
38
|
model: string | null;
|
|
32
39
|
}
|
|
33
40
|
export interface DispatchQuota {
|
|
@@ -37,6 +44,7 @@ export interface DispatchQuota {
|
|
|
37
44
|
resolved_limits: ResolvedLimits;
|
|
38
45
|
confidence: LimitConfidence;
|
|
39
46
|
source: LimitSource;
|
|
47
|
+
host_concurrency_limit: HostConcurrencyLimit | null;
|
|
40
48
|
wave_size: number;
|
|
41
49
|
estimated_wave_tokens: number;
|
|
42
50
|
cooldown_until: string | null;
|
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
function normalizeText(value) {
|
|
2
2
|
return (value ?? "").trim().toLowerCase();
|
|
3
3
|
}
|
|
4
|
+
function wordSet(text) {
|
|
5
|
+
return new Set(text
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
8
|
+
.split(/\s+/)
|
|
9
|
+
.filter(Boolean));
|
|
10
|
+
}
|
|
11
|
+
function wordJaccard(a, b) {
|
|
12
|
+
const sa = wordSet(a);
|
|
13
|
+
const sb = wordSet(b);
|
|
14
|
+
let intersection = 0;
|
|
15
|
+
for (const w of sa) {
|
|
16
|
+
if (sb.has(w))
|
|
17
|
+
intersection++;
|
|
18
|
+
}
|
|
19
|
+
const union = sa.size + sb.size - intersection;
|
|
20
|
+
return union === 0 ? 0 : intersection / union;
|
|
21
|
+
}
|
|
22
|
+
function filePathOverlap(a, b) {
|
|
23
|
+
const setA = new Set(a.affected_files.map((f) => f.path));
|
|
24
|
+
const setB = new Set(b.affected_files.map((f) => f.path));
|
|
25
|
+
let intersection = 0;
|
|
26
|
+
for (const path of setA) {
|
|
27
|
+
if (setB.has(path))
|
|
28
|
+
intersection++;
|
|
29
|
+
}
|
|
30
|
+
const union = setA.size + setB.size - intersection;
|
|
31
|
+
return union === 0 ? 0 : intersection / union;
|
|
32
|
+
}
|
|
4
33
|
function primaryPath(finding) {
|
|
5
34
|
return finding.affected_files[0]?.path ?? "";
|
|
6
35
|
}
|
|
@@ -63,6 +92,62 @@ function mergeAffectedFiles(existing, incoming) {
|
|
|
63
92
|
}
|
|
64
93
|
existing.affected_files.sort((a, b) => a.path.localeCompare(b.path) || (a.line_start ?? 0) - (b.line_start ?? 0));
|
|
65
94
|
}
|
|
95
|
+
function deduplicateCrossLens(findings) {
|
|
96
|
+
const groups = new Map();
|
|
97
|
+
for (const finding of findings) {
|
|
98
|
+
const key = primaryPath(finding);
|
|
99
|
+
const group = groups.get(key);
|
|
100
|
+
if (group) {
|
|
101
|
+
group.push(finding);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
groups.set(key, [finding]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const removed = new Set();
|
|
108
|
+
for (const group of groups.values()) {
|
|
109
|
+
if (group.length < 2)
|
|
110
|
+
continue;
|
|
111
|
+
for (let i = 0; i < group.length; i++) {
|
|
112
|
+
if (removed.has(group[i]))
|
|
113
|
+
continue;
|
|
114
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
115
|
+
if (removed.has(group[j]))
|
|
116
|
+
continue;
|
|
117
|
+
const a = group[i];
|
|
118
|
+
const b = group[j];
|
|
119
|
+
if (normalizeText(a.lens) === normalizeText(b.lens))
|
|
120
|
+
continue;
|
|
121
|
+
const titleSim = wordJaccard(a.title, b.title);
|
|
122
|
+
const catMatch = normalizeText(a.category) === normalizeText(b.category);
|
|
123
|
+
const threshold = catMatch ? 0.4 : 0.5;
|
|
124
|
+
if (titleSim < threshold)
|
|
125
|
+
continue;
|
|
126
|
+
if (filePathOverlap(a, b) < 0.5)
|
|
127
|
+
continue;
|
|
128
|
+
const aSev = severityRank(a.severity);
|
|
129
|
+
const bSev = severityRank(b.severity);
|
|
130
|
+
const aConf = confidenceRank(a.confidence);
|
|
131
|
+
const bConf = confidenceRank(b.confidence);
|
|
132
|
+
const keepA = aSev > bSev || (aSev === bSev && aConf >= bConf);
|
|
133
|
+
const [survivor, absorbed] = keepA ? [a, b] : [b, a];
|
|
134
|
+
mergeAffectedFiles(survivor, absorbed);
|
|
135
|
+
survivor.evidence = [
|
|
136
|
+
...new Set([
|
|
137
|
+
...(survivor.evidence ?? []),
|
|
138
|
+
...(absorbed.evidence ?? []),
|
|
139
|
+
]),
|
|
140
|
+
];
|
|
141
|
+
survivor.systemic = Boolean(survivor.systemic || absorbed.systemic);
|
|
142
|
+
if (absorbed.summary.length > survivor.summary.length) {
|
|
143
|
+
survivor.summary = absorbed.summary;
|
|
144
|
+
}
|
|
145
|
+
removed.add(absorbed);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return findings.filter((f) => !removed.has(f));
|
|
150
|
+
}
|
|
66
151
|
export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
67
152
|
const merged = new Map();
|
|
68
153
|
const runtimeEvidence = runtimeSummary(runtimeReport);
|
|
@@ -109,7 +194,7 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
109
194
|
];
|
|
110
195
|
}
|
|
111
196
|
}
|
|
112
|
-
return [...merged.values()].sort((a, b) => {
|
|
197
|
+
return deduplicateCrossLens([...merged.values()]).sort((a, b) => {
|
|
113
198
|
const severityDelta = severityRank(b.severity) - severityRank(a.severity);
|
|
114
199
|
if (severityDelta !== 0)
|
|
115
200
|
return severityDelta;
|
|
@@ -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
|
+
/** Hard host ceiling for simultaneously active conversation subagents. */
|
|
48
|
+
host_active_subagent_limit?: number;
|
|
47
49
|
/** Per-model overrides keyed by "provider/model". */
|
|
48
50
|
models?: Record<string, QuotaModelLimits>;
|
|
49
51
|
}
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"resolved_limits",
|
|
12
12
|
"confidence",
|
|
13
13
|
"source",
|
|
14
|
+
"host_concurrency_limit",
|
|
14
15
|
"wave_size",
|
|
15
16
|
"estimated_wave_tokens",
|
|
16
17
|
"cooldown_until"
|
|
@@ -58,6 +59,30 @@
|
|
|
58
59
|
"enum": ["explicit_config", "cli_flags", "known_metadata", "learned", "default"],
|
|
59
60
|
"description": "Where the resolved limits came from."
|
|
60
61
|
},
|
|
62
|
+
"host_concurrency_limit": {
|
|
63
|
+
"type": ["object", "null"],
|
|
64
|
+
"description": "A hard host ceiling on simultaneously active subagents, if detected or reported.",
|
|
65
|
+
"required": [
|
|
66
|
+
"active_subagents",
|
|
67
|
+
"source",
|
|
68
|
+
"description"
|
|
69
|
+
],
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"properties": {
|
|
72
|
+
"active_subagents": {
|
|
73
|
+
"type": "integer",
|
|
74
|
+
"minimum": 1
|
|
75
|
+
},
|
|
76
|
+
"source": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"enum": ["cli_flags", "session_config", "environment"]
|
|
79
|
+
},
|
|
80
|
+
"description": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"minLength": 1
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
61
86
|
"wave_size": {
|
|
62
87
|
"type": "integer",
|
|
63
88
|
"minimum": 1,
|