@guilz-dev/belay 0.3.0 → 0.6.0
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/README.md +7 -1
- package/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +12 -16
- package/dist/adapters/shared/gate-runtime.js +499 -43
- package/dist/bundle/claude-runtime.mjs +8004 -2676
- package/dist/bundle/codex-runtime.mjs +8021 -2683
- package/dist/bundle/cursor-runtime.mjs +7993 -2667
- package/dist/cli.js +180 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +264 -58
- package/dist/commands/doctor.js +47 -2
- package/dist/commands/explain.js +17 -2
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +24 -4
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +59 -17
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +14 -9
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +53 -4
- package/dist/core/approval.d.ts +26 -3
- package/dist/core/approval.js +61 -7
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +108 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +93 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +10 -1
- package/dist/core/capability/index.js +8 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +70 -0
- package/dist/core/capability/policy-engine.js +476 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +45 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +40 -22
- package/dist/core/classify-tool.js +157 -64
- package/dist/core/config.d.ts +32 -3
- package/dist/core/config.js +105 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +12 -7
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +90 -11
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +60 -7
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +178 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +36 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +19 -6
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +33 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +49 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +291 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +139 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
- package/dist/core/verdict/judge-runtime-config.js +96 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +200 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +44 -0
- package/dist/core/verdict/judge-transport.js +268 -0
- package/dist/core/verdict/judge.d.ts +14 -14
- package/dist/core/verdict/judge.js +5 -67
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +25 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +26 -1
- package/dist/core/verdict/verdict.js +300 -179
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +61 -8
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +162 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +43 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +99 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +137 -0
- package/dist/services/sandbox-service.d.ts +5 -0
- package/dist/services/sandbox-service.js +30 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { repoLocalStateDirFor } from '../../config-io.js';
|
|
4
|
-
import { normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
|
|
4
|
+
import { belayStateDir, normalizeJudgeProvider, scrubOptionsFromConfig } from '../config.js';
|
|
5
5
|
import { resolveJudgeCredential } from '../judge-api-key.js';
|
|
6
6
|
import { hasValidCloudConsent, isCloudJudgeConfig } from '../judge-config.js';
|
|
7
7
|
import { rejectDeprecatedJudgeModelAuto } from '../judge-model-policy.js';
|
|
@@ -63,7 +63,8 @@ function providerIdFromJudge(judge) {
|
|
|
63
63
|
}
|
|
64
64
|
return inferProviderIdFromConfig(judge);
|
|
65
65
|
}
|
|
66
|
-
function createCliJudgeForProvider(providerId, judgeConfig, config) {
|
|
66
|
+
function createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot) {
|
|
67
|
+
const resolvedRepoRoot = repoRoot ?? process.cwd();
|
|
67
68
|
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
68
69
|
const options = {
|
|
69
70
|
modelRequested: judgeConfig.model,
|
|
@@ -71,6 +72,10 @@ function createCliJudgeForProvider(providerId, judgeConfig, config) {
|
|
|
71
72
|
timeoutMs: judgeConfig.timeoutMs,
|
|
72
73
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
73
74
|
scrubOptions: scrubOptionsFromConfig(config),
|
|
75
|
+
runtime: judgeConfig.runtime,
|
|
76
|
+
repoRoot: resolvedRepoRoot,
|
|
77
|
+
stateDir: belayStateDir(config, repoLocalStateDirFor(resolvedRepoRoot, config)),
|
|
78
|
+
judgeMode: config.mode,
|
|
74
79
|
};
|
|
75
80
|
if (providerId === 'codex') {
|
|
76
81
|
return createCodexCliJudge(options);
|
|
@@ -99,11 +104,12 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
99
104
|
const catalogSpec = getJudgeProviderSpec(providerId);
|
|
100
105
|
const { resolved } = resolveJudgeModel(judgeConfig);
|
|
101
106
|
const runtime = detectJudgeRuntimeCapabilities(providerId);
|
|
107
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
102
108
|
if (provider === 'openai-compatible') {
|
|
103
109
|
const endpoint = judgeConfig.endpoint?.trim();
|
|
104
110
|
if (!endpoint && runtime.cliTransport) {
|
|
105
111
|
if (providerId === 'codex' || providerId === 'cursor') {
|
|
106
|
-
return createCliJudgeForProvider(providerId, judgeConfig, config);
|
|
112
|
+
return createCliJudgeForProvider(providerId, judgeConfig, config, repoRoot);
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
115
|
if (!endpoint) {
|
|
@@ -122,7 +128,6 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
122
128
|
modelResolved: resolved,
|
|
123
129
|
});
|
|
124
130
|
}
|
|
125
|
-
const repoRoot = options.repoRoot ?? process.cwd();
|
|
126
131
|
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
127
132
|
return createOpenAiCompatibleJudge({
|
|
128
133
|
endpoint,
|
|
@@ -156,6 +161,10 @@ export function createJudgeFromConfig(config, options = {}) {
|
|
|
156
161
|
timeoutMs: judgeConfig.timeoutMs,
|
|
157
162
|
sensitivePaths: config.classifier.sensitivePaths,
|
|
158
163
|
scrubOptions: scrubOptionsFromConfig(config),
|
|
164
|
+
runtime: judgeConfig.runtime,
|
|
165
|
+
repoRoot,
|
|
166
|
+
stateDir: belayStateDir(config, repoLocalStateDirFor(repoRoot, config)),
|
|
167
|
+
judgeMode: config.mode,
|
|
159
168
|
});
|
|
160
169
|
}
|
|
161
170
|
return createFailClosedJudge({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { JudgeProviderId } from './judge-catalog.js';
|
|
2
|
+
export type JudgeSessionFailureMode = 'parse_error' | 'non_json_response' | 'timeout' | 'cli_nonzero_exit' | 'io_error' | 'version_mismatch' | 'unsupported_resume' | 'unsafe_option_rejected';
|
|
3
|
+
export interface JudgeProviderSessionCapability {
|
|
4
|
+
providerId: Exclude<JudgeProviderId, 'ollama'>;
|
|
5
|
+
supportsSession: boolean;
|
|
6
|
+
resumeFlag: string | null;
|
|
7
|
+
createChatFlag: string | null;
|
|
8
|
+
readOnlyArgs: string[];
|
|
9
|
+
rejectedUnsafeOptions: string[];
|
|
10
|
+
failureModes: JudgeSessionFailureMode[];
|
|
11
|
+
notes: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const JUDGE_PROVIDER_SESSION_MATRIX: Record<Exclude<JudgeProviderId, 'ollama'>, JudgeProviderSessionCapability>;
|
|
14
|
+
export declare function providerSupportsSession(providerId: Exclude<JudgeProviderId, 'ollama'>, allowlist: JudgeProviderId[]): boolean;
|
|
15
|
+
export declare function assertReadOnlyInvocationArgs(providerId: Exclude<JudgeProviderId, 'ollama'>, args: string[]): {
|
|
16
|
+
ok: true;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: JudgeSessionFailureMode;
|
|
20
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export const JUDGE_PROVIDER_SESSION_MATRIX = {
|
|
2
|
+
cursor: {
|
|
3
|
+
providerId: 'cursor',
|
|
4
|
+
supportsSession: true,
|
|
5
|
+
resumeFlag: '--resume',
|
|
6
|
+
createChatFlag: null,
|
|
7
|
+
readOnlyArgs: ['--mode', 'ask', '--sandbox', 'enabled', '--trust'],
|
|
8
|
+
rejectedUnsafeOptions: ['--force', '--yolo', '--dangerously-skip-permissions'],
|
|
9
|
+
failureModes: [
|
|
10
|
+
'parse_error',
|
|
11
|
+
'non_json_response',
|
|
12
|
+
'timeout',
|
|
13
|
+
'cli_nonzero_exit',
|
|
14
|
+
'io_error',
|
|
15
|
+
'version_mismatch',
|
|
16
|
+
],
|
|
17
|
+
notes: 'Pilot provider: persistent ACP in ask mode; bounded conversation reuse is broker-controlled.',
|
|
18
|
+
},
|
|
19
|
+
codex: {
|
|
20
|
+
providerId: 'codex',
|
|
21
|
+
supportsSession: false,
|
|
22
|
+
resumeFlag: null,
|
|
23
|
+
createChatFlag: null,
|
|
24
|
+
readOnlyArgs: ['--sandbox', 'read-only'],
|
|
25
|
+
rejectedUnsafeOptions: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
26
|
+
failureModes: ['unsupported_resume', 'parse_error', 'timeout'],
|
|
27
|
+
notes: 'Session transport disabled by default; spawn read-only exec remains canonical.',
|
|
28
|
+
},
|
|
29
|
+
claude: {
|
|
30
|
+
providerId: 'claude',
|
|
31
|
+
supportsSession: false,
|
|
32
|
+
resumeFlag: '--continue',
|
|
33
|
+
createChatFlag: null,
|
|
34
|
+
readOnlyArgs: ['--permission-mode', 'plan', '--tools', '', '--bare'],
|
|
35
|
+
rejectedUnsafeOptions: ['--dangerously-skip-permissions'],
|
|
36
|
+
failureModes: ['unsupported_resume', 'parse_error', 'timeout'],
|
|
37
|
+
notes: 'Session transport gated behind allowlist after capability verification.',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export function providerSupportsSession(providerId, allowlist) {
|
|
41
|
+
const capability = JUDGE_PROVIDER_SESSION_MATRIX[providerId];
|
|
42
|
+
return capability.supportsSession && allowlist.includes(providerId);
|
|
43
|
+
}
|
|
44
|
+
export function assertReadOnlyInvocationArgs(providerId, args) {
|
|
45
|
+
const capability = JUDGE_PROVIDER_SESSION_MATRIX[providerId];
|
|
46
|
+
for (const unsafe of capability.rejectedUnsafeOptions) {
|
|
47
|
+
if (args.includes(unsafe)) {
|
|
48
|
+
return { ok: false, reason: 'unsafe_option_rejected' };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (let index = 0; index < capability.readOnlyArgs.length; index += 2) {
|
|
52
|
+
const flag = capability.readOnlyArgs[index];
|
|
53
|
+
const value = capability.readOnlyArgs[index + 1];
|
|
54
|
+
if (!flag) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const flagIndex = args.indexOf(flag);
|
|
58
|
+
if (flagIndex === -1) {
|
|
59
|
+
return { ok: false, reason: 'unsafe_option_rejected' };
|
|
60
|
+
}
|
|
61
|
+
if (value !== undefined && args[flagIndex + 1] !== value) {
|
|
62
|
+
return { ok: false, reason: 'unsafe_option_rejected' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { ok: true };
|
|
66
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { JudgeProviderId } from './judge-catalog.js';
|
|
2
|
+
export interface BelayJudgeSessionConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
maxTurns: number;
|
|
5
|
+
maxAgeMs: number;
|
|
6
|
+
maxIdleMs: number;
|
|
7
|
+
maxPromptBytes: number;
|
|
8
|
+
providerAllowlist: JudgeProviderId[];
|
|
9
|
+
connectTimeoutMs: number;
|
|
10
|
+
evalTimeoutMs: number | null;
|
|
11
|
+
parseTimeoutMs: number;
|
|
12
|
+
maxSessionsPerProvider: number;
|
|
13
|
+
}
|
|
14
|
+
export interface BelayJudgeShadowConfig {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
sampleRate: number;
|
|
17
|
+
sampleRateMax: number;
|
|
18
|
+
dailyRequestCap: number;
|
|
19
|
+
mismatchRateThreshold: number;
|
|
20
|
+
providerAllowlist: JudgeProviderId[];
|
|
21
|
+
windowSize: number;
|
|
22
|
+
}
|
|
23
|
+
export interface BelayJudgeRuntimeConfig {
|
|
24
|
+
session: BelayJudgeSessionConfig;
|
|
25
|
+
shadow: BelayJudgeShadowConfig;
|
|
26
|
+
}
|
|
27
|
+
export declare const JUDGE_LATENCY_SLO: {
|
|
28
|
+
/** Target: Tier1 p95 at least 40% below spawn baseline. */
|
|
29
|
+
readonly tier1P95ReductionTarget: 0.4;
|
|
30
|
+
/** Reference spawn baseline (ms) from dogfood measurement. */
|
|
31
|
+
readonly spawnBaselineP95Ms: 25000;
|
|
32
|
+
readonly spawnBaselineP50Ms: 1300;
|
|
33
|
+
readonly tier0BaselineP95Ms: 60;
|
|
34
|
+
/** Session path must not exceed spawn p95 when enabled. */
|
|
35
|
+
readonly sessionMaxP95Ms: 15000;
|
|
36
|
+
};
|
|
37
|
+
export declare const DEFAULT_JUDGE_SESSION_CONFIG: BelayJudgeSessionConfig;
|
|
38
|
+
export declare const DEFAULT_JUDGE_SHADOW_CONFIG: BelayJudgeShadowConfig;
|
|
39
|
+
export declare const DEFAULT_JUDGE_RUNTIME_CONFIG: BelayJudgeRuntimeConfig;
|
|
40
|
+
export declare function normalizeJudgeRuntimeConfig(runtime: Partial<BelayJudgeRuntimeConfig> | undefined): BelayJudgeRuntimeConfig;
|
|
41
|
+
export declare function normalizeJudgeSessionConfig(session: Partial<BelayJudgeSessionConfig> | undefined): BelayJudgeSessionConfig;
|
|
42
|
+
export declare function resolveSessionEvalTimeoutMs(sessionConfig: BelayJudgeSessionConfig, judgeTimeoutMs: number): number;
|
|
43
|
+
/** Live CLI smoke probes use the configured judge timeout without an artificial cap. */
|
|
44
|
+
export declare function resolveJudgeSmokeProbeTimeoutMs(judgeTimeoutMs: number): number;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export const JUDGE_LATENCY_SLO = {
|
|
2
|
+
/** Target: Tier1 p95 at least 40% below spawn baseline. */
|
|
3
|
+
tier1P95ReductionTarget: 0.4,
|
|
4
|
+
/** Reference spawn baseline (ms) from dogfood measurement. */
|
|
5
|
+
spawnBaselineP95Ms: 25_000,
|
|
6
|
+
spawnBaselineP50Ms: 1_300,
|
|
7
|
+
tier0BaselineP95Ms: 60,
|
|
8
|
+
/** Session path must not exceed spawn p95 when enabled. */
|
|
9
|
+
sessionMaxP95Ms: 15_000,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_JUDGE_SESSION_CONFIG = {
|
|
12
|
+
enabled: true,
|
|
13
|
+
maxTurns: 8,
|
|
14
|
+
maxAgeMs: 10 * 60 * 1000,
|
|
15
|
+
maxIdleMs: 2 * 60 * 1000,
|
|
16
|
+
maxPromptBytes: 32 * 1024,
|
|
17
|
+
providerAllowlist: ['cursor'],
|
|
18
|
+
connectTimeoutMs: 5_000,
|
|
19
|
+
evalTimeoutMs: null,
|
|
20
|
+
parseTimeoutMs: 2_000,
|
|
21
|
+
maxSessionsPerProvider: 1,
|
|
22
|
+
};
|
|
23
|
+
export const DEFAULT_JUDGE_SHADOW_CONFIG = {
|
|
24
|
+
enabled: false,
|
|
25
|
+
sampleRate: 0.01,
|
|
26
|
+
sampleRateMax: 0.05,
|
|
27
|
+
dailyRequestCap: 500,
|
|
28
|
+
mismatchRateThreshold: 0.02,
|
|
29
|
+
providerAllowlist: ['cursor'],
|
|
30
|
+
windowSize: 100,
|
|
31
|
+
};
|
|
32
|
+
export const DEFAULT_JUDGE_RUNTIME_CONFIG = {
|
|
33
|
+
session: { ...DEFAULT_JUDGE_SESSION_CONFIG },
|
|
34
|
+
shadow: { ...DEFAULT_JUDGE_SHADOW_CONFIG },
|
|
35
|
+
};
|
|
36
|
+
const CLI_PROVIDER_IDS = ['codex', 'cursor', 'claude'];
|
|
37
|
+
function normalizeProviderAllowlist(value, fallback) {
|
|
38
|
+
if (!Array.isArray(value)) {
|
|
39
|
+
return [...fallback];
|
|
40
|
+
}
|
|
41
|
+
const allowed = value
|
|
42
|
+
.map((entry) => String(entry).trim())
|
|
43
|
+
.filter((entry) => CLI_PROVIDER_IDS.includes(entry));
|
|
44
|
+
return allowed.length > 0 ? [...new Set(allowed)] : [...fallback];
|
|
45
|
+
}
|
|
46
|
+
function normalizePositiveInt(value, fallback) {
|
|
47
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
48
|
+
? Math.floor(value)
|
|
49
|
+
: fallback;
|
|
50
|
+
}
|
|
51
|
+
function normalizeRate(value, fallback, max) {
|
|
52
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
return Math.min(value, max);
|
|
56
|
+
}
|
|
57
|
+
export function normalizeJudgeRuntimeConfig(runtime) {
|
|
58
|
+
const session = runtime?.session ?? {};
|
|
59
|
+
const shadow = runtime?.shadow ?? {};
|
|
60
|
+
return {
|
|
61
|
+
session: normalizeJudgeSessionConfig(session),
|
|
62
|
+
shadow: {
|
|
63
|
+
enabled: shadow.enabled === true,
|
|
64
|
+
sampleRate: normalizeRate(shadow.sampleRate, DEFAULT_JUDGE_SHADOW_CONFIG.sampleRate, 1),
|
|
65
|
+
sampleRateMax: normalizeRate(shadow.sampleRateMax, DEFAULT_JUDGE_SHADOW_CONFIG.sampleRateMax, 1),
|
|
66
|
+
dailyRequestCap: normalizePositiveInt(shadow.dailyRequestCap, DEFAULT_JUDGE_SHADOW_CONFIG.dailyRequestCap),
|
|
67
|
+
mismatchRateThreshold: normalizeRate(shadow.mismatchRateThreshold, DEFAULT_JUDGE_SHADOW_CONFIG.mismatchRateThreshold, 1),
|
|
68
|
+
providerAllowlist: normalizeProviderAllowlist(shadow.providerAllowlist, DEFAULT_JUDGE_SHADOW_CONFIG.providerAllowlist),
|
|
69
|
+
windowSize: normalizePositiveInt(shadow.windowSize, DEFAULT_JUDGE_SHADOW_CONFIG.windowSize),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function normalizeJudgeSessionConfig(session) {
|
|
74
|
+
const input = session ?? {};
|
|
75
|
+
return {
|
|
76
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : DEFAULT_JUDGE_SESSION_CONFIG.enabled,
|
|
77
|
+
maxTurns: normalizePositiveInt(input.maxTurns, DEFAULT_JUDGE_SESSION_CONFIG.maxTurns),
|
|
78
|
+
maxAgeMs: normalizePositiveInt(input.maxAgeMs, DEFAULT_JUDGE_SESSION_CONFIG.maxAgeMs),
|
|
79
|
+
maxIdleMs: normalizePositiveInt(input.maxIdleMs, DEFAULT_JUDGE_SESSION_CONFIG.maxIdleMs),
|
|
80
|
+
maxPromptBytes: normalizePositiveInt(input.maxPromptBytes, DEFAULT_JUDGE_SESSION_CONFIG.maxPromptBytes),
|
|
81
|
+
providerAllowlist: normalizeProviderAllowlist(input.providerAllowlist, DEFAULT_JUDGE_SESSION_CONFIG.providerAllowlist),
|
|
82
|
+
connectTimeoutMs: normalizePositiveInt(input.connectTimeoutMs, DEFAULT_JUDGE_SESSION_CONFIG.connectTimeoutMs),
|
|
83
|
+
evalTimeoutMs: typeof input.evalTimeoutMs === 'number' && input.evalTimeoutMs > 0
|
|
84
|
+
? Math.floor(input.evalTimeoutMs)
|
|
85
|
+
: null,
|
|
86
|
+
parseTimeoutMs: normalizePositiveInt(input.parseTimeoutMs, DEFAULT_JUDGE_SESSION_CONFIG.parseTimeoutMs),
|
|
87
|
+
maxSessionsPerProvider: normalizePositiveInt(input.maxSessionsPerProvider, DEFAULT_JUDGE_SESSION_CONFIG.maxSessionsPerProvider),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function resolveSessionEvalTimeoutMs(sessionConfig, judgeTimeoutMs) {
|
|
91
|
+
return sessionConfig.evalTimeoutMs ?? judgeTimeoutMs;
|
|
92
|
+
}
|
|
93
|
+
/** Live CLI smoke probes use the configured judge timeout without an artificial cap. */
|
|
94
|
+
export function resolveJudgeSmokeProbeTimeoutMs(judgeTimeoutMs) {
|
|
95
|
+
return judgeTimeoutMs;
|
|
96
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { JudgeProviderId } from './judge-catalog.js';
|
|
2
|
+
import type { CliInvocation, CliJudgeRunCommand } from './judge-cli.js';
|
|
3
|
+
import type { BelayJudgeSessionConfig } from './judge-runtime-config.js';
|
|
4
|
+
import { type JudgeSessionBudget, type JudgeSessionKeyParts, type JudgeSessionResetReason } from './judge-session-guard.js';
|
|
5
|
+
export type BrokerSessionState = 'idle' | 'busy';
|
|
6
|
+
export interface BrokerSessionRecord {
|
|
7
|
+
keyParts: JudgeSessionKeyParts;
|
|
8
|
+
budget: JudgeSessionBudget;
|
|
9
|
+
state: BrokerSessionState;
|
|
10
|
+
providerResumeId?: string;
|
|
11
|
+
lastResetReason?: JudgeSessionResetReason;
|
|
12
|
+
}
|
|
13
|
+
export interface BrokerEvaluateRequest {
|
|
14
|
+
keyParts: JudgeSessionKeyParts;
|
|
15
|
+
invocation: CliInvocation;
|
|
16
|
+
promptBytes: number;
|
|
17
|
+
}
|
|
18
|
+
export interface BrokerEvaluateResult {
|
|
19
|
+
raw: string;
|
|
20
|
+
reused: boolean;
|
|
21
|
+
resetReason?: JudgeSessionResetReason;
|
|
22
|
+
providerResumeId?: string;
|
|
23
|
+
}
|
|
24
|
+
type RunCommand = CliJudgeRunCommand;
|
|
25
|
+
export interface JudgeSessionBrokerOptions {
|
|
26
|
+
config: BelayJudgeSessionConfig;
|
|
27
|
+
runCommand: RunCommand;
|
|
28
|
+
extractResumeId?: (providerId: Exclude<JudgeProviderId, 'ollama'>, raw: string) => string | null;
|
|
29
|
+
}
|
|
30
|
+
export declare class JudgeSessionBroker {
|
|
31
|
+
private readonly sessions;
|
|
32
|
+
private readonly providerMutexes;
|
|
33
|
+
private readonly options;
|
|
34
|
+
constructor(options: JudgeSessionBrokerOptions);
|
|
35
|
+
listSessions(): BrokerSessionRecord[];
|
|
36
|
+
invalidateSession(sessionKey: string, reason: JudgeSessionResetReason): void;
|
|
37
|
+
stopAll(reason?: JudgeSessionResetReason): number;
|
|
38
|
+
evaluate(request: BrokerEvaluateRequest, timeoutMs: number): Promise<BrokerEvaluateResult>;
|
|
39
|
+
private evaluateExclusive;
|
|
40
|
+
private enforceProviderSessionCap;
|
|
41
|
+
private buildResumeInvocation;
|
|
42
|
+
}
|
|
43
|
+
export declare function extractProviderResumeId(providerId: Exclude<JudgeProviderId, 'ollama'>, raw: string): string | null;
|
|
44
|
+
export declare function getRepoJudgeSessionBroker(repoRoot: string, options: JudgeSessionBrokerOptions): JudgeSessionBroker;
|
|
45
|
+
export declare function stopRepoJudgeSessionBroker(repoRoot: string, reason?: JudgeSessionResetReason): number;
|
|
46
|
+
export declare function resetJudgeSessionBrokersForTests(): void;
|
|
47
|
+
export declare function listRepoJudgeSessionBrokers(): string[];
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { buildJudgeSessionKey, evaluateSessionReuse, } from './judge-session-guard.js';
|
|
2
|
+
import { MutexRegistry } from './judge-session-mutex.js';
|
|
3
|
+
export class JudgeSessionBroker {
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
providerMutexes = new MutexRegistry();
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
listSessions() {
|
|
11
|
+
return [...this.sessions.values()];
|
|
12
|
+
}
|
|
13
|
+
invalidateSession(sessionKey, reason) {
|
|
14
|
+
const session = this.sessions.get(sessionKey);
|
|
15
|
+
if (session) {
|
|
16
|
+
session.lastResetReason = reason;
|
|
17
|
+
session.state = 'idle';
|
|
18
|
+
}
|
|
19
|
+
this.sessions.delete(sessionKey);
|
|
20
|
+
}
|
|
21
|
+
stopAll(reason = 'manual_stop') {
|
|
22
|
+
const count = this.sessions.size;
|
|
23
|
+
for (const session of this.sessions.values()) {
|
|
24
|
+
session.lastResetReason = reason;
|
|
25
|
+
session.state = 'idle';
|
|
26
|
+
}
|
|
27
|
+
this.sessions.clear();
|
|
28
|
+
this.providerMutexes.clear();
|
|
29
|
+
return count;
|
|
30
|
+
}
|
|
31
|
+
async evaluate(request, timeoutMs) {
|
|
32
|
+
// One persistent provider process may back several logical session keys. Serialize at the
|
|
33
|
+
// provider boundary so a key rotation cannot close a conversation that is still evaluating.
|
|
34
|
+
return this.providerMutexes
|
|
35
|
+
.forKey(request.keyParts.providerId)
|
|
36
|
+
.run(() => this.evaluateExclusive(request, timeoutMs));
|
|
37
|
+
}
|
|
38
|
+
async evaluateExclusive(request, timeoutMs) {
|
|
39
|
+
const sessionKey = buildJudgeSessionKey(request.keyParts);
|
|
40
|
+
const providerId = request.keyParts.providerId;
|
|
41
|
+
const existing = this.sessions.get(sessionKey);
|
|
42
|
+
const reuseDecision = evaluateSessionReuse(existing?.keyParts ?? null, request.keyParts, existing?.budget ?? null, this.options.config, Date.now(), request.promptBytes);
|
|
43
|
+
const resetReason = reuseDecision.canReuse ? undefined : reuseDecision.resetReason;
|
|
44
|
+
let session = existing;
|
|
45
|
+
if (!reuseDecision.canReuse || !session) {
|
|
46
|
+
session = {
|
|
47
|
+
keyParts: request.keyParts,
|
|
48
|
+
budget: {
|
|
49
|
+
turnCount: 0,
|
|
50
|
+
createdAtMs: Date.now(),
|
|
51
|
+
lastUsedAtMs: Date.now(),
|
|
52
|
+
promptBytes: 0,
|
|
53
|
+
},
|
|
54
|
+
state: 'busy',
|
|
55
|
+
lastResetReason: resetReason ?? 'initial',
|
|
56
|
+
};
|
|
57
|
+
this.enforceProviderSessionCap(providerId, sessionKey);
|
|
58
|
+
this.sessions.set(sessionKey, session);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
session.state = 'busy';
|
|
62
|
+
session.budget.lastUsedAtMs = Date.now();
|
|
63
|
+
}
|
|
64
|
+
const invocation = reuseDecision.canReuse && session.providerResumeId
|
|
65
|
+
? this.buildResumeInvocation(request.invocation, providerId, session.providerResumeId)
|
|
66
|
+
: request.invocation;
|
|
67
|
+
const resumed = reuseDecision.canReuse && Boolean(session.providerResumeId);
|
|
68
|
+
const appliedResetReason = session.lastResetReason;
|
|
69
|
+
try {
|
|
70
|
+
const raw = await this.options.runCommand(invocation, timeoutMs);
|
|
71
|
+
session.budget.turnCount += 1;
|
|
72
|
+
session.budget.promptBytes += request.promptBytes;
|
|
73
|
+
session.budget.lastUsedAtMs = Date.now();
|
|
74
|
+
session.state = 'idle';
|
|
75
|
+
session.lastResetReason = undefined;
|
|
76
|
+
const extracted = this.options.extractResumeId?.(providerId, raw) ??
|
|
77
|
+
extractProviderResumeId(providerId, raw) ??
|
|
78
|
+
undefined;
|
|
79
|
+
if (extracted) {
|
|
80
|
+
session.providerResumeId = extracted;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
raw,
|
|
84
|
+
reused: resumed,
|
|
85
|
+
resetReason: appliedResetReason,
|
|
86
|
+
providerResumeId: session.providerResumeId,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
session.state = 'idle';
|
|
91
|
+
session.lastResetReason = 'cli_error';
|
|
92
|
+
this.sessions.delete(sessionKey);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
enforceProviderSessionCap(providerId, keepKey) {
|
|
97
|
+
const cap = this.options.config.maxSessionsPerProvider;
|
|
98
|
+
const sameProvider = [...this.sessions.entries()].filter(([, record]) => record.keyParts.providerId === providerId);
|
|
99
|
+
if (sameProvider.length < cap) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const [key] of sameProvider) {
|
|
103
|
+
if (key === keepKey) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
this.sessions.delete(key);
|
|
107
|
+
if (this.sessions.size < cap) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
buildResumeInvocation(base, providerId, resumeId) {
|
|
113
|
+
if (providerId === 'cursor') {
|
|
114
|
+
const args = [...base.args];
|
|
115
|
+
const modelIndex = args.indexOf('--model');
|
|
116
|
+
const model = modelIndex >= 0 ? args[modelIndex + 1] : undefined;
|
|
117
|
+
const prompt = args[args.length - 1];
|
|
118
|
+
return {
|
|
119
|
+
binary: base.binary,
|
|
120
|
+
args: [
|
|
121
|
+
'--print',
|
|
122
|
+
'--output-format',
|
|
123
|
+
'json',
|
|
124
|
+
'--mode',
|
|
125
|
+
'ask',
|
|
126
|
+
'--sandbox',
|
|
127
|
+
'enabled',
|
|
128
|
+
'--trust',
|
|
129
|
+
'--resume',
|
|
130
|
+
resumeId,
|
|
131
|
+
...(model ? ['--model', model] : []),
|
|
132
|
+
prompt,
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (providerId === 'claude') {
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
args: [...base.args, '--continue', resumeId],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return base;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function extractProviderResumeId(providerId, raw) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(raw);
|
|
148
|
+
const candidates = [
|
|
149
|
+
parsed.chat_id,
|
|
150
|
+
parsed.chatId,
|
|
151
|
+
parsed.session_id,
|
|
152
|
+
parsed.sessionId,
|
|
153
|
+
parsed.result?.chat_id,
|
|
154
|
+
parsed.result?.session_id,
|
|
155
|
+
];
|
|
156
|
+
for (const candidate of candidates) {
|
|
157
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
158
|
+
return candidate.trim();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// fall through
|
|
164
|
+
}
|
|
165
|
+
if (providerId === 'cursor') {
|
|
166
|
+
const match = raw.match(/"chat_id"\s*:\s*"([^"]+)"/);
|
|
167
|
+
if (match?.[1]) {
|
|
168
|
+
return match[1];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const repoBrokers = new Map();
|
|
174
|
+
export function getRepoJudgeSessionBroker(repoRoot, options) {
|
|
175
|
+
const existing = repoBrokers.get(repoRoot);
|
|
176
|
+
if (existing) {
|
|
177
|
+
return existing;
|
|
178
|
+
}
|
|
179
|
+
const broker = new JudgeSessionBroker(options);
|
|
180
|
+
repoBrokers.set(repoRoot, broker);
|
|
181
|
+
return broker;
|
|
182
|
+
}
|
|
183
|
+
export function stopRepoJudgeSessionBroker(repoRoot, reason = 'manual_stop') {
|
|
184
|
+
const broker = repoBrokers.get(repoRoot);
|
|
185
|
+
if (!broker) {
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
const count = broker.stopAll(reason);
|
|
189
|
+
repoBrokers.delete(repoRoot);
|
|
190
|
+
return count;
|
|
191
|
+
}
|
|
192
|
+
export function resetJudgeSessionBrokersForTests() {
|
|
193
|
+
for (const broker of repoBrokers.values()) {
|
|
194
|
+
broker.stopAll('manual_stop');
|
|
195
|
+
}
|
|
196
|
+
repoBrokers.clear();
|
|
197
|
+
}
|
|
198
|
+
export function listRepoJudgeSessionBrokers() {
|
|
199
|
+
return [...repoBrokers.keys()];
|
|
200
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { JudgeProviderId } from './judge-catalog.js';
|
|
2
|
+
import type { BelayJudgeSessionConfig } from './judge-runtime-config.js';
|
|
3
|
+
export type JudgeSessionResetReason = 'provider_mismatch' | 'model_mismatch' | 'repo_mismatch' | 'mode_mismatch' | 'cli_version_mismatch' | 'max_turns_exceeded' | 'max_age_exceeded' | 'max_idle_exceeded' | 'max_prompt_bytes_exceeded' | 'parse_failure' | 'non_json_response' | 'cli_error' | 'timeout' | 'kill_switch' | 'manual_stop' | 'budget_exhausted' | 'session_disabled' | 'provider_not_allowlisted' | 'initial';
|
|
4
|
+
export type JudgeFallbackReason = 'session_disabled' | 'provider_not_allowlisted' | 'session_unavailable' | 'guard_reset' | 'parse_error' | 'non_json_response' | 'cli_error' | 'timeout' | 'connect_timeout' | 'eval_timeout' | 'parse_timeout' | 'version_mismatch' | 'kill_switch' | 'shadow_forced_spawn' | 'unsafe_option_rejected' | 'broker_busy_reset' | `${Exclude<JudgeProviderId, 'ollama'>}_cli_spawn_error` | `${Exclude<JudgeProviderId, 'ollama'>}_cli_nonzero` | `${Exclude<JudgeProviderId, 'ollama'>}_cli_unavailable` | `${Exclude<JudgeProviderId, 'ollama'>}_cli_parse_error`;
|
|
5
|
+
export interface JudgeSessionKeyParts {
|
|
6
|
+
providerId: JudgeProviderId;
|
|
7
|
+
model: string;
|
|
8
|
+
repoRoot: string;
|
|
9
|
+
judgeMode: string;
|
|
10
|
+
cliVersion: string;
|
|
11
|
+
}
|
|
12
|
+
export interface JudgeSessionBudget {
|
|
13
|
+
turnCount: number;
|
|
14
|
+
createdAtMs: number;
|
|
15
|
+
lastUsedAtMs: number;
|
|
16
|
+
promptBytes: number;
|
|
17
|
+
}
|
|
18
|
+
export interface JudgeSessionGuardDecision {
|
|
19
|
+
canReuse: boolean;
|
|
20
|
+
resetReason?: JudgeSessionResetReason;
|
|
21
|
+
sessionKey: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildJudgeSessionKey(parts: JudgeSessionKeyParts): string;
|
|
24
|
+
export declare function evaluateSessionReuse(existing: JudgeSessionKeyParts | null, requested: JudgeSessionKeyParts, budget: JudgeSessionBudget | null, config: BelayJudgeSessionConfig, now?: number, incomingPromptBytes?: number): JudgeSessionGuardDecision;
|
|
25
|
+
export declare function exceedsPromptBudget(promptBytes: number, config: BelayJudgeSessionConfig, existingBudget?: JudgeSessionBudget | null): boolean;
|
|
26
|
+
export declare function guardFailClosedFallbackReason(resetReason: JudgeSessionResetReason): JudgeFallbackReason;
|
|
27
|
+
export declare function transportFallbackToFailClosedReason(providerId: Exclude<JudgeProviderId, 'ollama'>, fallback: JudgeFallbackReason | undefined): string;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export function buildJudgeSessionKey(parts) {
|
|
2
|
+
return [parts.providerId, parts.model, parts.repoRoot, parts.judgeMode, parts.cliVersion].join(':');
|
|
3
|
+
}
|
|
4
|
+
export function evaluateSessionReuse(existing, requested, budget, config, now = Date.now(), incomingPromptBytes = 0) {
|
|
5
|
+
const sessionKey = buildJudgeSessionKey(requested);
|
|
6
|
+
if (!config.enabled) {
|
|
7
|
+
return { canReuse: false, resetReason: 'session_disabled', sessionKey };
|
|
8
|
+
}
|
|
9
|
+
if (!config.providerAllowlist.includes(requested.providerId)) {
|
|
10
|
+
return { canReuse: false, resetReason: 'provider_not_allowlisted', sessionKey };
|
|
11
|
+
}
|
|
12
|
+
if (!existing || !budget) {
|
|
13
|
+
return { canReuse: false, resetReason: 'initial', sessionKey };
|
|
14
|
+
}
|
|
15
|
+
const existingKey = buildJudgeSessionKey(existing);
|
|
16
|
+
if (existingKey !== sessionKey) {
|
|
17
|
+
if (existing.providerId !== requested.providerId) {
|
|
18
|
+
return { canReuse: false, resetReason: 'provider_mismatch', sessionKey };
|
|
19
|
+
}
|
|
20
|
+
if (existing.model !== requested.model) {
|
|
21
|
+
return { canReuse: false, resetReason: 'model_mismatch', sessionKey };
|
|
22
|
+
}
|
|
23
|
+
if (existing.repoRoot !== requested.repoRoot) {
|
|
24
|
+
return { canReuse: false, resetReason: 'repo_mismatch', sessionKey };
|
|
25
|
+
}
|
|
26
|
+
if (existing.judgeMode !== requested.judgeMode) {
|
|
27
|
+
return { canReuse: false, resetReason: 'mode_mismatch', sessionKey };
|
|
28
|
+
}
|
|
29
|
+
if (existing.cliVersion !== requested.cliVersion) {
|
|
30
|
+
return { canReuse: false, resetReason: 'cli_version_mismatch', sessionKey };
|
|
31
|
+
}
|
|
32
|
+
return { canReuse: false, resetReason: 'provider_mismatch', sessionKey };
|
|
33
|
+
}
|
|
34
|
+
if (budget.turnCount >= config.maxTurns) {
|
|
35
|
+
return { canReuse: false, resetReason: 'max_turns_exceeded', sessionKey };
|
|
36
|
+
}
|
|
37
|
+
if (now - budget.createdAtMs >= config.maxAgeMs) {
|
|
38
|
+
return { canReuse: false, resetReason: 'max_age_exceeded', sessionKey };
|
|
39
|
+
}
|
|
40
|
+
if (now - budget.lastUsedAtMs >= config.maxIdleMs) {
|
|
41
|
+
return { canReuse: false, resetReason: 'max_idle_exceeded', sessionKey };
|
|
42
|
+
}
|
|
43
|
+
if (budget.promptBytes + incomingPromptBytes > config.maxPromptBytes) {
|
|
44
|
+
return { canReuse: false, resetReason: 'max_prompt_bytes_exceeded', sessionKey };
|
|
45
|
+
}
|
|
46
|
+
return { canReuse: true, sessionKey };
|
|
47
|
+
}
|
|
48
|
+
export function exceedsPromptBudget(promptBytes, config, existingBudget = null) {
|
|
49
|
+
const total = (existingBudget?.promptBytes ?? 0) + promptBytes;
|
|
50
|
+
return total > config.maxPromptBytes;
|
|
51
|
+
}
|
|
52
|
+
export function guardFailClosedFallbackReason(resetReason) {
|
|
53
|
+
switch (resetReason) {
|
|
54
|
+
case 'parse_failure':
|
|
55
|
+
return 'parse_error';
|
|
56
|
+
case 'non_json_response':
|
|
57
|
+
return 'non_json_response';
|
|
58
|
+
case 'cli_error':
|
|
59
|
+
return 'cli_error';
|
|
60
|
+
case 'timeout':
|
|
61
|
+
return 'timeout';
|
|
62
|
+
case 'cli_version_mismatch':
|
|
63
|
+
return 'version_mismatch';
|
|
64
|
+
case 'kill_switch':
|
|
65
|
+
return 'kill_switch';
|
|
66
|
+
case 'session_disabled':
|
|
67
|
+
return 'session_disabled';
|
|
68
|
+
case 'provider_not_allowlisted':
|
|
69
|
+
return 'provider_not_allowlisted';
|
|
70
|
+
case 'max_prompt_bytes_exceeded':
|
|
71
|
+
return 'guard_reset';
|
|
72
|
+
default:
|
|
73
|
+
return 'guard_reset';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function transportFallbackToFailClosedReason(providerId, fallback) {
|
|
77
|
+
if (!fallback) {
|
|
78
|
+
return `${providerId}_cli_parse_error`;
|
|
79
|
+
}
|
|
80
|
+
if (fallback.endsWith('_cli_unavailable') || fallback.endsWith('_cli_parse_error')) {
|
|
81
|
+
return fallback;
|
|
82
|
+
}
|
|
83
|
+
switch (fallback) {
|
|
84
|
+
case 'non_json_response':
|
|
85
|
+
case 'parse_error':
|
|
86
|
+
case 'parse_timeout':
|
|
87
|
+
return `${providerId}_cli_parse_error`;
|
|
88
|
+
default:
|
|
89
|
+
return `${providerId}_cli_unavailable`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const JUDGE_SESSION_KILL_FILE = "judge-session-kill.json";
|
|
2
|
+
export interface JudgeSessionKillSwitchRecord {
|
|
3
|
+
triggered: boolean;
|
|
4
|
+
at: string;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function judgeSessionKillSwitchPath(stateDir: string): string;
|
|
8
|
+
export declare function readJudgeSessionKillSwitch(stateDir: string): Promise<JudgeSessionKillSwitchRecord | null>;
|
|
9
|
+
export declare function isJudgeSessionKillSwitchPersisted(stateDir: string): Promise<boolean>;
|
|
10
|
+
export declare function persistJudgeSessionKillSwitch(stateDir: string, reason?: string): Promise<void>;
|
|
11
|
+
export declare function clearJudgeSessionKillSwitch(stateDir: string): Promise<void>;
|