perf(context): load the registry once per resolution and routing index build

compileContextProfile now validates its options, loads the registry and
calls compileContextPlan(registry, options), which compiles against a
registry the caller loaded in the same call. resolveTaskContext and
routingEntries load the registry once and compile from it, so the sources
are hashed once per call instead of twice.

On this repository (full@1, Windows 11, median of 5):
resolveTaskContext 1332 ms -> 645 ms, routingEntries 1321 ms -> 721 ms.

The registry never comes from outside the call: nothing caches it or
accepts one from a caller, so a stale or forged registry cannot be passed
in. The two "Registry changed during ..." checks compared the two loads and
are gone with the second load; each call now works from one snapshot, and
skill bytes are still verified against it when read. compileContextProfile
keeps its signature, output and error order.

Split out of #2945, whose prompt hook is superseded by #3453.
This commit is contained in:
montjeffrey
2026-10-11 10:33:21 -04:00
parent 626ea56291
commit 8e99eaa4e3
2 changed files with 24 additions and 10 deletions
+18 -5
View File
@@ -86,13 +86,26 @@ function estimateMetadata(entries, target, profile) {
};
}
function compileContextProfile({
function validatePlanOptions({ selectionMode = 'manual', target = 'codex' }) {
validateTarget(target);
if (!MODES.includes(selectionMode)) throw new Error(`Unknown selection mode: ${selectionMode}`);
}
function compileContextProfile(options = {}) {
validatePlanOptions(options);
return compileContextPlan(loadContextRegistry({ repoRoot: options.repoRoot || DEFAULT_REPO_ROOT }), options);
}
/**
* Compile a plan against a registry the caller loaded in the same call, so a
* caller that also needs the registry hashes its sources once. Callers pass
* the result of `loadContextRegistry` directly; nothing keeps one across calls.
*/
function compileContextPlan(registry, {
repoRoot = DEFAULT_REPO_ROOT, profileId = 'lean@1', selectionMode = 'manual',
target = 'codex', include = [], exclude = [],
} = {}) {
validateTarget(target);
if (!MODES.includes(selectionMode)) throw new Error(`Unknown selection mode: ${selectionMode}`);
const registry = loadContextRegistry({ repoRoot });
validatePlanOptions({ selectionMode, target });
const profile = loadContextProfile(profileId, { repoRoot });
if (profile.registryId !== registry.id) throw new Error('Profile registry ID mismatch');
const selected = resolveSelection(registry, profile, include, exclude);
@@ -130,4 +143,4 @@ function compileContextProfile({
return plan;
}
module.exports = { compileContextProfile, explainContextEntry, loadContextProfile };
module.exports = { compileContextProfile, compileContextPlan, explainContextEntry, loadContextProfile };
+6 -5
View File
@@ -2,7 +2,7 @@
const yaml = require('js-yaml');
const { loadContextRegistry, loadSkillTriggers } = require('./context-pack-registry');
const { compileContextProfile } = require('./context-profiles');
const { compileContextPlan } = require('./context-profiles');
const { buildRetrievalIndex, searchRetrieval } = require('./context-retrieval');
const { DEFAULT_REPO_ROOT, createSourceReader, digestObject } = require('./context-profile-support');
@@ -310,10 +310,10 @@ function admissibility(byId, excluded, reader) {
* without explicit selection. Metadata only; bodies are never returned. */
function routingEntries({ repoRoot = DEFAULT_REPO_ROOT, profileId = 'lean@1', target = 'codex',
selectionMode = 'auto', include = [], exclude = [] } = {}) {
const plan = compileContextProfile({ repoRoot, profileId, target, selectionMode, include, exclude });
// One registry load serves the plan and the routing entries.
const registry = loadContextRegistry({ repoRoot });
const plan = compileContextPlan(registry, { repoRoot, profileId, target, selectionMode, include, exclude });
const { triggers } = loadSkillTriggers({ repoRoot });
if (registry.registryDigest !== plan.registryDigest) throw new Error('Registry changed during routing index build');
const byId = new Map(registry.entries.map(entry => [entry.id, entry]));
const excluded = new Set(plan.excludedIds);
const admissible = admissibility(byId, excluded, createSourceReader(repoRoot));
@@ -335,10 +335,11 @@ function resolveTaskContext({ repoRoot = DEFAULT_REPO_ROOT, task, profileId = 'l
selectionMode = 'auto', include = [], exclude = [], load = false, previous = null, expectedDigest = null } = {}) {
validateTask(task);
validatePrevious(previous);
const plan = compileContextProfile({ repoRoot, profileId, target, selectionMode, include, exclude });
// One registry load serves the plan and the selection; skill bytes are
// still verified against it when they are read.
const registry = loadContextRegistry({ repoRoot });
const plan = compileContextPlan(registry, { repoRoot, profileId, target, selectionMode, include, exclude });
const { triggers } = loadSkillTriggers({ repoRoot });
if (registry.registryDigest !== plan.registryDigest) throw new Error('Registry changed during task selection');
const reader = createSourceReader(repoRoot);
const byId = new Map(registry.entries.map(entry => [entry.id, entry]));
const excluded = new Set(plan.excludedIds);