fix(coding-agent): scope SDK extension packages

This commit is contained in:
Sunil Srivatsa
2026-08-01 13:50:53 -04:00
parent df38e5b48e
commit f22c0edbfa
4 changed files with 157 additions and 8 deletions
@@ -15,6 +15,7 @@
* @see ./omp-plugins.ts
* @see ./builtin.ts `loadExtensionModules`
*/
import { AsyncLocalStorage } from "node:async_hooks";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { getAgentDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
@@ -41,8 +42,18 @@ interface InjectedRoot {
level: "user" | "project";
}
export type OmpExtensionRootMode = "merge" | "explicit-only";
interface InvocationRootScope {
/** Raw SDK spellings, resolved against the LoadContext that performs discovery. */
paths: readonly string[];
mode: OmpExtensionRootMode;
}
const invocationRootScope = new AsyncLocalStorage<InvocationRootScope>();
let injectedCliRoots: InjectedRoot[] = [];
let injectedCliRootMode: "merge" | "explicit-only" = "merge";
let injectedCliRootMode: OmpExtensionRootMode = "merge";
export interface InjectOmpExtensionCliRootOptions {
/**
@@ -50,11 +61,25 @@ export interface InjectOmpExtensionCliRootOptions {
* with `--no-extensions` so configured and installed packages cannot
* contribute sibling capabilities through the `omp-plugins` provider.
*/
mode?: "merge" | "explicit-only";
mode?: OmpExtensionRootMode;
/** Replace roots from an earlier invocation instead of extending them. */
replace?: boolean;
}
/**
* Run one SDK invocation with its own extension-package roots. Async resources
* started inside `callback` retain this scope, including discovery deliberately
* deferred until the end of session startup. Raw relative paths are resolved by
* {@link listOmpExtensionRoots} against that invocation's active cwd.
*/
export function withOmpExtensionRootScope<T>(
paths: readonly string[],
mode: OmpExtensionRootMode,
callback: () => T,
): T {
return invocationRootScope.run({ paths: [...paths], mode }, callback);
}
/**
* Register CLI-provided extension package paths (e.g. from `--extension`/`-e`)
* so the sub-discovery providers can find their sibling `skills/`, `hooks/`,
@@ -146,7 +171,8 @@ async function isDirectory(p: string): Promise<boolean> {
* Sources, in order of precedence (later entries with the same absolute path
* are dropped):
*
* 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
* 1. Invocation-scoped SDK roots, when present; otherwise CLI roots injected
* via {@link injectOmpExtensionCliRoots}
* 2. Project `<cwd>/.omp/settings.json#extensions`
* 3. User `~/.omp/agent/settings.json#extensions`
* 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
@@ -159,10 +185,14 @@ async function isDirectory(p: string): Promise<boolean> {
* other sources still surface.
*/
export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]> {
let candidates: InjectedRoot[] = injectedCliRoots.map(root =>
root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
);
if (injectedCliRootMode === "merge") {
const scopedRoots = invocationRootScope.getStore();
const rootMode = scopedRoots?.mode ?? injectedCliRootMode;
let candidates: InjectedRoot[] = scopedRoots
? scopedRoots.paths.map(raw => ({ path: resolveAgainst(raw, ctx), level: "user" }))
: injectedCliRoots.map(root =>
root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
);
if (rootMode === "merge") {
const { project, user } = scopeDirs(ctx);
const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
readSettingsExtensions(path.join(project, "settings.json")),
@@ -177,7 +207,7 @@ export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtens
];
}
// First-seen-wins dedup preserves CLI > project-settings > user-settings > installed precedence.
// First-seen-wins dedup preserves invocation/CLI > project-settings > user-settings > installed precedence.
const seen = new Set<string>();
const unique: InjectedRoot[] = [];
for (const candidate of candidates) {
+8
View File
@@ -66,6 +66,7 @@ import { CursorExecHandlers, type CursorMcpResourceAdapter } from "./cursor";
import { createBridgeEditTool, createBridgeGrepFactory } from "./cursor-bridge-tools";
import "./discovery";
import { initializeWithSettings } from "./discovery";
import { withOmpExtensionRootScope } from "./discovery/omp-extension-roots";
import { disposeAllJuliaKernelSessions, disposeJuliaKernelSessionsByOwner } from "./eval/jl/executor";
import { disposeVmContextsByOwner } from "./eval/js/context-manager";
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
@@ -1217,6 +1218,13 @@ export function createAutoLearnCaptureRunner(
* ```
*/
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
const rootMode = options.disableExtensionDiscovery ? "explicit-only" : "merge";
return await withOmpExtensionRootScope(options.additionalExtensionPaths ?? [], rootMode, () =>
createAgentSessionScoped(options),
);
}
async function createAgentSessionScoped(options: CreateAgentSessionOptions): Promise<CreateAgentSessionResult> {
const cwd = options.cwd ?? getProjectDir();
const agentDir = options.agentDir ?? getAgentDir();
const eventBus = options.eventBus ?? new EventBus();