open-multi-agent-kit 0.97.0 → 0.98.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/CHANGELOG.md +35 -0
- package/README.md +41 -9
- package/dist/cli/args.d.ts +2 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +13 -189
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +194 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/initial-message.d.ts.map +1 -1
- package/dist/cli/initial-message.js +2 -1
- package/dist/cli/initial-message.js.map +1 -1
- package/dist/cli/resource-paths.d.ts +4 -0
- package/dist/cli/resource-paths.d.ts.map +1 -0
- package/dist/cli/resource-paths.js +59 -0
- package/dist/cli/resource-paths.js.map +1 -0
- package/dist/commands/adaptorch-doctor-cli.d.ts +13 -0
- package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -0
- package/dist/commands/adaptorch-doctor-cli.js +115 -0
- package/dist/commands/adaptorch-doctor-cli.js.map +1 -0
- package/dist/commands/resource-doctor-cli.d.ts +4 -0
- package/dist/commands/resource-doctor-cli.d.ts.map +1 -1
- package/dist/commands/resource-doctor-cli.js +35 -5
- package/dist/commands/resource-doctor-cli.js.map +1 -1
- package/dist/core/adaptorch-bridge.d.ts +11 -13
- package/dist/core/adaptorch-bridge.d.ts.map +1 -1
- package/dist/core/adaptorch-bridge.js +11 -13
- package/dist/core/adaptorch-bridge.js.map +1 -1
- package/dist/core/agent-session-snapshot.d.ts +16 -0
- package/dist/core/agent-session-snapshot.d.ts.map +1 -0
- package/dist/core/agent-session-snapshot.js +111 -0
- package/dist/core/agent-session-snapshot.js.map +1 -0
- package/dist/core/agent-session.d.ts +5 -9
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +27 -219
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +10 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -1
- package/dist/core/agent-tool-settings.js +10 -1
- package/dist/core/agent-tool-settings.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +7 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +25 -61
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/index.d.ts +1 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +1 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/knowledge-triage-format.d.ts +9 -0
- package/dist/core/compaction/knowledge-triage-format.d.ts.map +1 -0
- package/dist/core/compaction/knowledge-triage-format.js +71 -0
- package/dist/core/compaction/knowledge-triage-format.js.map +1 -0
- package/dist/core/compaction/knowledge-triage-types.d.ts +7 -0
- package/dist/core/compaction/knowledge-triage-types.d.ts.map +1 -0
- package/dist/core/compaction/knowledge-triage-types.js +2 -0
- package/dist/core/compaction/knowledge-triage-types.js.map +1 -0
- package/dist/core/compaction/knowledge-triage.d.ts +29 -0
- package/dist/core/compaction/knowledge-triage.d.ts.map +1 -0
- package/dist/core/compaction/knowledge-triage.js +189 -0
- package/dist/core/compaction/knowledge-triage.js.map +1 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -1
- package/dist/core/compaction/transaction.js +1 -1
- package/dist/core/compaction/transaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +4 -0
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +37 -0
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/completion-sound-io.d.ts +14 -0
- package/dist/core/completion-sound-io.d.ts.map +1 -0
- package/dist/core/completion-sound-io.js +64 -0
- package/dist/core/completion-sound-io.js.map +1 -0
- package/dist/core/completion-sound.d.ts +4 -10
- package/dist/core/completion-sound.d.ts.map +1 -1
- package/dist/core/completion-sound.js +28 -58
- package/dist/core/completion-sound.js.map +1 -1
- package/dist/core/context-budget-relevance.d.ts +15 -2
- package/dist/core/context-budget-relevance.d.ts.map +1 -1
- package/dist/core/context-budget-relevance.js +21 -9
- package/dist/core/context-budget-relevance.js.map +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts +1 -1
- package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt-items.js.map +1 -1
- package/dist/core/context-budget-system-prompt.d.ts +1 -1
- package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
- package/dist/core/context-budget-system-prompt.js.map +1 -1
- package/dist/core/context-file.d.ts +23 -0
- package/dist/core/context-file.d.ts.map +1 -0
- package/dist/core/context-file.js +17 -0
- package/dist/core/context-file.js.map +1 -0
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +6 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-prompt-policy.d.ts +1 -1
- package/dist/core/model-prompt-policy.d.ts.map +1 -1
- package/dist/core/model-prompt-policy.js.map +1 -1
- package/dist/core/prompt-settlement.d.ts +6 -3
- package/dist/core/prompt-settlement.d.ts.map +1 -1
- package/dist/core/prompt-settlement.js +10 -14
- package/dist/core/prompt-settlement.js.map +1 -1
- package/dist/core/reasoning-router-promotion.d.ts +41 -1
- package/dist/core/reasoning-router-promotion.d.ts.map +1 -1
- package/dist/core/reasoning-router-promotion.js +31 -2
- package/dist/core/reasoning-router-promotion.js.map +1 -1
- package/dist/core/reasoning-router-replay-stability.d.ts +65 -0
- package/dist/core/reasoning-router-replay-stability.d.ts.map +1 -0
- package/dist/core/reasoning-router-replay-stability.js +67 -0
- package/dist/core/reasoning-router-replay-stability.js.map +1 -0
- package/dist/core/reasoning-router-v4-weights.d.ts +6 -6
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4-weights.js +3 -3
- package/dist/core/reasoning-router-v4-weights.js.map +1 -1
- package/dist/core/reasoning-router-v4.d.ts.map +1 -1
- package/dist/core/reasoning-router-v4.js +6 -6
- package/dist/core/reasoning-router-v4.js.map +1 -1
- package/dist/core/redaction.d.ts.map +1 -1
- package/dist/core/redaction.js +1 -1
- package/dist/core/redaction.js.map +1 -1
- package/dist/core/resource-admission.d.ts +2 -1
- package/dist/core/resource-admission.d.ts.map +1 -1
- package/dist/core/resource-admission.js +11 -0
- package/dist/core/resource-admission.js.map +1 -1
- package/dist/core/resource-governor-format.d.ts +1 -1
- package/dist/core/resource-governor-format.d.ts.map +1 -1
- package/dist/core/resource-governor-format.js.map +1 -1
- package/dist/core/resource-governor-settings.d.ts +6 -12
- package/dist/core/resource-governor-settings.d.ts.map +1 -1
- package/dist/core/resource-governor-settings.js.map +1 -1
- package/dist/core/resource-governor-types.d.ts +24 -0
- package/dist/core/resource-governor-types.d.ts.map +1 -0
- package/dist/core/resource-governor-types.js +15 -0
- package/dist/core/resource-governor-types.js.map +1 -0
- package/dist/core/resource-loader.d.ts +1 -7
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/resource-observation-journal.d.ts +2 -1
- package/dist/core/resource-observation-journal.d.ts.map +1 -1
- package/dist/core/resource-observation-journal.js +4 -3
- package/dist/core/resource-observation-journal.js.map +1 -1
- package/dist/core/resource-observation-report-reader.d.ts +7 -0
- package/dist/core/resource-observation-report-reader.d.ts.map +1 -0
- package/dist/core/resource-observation-report-reader.js +157 -0
- package/dist/core/resource-observation-report-reader.js.map +1 -0
- package/dist/core/resource-observation-report.d.ts +24 -0
- package/dist/core/resource-observation-report.d.ts.map +1 -0
- package/dist/core/resource-observation-report.js +101 -0
- package/dist/core/resource-observation-report.js.map +1 -0
- package/dist/core/router-feedback-collector.d.ts +1 -1
- package/dist/core/router-feedback-collector.d.ts.map +1 -1
- package/dist/core/router-feedback-collector.js.map +1 -1
- package/dist/core/session-failure-cause.d.ts +33 -0
- package/dist/core/session-failure-cause.d.ts.map +1 -0
- package/dist/core/session-failure-cause.js +141 -0
- package/dist/core/session-failure-cause.js.map +1 -0
- package/dist/core/session-system-prompt.d.ts +1 -1
- package/dist/core/session-system-prompt.d.ts.map +1 -1
- package/dist/core/session-system-prompt.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +7 -7
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/workload-classifier.d.ts +2 -10
- package/dist/core/workload-classifier.d.ts.map +1 -1
- package/dist/core/workload-classifier.js +17 -0
- package/dist/core/workload-classifier.js.map +1 -1
- package/dist/core/workload-family-matcher.d.ts +1 -1
- package/dist/core/workload-family-matcher.d.ts.map +1 -1
- package/dist/core/workload-family-matcher.js.map +1 -1
- package/dist/core/workload-shard-execution-types.d.ts +40 -0
- package/dist/core/workload-shard-execution-types.d.ts.map +1 -0
- package/dist/core/workload-shard-execution-types.js +14 -0
- package/dist/core/workload-shard-execution-types.js.map +1 -0
- package/dist/core/workload-shard-executor.d.ts +3 -27
- package/dist/core/workload-shard-executor.d.ts.map +1 -1
- package/dist/core/workload-shard-executor.js.map +1 -1
- package/dist/core/workload-shard-runner.d.ts +1 -1
- package/dist/core/workload-shard-runner.d.ts.map +1 -1
- package/dist/core/workload-shard-runner.js.map +1 -1
- package/dist/core/workload-types.d.ts +24 -0
- package/dist/core/workload-types.d.ts.map +1 -0
- package/dist/core/workload-types.js +15 -0
- package/dist/core/workload-types.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +27 -47
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/theme/omk-control-panel.json +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +42 -33
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +14 -2
- package/dist/utils/clipboard.js.map +1 -1
- package/docs/adaptorch-preview-spec.md +20 -18
- package/docs/adaptorch-preview.md +9 -9
- package/docs/compaction.md +24 -3
- package/docs/correctness-wall.md +10 -10
- package/docs/docs.json +12 -0
- package/docs/environment-variables.md +9 -2
- package/docs/index.md +3 -0
- package/docs/runtime-algorithms.md +359 -0
- package/docs/sdk.md +2 -2
- package/docs/settings.md +11 -10
- package/docs/usage.md +18 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +18 -18
- package/package.json +6 -6
- package/dist/core/context-budget-governor.d.ts +0 -67
- package/dist/core/context-budget-governor.d.ts.map +0 -1
- package/dist/core/context-budget-governor.js +0 -224
- package/dist/core/context-budget-governor.js.map +0 -1
- package/dist/core/lean-ctx.d.ts +0 -99
- package/dist/core/lean-ctx.d.ts.map +0 -1
- package/dist/core/lean-ctx.js +0 -225
- package/dist/core/lean-ctx.js.map +0 -1
- package/dist/core/read-anchors.d.ts +0 -110
- package/dist/core/read-anchors.d.ts.map +0 -1
- package/dist/core/read-anchors.js +0 -193
- package/dist/core/read-anchors.js.map +0 -1
- package/dist/core/recovery-checkpoint.d.ts +0 -196
- package/dist/core/recovery-checkpoint.d.ts.map +0 -1
- package/dist/core/recovery-checkpoint.js +0 -382
- package/dist/core/recovery-checkpoint.js.map +0 -1
- package/dist/core/sandbox-policy-v2.d.ts +0 -49
- package/dist/core/sandbox-policy-v2.d.ts.map +0 -1
- package/dist/core/sandbox-policy-v2.js +0 -368
- package/dist/core/sandbox-policy-v2.js.map +0 -1
- package/dist/guardrails/audit-logger.d.ts +0 -4
- package/dist/guardrails/audit-logger.d.ts.map +0 -1
- package/dist/guardrails/audit-logger.js +0 -28
- package/dist/guardrails/audit-logger.js.map +0 -1
- package/dist/guardrails/freedomd-adapter.d.ts +0 -16
- package/dist/guardrails/freedomd-adapter.d.ts.map +0 -1
- package/dist/guardrails/freedomd-adapter.js +0 -87
- package/dist/guardrails/freedomd-adapter.js.map +0 -1
- package/dist/orchestration/lane-grant-auditor.d.ts +0 -4
- package/dist/orchestration/lane-grant-auditor.d.ts.map +0 -1
- package/dist/orchestration/lane-grant-auditor.js +0 -87
- package/dist/orchestration/lane-grant-auditor.js.map +0 -1
- package/dist/types/guardrails.d.ts +0 -61
- package/dist/types/guardrails.d.ts.map +0 -1
- package/dist/types/guardrails.js +0 -9
- package/dist/types/guardrails.js.map +0 -1
- package/dist/types/lane-grant.d.ts +0 -58
- package/dist/types/lane-grant.d.ts.map +0 -1
- package/dist/types/lane-grant.js +0 -9
- package/dist/types/lane-grant.js.map +0 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { RESOURCE_OBSERVATION_JOURNAL_FILE } from "./resource-observation-journal.js";
|
|
4
|
+
const MAX_DIRECTORY_ENTRIES = 5000;
|
|
5
|
+
const MAX_JOURNAL_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
export function collectResourceJournalTexts(cwd, maxJournals) {
|
|
7
|
+
const root = resolveRunsRoot(cwd);
|
|
8
|
+
if (root.kind === "missing")
|
|
9
|
+
return { journals: [], diagnostics: 0, truncated: false };
|
|
10
|
+
if (root.kind === "invalid")
|
|
11
|
+
return { journals: [], diagnostics: 1, truncated: true };
|
|
12
|
+
const journals = [];
|
|
13
|
+
let diagnostics = 0;
|
|
14
|
+
let truncated = false;
|
|
15
|
+
let visited = 0;
|
|
16
|
+
let directory;
|
|
17
|
+
try {
|
|
18
|
+
directory = fs.opendirSync(root.path);
|
|
19
|
+
while (true) {
|
|
20
|
+
const entry = directory.readSync();
|
|
21
|
+
if (entry === null)
|
|
22
|
+
break;
|
|
23
|
+
visited += 1;
|
|
24
|
+
if (visited > MAX_DIRECTORY_ENTRIES) {
|
|
25
|
+
diagnostics += 1;
|
|
26
|
+
truncated = true;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (entry.isSymbolicLink()) {
|
|
30
|
+
diagnostics += 1;
|
|
31
|
+
truncated = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!entry.isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
const runDir = safeRealDirectory(root.path, entry.name);
|
|
37
|
+
if (runDir === null) {
|
|
38
|
+
diagnostics += 1;
|
|
39
|
+
truncated = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const journalPath = path.join(runDir, RESOURCE_OBSERVATION_JOURNAL_FILE);
|
|
43
|
+
const candidate = inspectJournalCandidate(journalPath);
|
|
44
|
+
if (candidate === "missing")
|
|
45
|
+
continue;
|
|
46
|
+
if (candidate === "invalid") {
|
|
47
|
+
diagnostics += 1;
|
|
48
|
+
truncated = true;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (journals.length >= maxJournals) {
|
|
52
|
+
truncated = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
const read = readBoundedJournal(journalPath);
|
|
56
|
+
if (read.kind !== "ok" || read.text === undefined) {
|
|
57
|
+
diagnostics += 1;
|
|
58
|
+
truncated = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
journals.push(read.text);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
diagnostics += 1;
|
|
66
|
+
truncated = true;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
directory?.closeSync();
|
|
70
|
+
}
|
|
71
|
+
return { journals, diagnostics, truncated };
|
|
72
|
+
}
|
|
73
|
+
function resolveRunsRoot(cwd) {
|
|
74
|
+
try {
|
|
75
|
+
const root = fs.realpathSync(cwd);
|
|
76
|
+
const omkDir = path.join(root, ".omk");
|
|
77
|
+
const runsDir = path.join(omkDir, "runs");
|
|
78
|
+
for (const candidate of [omkDir, runsDir]) {
|
|
79
|
+
const stat = fs.lstatSync(candidate);
|
|
80
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
81
|
+
return { kind: "invalid" };
|
|
82
|
+
}
|
|
83
|
+
const realRuns = fs.realpathSync(runsDir);
|
|
84
|
+
return isWithin(root, realRuns) ? { kind: "ok", path: realRuns } : { kind: "invalid" };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return error.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function safeRealDirectory(root, name) {
|
|
91
|
+
try {
|
|
92
|
+
const candidate = path.join(root, name);
|
|
93
|
+
const before = fs.lstatSync(candidate);
|
|
94
|
+
if (before.isSymbolicLink() || !before.isDirectory())
|
|
95
|
+
return null;
|
|
96
|
+
const real = fs.realpathSync(candidate);
|
|
97
|
+
return isWithin(root, real) ? real : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function inspectJournalCandidate(filePath) {
|
|
104
|
+
try {
|
|
105
|
+
const stat = fs.lstatSync(filePath);
|
|
106
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_JOURNAL_BYTES)
|
|
107
|
+
return "invalid";
|
|
108
|
+
return "valid";
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return error.code === "ENOENT" ? "missing" : "invalid";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function readBoundedJournal(filePath) {
|
|
115
|
+
let descriptor;
|
|
116
|
+
try {
|
|
117
|
+
const before = fs.lstatSync(filePath);
|
|
118
|
+
if (before.isSymbolicLink() || !before.isFile())
|
|
119
|
+
return { kind: "invalid" };
|
|
120
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
121
|
+
const opened = fs.fstatSync(descriptor);
|
|
122
|
+
if (!opened.isFile() ||
|
|
123
|
+
opened.dev !== before.dev ||
|
|
124
|
+
opened.ino !== before.ino ||
|
|
125
|
+
opened.size > MAX_JOURNAL_BYTES) {
|
|
126
|
+
return { kind: "invalid" };
|
|
127
|
+
}
|
|
128
|
+
const buffer = Buffer.alloc(MAX_JOURNAL_BYTES + 1);
|
|
129
|
+
let bytesRead = 0;
|
|
130
|
+
while (bytesRead < buffer.length) {
|
|
131
|
+
const count = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
132
|
+
if (count === 0)
|
|
133
|
+
break;
|
|
134
|
+
bytesRead += count;
|
|
135
|
+
}
|
|
136
|
+
const after = fs.fstatSync(descriptor);
|
|
137
|
+
if (bytesRead > MAX_JOURNAL_BYTES ||
|
|
138
|
+
after.dev !== opened.dev ||
|
|
139
|
+
after.ino !== opened.ino ||
|
|
140
|
+
after.size !== opened.size) {
|
|
141
|
+
return { kind: "invalid" };
|
|
142
|
+
}
|
|
143
|
+
return { kind: "ok", text: buffer.subarray(0, bytesRead).toString("utf8") };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return error.code === "ENOENT" ? { kind: "missing" } : { kind: "invalid" };
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
if (descriptor !== undefined)
|
|
150
|
+
fs.closeSync(descriptor);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function isWithin(root, candidate) {
|
|
154
|
+
const relative = path.relative(root, candidate);
|
|
155
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=resource-observation-report-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-observation-report-reader.js","sourceRoot":"","sources":["../../src/core/resource-observation-report-reader.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { RESOURCE_OBSERVATION_JOURNAL_FILE } from \"./resource-observation-journal.ts\";\n\nconst MAX_DIRECTORY_ENTRIES = 5000;\nconst MAX_JOURNAL_BYTES = 2 * 1024 * 1024;\n\nexport interface ResourceJournalCollection {\n\treadonly journals: readonly string[];\n\treadonly diagnostics: number;\n\treadonly truncated: boolean;\n}\n\ninterface JournalReadResult {\n\treadonly kind: \"missing\" | \"invalid\" | \"ok\";\n\treadonly text?: string;\n}\n\nexport function collectResourceJournalTexts(cwd: string, maxJournals: number): ResourceJournalCollection {\n\tconst root = resolveRunsRoot(cwd);\n\tif (root.kind === \"missing\") return { journals: [], diagnostics: 0, truncated: false };\n\tif (root.kind === \"invalid\") return { journals: [], diagnostics: 1, truncated: true };\n\tconst journals: string[] = [];\n\tlet diagnostics = 0;\n\tlet truncated = false;\n\tlet visited = 0;\n\tlet directory: fs.Dir | undefined;\n\ttry {\n\t\tdirectory = fs.opendirSync(root.path);\n\t\twhile (true) {\n\t\t\tconst entry = directory.readSync();\n\t\t\tif (entry === null) break;\n\t\t\tvisited += 1;\n\t\t\tif (visited > MAX_DIRECTORY_ENTRIES) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!entry.isDirectory()) continue;\n\t\t\tconst runDir = safeRealDirectory(root.path, entry.name);\n\t\t\tif (runDir === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst journalPath = path.join(runDir, RESOURCE_OBSERVATION_JOURNAL_FILE);\n\t\t\tconst candidate = inspectJournalCandidate(journalPath);\n\t\t\tif (candidate === \"missing\") continue;\n\t\t\tif (candidate === \"invalid\") {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (journals.length >= maxJournals) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst read = readBoundedJournal(journalPath);\n\t\t\tif (read.kind !== \"ok\" || read.text === undefined) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tjournals.push(read.text);\n\t\t}\n\t} catch {\n\t\tdiagnostics += 1;\n\t\ttruncated = true;\n\t} finally {\n\t\tdirectory?.closeSync();\n\t}\n\treturn { journals, diagnostics, truncated };\n}\n\nfunction resolveRunsRoot(\n\tcwd: string,\n): { readonly kind: \"missing\" } | { readonly kind: \"invalid\" } | { readonly kind: \"ok\"; readonly path: string } {\n\ttry {\n\t\tconst root = fs.realpathSync(cwd);\n\t\tconst omkDir = path.join(root, \".omk\");\n\t\tconst runsDir = path.join(omkDir, \"runs\");\n\t\tfor (const candidate of [omkDir, runsDir]) {\n\t\t\tconst stat = fs.lstatSync(candidate);\n\t\t\tif (stat.isSymbolicLink() || !stat.isDirectory()) return { kind: \"invalid\" };\n\t\t}\n\t\tconst realRuns = fs.realpathSync(runsDir);\n\t\treturn isWithin(root, realRuns) ? { kind: \"ok\", path: realRuns } : { kind: \"invalid\" };\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\" ? { kind: \"missing\" } : { kind: \"invalid\" };\n\t}\n}\n\nfunction safeRealDirectory(root: string, name: string): string | null {\n\ttry {\n\t\tconst candidate = path.join(root, name);\n\t\tconst before = fs.lstatSync(candidate);\n\t\tif (before.isSymbolicLink() || !before.isDirectory()) return null;\n\t\tconst real = fs.realpathSync(candidate);\n\t\treturn isWithin(root, real) ? real : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction inspectJournalCandidate(filePath: string): \"missing\" | \"invalid\" | \"valid\" {\n\ttry {\n\t\tconst stat = fs.lstatSync(filePath);\n\t\tif (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_JOURNAL_BYTES) return \"invalid\";\n\t\treturn \"valid\";\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\" ? \"missing\" : \"invalid\";\n\t}\n}\n\nfunction readBoundedJournal(filePath: string): JournalReadResult {\n\tlet descriptor: number | undefined;\n\ttry {\n\t\tconst before = fs.lstatSync(filePath);\n\t\tif (before.isSymbolicLink() || !before.isFile()) return { kind: \"invalid\" };\n\t\tdescriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);\n\t\tconst opened = fs.fstatSync(descriptor);\n\t\tif (\n\t\t\t!opened.isFile() ||\n\t\t\topened.dev !== before.dev ||\n\t\t\topened.ino !== before.ino ||\n\t\t\topened.size > MAX_JOURNAL_BYTES\n\t\t) {\n\t\t\treturn { kind: \"invalid\" };\n\t\t}\n\t\tconst buffer = Buffer.alloc(MAX_JOURNAL_BYTES + 1);\n\t\tlet bytesRead = 0;\n\t\twhile (bytesRead < buffer.length) {\n\t\t\tconst count = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, bytesRead);\n\t\t\tif (count === 0) break;\n\t\t\tbytesRead += count;\n\t\t}\n\t\tconst after = fs.fstatSync(descriptor);\n\t\tif (\n\t\t\tbytesRead > MAX_JOURNAL_BYTES ||\n\t\t\tafter.dev !== opened.dev ||\n\t\t\tafter.ino !== opened.ino ||\n\t\t\tafter.size !== opened.size\n\t\t) {\n\t\t\treturn { kind: \"invalid\" };\n\t\t}\n\t\treturn { kind: \"ok\", text: buffer.subarray(0, bytesRead).toString(\"utf8\") };\n\t} catch (error) {\n\t\treturn (error as NodeJS.ErrnoException).code === \"ENOENT\" ? { kind: \"missing\" } : { kind: \"invalid\" };\n\t} finally {\n\t\tif (descriptor !== undefined) fs.closeSync(descriptor);\n\t}\n}\n\nfunction isWithin(root: string, candidate: string): boolean {\n\tconst relative = path.relative(root, candidate);\n\treturn relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const RESOURCE_REPORT_MIN_ADMISSIONS = 30;
|
|
2
|
+
export interface ResourceObservationReport {
|
|
3
|
+
readonly schemaVersion: 1;
|
|
4
|
+
readonly journalsScanned: number;
|
|
5
|
+
readonly admissionRecords: number;
|
|
6
|
+
readonly reasonRecords: number;
|
|
7
|
+
readonly reasonCoverageComplete: boolean;
|
|
8
|
+
readonly pressure: Readonly<Record<"normal" | "constrained" | "critical", number>>;
|
|
9
|
+
readonly actions: Readonly<Record<"allow" | "throttle" | "defer-heavy", number>>;
|
|
10
|
+
readonly wouldHaveThrottled: number;
|
|
11
|
+
readonly probePartial: number;
|
|
12
|
+
readonly probeTimeout: number;
|
|
13
|
+
readonly diagnostics: number;
|
|
14
|
+
readonly truncated: boolean;
|
|
15
|
+
readonly minimumSampleSize: number;
|
|
16
|
+
readonly minimumSampleMet: boolean;
|
|
17
|
+
readonly humanReviewRequired: true;
|
|
18
|
+
}
|
|
19
|
+
export interface ResourceObservationReportOptions {
|
|
20
|
+
readonly maxJournals?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Aggregate bounded, identifier-free admission evidence from local run journals. */
|
|
23
|
+
export declare function collectResourceObservationReport(cwd: string, options?: ResourceObservationReportOptions): ResourceObservationReport;
|
|
24
|
+
//# sourceMappingURL=resource-observation-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-observation-report.d.ts","sourceRoot":"","sources":["../../src/core/resource-observation-report.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAKjD,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,QAAQ,GAAG,aAAa,GAAG,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IACnF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,GAAG,UAAU,GAAG,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IACjF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,mBAAmB,EAAE,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,gCAAgC;IAChD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,qFAAqF;AACrF,wBAAgB,gCAAgC,CAC/C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,gCAAqC,GAC5C,yBAAyB,CAkE3B","sourcesContent":["import { RESOURCE_REASON_CODES, type ResourceReasonCode } from \"./resource-admission.ts\";\nimport { parseResourceObservationLine, type ResourceObservationRecord } from \"./resource-observation-journal.ts\";\nimport { collectResourceJournalTexts } from \"./resource-observation-report-reader.ts\";\n\nexport const RESOURCE_REPORT_MIN_ADMISSIONS = 30;\nconst DEFAULT_MAX_JOURNALS = 500;\nconst MAX_RECORDS_PER_JOURNAL = 10_000;\nconst REASON_CODES: ReadonlySet<string> = new Set(RESOURCE_REASON_CODES);\n\nexport interface ResourceObservationReport {\n\treadonly schemaVersion: 1;\n\treadonly journalsScanned: number;\n\treadonly admissionRecords: number;\n\treadonly reasonRecords: number;\n\treadonly reasonCoverageComplete: boolean;\n\treadonly pressure: Readonly<Record<\"normal\" | \"constrained\" | \"critical\", number>>;\n\treadonly actions: Readonly<Record<\"allow\" | \"throttle\" | \"defer-heavy\", number>>;\n\treadonly wouldHaveThrottled: number;\n\treadonly probePartial: number;\n\treadonly probeTimeout: number;\n\treadonly diagnostics: number;\n\treadonly truncated: boolean;\n\treadonly minimumSampleSize: number;\n\treadonly minimumSampleMet: boolean;\n\treadonly humanReviewRequired: true;\n}\n\nexport interface ResourceObservationReportOptions {\n\treadonly maxJournals?: number;\n}\n\n/** Aggregate bounded, identifier-free admission evidence from local run journals. */\nexport function collectResourceObservationReport(\n\tcwd: string,\n\toptions: ResourceObservationReportOptions = {},\n): ResourceObservationReport {\n\tconst maxJournals = boundedMaxJournals(options.maxJournals);\n\tconst collection = collectResourceJournalTexts(cwd, maxJournals);\n\tconst pressure = { normal: 0, constrained: 0, critical: 0 };\n\tconst actions = { allow: 0, throttle: 0, \"defer-heavy\": 0 };\n\tlet admissionRecords = 0;\n\tlet reasonRecords = 0;\n\tlet wouldHaveThrottled = 0;\n\tlet probePartial = 0;\n\tlet probeTimeout = 0;\n\tlet diagnostics = collection.diagnostics;\n\tlet truncated = collection.truncated;\n\n\tfor (const raw of collection.journals) {\n\t\tlet recordsSeen = 0;\n\t\tfor (const rawLine of raw.split(\"\\n\")) {\n\t\t\tconst line = rawLine.trim();\n\t\t\tif (line === \"\") continue;\n\t\t\tif (recordsSeen >= MAX_RECORDS_PER_JOURNAL) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trecordsSeen += 1;\n\t\t\tconst record = parseResourceObservationLine(line);\n\t\t\tif (record === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (record.kind !== \"resource_admission_v1\") continue;\n\t\t\tconst admission = parseAdmissionFacts(record);\n\t\t\tif (admission === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tadmissionRecords += 1;\n\t\t\tpressure[admission.pressure] += 1;\n\t\t\tactions[admission.action] += 1;\n\t\t\tif (admission.action !== \"allow\") wouldHaveThrottled += 1;\n\t\t\tif (!admission.hasReasonEvidence) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treasonRecords += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.partial\")) probePartial += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.timeout\")) probeTimeout += 1;\n\t\t}\n\t}\n\n\treturn {\n\t\tschemaVersion: 1,\n\t\tjournalsScanned: collection.journals.length,\n\t\tadmissionRecords,\n\t\treasonRecords,\n\t\treasonCoverageComplete: admissionRecords > 0 && reasonRecords === admissionRecords,\n\t\tpressure,\n\t\tactions,\n\t\twouldHaveThrottled,\n\t\tprobePartial,\n\t\tprobeTimeout,\n\t\tdiagnostics,\n\t\ttruncated,\n\t\tminimumSampleSize: RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\tminimumSampleMet: reasonRecords >= RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\thumanReviewRequired: true,\n\t};\n}\n\nfunction boundedMaxJournals(value: number | undefined): number {\n\tif (value === undefined || !Number.isSafeInteger(value)) return DEFAULT_MAX_JOURNALS;\n\treturn Math.max(1, Math.min(DEFAULT_MAX_JOURNALS, value));\n}\n\nfunction parseAdmissionFacts(record: ResourceObservationRecord): {\n\treadonly pressure: \"normal\" | \"constrained\" | \"critical\";\n\treadonly action: \"allow\" | \"throttle\" | \"defer-heavy\";\n\treadonly reasons: readonly ResourceReasonCode[];\n\treadonly hasReasonEvidence: boolean;\n} | null {\n\tconst pressure = record.facts.pressure;\n\tconst action = record.facts.action;\n\tif (pressure !== \"normal\" && pressure !== \"constrained\" && pressure !== \"critical\") return null;\n\tif (action !== \"allow\" && action !== \"throttle\" && action !== \"defer-heavy\") return null;\n\tconst rawReasons = record.facts.reasons;\n\tif (rawReasons === undefined) return { pressure, action, reasons: [], hasReasonEvidence: false };\n\tif (typeof rawReasons !== \"string\") return null;\n\tconst reasons = rawReasons === \"\" ? [] : rawReasons.split(\",\");\n\tif (reasons.some((reason) => !REASON_CODES.has(reason))) return null;\n\treturn { pressure, action, reasons: reasons as ResourceReasonCode[], hasReasonEvidence: true };\n}\n"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { RESOURCE_REASON_CODES } from "./resource-admission.js";
|
|
2
|
+
import { parseResourceObservationLine } from "./resource-observation-journal.js";
|
|
3
|
+
import { collectResourceJournalTexts } from "./resource-observation-report-reader.js";
|
|
4
|
+
export const RESOURCE_REPORT_MIN_ADMISSIONS = 30;
|
|
5
|
+
const DEFAULT_MAX_JOURNALS = 500;
|
|
6
|
+
const MAX_RECORDS_PER_JOURNAL = 10_000;
|
|
7
|
+
const REASON_CODES = new Set(RESOURCE_REASON_CODES);
|
|
8
|
+
/** Aggregate bounded, identifier-free admission evidence from local run journals. */
|
|
9
|
+
export function collectResourceObservationReport(cwd, options = {}) {
|
|
10
|
+
const maxJournals = boundedMaxJournals(options.maxJournals);
|
|
11
|
+
const collection = collectResourceJournalTexts(cwd, maxJournals);
|
|
12
|
+
const pressure = { normal: 0, constrained: 0, critical: 0 };
|
|
13
|
+
const actions = { allow: 0, throttle: 0, "defer-heavy": 0 };
|
|
14
|
+
let admissionRecords = 0;
|
|
15
|
+
let reasonRecords = 0;
|
|
16
|
+
let wouldHaveThrottled = 0;
|
|
17
|
+
let probePartial = 0;
|
|
18
|
+
let probeTimeout = 0;
|
|
19
|
+
let diagnostics = collection.diagnostics;
|
|
20
|
+
let truncated = collection.truncated;
|
|
21
|
+
for (const raw of collection.journals) {
|
|
22
|
+
let recordsSeen = 0;
|
|
23
|
+
for (const rawLine of raw.split("\n")) {
|
|
24
|
+
const line = rawLine.trim();
|
|
25
|
+
if (line === "")
|
|
26
|
+
continue;
|
|
27
|
+
if (recordsSeen >= MAX_RECORDS_PER_JOURNAL) {
|
|
28
|
+
diagnostics += 1;
|
|
29
|
+
truncated = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
recordsSeen += 1;
|
|
33
|
+
const record = parseResourceObservationLine(line);
|
|
34
|
+
if (record === null) {
|
|
35
|
+
diagnostics += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (record.kind !== "resource_admission_v1")
|
|
39
|
+
continue;
|
|
40
|
+
const admission = parseAdmissionFacts(record);
|
|
41
|
+
if (admission === null) {
|
|
42
|
+
diagnostics += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
admissionRecords += 1;
|
|
46
|
+
pressure[admission.pressure] += 1;
|
|
47
|
+
actions[admission.action] += 1;
|
|
48
|
+
if (admission.action !== "allow")
|
|
49
|
+
wouldHaveThrottled += 1;
|
|
50
|
+
if (!admission.hasReasonEvidence) {
|
|
51
|
+
diagnostics += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
reasonRecords += 1;
|
|
55
|
+
if (admission.reasons.includes("resource.probe.partial"))
|
|
56
|
+
probePartial += 1;
|
|
57
|
+
if (admission.reasons.includes("resource.probe.timeout"))
|
|
58
|
+
probeTimeout += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
journalsScanned: collection.journals.length,
|
|
64
|
+
admissionRecords,
|
|
65
|
+
reasonRecords,
|
|
66
|
+
reasonCoverageComplete: admissionRecords > 0 && reasonRecords === admissionRecords,
|
|
67
|
+
pressure,
|
|
68
|
+
actions,
|
|
69
|
+
wouldHaveThrottled,
|
|
70
|
+
probePartial,
|
|
71
|
+
probeTimeout,
|
|
72
|
+
diagnostics,
|
|
73
|
+
truncated,
|
|
74
|
+
minimumSampleSize: RESOURCE_REPORT_MIN_ADMISSIONS,
|
|
75
|
+
minimumSampleMet: reasonRecords >= RESOURCE_REPORT_MIN_ADMISSIONS,
|
|
76
|
+
humanReviewRequired: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function boundedMaxJournals(value) {
|
|
80
|
+
if (value === undefined || !Number.isSafeInteger(value))
|
|
81
|
+
return DEFAULT_MAX_JOURNALS;
|
|
82
|
+
return Math.max(1, Math.min(DEFAULT_MAX_JOURNALS, value));
|
|
83
|
+
}
|
|
84
|
+
function parseAdmissionFacts(record) {
|
|
85
|
+
const pressure = record.facts.pressure;
|
|
86
|
+
const action = record.facts.action;
|
|
87
|
+
if (pressure !== "normal" && pressure !== "constrained" && pressure !== "critical")
|
|
88
|
+
return null;
|
|
89
|
+
if (action !== "allow" && action !== "throttle" && action !== "defer-heavy")
|
|
90
|
+
return null;
|
|
91
|
+
const rawReasons = record.facts.reasons;
|
|
92
|
+
if (rawReasons === undefined)
|
|
93
|
+
return { pressure, action, reasons: [], hasReasonEvidence: false };
|
|
94
|
+
if (typeof rawReasons !== "string")
|
|
95
|
+
return null;
|
|
96
|
+
const reasons = rawReasons === "" ? [] : rawReasons.split(",");
|
|
97
|
+
if (reasons.some((reason) => !REASON_CODES.has(reason)))
|
|
98
|
+
return null;
|
|
99
|
+
return { pressure, action, reasons: reasons, hasReasonEvidence: true };
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=resource-observation-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-observation-report.js","sourceRoot":"","sources":["../../src/core/resource-observation-report.ts"],"names":[],"mappings":"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","sourcesContent":["import { RESOURCE_REASON_CODES, type ResourceReasonCode } from \"./resource-admission.ts\";\nimport { parseResourceObservationLine, type ResourceObservationRecord } from \"./resource-observation-journal.ts\";\nimport { collectResourceJournalTexts } from \"./resource-observation-report-reader.ts\";\n\nexport const RESOURCE_REPORT_MIN_ADMISSIONS = 30;\nconst DEFAULT_MAX_JOURNALS = 500;\nconst MAX_RECORDS_PER_JOURNAL = 10_000;\nconst REASON_CODES: ReadonlySet<string> = new Set(RESOURCE_REASON_CODES);\n\nexport interface ResourceObservationReport {\n\treadonly schemaVersion: 1;\n\treadonly journalsScanned: number;\n\treadonly admissionRecords: number;\n\treadonly reasonRecords: number;\n\treadonly reasonCoverageComplete: boolean;\n\treadonly pressure: Readonly<Record<\"normal\" | \"constrained\" | \"critical\", number>>;\n\treadonly actions: Readonly<Record<\"allow\" | \"throttle\" | \"defer-heavy\", number>>;\n\treadonly wouldHaveThrottled: number;\n\treadonly probePartial: number;\n\treadonly probeTimeout: number;\n\treadonly diagnostics: number;\n\treadonly truncated: boolean;\n\treadonly minimumSampleSize: number;\n\treadonly minimumSampleMet: boolean;\n\treadonly humanReviewRequired: true;\n}\n\nexport interface ResourceObservationReportOptions {\n\treadonly maxJournals?: number;\n}\n\n/** Aggregate bounded, identifier-free admission evidence from local run journals. */\nexport function collectResourceObservationReport(\n\tcwd: string,\n\toptions: ResourceObservationReportOptions = {},\n): ResourceObservationReport {\n\tconst maxJournals = boundedMaxJournals(options.maxJournals);\n\tconst collection = collectResourceJournalTexts(cwd, maxJournals);\n\tconst pressure = { normal: 0, constrained: 0, critical: 0 };\n\tconst actions = { allow: 0, throttle: 0, \"defer-heavy\": 0 };\n\tlet admissionRecords = 0;\n\tlet reasonRecords = 0;\n\tlet wouldHaveThrottled = 0;\n\tlet probePartial = 0;\n\tlet probeTimeout = 0;\n\tlet diagnostics = collection.diagnostics;\n\tlet truncated = collection.truncated;\n\n\tfor (const raw of collection.journals) {\n\t\tlet recordsSeen = 0;\n\t\tfor (const rawLine of raw.split(\"\\n\")) {\n\t\t\tconst line = rawLine.trim();\n\t\t\tif (line === \"\") continue;\n\t\t\tif (recordsSeen >= MAX_RECORDS_PER_JOURNAL) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\trecordsSeen += 1;\n\t\t\tconst record = parseResourceObservationLine(line);\n\t\t\tif (record === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (record.kind !== \"resource_admission_v1\") continue;\n\t\t\tconst admission = parseAdmissionFacts(record);\n\t\t\tif (admission === null) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tadmissionRecords += 1;\n\t\t\tpressure[admission.pressure] += 1;\n\t\t\tactions[admission.action] += 1;\n\t\t\tif (admission.action !== \"allow\") wouldHaveThrottled += 1;\n\t\t\tif (!admission.hasReasonEvidence) {\n\t\t\t\tdiagnostics += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treasonRecords += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.partial\")) probePartial += 1;\n\t\t\tif (admission.reasons.includes(\"resource.probe.timeout\")) probeTimeout += 1;\n\t\t}\n\t}\n\n\treturn {\n\t\tschemaVersion: 1,\n\t\tjournalsScanned: collection.journals.length,\n\t\tadmissionRecords,\n\t\treasonRecords,\n\t\treasonCoverageComplete: admissionRecords > 0 && reasonRecords === admissionRecords,\n\t\tpressure,\n\t\tactions,\n\t\twouldHaveThrottled,\n\t\tprobePartial,\n\t\tprobeTimeout,\n\t\tdiagnostics,\n\t\ttruncated,\n\t\tminimumSampleSize: RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\tminimumSampleMet: reasonRecords >= RESOURCE_REPORT_MIN_ADMISSIONS,\n\t\thumanReviewRequired: true,\n\t};\n}\n\nfunction boundedMaxJournals(value: number | undefined): number {\n\tif (value === undefined || !Number.isSafeInteger(value)) return DEFAULT_MAX_JOURNALS;\n\treturn Math.max(1, Math.min(DEFAULT_MAX_JOURNALS, value));\n}\n\nfunction parseAdmissionFacts(record: ResourceObservationRecord): {\n\treadonly pressure: \"normal\" | \"constrained\" | \"critical\";\n\treadonly action: \"allow\" | \"throttle\" | \"defer-heavy\";\n\treadonly reasons: readonly ResourceReasonCode[];\n\treadonly hasReasonEvidence: boolean;\n} | null {\n\tconst pressure = record.facts.pressure;\n\tconst action = record.facts.action;\n\tif (pressure !== \"normal\" && pressure !== \"constrained\" && pressure !== \"critical\") return null;\n\tif (action !== \"allow\" && action !== \"throttle\" && action !== \"defer-heavy\") return null;\n\tconst rawReasons = record.facts.reasons;\n\tif (rawReasons === undefined) return { pressure, action, reasons: [], hasReasonEvidence: false };\n\tif (typeof rawReasons !== \"string\") return null;\n\tconst reasons = rawReasons === \"\" ? [] : rawReasons.split(\",\");\n\tif (reasons.some((reason) => !REASON_CODES.has(reason))) return null;\n\treturn { pressure, action, reasons: reasons as ResourceReasonCode[], hasReasonEvidence: true };\n}\n"]}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* an exact key-set check, not by a best-effort denylist of forbidden names.
|
|
20
20
|
* ============================================================================
|
|
21
21
|
*/
|
|
22
|
-
import type { ReasoningLaneTypeV4, TaskClassV4 } from "./reasoning-router-v4.ts";
|
|
22
|
+
import type { ReasoningLaneTypeV4, TaskClassV4 } from "./reasoning-router-v4-weights.ts";
|
|
23
23
|
/** Router version eligible to tag a feedback record. */
|
|
24
24
|
export type RouterFeedbackVersion = "v4";
|
|
25
25
|
/** Lane type at record time, or "none" for a non-subagent turn. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-feedback-collector.d.ts","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAgBH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEjF,wDAAwD;AACxD,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEzC,mEAAmE;AACnE,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAElE,6CAA6C;AAC7C,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAEpG,8BAA8B;AAC9B,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,WAAW,GAAG,iBAAiB,GAAG,eAAe,CAAC;AAErG,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE9G,4FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEpE;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,wBAAwB,EAAE,SAAS,qBAAqB,EAAW,CAAC;AACjF,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAQvE,CAAC;AACF,eAAO,MAAM,4BAA4B,EAAE,SAAS,uBAAuB,EAQ1E,CAAC;AACF,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAQhE,CAAC;AAgBF,eAAO,MAAM,2BAA2B,EAAE,SAAS,uBAAuB,EAA6B,CAAC;AA6BxG;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAgBpF;AAED,6GAA6G;AAC7G,wBAAgB,kCAAkC,IAAI,MAAM,CAE3D;AAED,MAAM,WAAW,2BAA2B;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,0BAA0B,GACnC,UAAU,GACV,gBAAgB,GAChB,iBAAiB,GACjB,kBAAkB,GAClB,UAAU,CAAC;AAEd,MAAM,MAAM,0BAA0B,GACnC;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAAE,CAAC;AAgD7E;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACzC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,2BAA2B,GAClC,0BAA0B,CAwD5B","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router-feedback-collector.d.ts","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAmBH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAEzF,wDAAwD;AACxD,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEzC,mEAAmE;AACnE,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAElE,6CAA6C;AAC7C,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAEpG,8BAA8B;AAC9B,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,WAAW,GAAG,iBAAiB,GAAG,eAAe,CAAC;AAErG,mEAAmE;AACnE,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAE9G,4FAA0F;AAC1F,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEpE;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,CAAC,cAAc,EAAE,uBAAuB,CAAC;IACjD,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,aAAa,EAAE,mBAAmB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,eAAO,MAAM,wBAAwB,EAAE,SAAS,qBAAqB,EAAW,CAAC;AACjF,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAQvE,CAAC;AACF,eAAO,MAAM,4BAA4B,EAAE,SAAS,uBAAuB,EAQ1E,CAAC;AACF,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAQhE,CAAC;AAgBF,eAAO,MAAM,2BAA2B,EAAE,SAAS,uBAAuB,EAA6B,CAAC;AA6BxG;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAgBpF;AAED,6GAA6G;AAC7G,wBAAgB,kCAAkC,IAAI,MAAM,CAE3D;AAED,MAAM,WAAW,2BAA2B;IAC3C,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qGAAqG;IACrG,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,0BAA0B,GACnC,UAAU,GACV,gBAAgB,GAChB,iBAAiB,GACjB,kBAAkB,GAClB,UAAU,CAAC;AAEd,MAAM,MAAM,0BAA0B,GACnC;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,0BAA0B,CAAA;CAAE,CAAC;AAgD7E;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CACzC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,2BAA2B,GAClC,0BAA0B,CAwD5B","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\n// Imported from the weights module, where both types are declared. Routing\n// through `reasoning-router-v4.ts` — which only re-exports them — made the\n// classifier and this ledger import each other.\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-feedback-collector.js","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router-feedback-collector.js","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Router feedback collector — privacy-safe learning ledger for `/think auto`.\n *\n * Default-off, privacy-safe local ledger for the v4 reasoning-router learning\n * path. This module is a pure utility; consent and wiring live in settings and\n * agent-session.ts.\n *\n * ============================================================================\n * SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)\n * ============================================================================\n * routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,\n * signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded\n * enum/number/boolean value. There is no field, in this type or in the runtime\n * validator below, that can carry raw prompt text, a prompt hash, an exact\n * prompt length, a file path, a diff/code blob, a session id, a model id, a\n * provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`\n * rejects ANY object with an unrecognized key (including all of the fields\n * just listed) — the schema is the redaction policy, enforced structurally by\n * an exact key-set check, not by a best-effort denylist of forbidden names.\n * ============================================================================\n */\n\nimport {\n\tchmodSync,\n\tcloseSync,\n\texistsSync,\n\tconstants as fsConstants,\n\tfstatSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\twriteSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.ts\";\n// Imported from the weights module, where both types are declared. Routing\n// through `reasoning-router-v4.ts` — which only re-exports them — made the\n// classifier and this ledger import each other.\nimport type { ReasoningLaneTypeV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\n\n/** Router version eligible to tag a feedback record. */\nexport type RouterFeedbackVersion = \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV4 | \"none\";\n\n/** Router task class; never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV4;\n\n/**\n * Reasoning ladder level, excluding \"off\". The router never *resolves* to\n * \"off\"; turning thinking off entirely is a distinct manual action outside\n * this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\" | \"ultra\";\n\n/** Feedback signal source. */\nexport type RouterFeedbackSignal = \"s1-override\" | \"s2-accept\" | \"s3-hook-outcome\" | \"s4-regression\";\n\n/** Coarse outcome tag; never hook stdout/stderr or tool output. */\nexport type RouterFeedbackOutcome = \"up\" | \"down\" | \"same\" | \"accepted\" | \"pass\" | \"fail\" | \"debug-follow-up\";\n\n/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */\nexport type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;\n\n/**\n * One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`\n * for the runtime allowlist enforcement.\n */\nexport interface RouterFeedbackRecord {\n\treadonly routerVersion: RouterFeedbackVersion;\n\treadonly laneType: RouterFeedbackLaneType;\n\treadonly predictedClass: RouterFeedbackTaskClass;\n\treadonly resolvedLevel: RouterFeedbackLevel;\n\treadonly acceptedLevel: RouterFeedbackLevel;\n\treadonly signal: RouterFeedbackSignal;\n\treadonly outcome: RouterFeedbackOutcome;\n\treadonly lenBucket: RouterFeedbackLenBucket;\n\treadonly hadFence: boolean;\n\treadonly hadDiff: boolean;\n}\n\nexport const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[] = [\"v4\"];\nexport const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[] = [\n\t\"planner\",\n\t\"security\",\n\t\"explorer\",\n\t\"coder\",\n\t\"reviewer\",\n\t\"tester\",\n\t\"none\",\n];\nexport const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[] = [\n\t\"trivial\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n];\nexport const ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\n\t\"minimal\",\n\t\"low\",\n\t\"medium\",\n\t\"high\",\n\t\"xhigh\",\n\t\"max\",\n\t\"ultra\",\n];\nconst ROUTER_FEEDBACK_SIGNALS: readonly RouterFeedbackSignal[] = [\n\t\"s1-override\",\n\t\"s2-accept\",\n\t\"s3-hook-outcome\",\n\t\"s4-regression\",\n];\nconst ROUTER_FEEDBACK_OUTCOMES: readonly RouterFeedbackOutcome[] = [\n\t\"up\",\n\t\"down\",\n\t\"same\",\n\t\"accepted\",\n\t\"pass\",\n\t\"fail\",\n\t\"debug-follow-up\",\n];\nexport const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[] = [0, 1, 2, 3, 4, 5, 6, 7];\n\n/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */\nconst ROUTER_FEEDBACK_RECORD_KEYS: readonly string[] = [\n\t\"routerVersion\",\n\t\"laneType\",\n\t\"predictedClass\",\n\t\"resolvedLevel\",\n\t\"acceptedLevel\",\n\t\"signal\",\n\t\"outcome\",\n\t\"lenBucket\",\n\t\"hadFence\",\n\t\"hadDiff\",\n].sort();\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasExactRouterFeedbackKeySet(value: Record<string, unknown>): boolean {\n\tconst keys = Object.keys(value).sort();\n\tif (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length) return false;\n\tfor (let i = 0; i < keys.length; i++) {\n\t\tif (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i]) return false;\n\t}\n\treturn true;\n}\n\n/**\n * Strict runtime validator: exactly the ten allowed keys (no more, no fewer),\n * each within its bounded value set. Returns false for any extra key —\n * including a raw prompt, file path, diff, session id, model id, provider\n * payload, or hook/tool-output-shaped field — because ANY unrecognized key\n * fails the exact key-set check before any per-field check runs.\n */\nexport function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord {\n\tif (!isPlainObject(value)) return false;\n\tif (!hasExactRouterFeedbackKeySet(value)) return false;\n\n\tif (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion as RouterFeedbackVersion)) return false;\n\tif (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType as RouterFeedbackLaneType)) return false;\n\tif (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass as RouterFeedbackTaskClass)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel as RouterFeedbackLevel)) return false;\n\tif (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal as RouterFeedbackSignal)) return false;\n\tif (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome as RouterFeedbackOutcome)) return false;\n\tif (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket as RouterFeedbackLenBucket)) return false;\n\tif (typeof value.hadFence !== \"boolean\") return false;\n\tif (typeof value.hadDiff !== \"boolean\") return false;\n\n\treturn true;\n}\n\n/** Legacy shared ledger path. Agent sessions use repository-scoped defaults unless explicitly overridden. */\nexport function getDefaultRouterFeedbackLedgerPath(): string {\n\treturn join(getAgentDir(), \"router-feedback\", \"ledger.jsonl\");\n}\n\nexport interface AppendRouterFeedbackOptions {\n\t/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */\n\treadonly enabled: boolean;\n\t/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */\n\treadonly ledgerPath?: string;\n}\n\nexport type AppendRouterFeedbackReason =\n\t| \"disabled\"\n\t| \"invalid-schema\"\n\t| \"symlink-refused\"\n\t| \"not-regular-file\"\n\t| \"io-error\";\n\nexport type AppendRouterFeedbackResult =\n\t| { readonly appended: true }\n\t| { readonly appended: false; readonly reason: AppendRouterFeedbackReason };\n\n/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */\nfunction isSymlinkPath(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isSymbolicLink();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error\n\t\t? String((error as { code?: unknown }).code)\n\t\t: undefined;\n}\n\nfunction acquireLedgerLockSync(path: string): () => void {\n\tconst maxAttempts = 10;\n\tconst delayMs = 20;\n\tlet lastError: unknown;\n\n\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\ttry {\n\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t} catch (error) {\n\t\t\tif (errorCode(error) !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tlastError = error;\n\t\t\tconst start = Date.now();\n\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.\n\t\t\t}\n\t\t}\n\t}\n\n\tthrow (lastError as Error) ?? new Error(\"Failed to acquire router-feedback ledger lock\");\n}\n\n/**\n * O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps\n * the flag combination valid there, and the `lstat` pre-checks above/below\n * remain the fallback symlink guard on platforms without O_NOFOLLOW.\n */\nconst LEDGER_OPEN_FLAGS =\n\tfsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);\n\n/**\n * Append one validated feedback record as a single JSONL line.\n *\n * Requires `options.enabled === true` (checked first, before any filesystem\n * access whatsoever — disabled or omitted consent never touches disk).\n * Validates the record against the exact ten-key schema before any I/O.\n * Refuses symlinks and non-regular files where the platform allows detecting\n * them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`\n * is unavailable on some platforms, hence \"when possible\"). Reuses the same\n * `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for\n * concurrent-session safety. Never throws for expected failure modes — it\n * returns a tagged result so a caller can fail closed silently.\n */\nexport function appendRouterFeedbackRecord(\n\trecord: unknown,\n\toptions: AppendRouterFeedbackOptions,\n): AppendRouterFeedbackResult {\n\tif (options.enabled !== true) {\n\t\treturn { appended: false, reason: \"disabled\" };\n\t}\n\tif (!isRouterFeedbackRecord(record)) {\n\t\treturn { appended: false, reason: \"invalid-schema\" };\n\t}\n\n\tconst ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();\n\tconst ledgerDir = dirname(ledgerPath);\n\n\ttry {\n\t\tif (isSymlinkPath(ledgerDir)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\t\tif (!existsSync(ledgerDir)) {\n\t\t\tmkdirSync(ledgerDir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t}\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = acquireLedgerLockSync(ledgerPath);\n\n\t\t\tif (isSymlinkPath(ledgerPath)) {\n\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t}\n\n\t\t\tlet fd: number;\n\t\t\ttry {\n\t\t\t\tfd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);\n\t\t\t} catch (error) {\n\t\t\t\tif (errorCode(error) === \"ELOOP\") {\n\t\t\t\t\treturn { appended: false, reason: \"symlink-refused\" };\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (!fstatSync(fd).isFile()) {\n\t\t\t\t\treturn { appended: false, reason: \"not-regular-file\" };\n\t\t\t\t}\n\t\t\t\twriteSync(fd, `${JSON.stringify(record)}\\n`);\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t\tchmodSync(ledgerPath, 0o600);\n\t\t\treturn { appended: true };\n\t\t} finally {\n\t\t\tif (release) release();\n\t\t}\n\t} catch {\n\t\treturn { appended: false, reason: \"io-error\" };\n\t}\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure-text to `SessionTerminationCause` classification.
|
|
3
|
+
*
|
|
4
|
+
* This is the layer *below* `session-termination.ts`: that module turns a cause
|
|
5
|
+
* into a kind/phase/next-action record, while this one decides which cause a
|
|
6
|
+
* provider error, a preflight rejection, or a thrown runtime error represents.
|
|
7
|
+
*
|
|
8
|
+
* The functions are pure so the ordering can be pinned by direct tests. Ordering
|
|
9
|
+
* is load-bearing in two places that are easy to regress:
|
|
10
|
+
*
|
|
11
|
+
* - quota/billing exhaustion is matched *before* the generic 401/403 auth
|
|
12
|
+
* patterns, because `403 ... usage limit for this billing cycle` is
|
|
13
|
+
* transient per cycle and must fail over instead of terminating as auth;
|
|
14
|
+
* - upstream 5xx/dropped streams are matched *before* the protocol fallback,
|
|
15
|
+
* so guidance points at retry/model-switch rather than transcript sanitize.
|
|
16
|
+
*/
|
|
17
|
+
import { type AssistantMessage } from "omk-ai";
|
|
18
|
+
import { type SessionTerminationCause } from "./session-termination.ts";
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a free-text failure message for the termination record: redact
|
|
21
|
+
* secret-shaped content, drop NUL bytes, and bound the length.
|
|
22
|
+
*/
|
|
23
|
+
export declare function terminationMessage(value: string | undefined, fallback: string): string;
|
|
24
|
+
/** Classify a failed provider turn from the assistant message it produced. */
|
|
25
|
+
export declare function providerFailureCause(message: AssistantMessage, contextWindow: number): SessionTerminationCause;
|
|
26
|
+
/**
|
|
27
|
+
* Classify a preflight rejection raised before the provider was called.
|
|
28
|
+
* `hasModel` is false when no model is selected for the session.
|
|
29
|
+
*/
|
|
30
|
+
export declare function preflightFailureCause(message: string, hasModel: boolean): SessionTerminationCause;
|
|
31
|
+
/** Classify a value thrown out of the session runtime itself. */
|
|
32
|
+
export declare function runtimeFailureCause(error: unknown): SessionTerminationCause;
|
|
33
|
+
//# sourceMappingURL=session-failure-cause.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-failure-cause.d.ts","sourceRoot":"","sources":["../../src/core/session-failure-cause.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAqB,MAAM,QAAQ,CAAC;AAGlE,OAAO,EAA0C,KAAK,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAmBhH;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,GAAG,uBAAuB,CAwC9G;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,uBAAuB,CA0BjG;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,CAW3E","sourcesContent":["/**\n * Failure-text to `SessionTerminationCause` classification.\n *\n * This is the layer *below* `session-termination.ts`: that module turns a cause\n * into a kind/phase/next-action record, while this one decides which cause a\n * provider error, a preflight rejection, or a thrown runtime error represents.\n *\n * The functions are pure so the ordering can be pinned by direct tests. Ordering\n * is load-bearing in two places that are easy to regress:\n *\n * - quota/billing exhaustion is matched *before* the generic 401/403 auth\n * patterns, because `403 ... usage limit for this billing cycle` is\n * transient per cycle and must fail over instead of terminating as auth;\n * - upstream 5xx/dropped streams are matched *before* the protocol fallback,\n * so guidance points at retry/model-switch rather than transcript sanitize.\n */\n\nimport { type AssistantMessage, isContextOverflow } from \"omk-ai\";\nimport { isQuotaExhaustionMessage, isUpstreamUnavailableMessage } from \"./provider-resilience.ts\";\nimport { redactSensitiveText } from \"./redaction.ts\";\nimport { MAX_SESSION_TERMINATION_MESSAGE_LENGTH, type SessionTerminationCause } from \"./session-termination.ts\";\n\n/**\n * Errno codes that mean a write could not be durably completed. Anything else\n * (a missing path, for example) is not evidence of a persistence fault.\n */\nconst PERSISTENCE_ERRNO_CODES: ReadonlySet<string> = new Set([\n\t\"EACCES\",\n\t\"EDQUOT\",\n\t\"EFBIG\",\n\t\"EIO\",\n\t\"EISDIR\",\n\t\"EMFILE\",\n\t\"ENFILE\",\n\t\"ENOSPC\",\n\t\"EPERM\",\n\t\"EROFS\",\n]);\n\n/**\n * Normalize a free-text failure message for the termination record: redact\n * secret-shaped content, drop NUL bytes, and bound the length.\n */\nexport function terminationMessage(value: string | undefined, fallback: string): string {\n\tconst redacted = redactSensitiveText(value?.trim() || fallback)\n\t\t.replace(/\\0/g, \"\")\n\t\t.slice(0, MAX_SESSION_TERMINATION_MESSAGE_LENGTH);\n\treturn redacted || fallback;\n}\n\n/** Classify a failed provider turn from the assistant message it produced. */\nexport function providerFailureCause(message: AssistantMessage, contextWindow: number): SessionTerminationCause {\n\tconst text = message.errorMessage ?? \"\";\n\tif (isContextOverflow(message, contextWindow)) {\n\t\treturn { area: \"provider\", code: \"context_overflow\" };\n\t}\n\t// Fable/Claude often emit stop_reason=refusal on benign turns (false positive).\n\t// Match broadly so we never mislabel these as provider.protocol.\n\tif (\n\t\t/stop_reason\\s*=\\s*(refusal|sensitive)|content\\/safety stop|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\t\ttext,\n\t\t)\n\t) {\n\t\treturn { area: \"provider\", code: \"refusal\" };\n\t}\n\t// Quota/billing exhaustion is checked BEFORE the generic 401/403 auth\n\t// patterns: \"403 ... usage limit for this billing cycle\" is transient per\n\t// cycle and must fail over, not terminate the turn as an auth error.\n\tif (isQuotaExhaustionMessage(text) || /rate.?limit|too many requests|429/i.test(text)) {\n\t\treturn { area: \"provider\", code: \"rate_limit\" };\n\t}\n\tif (/auth|unauthori[sz]ed|forbidden|invalid.?api.?key|no api key|401|403|\\/login/i.test(text)) {\n\t\treturn { area: \"provider\", code: \"auth\" };\n\t}\n\t// Gateway/upstream 5xx and dropped streams are transport failures, not\n\t// transcript-shape problems. Classify as network so the guidance points to\n\t// retry/model-switch instead of the protocol sanitize path.\n\tif (isUpstreamUnavailableMessage(text)) {\n\t\treturn { area: \"provider\", code: \"network\" };\n\t}\n\t// Kimi/K3 + OpenAI-compat: orphan tool results after dropped error assistants.\n\t// Sanitize-and-retry (transform-messages drops orphans), not a hard tool fatal.\n\tif (/tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text)) {\n\t\treturn { area: \"provider\", code: \"protocol\" };\n\t}\n\tif (/tool.+timed? out|tool.+timeout/i.test(text)) return { area: \"tool\", code: \"timeout\" };\n\tif (/\\btool\\b/i.test(text) && !/tool_call_id/i.test(text)) return { area: \"tool\", code: \"fatal\" };\n\tif (/network|fetch failed|connection|socket|websocket|timed? out|timeout|dns|econn|^terminated$/i.test(text)) {\n\t\treturn { area: \"provider\", code: \"network\" };\n\t}\n\treturn { area: \"provider\", code: \"protocol\" };\n}\n\n/**\n * Classify a preflight rejection raised before the provider was called.\n * `hasModel` is false when no model is selected for the session.\n */\nexport function preflightFailureCause(message: string, hasModel: boolean): SessionTerminationCause {\n\tif (!hasModel || /no model|model selected|model is required/i.test(message)) {\n\t\treturn { area: \"configuration\", code: \"invalid\" };\n\t}\n\tif (isQuotaExhaustionMessage(message)) {\n\t\treturn { area: \"provider\", code: \"rate_limit\" };\n\t}\n\tif (/auth|api key|unauthori[sz]ed|forbidden|401|403|\\/login/i.test(message)) {\n\t\treturn { area: \"provider\", code: \"auth\" };\n\t}\n\tif (isUpstreamUnavailableMessage(message)) {\n\t\treturn { area: \"provider\", code: \"network\" };\n\t}\n\tif (/context.+overflow|context window|too many tokens/i.test(message)) {\n\t\treturn { area: \"provider\", code: \"context_overflow\" };\n\t}\n\tif (/duplicate.?result/i.test(message)) return { area: \"transcript\", code: \"duplicate_result\" };\n\tif (/orphan.?result/i.test(message)) return { area: \"transcript\", code: \"orphan_result\" };\n\tif (/duplicate.?call/i.test(message)) return { area: \"transcript\", code: \"duplicate_call_id\" };\n\tif (/transcript|missing.?result/i.test(message)) return { area: \"transcript\", code: \"missing_result\" };\n\tif (/compaction/i.test(message)) return { area: \"compaction\", code: \"failed\" };\n\tif (/fsync/i.test(message)) return { area: \"persistence\", code: \"fsync_failed\" };\n\tif (/lock/i.test(message)) return { area: \"persistence\", code: \"lock_failed\" };\n\tif (/append|persist|write/i.test(message)) return { area: \"persistence\", code: \"append_failed\" };\n\tif (/tool/i.test(message)) return { area: \"tool\", code: \"fatal\" };\n\treturn { area: \"internal\", code: \"unclassified\" };\n}\n\n/** Classify a value thrown out of the session runtime itself. */\nexport function runtimeFailureCause(error: unknown): SessionTerminationCause {\n\tconst code = typeof error === \"object\" && error !== null && \"code\" in error ? Reflect.get(error, \"code\") : undefined;\n\tif (typeof code === \"string\" && PERSISTENCE_ERRNO_CODES.has(code)) {\n\t\treturn { area: \"persistence\", code: \"append_failed\" };\n\t}\n\tconst message = error instanceof Error ? error.message : String(error);\n\tif (/compaction.+stale|session changed during compaction/i.test(message)) {\n\t\treturn { area: \"compaction\", code: \"stale\" };\n\t}\n\tif (/compaction/i.test(message)) return { area: \"compaction\", code: \"failed\" };\n\treturn { area: \"internal\", code: \"unclassified\" };\n}\n"]}
|