open-multi-agent-kit 0.90.3 → 0.90.4
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 +16 -0
- package/README.md +16 -14
- package/dist/config.d.ts +3 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -19
- package/dist/config.js.map +1 -1
- package/dist/core/adaptorch-bridge.d.ts +257 -0
- package/dist/core/adaptorch-bridge.d.ts.map +1 -0
- package/dist/core/adaptorch-bridge.js +473 -0
- package/dist/core/adaptorch-bridge.js.map +1 -0
- package/dist/core/agent-session.d.ts +130 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +286 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bang-skill-invocation.d.ts.map +1 -1
- package/dist/core/bang-skill-invocation.js +149 -0
- package/dist/core/bang-skill-invocation.js.map +1 -1
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +15 -2
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/domain-loadouts.d.ts.map +1 -1
- package/dist/core/domain-loadouts.js +1 -1
- package/dist/core/domain-loadouts.js.map +1 -1
- package/dist/core/reasoning-router-bias.d.ts +119 -0
- package/dist/core/reasoning-router-bias.d.ts.map +1 -0
- package/dist/core/reasoning-router-bias.js +287 -0
- package/dist/core/reasoning-router-bias.js.map +1 -0
- package/dist/core/reasoning-router-v2.d.ts +94 -0
- package/dist/core/reasoning-router-v2.d.ts.map +1 -0
- package/dist/core/reasoning-router-v2.js +288 -0
- package/dist/core/reasoning-router-v2.js.map +1 -0
- package/dist/core/reasoning-router-v3.d.ts +15 -0
- package/dist/core/reasoning-router-v3.d.ts.map +1 -0
- package/dist/core/reasoning-router-v3.js +193 -0
- package/dist/core/reasoning-router-v3.js.map +1 -0
- package/dist/core/reasoning-router-v4-weights.d.ts +124 -0
- package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -0
- package/dist/core/reasoning-router-v4-weights.js +112 -0
- package/dist/core/reasoning-router-v4-weights.js.map +1 -0
- package/dist/core/reasoning-router-v4.d.ts +163 -0
- package/dist/core/reasoning-router-v4.d.ts.map +1 -0
- package/dist/core/reasoning-router-v4.js +683 -0
- package/dist/core/reasoning-router-v4.js.map +1 -0
- package/dist/core/reasoning-router-weights.d.ts +155 -0
- package/dist/core/reasoning-router-weights.d.ts.map +1 -0
- package/dist/core/reasoning-router-weights.js +204 -0
- package/dist/core/reasoning-router-weights.js.map +1 -0
- package/dist/core/reasoning-router.d.ts +44 -0
- package/dist/core/reasoning-router.d.ts.map +1 -0
- package/dist/core/reasoning-router.js +138 -0
- package/dist/core/reasoning-router.js.map +1 -0
- package/dist/core/router-feedback-collector.d.ts +101 -0
- package/dist/core/router-feedback-collector.d.ts.map +1 -0
- package/dist/core/router-feedback-collector.js +239 -0
- package/dist/core/router-feedback-collector.js.map +1 -0
- package/dist/core/settings-manager.d.ts +28 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +16 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +15 -0
- package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
- package/dist/modes/interactive/components/control-panel-runtime-status.js +95 -5
- package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +143 -18
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/loadout-domains/README.md +8 -6
- package/docs/loadout-domains/ai-agent-ops.md +3 -2
- package/docs/loadout-domains/backend-api.md +3 -2
- package/docs/loadout-domains/data-science.md +3 -2
- package/docs/loadout-domains/devops-infra.md +2 -1
- package/docs/loadout-domains/docs-writing.md +7 -2
- package/docs/loadout-domains/frontend-ui.md +42 -11
- package/docs/loadout-domains/korean-document.md +83 -0
- package/docs/loadout-domains/mobile.md +1 -1
- package/docs/loadout-domains/qa-testing.md +4 -2
- package/docs/loadout-domains/security-audit.md +2 -1
- package/docs/loadout-domains/visual-qa.md +94 -0
- package/docs/skills.md +4 -1
- package/docs/usage.md +37 -3
- 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 +12 -12
- package/package.json +4 -4
- package/dist/agents/jailbreak/index.d.ts +0 -7
- package/dist/agents/jailbreak/index.d.ts.map +0 -1
- package/dist/agents/jailbreak/index.js +0 -7
- package/dist/agents/jailbreak/index.js.map +0 -1
- package/dist/agents/jailbreak/subagent-f.d.ts +0 -44
- package/dist/agents/jailbreak/subagent-f.d.ts.map +0 -1
- package/dist/agents/jailbreak/subagent-f.js +0 -60
- package/dist/agents/jailbreak/subagent-f.js.map +0 -1
- package/dist/cli/jailbreak-args.d.ts +0 -26
- package/dist/cli/jailbreak-args.d.ts.map +0 -1
- package/dist/cli/jailbreak-args.js +0 -86
- package/dist/cli/jailbreak-args.js.map +0 -1
- package/dist/commands/jailbreak.d.ts +0 -49
- package/dist/commands/jailbreak.d.ts.map +0 -1
- package/dist/commands/jailbreak.js +0 -201
- package/dist/commands/jailbreak.js.map +0 -1
- package/dist/encoding/index.d.ts +0 -6
- package/dist/encoding/index.d.ts.map +0 -1
- package/dist/encoding/index.js +0 -6
- package/dist/encoding/index.js.map +0 -1
- package/dist/encoding/l4-encoder.d.ts +0 -96
- package/dist/encoding/l4-encoder.d.ts.map +0 -1
- package/dist/encoding/l4-encoder.js +0 -474
- package/dist/encoding/l4-encoder.js.map +0 -1
- package/dist/fuzzing/index.d.ts +0 -6
- package/dist/fuzzing/index.d.ts.map +0 -1
- package/dist/fuzzing/index.js +0 -6
- package/dist/fuzzing/index.js.map +0 -1
- package/dist/fuzzing/mutation-engine.d.ts +0 -93
- package/dist/fuzzing/mutation-engine.d.ts.map +0 -1
- package/dist/fuzzing/mutation-engine.js +0 -244
- package/dist/fuzzing/mutation-engine.js.map +0 -1
- package/dist/jailbreak-extension.d.ts +0 -48
- package/dist/jailbreak-extension.d.ts.map +0 -1
- package/dist/jailbreak-extension.js +0 -47
- package/dist/jailbreak-extension.js.map +0 -1
- package/dist/modules/index.d.ts +0 -8
- package/dist/modules/index.d.ts.map +0 -1
- package/dist/modules/index.js +0 -7
- package/dist/modules/index.js.map +0 -1
- package/dist/modules/lrl-bypass.d.ts +0 -40
- package/dist/modules/lrl-bypass.d.ts.map +0 -1
- package/dist/modules/lrl-bypass.js +0 -84
- package/dist/modules/lrl-bypass.js.map +0 -1
- package/dist/modules/lrl-deps.d.ts +0 -38
- package/dist/modules/lrl-deps.d.ts.map +0 -1
- package/dist/modules/lrl-deps.js +0 -128
- package/dist/modules/lrl-deps.js.map +0 -1
- package/dist/multiturn/easl-chain.d.ts +0 -113
- package/dist/multiturn/easl-chain.d.ts.map +0 -1
- package/dist/multiturn/easl-chain.js +0 -351
- package/dist/multiturn/easl-chain.js.map +0 -1
- package/dist/multiturn/index.d.ts +0 -6
- package/dist/multiturn/index.d.ts.map +0 -1
- package/dist/multiturn/index.js +0 -6
- package/dist/multiturn/index.js.map +0 -1
- package/dist/routing/fallback-router.d.ts +0 -123
- package/dist/routing/fallback-router.d.ts.map +0 -1
- package/dist/routing/fallback-router.js +0 -355
- package/dist/routing/fallback-router.js.map +0 -1
- package/dist/routing/index.d.ts +0 -6
- package/dist/routing/index.d.ts.map +0 -1
- package/dist/routing/index.js +0 -6
- package/dist/routing/index.js.map +0 -1
- package/dist/types/jailbreak.d.ts +0 -246
- package/dist/types/jailbreak.d.ts.map +0 -1
- package/dist/types/jailbreak.js +0 -8
- package/dist/types/jailbreak.js.map +0 -1
- package/dist/utils/jailbreak/common.d.ts +0 -63
- package/dist/utils/jailbreak/common.d.ts.map +0 -1
- package/dist/utils/jailbreak/common.js +0 -130
- package/dist/utils/jailbreak/common.js.map +0 -1
- package/dist/utils/jailbreak/index.d.ts +0 -7
- package/dist/utils/jailbreak/index.d.ts.map +0 -1
- package/dist/utils/jailbreak/index.js +0 -7
- package/dist/utils/jailbreak/index.js.map +0 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning-effort router: pure, side-effect-free task classification and
|
|
3
|
+
* ThinkingLevel resolution for the `auto` thinking mode.
|
|
4
|
+
*
|
|
5
|
+
* Deterministic by construction: consumes only the prompt text and an optional
|
|
6
|
+
* subagent lane type. No clock, randomness, I/O, or imports from session, TUI,
|
|
7
|
+
* or provider code (the ThinkingLevel import below is type-only and erased).
|
|
8
|
+
*/
|
|
9
|
+
import type { ThinkingLevel } from "omk-agent-core";
|
|
10
|
+
/** Closed set of task classes the router can assign to a turn. */
|
|
11
|
+
export type TaskClass = "trivial" | "simple-edit" | "code-gen" | "debug" | "refactor" | "review" | "plan";
|
|
12
|
+
/** Subagent lane types recognized by the classifier and resolver. */
|
|
13
|
+
export type ReasoningLaneType = "planner" | "security" | "explorer" | "coder" | "reviewer" | "tester";
|
|
14
|
+
export interface TaskClassifierInput {
|
|
15
|
+
prompt: string;
|
|
16
|
+
laneType?: ReasoningLaneType;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Classify a turn deterministically. Signal precedence (first match wins):
|
|
20
|
+
* 1. Keyword families (debug > refactor > review > plan > simple-edit > code-gen)
|
|
21
|
+
* 2. Code fence or diff markers -> code-gen
|
|
22
|
+
* 3. Trimmed length < TRIVIAL_MAX_CHARS -> trivial
|
|
23
|
+
* 4. Trimmed length >= COMPLEX_PROSE_MIN_CHARS -> plan (long prose brief)
|
|
24
|
+
* 5. Lane fallback (see LANE_FALLBACK_CLASS)
|
|
25
|
+
* 6. Default -> code-gen
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyTask(input: TaskClassifierInput): TaskClass;
|
|
28
|
+
/** Static rule table: task class -> recommended ThinkingLevel (before lane adjustment and clamping). */
|
|
29
|
+
export declare const TASK_CLASS_THINKING_LEVELS: Readonly<Record<TaskClass, ThinkingLevel>>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the recommended ThinkingLevel for a task class:
|
|
32
|
+
* 1. Look up the static rule table.
|
|
33
|
+
* 2. Apply the lane adjustment (one ladder step, saturating at ladder ends).
|
|
34
|
+
* 3. Clamp to the highest level in `availableLevels` that is <= the target;
|
|
35
|
+
* if no available level is at/below the target, return the lowest available
|
|
36
|
+
* reasoning level. Never invents a level outside `availableLevels`.
|
|
37
|
+
*
|
|
38
|
+
* `"off"` is never a router output for reasoning models. If `availableLevels`
|
|
39
|
+
* contains no ladder level at all (e.g. a non-reasoning model exposing only
|
|
40
|
+
* "off"), the first available level is returned; callers are expected to
|
|
41
|
+
* bypass the router entirely for models with `reasoning: false`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveThinkingLevel(taskClass: TaskClass, availableLevels: readonly ThinkingLevel[], laneType?: ReasoningLaneType): ThinkingLevel;
|
|
44
|
+
//# sourceMappingURL=reasoning-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning-router.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,kEAAkE;AAClE,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE1G,qEAAqE;AACrE,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEtG,MAAM,WAAW,mBAAmB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC7B;AAqED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,mBAAmB,GAAG,SAAS,CAYlE;AAKD,wGAAwG;AACxG,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,CAAC,CAQjF,CAAC;AASF;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,SAAS,EACpB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,CAAC,EAAE,iBAAiB,GAC1B,aAAa,CAef","sourcesContent":["/**\n * Reasoning-effort router: pure, side-effect-free task classification and\n * ThinkingLevel resolution for the `auto` thinking mode.\n *\n * Deterministic by construction: consumes only the prompt text and an optional\n * subagent lane type. No clock, randomness, I/O, or imports from session, TUI,\n * or provider code (the ThinkingLevel import below is type-only and erased).\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\n\n/** Closed set of task classes the router can assign to a turn. */\nexport type TaskClass = \"trivial\" | \"simple-edit\" | \"code-gen\" | \"debug\" | \"refactor\" | \"review\" | \"plan\";\n\n/** Subagent lane types recognized by the classifier and resolver. */\nexport type ReasoningLaneType = \"planner\" | \"security\" | \"explorer\" | \"coder\" | \"reviewer\" | \"tester\";\n\nexport interface TaskClassifierInput {\n\tprompt: string;\n\tlaneType?: ReasoningLaneType;\n}\n\n/** Prompts shorter than this (trimmed) classify as trivial when no stronger signal matches. */\nconst TRIVIAL_MAX_CHARS = 40;\n/** Prompts at/above this length with no keyword/code/diff signal are treated as long prose briefs (plan). */\nconst COMPLEX_PROSE_MIN_CHARS = 2400;\n\n/**\n * Keyword families in FIXED precedence order:\n * debug > refactor > review > plan > simple-edit > code-gen.\n * The first matching family wins; ties are impossible by construction.\n * Patterns are case-insensitive and stateless (no `g` flag, so no lastIndex carry-over).\n */\nconst KEYWORD_FAMILIES: ReadonlyArray<{ taskClass: TaskClass; pattern: RegExp }> = [\n\t{\n\t\ttaskClass: \"debug\",\n\t\tpattern:\n\t\t\t/\\b(debug(ging)?|bugs?|fix(es|ing|ed)?|errors?|exceptions?|crash(es|ing|ed)?|stack\\s*trace|traceback|regressions?|broken|fail(s|ing|ed|ure|ures)?|flaky|reproduce)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"refactor\",\n\t\tpattern:\n\t\t\t/\\b(refactor(ing|ed)?|restructur(e|ing)|renam(e|ing)|extract(ing)?|clean\\s*up|cleanup|simplif(y|ying|ied)|deduplicat(e|ing)|reorganiz(e|ing)|rewrit(e|ing)|modulariz(e|ing))\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"review\",\n\t\tpattern: /\\b(review(s|ing|ed)?|audit(s|ing|ed)?|critique|assess(ing|ment)?|inspect(ing)?|lgtm|approve)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"plan\",\n\t\tpattern:\n\t\t\t/\\b(plan(s|ning)?|design(s|ing)?|architect(s|ure|ing)?|roadmap|spec(s|ification|ifications)?|strateg(y|ies|ize)|decompos(e|ing|ition)|break\\s+(this\\s+|it\\s+)?down|milestones?)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"simple-edit\",\n\t\tpattern:\n\t\t\t/\\b(typos?|tweak(s|ing|ed)?|one-?liner?|single\\s+line|bump(s|ing|ed)?|whitespace|reword(s|ing|ed)?|indentation|punctuation)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"code-gen\",\n\t\tpattern:\n\t\t\t/\\b(implement(s|ing|ation)?|writ(e|ing)|creat(e|es|ing)|add(s|ing|ed)?|build(s|ing)?|generat(e|es|ing)|scaffold(ing)?|prototype)\\b/i,\n\t},\n];\n\n/** Fallback class per lane when no keyword, code/diff, or length signal decides. */\nconst LANE_FALLBACK_CLASS: Record<ReasoningLaneType, TaskClass> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/**\n * Diff detection: explicit hunk (`@@ ... @@`) or `diff --git` headers count alone;\n * bare `+`/`-` line starts only count when both added AND removed lines are present\n * (avoids false positives on markdown bullet lists).\n */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\n/**\n * Classify a turn deterministically. Signal precedence (first match wins):\n * 1. Keyword families (debug > refactor > review > plan > simple-edit > code-gen)\n * 2. Code fence or diff markers -> code-gen\n * 3. Trimmed length < TRIVIAL_MAX_CHARS -> trivial\n * 4. Trimmed length >= COMPLEX_PROSE_MIN_CHARS -> plan (long prose brief)\n * 5. Lane fallback (see LANE_FALLBACK_CLASS)\n * 6. Default -> code-gen\n */\nexport function classifyTask(input: TaskClassifierInput): TaskClass {\n\tconst prompt = input.prompt.trim();\n\n\tfor (const family of KEYWORD_FAMILIES) {\n\t\tif (family.pattern.test(prompt)) return family.taskClass;\n\t}\n\n\tif (hasCodeFence(prompt) || hasDiffMarkers(prompt)) return \"code-gen\";\n\tif (prompt.length < TRIVIAL_MAX_CHARS) return \"trivial\";\n\tif (prompt.length >= COMPLEX_PROSE_MIN_CHARS) return \"plan\";\n\tif (input.laneType) return LANE_FALLBACK_CLASS[input.laneType];\n\treturn \"code-gen\";\n}\n\n/** Reasoning ladder used for targets and clamping. Intentionally excludes \"off\". */\nconst REASONING_LADDER: readonly ThinkingLevel[] = [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"];\n\n/** Static rule table: task class -> recommended ThinkingLevel (before lane adjustment and clamping). */\nexport const TASK_CLASS_THINKING_LEVELS: Readonly<Record<TaskClass, ThinkingLevel>> = {\n\ttrivial: \"minimal\",\n\t\"simple-edit\": \"low\",\n\t\"code-gen\": \"medium\",\n\tdebug: \"high\",\n\trefactor: \"high\",\n\treview: \"high\",\n\tplan: \"xhigh\",\n};\n\n/** Lane adjustment in ladder steps: planner/security escalate, explorer de-escalates. */\nconst LANE_STEP: Readonly<Partial<Record<ReasoningLaneType, 1 | -1>>> = {\n\tplanner: 1,\n\tsecurity: 1,\n\texplorer: -1,\n};\n\n/**\n * Resolve the recommended ThinkingLevel for a task class:\n * 1. Look up the static rule table.\n * 2. Apply the lane adjustment (one ladder step, saturating at ladder ends).\n * 3. Clamp to the highest level in `availableLevels` that is <= the target;\n * if no available level is at/below the target, return the lowest available\n * reasoning level. Never invents a level outside `availableLevels`.\n *\n * `\"off\"` is never a router output for reasoning models. If `availableLevels`\n * contains no ladder level at all (e.g. a non-reasoning model exposing only\n * \"off\"), the first available level is returned; callers are expected to\n * bypass the router entirely for models with `reasoning: false`.\n */\nexport function resolveThinkingLevel(\n\ttaskClass: TaskClass,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType?: ReasoningLaneType,\n): ThinkingLevel {\n\tconst baseIndex = REASONING_LADDER.indexOf(TASK_CLASS_THINKING_LEVELS[taskClass]);\n\tconst step = laneType ? (LANE_STEP[laneType] ?? 0) : 0;\n\tconst targetIndex = Math.min(Math.max(baseIndex + step, 0), REASONING_LADDER.length - 1);\n\n\tconst availableOnLadder = REASONING_LADDER.filter((level) => availableLevels.includes(level));\n\tif (availableOnLadder.length === 0) {\n\t\treturn availableLevels[0] ?? \"off\";\n\t}\n\n\tfor (let i = targetIndex; i >= 0; i--) {\n\t\tconst candidate = REASONING_LADDER[i];\n\t\tif (availableOnLadder.includes(candidate)) return candidate;\n\t}\n\treturn availableOnLadder[0];\n}\n"]}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning-effort router: pure, side-effect-free task classification and
|
|
3
|
+
* ThinkingLevel resolution for the `auto` thinking mode.
|
|
4
|
+
*
|
|
5
|
+
* Deterministic by construction: consumes only the prompt text and an optional
|
|
6
|
+
* subagent lane type. No clock, randomness, I/O, or imports from session, TUI,
|
|
7
|
+
* or provider code (the ThinkingLevel import below is type-only and erased).
|
|
8
|
+
*/
|
|
9
|
+
/** Prompts shorter than this (trimmed) classify as trivial when no stronger signal matches. */
|
|
10
|
+
const TRIVIAL_MAX_CHARS = 40;
|
|
11
|
+
/** Prompts at/above this length with no keyword/code/diff signal are treated as long prose briefs (plan). */
|
|
12
|
+
const COMPLEX_PROSE_MIN_CHARS = 2400;
|
|
13
|
+
/**
|
|
14
|
+
* Keyword families in FIXED precedence order:
|
|
15
|
+
* debug > refactor > review > plan > simple-edit > code-gen.
|
|
16
|
+
* The first matching family wins; ties are impossible by construction.
|
|
17
|
+
* Patterns are case-insensitive and stateless (no `g` flag, so no lastIndex carry-over).
|
|
18
|
+
*/
|
|
19
|
+
const KEYWORD_FAMILIES = [
|
|
20
|
+
{
|
|
21
|
+
taskClass: "debug",
|
|
22
|
+
pattern: /\b(debug(ging)?|bugs?|fix(es|ing|ed)?|errors?|exceptions?|crash(es|ing|ed)?|stack\s*trace|traceback|regressions?|broken|fail(s|ing|ed|ure|ures)?|flaky|reproduce)\b/i,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
taskClass: "refactor",
|
|
26
|
+
pattern: /\b(refactor(ing|ed)?|restructur(e|ing)|renam(e|ing)|extract(ing)?|clean\s*up|cleanup|simplif(y|ying|ied)|deduplicat(e|ing)|reorganiz(e|ing)|rewrit(e|ing)|modulariz(e|ing))\b/i,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
taskClass: "review",
|
|
30
|
+
pattern: /\b(review(s|ing|ed)?|audit(s|ing|ed)?|critique|assess(ing|ment)?|inspect(ing)?|lgtm|approve)\b/i,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
taskClass: "plan",
|
|
34
|
+
pattern: /\b(plan(s|ning)?|design(s|ing)?|architect(s|ure|ing)?|roadmap|spec(s|ification|ifications)?|strateg(y|ies|ize)|decompos(e|ing|ition)|break\s+(this\s+|it\s+)?down|milestones?)\b/i,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
taskClass: "simple-edit",
|
|
38
|
+
pattern: /\b(typos?|tweak(s|ing|ed)?|one-?liner?|single\s+line|bump(s|ing|ed)?|whitespace|reword(s|ing|ed)?|indentation|punctuation)\b/i,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
taskClass: "code-gen",
|
|
42
|
+
pattern: /\b(implement(s|ing|ation)?|writ(e|ing)|creat(e|es|ing)|add(s|ing|ed)?|build(s|ing)?|generat(e|es|ing)|scaffold(ing)?|prototype)\b/i,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
/** Fallback class per lane when no keyword, code/diff, or length signal decides. */
|
|
46
|
+
const LANE_FALLBACK_CLASS = {
|
|
47
|
+
planner: "plan",
|
|
48
|
+
security: "review",
|
|
49
|
+
explorer: "review",
|
|
50
|
+
coder: "code-gen",
|
|
51
|
+
reviewer: "review",
|
|
52
|
+
tester: "code-gen",
|
|
53
|
+
};
|
|
54
|
+
function hasCodeFence(text) {
|
|
55
|
+
return text.includes("```");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Diff detection: explicit hunk (`@@ ... @@`) or `diff --git` headers count alone;
|
|
59
|
+
* bare `+`/`-` line starts only count when both added AND removed lines are present
|
|
60
|
+
* (avoids false positives on markdown bullet lists).
|
|
61
|
+
*/
|
|
62
|
+
function hasDiffMarkers(text) {
|
|
63
|
+
if (/^@@[^\n]*@@/m.test(text) || /^diff --git /m.test(text))
|
|
64
|
+
return true;
|
|
65
|
+
return /^\+(?!\+)/m.test(text) && /^-(?!-)/m.test(text);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Classify a turn deterministically. Signal precedence (first match wins):
|
|
69
|
+
* 1. Keyword families (debug > refactor > review > plan > simple-edit > code-gen)
|
|
70
|
+
* 2. Code fence or diff markers -> code-gen
|
|
71
|
+
* 3. Trimmed length < TRIVIAL_MAX_CHARS -> trivial
|
|
72
|
+
* 4. Trimmed length >= COMPLEX_PROSE_MIN_CHARS -> plan (long prose brief)
|
|
73
|
+
* 5. Lane fallback (see LANE_FALLBACK_CLASS)
|
|
74
|
+
* 6. Default -> code-gen
|
|
75
|
+
*/
|
|
76
|
+
export function classifyTask(input) {
|
|
77
|
+
const prompt = input.prompt.trim();
|
|
78
|
+
for (const family of KEYWORD_FAMILIES) {
|
|
79
|
+
if (family.pattern.test(prompt))
|
|
80
|
+
return family.taskClass;
|
|
81
|
+
}
|
|
82
|
+
if (hasCodeFence(prompt) || hasDiffMarkers(prompt))
|
|
83
|
+
return "code-gen";
|
|
84
|
+
if (prompt.length < TRIVIAL_MAX_CHARS)
|
|
85
|
+
return "trivial";
|
|
86
|
+
if (prompt.length >= COMPLEX_PROSE_MIN_CHARS)
|
|
87
|
+
return "plan";
|
|
88
|
+
if (input.laneType)
|
|
89
|
+
return LANE_FALLBACK_CLASS[input.laneType];
|
|
90
|
+
return "code-gen";
|
|
91
|
+
}
|
|
92
|
+
/** Reasoning ladder used for targets and clamping. Intentionally excludes "off". */
|
|
93
|
+
const REASONING_LADDER = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
94
|
+
/** Static rule table: task class -> recommended ThinkingLevel (before lane adjustment and clamping). */
|
|
95
|
+
export const TASK_CLASS_THINKING_LEVELS = {
|
|
96
|
+
trivial: "minimal",
|
|
97
|
+
"simple-edit": "low",
|
|
98
|
+
"code-gen": "medium",
|
|
99
|
+
debug: "high",
|
|
100
|
+
refactor: "high",
|
|
101
|
+
review: "high",
|
|
102
|
+
plan: "xhigh",
|
|
103
|
+
};
|
|
104
|
+
/** Lane adjustment in ladder steps: planner/security escalate, explorer de-escalates. */
|
|
105
|
+
const LANE_STEP = {
|
|
106
|
+
planner: 1,
|
|
107
|
+
security: 1,
|
|
108
|
+
explorer: -1,
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the recommended ThinkingLevel for a task class:
|
|
112
|
+
* 1. Look up the static rule table.
|
|
113
|
+
* 2. Apply the lane adjustment (one ladder step, saturating at ladder ends).
|
|
114
|
+
* 3. Clamp to the highest level in `availableLevels` that is <= the target;
|
|
115
|
+
* if no available level is at/below the target, return the lowest available
|
|
116
|
+
* reasoning level. Never invents a level outside `availableLevels`.
|
|
117
|
+
*
|
|
118
|
+
* `"off"` is never a router output for reasoning models. If `availableLevels`
|
|
119
|
+
* contains no ladder level at all (e.g. a non-reasoning model exposing only
|
|
120
|
+
* "off"), the first available level is returned; callers are expected to
|
|
121
|
+
* bypass the router entirely for models with `reasoning: false`.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveThinkingLevel(taskClass, availableLevels, laneType) {
|
|
124
|
+
const baseIndex = REASONING_LADDER.indexOf(TASK_CLASS_THINKING_LEVELS[taskClass]);
|
|
125
|
+
const step = laneType ? (LANE_STEP[laneType] ?? 0) : 0;
|
|
126
|
+
const targetIndex = Math.min(Math.max(baseIndex + step, 0), REASONING_LADDER.length - 1);
|
|
127
|
+
const availableOnLadder = REASONING_LADDER.filter((level) => availableLevels.includes(level));
|
|
128
|
+
if (availableOnLadder.length === 0) {
|
|
129
|
+
return availableLevels[0] ?? "off";
|
|
130
|
+
}
|
|
131
|
+
for (let i = targetIndex; i >= 0; i--) {
|
|
132
|
+
const candidate = REASONING_LADDER[i];
|
|
133
|
+
if (availableOnLadder.includes(candidate))
|
|
134
|
+
return candidate;
|
|
135
|
+
}
|
|
136
|
+
return availableOnLadder[0];
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=reasoning-router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning-router.js","sourceRoot":"","sources":["../../src/core/reasoning-router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reasoning-effort router: pure, side-effect-free task classification and\n * ThinkingLevel resolution for the `auto` thinking mode.\n *\n * Deterministic by construction: consumes only the prompt text and an optional\n * subagent lane type. No clock, randomness, I/O, or imports from session, TUI,\n * or provider code (the ThinkingLevel import below is type-only and erased).\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\n\n/** Closed set of task classes the router can assign to a turn. */\nexport type TaskClass = \"trivial\" | \"simple-edit\" | \"code-gen\" | \"debug\" | \"refactor\" | \"review\" | \"plan\";\n\n/** Subagent lane types recognized by the classifier and resolver. */\nexport type ReasoningLaneType = \"planner\" | \"security\" | \"explorer\" | \"coder\" | \"reviewer\" | \"tester\";\n\nexport interface TaskClassifierInput {\n\tprompt: string;\n\tlaneType?: ReasoningLaneType;\n}\n\n/** Prompts shorter than this (trimmed) classify as trivial when no stronger signal matches. */\nconst TRIVIAL_MAX_CHARS = 40;\n/** Prompts at/above this length with no keyword/code/diff signal are treated as long prose briefs (plan). */\nconst COMPLEX_PROSE_MIN_CHARS = 2400;\n\n/**\n * Keyword families in FIXED precedence order:\n * debug > refactor > review > plan > simple-edit > code-gen.\n * The first matching family wins; ties are impossible by construction.\n * Patterns are case-insensitive and stateless (no `g` flag, so no lastIndex carry-over).\n */\nconst KEYWORD_FAMILIES: ReadonlyArray<{ taskClass: TaskClass; pattern: RegExp }> = [\n\t{\n\t\ttaskClass: \"debug\",\n\t\tpattern:\n\t\t\t/\\b(debug(ging)?|bugs?|fix(es|ing|ed)?|errors?|exceptions?|crash(es|ing|ed)?|stack\\s*trace|traceback|regressions?|broken|fail(s|ing|ed|ure|ures)?|flaky|reproduce)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"refactor\",\n\t\tpattern:\n\t\t\t/\\b(refactor(ing|ed)?|restructur(e|ing)|renam(e|ing)|extract(ing)?|clean\\s*up|cleanup|simplif(y|ying|ied)|deduplicat(e|ing)|reorganiz(e|ing)|rewrit(e|ing)|modulariz(e|ing))\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"review\",\n\t\tpattern: /\\b(review(s|ing|ed)?|audit(s|ing|ed)?|critique|assess(ing|ment)?|inspect(ing)?|lgtm|approve)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"plan\",\n\t\tpattern:\n\t\t\t/\\b(plan(s|ning)?|design(s|ing)?|architect(s|ure|ing)?|roadmap|spec(s|ification|ifications)?|strateg(y|ies|ize)|decompos(e|ing|ition)|break\\s+(this\\s+|it\\s+)?down|milestones?)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"simple-edit\",\n\t\tpattern:\n\t\t\t/\\b(typos?|tweak(s|ing|ed)?|one-?liner?|single\\s+line|bump(s|ing|ed)?|whitespace|reword(s|ing|ed)?|indentation|punctuation)\\b/i,\n\t},\n\t{\n\t\ttaskClass: \"code-gen\",\n\t\tpattern:\n\t\t\t/\\b(implement(s|ing|ation)?|writ(e|ing)|creat(e|es|ing)|add(s|ing|ed)?|build(s|ing)?|generat(e|es|ing)|scaffold(ing)?|prototype)\\b/i,\n\t},\n];\n\n/** Fallback class per lane when no keyword, code/diff, or length signal decides. */\nconst LANE_FALLBACK_CLASS: Record<ReasoningLaneType, TaskClass> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/**\n * Diff detection: explicit hunk (`@@ ... @@`) or `diff --git` headers count alone;\n * bare `+`/`-` line starts only count when both added AND removed lines are present\n * (avoids false positives on markdown bullet lists).\n */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\n/**\n * Classify a turn deterministically. Signal precedence (first match wins):\n * 1. Keyword families (debug > refactor > review > plan > simple-edit > code-gen)\n * 2. Code fence or diff markers -> code-gen\n * 3. Trimmed length < TRIVIAL_MAX_CHARS -> trivial\n * 4. Trimmed length >= COMPLEX_PROSE_MIN_CHARS -> plan (long prose brief)\n * 5. Lane fallback (see LANE_FALLBACK_CLASS)\n * 6. Default -> code-gen\n */\nexport function classifyTask(input: TaskClassifierInput): TaskClass {\n\tconst prompt = input.prompt.trim();\n\n\tfor (const family of KEYWORD_FAMILIES) {\n\t\tif (family.pattern.test(prompt)) return family.taskClass;\n\t}\n\n\tif (hasCodeFence(prompt) || hasDiffMarkers(prompt)) return \"code-gen\";\n\tif (prompt.length < TRIVIAL_MAX_CHARS) return \"trivial\";\n\tif (prompt.length >= COMPLEX_PROSE_MIN_CHARS) return \"plan\";\n\tif (input.laneType) return LANE_FALLBACK_CLASS[input.laneType];\n\treturn \"code-gen\";\n}\n\n/** Reasoning ladder used for targets and clamping. Intentionally excludes \"off\". */\nconst REASONING_LADDER: readonly ThinkingLevel[] = [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"];\n\n/** Static rule table: task class -> recommended ThinkingLevel (before lane adjustment and clamping). */\nexport const TASK_CLASS_THINKING_LEVELS: Readonly<Record<TaskClass, ThinkingLevel>> = {\n\ttrivial: \"minimal\",\n\t\"simple-edit\": \"low\",\n\t\"code-gen\": \"medium\",\n\tdebug: \"high\",\n\trefactor: \"high\",\n\treview: \"high\",\n\tplan: \"xhigh\",\n};\n\n/** Lane adjustment in ladder steps: planner/security escalate, explorer de-escalates. */\nconst LANE_STEP: Readonly<Partial<Record<ReasoningLaneType, 1 | -1>>> = {\n\tplanner: 1,\n\tsecurity: 1,\n\texplorer: -1,\n};\n\n/**\n * Resolve the recommended ThinkingLevel for a task class:\n * 1. Look up the static rule table.\n * 2. Apply the lane adjustment (one ladder step, saturating at ladder ends).\n * 3. Clamp to the highest level in `availableLevels` that is <= the target;\n * if no available level is at/below the target, return the lowest available\n * reasoning level. Never invents a level outside `availableLevels`.\n *\n * `\"off\"` is never a router output for reasoning models. If `availableLevels`\n * contains no ladder level at all (e.g. a non-reasoning model exposing only\n * \"off\"), the first available level is returned; callers are expected to\n * bypass the router entirely for models with `reasoning: false`.\n */\nexport function resolveThinkingLevel(\n\ttaskClass: TaskClass,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType?: ReasoningLaneType,\n): ThinkingLevel {\n\tconst baseIndex = REASONING_LADDER.indexOf(TASK_CLASS_THINKING_LEVELS[taskClass]);\n\tconst step = laneType ? (LANE_STEP[laneType] ?? 0) : 0;\n\tconst targetIndex = Math.min(Math.max(baseIndex + step, 0), REASONING_LADDER.length - 1);\n\n\tconst availableOnLadder = REASONING_LADDER.filter((level) => availableLevels.includes(level));\n\tif (availableOnLadder.length === 0) {\n\t\treturn availableLevels[0] ?? \"off\";\n\t}\n\n\tfor (let i = targetIndex; i >= 0; i--) {\n\t\tconst candidate = REASONING_LADDER[i];\n\t\tif (availableOnLadder.includes(candidate)) return candidate;\n\t}\n\treturn availableOnLadder[0];\n}\n"]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router feedback collector — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).
|
|
3
|
+
*
|
|
4
|
+
* Default-off, privacy-safe local ledger for a future reasoning-router learning
|
|
5
|
+
* feature. This module is a pure, standalone utility: nothing here is wired
|
|
6
|
+
* into agent-session.ts, settings-manager.ts, or any other product entry point
|
|
7
|
+
* by this lane. A future, separately-reviewed wiring lane decides where
|
|
8
|
+
* `enabled` comes from (a settings key) and when `appendRouterFeedbackRecord`
|
|
9
|
+
* is actually called.
|
|
10
|
+
*
|
|
11
|
+
* ============================================================================
|
|
12
|
+
* SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)
|
|
13
|
+
* ============================================================================
|
|
14
|
+
* routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,
|
|
15
|
+
* signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded
|
|
16
|
+
* enum/number/boolean value. There is no field, in this type or in the runtime
|
|
17
|
+
* validator below, that can carry raw prompt text, a prompt hash, an exact
|
|
18
|
+
* prompt length, a file path, a diff/code blob, a session id, a model id, a
|
|
19
|
+
* provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`
|
|
20
|
+
* rejects ANY object with an unrecognized key (including all of the fields
|
|
21
|
+
* just listed) — the schema is the redaction policy, enforced structurally by
|
|
22
|
+
* an exact key-set check, not by a best-effort denylist of forbidden names.
|
|
23
|
+
* ============================================================================
|
|
24
|
+
*/
|
|
25
|
+
import type { ReasoningLaneTypeV3, TaskClassV3 } from "./reasoning-router-v3.ts";
|
|
26
|
+
/** Router versions eligible to tag a feedback record (mirrors agent-session.ts's ThinkingRouterVersion). */
|
|
27
|
+
export type RouterFeedbackVersion = "v1" | "v2" | "v3" | "v4";
|
|
28
|
+
/** Lane type at record time, or "none" for a non-subagent turn. */
|
|
29
|
+
export type RouterFeedbackLaneType = ReasoningLaneTypeV3 | "none";
|
|
30
|
+
/** Router task class; reuses the v3 (== v2) closed task-class union — never a raw prompt. */
|
|
31
|
+
export type RouterFeedbackTaskClass = TaskClassV3;
|
|
32
|
+
/**
|
|
33
|
+
* Reasoning ladder level, excluding "off": mirrors REASONING_LADDER_V2 in
|
|
34
|
+
* reasoning-router-v2.ts. The router never *resolves* to "off"; turning
|
|
35
|
+
* thinking off entirely is a distinct manual action outside this ledger.
|
|
36
|
+
*/
|
|
37
|
+
export type RouterFeedbackLevel = "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
38
|
+
/** Feedback signal source. */
|
|
39
|
+
export type RouterFeedbackSignal = "s1-override" | "s2-accept" | "s3-hook-outcome" | "s4-regression";
|
|
40
|
+
/** Coarse outcome tag; never hook stdout/stderr or tool output. */
|
|
41
|
+
export type RouterFeedbackOutcome = "up" | "down" | "same" | "accepted" | "pass" | "fail" | "debug-follow-up";
|
|
42
|
+
/** floor(log2(len+1)) clamped to [0,7] — a coarse bucket, never the raw prompt length. */
|
|
43
|
+
export type RouterFeedbackLenBucket = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
44
|
+
/**
|
|
45
|
+
* One JSONL ledger record. Exactly these ten keys; see `isRouterFeedbackRecord`
|
|
46
|
+
* for the runtime allowlist enforcement.
|
|
47
|
+
*/
|
|
48
|
+
export interface RouterFeedbackRecord {
|
|
49
|
+
readonly routerVersion: RouterFeedbackVersion;
|
|
50
|
+
readonly laneType: RouterFeedbackLaneType;
|
|
51
|
+
readonly predictedClass: RouterFeedbackTaskClass;
|
|
52
|
+
readonly resolvedLevel: RouterFeedbackLevel;
|
|
53
|
+
readonly acceptedLevel: RouterFeedbackLevel;
|
|
54
|
+
readonly signal: RouterFeedbackSignal;
|
|
55
|
+
readonly outcome: RouterFeedbackOutcome;
|
|
56
|
+
readonly lenBucket: RouterFeedbackLenBucket;
|
|
57
|
+
readonly hadFence: boolean;
|
|
58
|
+
readonly hadDiff: boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare const ROUTER_FEEDBACK_VERSIONS: readonly RouterFeedbackVersion[];
|
|
61
|
+
export declare const ROUTER_FEEDBACK_LANE_TYPES: readonly RouterFeedbackLaneType[];
|
|
62
|
+
export declare const ROUTER_FEEDBACK_TASK_CLASSES: readonly RouterFeedbackTaskClass[];
|
|
63
|
+
export declare const ROUTER_FEEDBACK_LEN_BUCKETS: readonly RouterFeedbackLenBucket[];
|
|
64
|
+
/**
|
|
65
|
+
* Strict runtime validator: exactly the ten allowed keys (no more, no fewer),
|
|
66
|
+
* each within its bounded value set. Returns false for any extra key —
|
|
67
|
+
* including a raw prompt, file path, diff, session id, model id, provider
|
|
68
|
+
* payload, or hook/tool-output-shaped field — because ANY unrecognized key
|
|
69
|
+
* fails the exact key-set check before any per-field check runs.
|
|
70
|
+
*/
|
|
71
|
+
export declare function isRouterFeedbackRecord(value: unknown): value is RouterFeedbackRecord;
|
|
72
|
+
/** Default ledger path: `<agentDir>/router-feedback/ledger.jsonl` (owner-only, never repo-local). */
|
|
73
|
+
export declare function getDefaultRouterFeedbackLedgerPath(): string;
|
|
74
|
+
export interface AppendRouterFeedbackOptions {
|
|
75
|
+
/** Consent gate. Must be exactly `true`; anything else performs zero filesystem access. */
|
|
76
|
+
readonly enabled: boolean;
|
|
77
|
+
/** Ledger file path override (tests / callers only); defaults to the owner-only agent-dir ledger. */
|
|
78
|
+
readonly ledgerPath?: string;
|
|
79
|
+
}
|
|
80
|
+
export type AppendRouterFeedbackReason = "disabled" | "invalid-schema" | "symlink-refused" | "not-regular-file" | "io-error";
|
|
81
|
+
export type AppendRouterFeedbackResult = {
|
|
82
|
+
readonly appended: true;
|
|
83
|
+
} | {
|
|
84
|
+
readonly appended: false;
|
|
85
|
+
readonly reason: AppendRouterFeedbackReason;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Append one validated feedback record as a single JSONL line.
|
|
89
|
+
*
|
|
90
|
+
* Requires `options.enabled === true` (checked first, before any filesystem
|
|
91
|
+
* access whatsoever — disabled or omitted consent never touches disk).
|
|
92
|
+
* Validates the record against the exact ten-key schema before any I/O.
|
|
93
|
+
* Refuses symlinks and non-regular files where the platform allows detecting
|
|
94
|
+
* them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`
|
|
95
|
+
* is unavailable on some platforms, hence "when possible"). Reuses the same
|
|
96
|
+
* `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for
|
|
97
|
+
* concurrent-session safety. Never throws for expected failure modes — it
|
|
98
|
+
* returns a tagged result so a caller can fail closed silently.
|
|
99
|
+
*/
|
|
100
|
+
export declare function appendRouterFeedbackRecord(record: unknown, options: AppendRouterFeedbackOptions): AppendRouterFeedbackResult;
|
|
101
|
+
//# sourceMappingURL=router-feedback-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router-feedback-collector.d.ts","sourceRoot":"","sources":["../../src/core/router-feedback-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAgBH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAEjF,4GAA4G;AAC5G,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9D,mEAAmE;AACnE,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,MAAM,CAAC;AAElE,+FAA6F;AAC7F,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1F,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,EAA6B,CAAC;AACnG,eAAO,MAAM,0BAA0B,EAAE,SAAS,sBAAsB,EAQvE,CAAC;AACF,eAAO,MAAM,4BAA4B,EAAE,SAAS,uBAAuB,EAQ1E,CAAC;AAiBF,eAAO,MAAM,2BAA2B,EAAE,SAAS,uBAAuB,EAA6B,CAAC;AA6BxG;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAgBpF;AAED,qGAAqG;AACrG,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 — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).\n *\n * Default-off, privacy-safe local ledger for a future reasoning-router learning\n * feature. This module is a pure, standalone utility: nothing here is wired\n * into agent-session.ts, settings-manager.ts, or any other product entry point\n * by this lane. A future, separately-reviewed wiring lane decides where\n * `enabled` comes from (a settings key) and when `appendRouterFeedbackRecord`\n * is actually called.\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 { ReasoningLaneTypeV3, TaskClassV3 } from \"./reasoning-router-v3.ts\";\n\n/** Router versions eligible to tag a feedback record (mirrors agent-session.ts's ThinkingRouterVersion). */\nexport type RouterFeedbackVersion = \"v1\" | \"v2\" | \"v3\" | \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV3 | \"none\";\n\n/** Router task class; reuses the v3 (== v2) closed task-class union — never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV3;\n\n/**\n * Reasoning ladder level, excluding \"off\": mirrors REASONING_LADDER_V2 in\n * reasoning-router-v2.ts. The router never *resolves* to \"off\"; turning\n * thinking off entirely is a distinct manual action outside this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\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[] = [\"v1\", \"v2\", \"v3\", \"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];\nconst ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"];\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/** Default ledger path: `<agentDir>/router-feedback/ledger.jsonl` (owner-only, never repo-local). */\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,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router feedback collector — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).
|
|
3
|
+
*
|
|
4
|
+
* Default-off, privacy-safe local ledger for a future reasoning-router learning
|
|
5
|
+
* feature. This module is a pure, standalone utility: nothing here is wired
|
|
6
|
+
* into agent-session.ts, settings-manager.ts, or any other product entry point
|
|
7
|
+
* by this lane. A future, separately-reviewed wiring lane decides where
|
|
8
|
+
* `enabled` comes from (a settings key) and when `appendRouterFeedbackRecord`
|
|
9
|
+
* is actually called.
|
|
10
|
+
*
|
|
11
|
+
* ============================================================================
|
|
12
|
+
* SCHEMA (exactly ten allowed keys — see `RouterFeedbackRecord`)
|
|
13
|
+
* ============================================================================
|
|
14
|
+
* routerVersion, laneType, predictedClass, resolvedLevel, acceptedLevel,
|
|
15
|
+
* signal, outcome, lenBucket, hadFence, hadDiff. Every field is a bounded
|
|
16
|
+
* enum/number/boolean value. There is no field, in this type or in the runtime
|
|
17
|
+
* validator below, that can carry raw prompt text, a prompt hash, an exact
|
|
18
|
+
* prompt length, a file path, a diff/code blob, a session id, a model id, a
|
|
19
|
+
* provider payload, or any hook/tool stdout/stderr. `isRouterFeedbackRecord`
|
|
20
|
+
* rejects ANY object with an unrecognized key (including all of the fields
|
|
21
|
+
* just listed) — the schema is the redaction policy, enforced structurally by
|
|
22
|
+
* an exact key-set check, not by a best-effort denylist of forbidden names.
|
|
23
|
+
* ============================================================================
|
|
24
|
+
*/
|
|
25
|
+
import { chmodSync, closeSync, existsSync, constants as fsConstants, fstatSync, lstatSync, mkdirSync, openSync, writeSync, } from "fs";
|
|
26
|
+
import { dirname, join } from "path";
|
|
27
|
+
import lockfile from "proper-lockfile";
|
|
28
|
+
import { getAgentDir } from "../config.js";
|
|
29
|
+
export const ROUTER_FEEDBACK_VERSIONS = ["v1", "v2", "v3", "v4"];
|
|
30
|
+
export const ROUTER_FEEDBACK_LANE_TYPES = [
|
|
31
|
+
"planner",
|
|
32
|
+
"security",
|
|
33
|
+
"explorer",
|
|
34
|
+
"coder",
|
|
35
|
+
"reviewer",
|
|
36
|
+
"tester",
|
|
37
|
+
"none",
|
|
38
|
+
];
|
|
39
|
+
export const ROUTER_FEEDBACK_TASK_CLASSES = [
|
|
40
|
+
"trivial",
|
|
41
|
+
"simple-edit",
|
|
42
|
+
"code-gen",
|
|
43
|
+
"debug",
|
|
44
|
+
"refactor",
|
|
45
|
+
"review",
|
|
46
|
+
"plan",
|
|
47
|
+
];
|
|
48
|
+
const ROUTER_FEEDBACK_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
49
|
+
const ROUTER_FEEDBACK_SIGNALS = [
|
|
50
|
+
"s1-override",
|
|
51
|
+
"s2-accept",
|
|
52
|
+
"s3-hook-outcome",
|
|
53
|
+
"s4-regression",
|
|
54
|
+
];
|
|
55
|
+
const ROUTER_FEEDBACK_OUTCOMES = [
|
|
56
|
+
"up",
|
|
57
|
+
"down",
|
|
58
|
+
"same",
|
|
59
|
+
"accepted",
|
|
60
|
+
"pass",
|
|
61
|
+
"fail",
|
|
62
|
+
"debug-follow-up",
|
|
63
|
+
];
|
|
64
|
+
export const ROUTER_FEEDBACK_LEN_BUCKETS = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
65
|
+
/** Exact allowed key set for `RouterFeedbackRecord`, sorted for canonical set-equality comparison. */
|
|
66
|
+
const ROUTER_FEEDBACK_RECORD_KEYS = [
|
|
67
|
+
"routerVersion",
|
|
68
|
+
"laneType",
|
|
69
|
+
"predictedClass",
|
|
70
|
+
"resolvedLevel",
|
|
71
|
+
"acceptedLevel",
|
|
72
|
+
"signal",
|
|
73
|
+
"outcome",
|
|
74
|
+
"lenBucket",
|
|
75
|
+
"hadFence",
|
|
76
|
+
"hadDiff",
|
|
77
|
+
].sort();
|
|
78
|
+
function isPlainObject(value) {
|
|
79
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function hasExactRouterFeedbackKeySet(value) {
|
|
82
|
+
const keys = Object.keys(value).sort();
|
|
83
|
+
if (keys.length !== ROUTER_FEEDBACK_RECORD_KEYS.length)
|
|
84
|
+
return false;
|
|
85
|
+
for (let i = 0; i < keys.length; i++) {
|
|
86
|
+
if (keys[i] !== ROUTER_FEEDBACK_RECORD_KEYS[i])
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Strict runtime validator: exactly the ten allowed keys (no more, no fewer),
|
|
93
|
+
* each within its bounded value set. Returns false for any extra key —
|
|
94
|
+
* including a raw prompt, file path, diff, session id, model id, provider
|
|
95
|
+
* payload, or hook/tool-output-shaped field — because ANY unrecognized key
|
|
96
|
+
* fails the exact key-set check before any per-field check runs.
|
|
97
|
+
*/
|
|
98
|
+
export function isRouterFeedbackRecord(value) {
|
|
99
|
+
if (!isPlainObject(value))
|
|
100
|
+
return false;
|
|
101
|
+
if (!hasExactRouterFeedbackKeySet(value))
|
|
102
|
+
return false;
|
|
103
|
+
if (!ROUTER_FEEDBACK_VERSIONS.includes(value.routerVersion))
|
|
104
|
+
return false;
|
|
105
|
+
if (!ROUTER_FEEDBACK_LANE_TYPES.includes(value.laneType))
|
|
106
|
+
return false;
|
|
107
|
+
if (!ROUTER_FEEDBACK_TASK_CLASSES.includes(value.predictedClass))
|
|
108
|
+
return false;
|
|
109
|
+
if (!ROUTER_FEEDBACK_LEVELS.includes(value.resolvedLevel))
|
|
110
|
+
return false;
|
|
111
|
+
if (!ROUTER_FEEDBACK_LEVELS.includes(value.acceptedLevel))
|
|
112
|
+
return false;
|
|
113
|
+
if (!ROUTER_FEEDBACK_SIGNALS.includes(value.signal))
|
|
114
|
+
return false;
|
|
115
|
+
if (!ROUTER_FEEDBACK_OUTCOMES.includes(value.outcome))
|
|
116
|
+
return false;
|
|
117
|
+
if (!ROUTER_FEEDBACK_LEN_BUCKETS.includes(value.lenBucket))
|
|
118
|
+
return false;
|
|
119
|
+
if (typeof value.hadFence !== "boolean")
|
|
120
|
+
return false;
|
|
121
|
+
if (typeof value.hadDiff !== "boolean")
|
|
122
|
+
return false;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/** Default ledger path: `<agentDir>/router-feedback/ledger.jsonl` (owner-only, never repo-local). */
|
|
126
|
+
export function getDefaultRouterFeedbackLedgerPath() {
|
|
127
|
+
return join(getAgentDir(), "router-feedback", "ledger.jsonl");
|
|
128
|
+
}
|
|
129
|
+
/** True when `path` exists and is a symlink. Uses `lstat` so it never follows the link. */
|
|
130
|
+
function isSymlinkPath(path) {
|
|
131
|
+
try {
|
|
132
|
+
return lstatSync(path).isSymbolicLink();
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function errorCode(error) {
|
|
139
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
140
|
+
? String(error.code)
|
|
141
|
+
: undefined;
|
|
142
|
+
}
|
|
143
|
+
function acquireLedgerLockSync(path) {
|
|
144
|
+
const maxAttempts = 10;
|
|
145
|
+
const delayMs = 20;
|
|
146
|
+
let lastError;
|
|
147
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
148
|
+
try {
|
|
149
|
+
return lockfile.lockSync(path, { realpath: false });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (errorCode(error) !== "ELOCKED" || attempt === maxAttempts) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
lastError = error;
|
|
156
|
+
const start = Date.now();
|
|
157
|
+
while (Date.now() - start < delayMs) {
|
|
158
|
+
// Busy-wait briefly; mirrors the auth-storage/settings-manager sync retry pattern.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw lastError ?? new Error("Failed to acquire router-feedback ledger lock");
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* O_NOFOLLOW is undefined on some platforms (notably Windows); `?? 0` keeps
|
|
166
|
+
* the flag combination valid there, and the `lstat` pre-checks above/below
|
|
167
|
+
* remain the fallback symlink guard on platforms without O_NOFOLLOW.
|
|
168
|
+
*/
|
|
169
|
+
const LEDGER_OPEN_FLAGS = fsConstants.O_CREAT | fsConstants.O_APPEND | fsConstants.O_WRONLY | (fsConstants.O_NOFOLLOW ?? 0);
|
|
170
|
+
/**
|
|
171
|
+
* Append one validated feedback record as a single JSONL line.
|
|
172
|
+
*
|
|
173
|
+
* Requires `options.enabled === true` (checked first, before any filesystem
|
|
174
|
+
* access whatsoever — disabled or omitted consent never touches disk).
|
|
175
|
+
* Validates the record against the exact ten-key schema before any I/O.
|
|
176
|
+
* Refuses symlinks and non-regular files where the platform allows detecting
|
|
177
|
+
* them (`O_NOFOLLOW` plus an `lstat` pre-check as defense-in-depth; `O_NOFOLLOW`
|
|
178
|
+
* is unavailable on some platforms, hence "when possible"). Reuses the same
|
|
179
|
+
* `proper-lockfile` dependency as settings-manager.ts/auth-storage.ts for
|
|
180
|
+
* concurrent-session safety. Never throws for expected failure modes — it
|
|
181
|
+
* returns a tagged result so a caller can fail closed silently.
|
|
182
|
+
*/
|
|
183
|
+
export function appendRouterFeedbackRecord(record, options) {
|
|
184
|
+
if (options.enabled !== true) {
|
|
185
|
+
return { appended: false, reason: "disabled" };
|
|
186
|
+
}
|
|
187
|
+
if (!isRouterFeedbackRecord(record)) {
|
|
188
|
+
return { appended: false, reason: "invalid-schema" };
|
|
189
|
+
}
|
|
190
|
+
const ledgerPath = options.ledgerPath ?? getDefaultRouterFeedbackLedgerPath();
|
|
191
|
+
const ledgerDir = dirname(ledgerPath);
|
|
192
|
+
try {
|
|
193
|
+
if (isSymlinkPath(ledgerDir)) {
|
|
194
|
+
return { appended: false, reason: "symlink-refused" };
|
|
195
|
+
}
|
|
196
|
+
if (!existsSync(ledgerDir)) {
|
|
197
|
+
mkdirSync(ledgerDir, { recursive: true, mode: 0o700 });
|
|
198
|
+
}
|
|
199
|
+
if (isSymlinkPath(ledgerPath)) {
|
|
200
|
+
return { appended: false, reason: "symlink-refused" };
|
|
201
|
+
}
|
|
202
|
+
let release;
|
|
203
|
+
try {
|
|
204
|
+
release = acquireLedgerLockSync(ledgerPath);
|
|
205
|
+
if (isSymlinkPath(ledgerPath)) {
|
|
206
|
+
return { appended: false, reason: "symlink-refused" };
|
|
207
|
+
}
|
|
208
|
+
let fd;
|
|
209
|
+
try {
|
|
210
|
+
fd = openSync(ledgerPath, LEDGER_OPEN_FLAGS, 0o600);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (errorCode(error) === "ELOOP") {
|
|
214
|
+
return { appended: false, reason: "symlink-refused" };
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
if (!fstatSync(fd).isFile()) {
|
|
220
|
+
return { appended: false, reason: "not-regular-file" };
|
|
221
|
+
}
|
|
222
|
+
writeSync(fd, `${JSON.stringify(record)}\n`);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
closeSync(fd);
|
|
226
|
+
}
|
|
227
|
+
chmodSync(ledgerPath, 0o600);
|
|
228
|
+
return { appended: true };
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
if (release)
|
|
232
|
+
release();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return { appended: false, reason: "io-error" };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=router-feedback-collector.js.map
|
|
@@ -0,0 +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 — Goal 009 Wave 1 Lane L (privacy-safe learning ledger).\n *\n * Default-off, privacy-safe local ledger for a future reasoning-router learning\n * feature. This module is a pure, standalone utility: nothing here is wired\n * into agent-session.ts, settings-manager.ts, or any other product entry point\n * by this lane. A future, separately-reviewed wiring lane decides where\n * `enabled` comes from (a settings key) and when `appendRouterFeedbackRecord`\n * is actually called.\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 { ReasoningLaneTypeV3, TaskClassV3 } from \"./reasoning-router-v3.ts\";\n\n/** Router versions eligible to tag a feedback record (mirrors agent-session.ts's ThinkingRouterVersion). */\nexport type RouterFeedbackVersion = \"v1\" | \"v2\" | \"v3\" | \"v4\";\n\n/** Lane type at record time, or \"none\" for a non-subagent turn. */\nexport type RouterFeedbackLaneType = ReasoningLaneTypeV3 | \"none\";\n\n/** Router task class; reuses the v3 (== v2) closed task-class union — never a raw prompt. */\nexport type RouterFeedbackTaskClass = TaskClassV3;\n\n/**\n * Reasoning ladder level, excluding \"off\": mirrors REASONING_LADDER_V2 in\n * reasoning-router-v2.ts. The router never *resolves* to \"off\"; turning\n * thinking off entirely is a distinct manual action outside this ledger.\n */\nexport type RouterFeedbackLevel = \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\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[] = [\"v1\", \"v2\", \"v3\", \"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];\nconst ROUTER_FEEDBACK_LEVELS: readonly RouterFeedbackLevel[] = [\"minimal\", \"low\", \"medium\", \"high\", \"xhigh\", \"max\"];\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/** Default ledger path: `<agentDir>/router-feedback/ledger.jsonl` (owner-only, never repo-local). */\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"]}
|