Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/pi-plugin/src/context-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ import {
resolvePiUsableContextLimit,
resolvePiWindowGeometry,
} from "./pi-context-limit";
import { captureOmpHostContext } from "./omp-subagent-runner";
import { type PiHistorianDeps, runPiHistorian } from "./pi-historian-runner";
import {
formatPiPressureForLog,
Expand Down Expand Up @@ -3645,6 +3646,11 @@ function spawnPiHistorianRun(args: {
}
const renewal = startPiCompartmentLeaseRenewal(db, sessionId, holderId);
try {
// Refresh the OMP runner's host snapshot from the live context right
// before spawning: session_start may not have fired for this session
// (resume paths), and /model switches mid-session would otherwise
// leave a stale registry/model. No-op on non-OMP runners.
captureOmpHostContext(historian.runner, ctx);
await runPiHistorian({
db,
sessionId,
Expand Down
39 changes: 38 additions & 1 deletion packages/pi-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,11 @@ import { registerStatusLine, updateStatusLine } from "./status-line";
import { stripTagPrefixFromAssistantMessage } from "./strip-tag-prefix";
import {
configurePiSubagentExtensions,
isOmpHostProcess,
MAGIC_CONTEXT_PI_SUBAGENT_ENV,
PiSubagentRunner,
} from "./subagent-runner";
import { captureOmpHostContext, OmpSubagentRunner } from "./omp-subagent-runner";
import {
buildMagicContextBlock,
clearPiSystemPromptSession,
Expand Down Expand Up @@ -675,6 +677,29 @@ export function resolveSidekickFromConfig(
};
}


/**
* Shared OMP-native historian runner, constructed at most once per process.
* `undefined` when this is not an OMP host — the caller then wires the
* subprocess runner. Construction is deliberately lazy and sync (the OMP
* surface load happens on first `run()`), so this never touches host modules
* at boot.
*/
let ompRunnerSingleton: OmpSubagentRunner | undefined;
let ompRunnerResolved = false;

function getOmpHistorianRunner(): OmpSubagentRunner | undefined {
if (!ompRunnerResolved) {
ompRunnerResolved = true;
if (isOmpHostProcess()) {
ompRunnerSingleton = new OmpSubagentRunner();
}
}
return ompRunnerSingleton;
}

const resolveOmpRunner = getOmpHistorianRunner();

export function resolveHistorianFromConfig(
config: MagicContextConfig,
): PiHistorianOptions | undefined {
Expand All @@ -699,7 +724,14 @@ export function resolveHistorianFromConfig(
const fallbackModels = resolved.fallbacks;

return {
runner: new PiSubagentRunner(),
// On OMP hosts, run the historian through OMP's native subagent
// machinery (visible in the subagent pane, yield-terminated) instead
// of a headless `--print` process. Falls back to the subprocess
// runner whenever the OMP surface is unavailable (cortexkit/magic-context#416).
runner:
isOmpHostProcess() && resolveOmpRunner !== undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When an OMP host is detected but the native surface cannot load, this branch still selects OmpSubagentRunner because surface loading is deferred until run(). The historian then returns spawn_failed and never invokes PiSubagentRunner, so the documented subprocess fallback does not occur; resolve the surface before selecting the runner or make the OMP runner delegate to the subprocess runner on surface failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/index.ts, line 732:

<comment>When an OMP host is detected but the native surface cannot load, this branch still selects `OmpSubagentRunner` because surface loading is deferred until `run()`. The historian then returns `spawn_failed` and never invokes `PiSubagentRunner`, so the documented subprocess fallback does not occur; resolve the surface before selecting the runner or make the OMP runner delegate to the subprocess runner on surface failure.</comment>

<file context>
@@ -699,7 +724,14 @@ export function resolveHistorianFromConfig(
+		// of a headless `--print` process. Falls back to the subprocess
+		// runner whenever the OMP surface is unavailable (cortexkit/magic-context#416).
+		runner:
+			isOmpHostProcess() && resolveOmpRunner !== undefined
+				? resolveOmpRunner
+				: new PiSubagentRunner(),
</file context>

? resolveOmpRunner
: new PiSubagentRunner(),
Comment thread
greptile-apps[bot] marked this conversation as resolved.
model,
fallbackModels,
historianChunkTokens,
Expand Down Expand Up @@ -1291,6 +1323,11 @@ async function startPiMagicContextRuntime(
const current = resolveCurrentProjectDeps(ctx);
syncCtxMemoryToolEnabled(pi, current.config.memory.enabled);

// Give the OMP-native runner the live host context (parent registry so
// extension-registered runtime providers resolve in structured spawns,
// plus the active model ref and session ids). Best-effort.
captureOmpHostContext(resolveOmpRunner, ctx);

await handlePiCloneSessionStart(event, ctx, {
db,
signalPendingMarker: signalPiDeferredCompactionMarkerDrain,
Expand Down
248 changes: 248 additions & 0 deletions packages/pi-plugin/src/omp-host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/**
* OMP host surface loader.
*
* Locates the running OMP host's structured-subagent spawn API
* (`runStructuredSubagent`) so `OmpSubagentRunner` can spawn MC subagents
* in-process through OMP's native task machinery instead of headless
* `--print` subprocesses (see `omp-subagent-runner.ts` and
* cortexkit/magic-context#416).
*
* Dynamic imports are REQUIRED here, not style drift (same pattern as
* `dreamer/pi-session-api.ts`'s module ladder): the module specifier is
* genuinely runtime-selected — it is derived from the running OMP host's
* install location, and a static import of `@oh-my-pi/pi-coding-agent`
* would execute host-native addons at extension-bundle load time even on
* non-OMP hosts where the package may be absent. Runtime loading also lets
* a failure resolve to `null` (fallback to the subprocess runner) instead
* of failing the plugin boot.
*
* OMP ships its `src/` tree raw and maps package subpaths via the
* `"./*": { import: "./src/*.ts" }` exports wildcard, so
* `@oh-my-pi/pi-coding-agent/task/structured-subagent` resolves to the same
* source module the running CLI executes. The module must be imported
* in-process (inside the OMP extension host) — standalone imports fail on
* native-addon loading outside the host process, which is fine: this loader
* only runs inside OMP where the addon is already loaded.
*
* Resolution order (memoized, never throws):
* 1. Walk up from the running entry (`process.argv[1]`, then
* `process.execPath`) to the `@oh-my-pi/pi-coding-agent` package root
* (same walking rules as `subagent-runner.ts`'s `isOmpHostProcess`), then
* import `task/structured-subagent` directly from its `src/` tree.
* 2. Bare ESM subpath import of
* `@oh-my-pi/pi-coding-agent/task/structured-subagent`.
*
* Any failure resolves to `null`; callers fall back to the subprocess
* runner. This module deliberately throws nothing.
*/

import { existsSync, readFileSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

const OMP_PACKAGE_NAME = "@oh-my-pi/pi-coding-agent";
const STRUCTURED_SUBAGENT_SUBPATH = "task/structured-subagent";

/** The in-process spawn surface MC needs from the OMP host. */
export interface OmpSubagentSurface {
runStructuredSubagent: (request: Record<string, unknown>) => Promise<{
result: {
exitCode: number;
output: string;
stderr: string;
truncated: boolean;
error?: string;
aborted?: boolean;
durationMs: number;
tokens?: number;
usage?: { input?: number; output?: number; cacheWrite?: number; cacheRead?: number };
resolvedModel?: string;
};
}>;
/** OMP's `Settings` class — the synthetic ToolSession requires a live instance. */
Settings: {
init: (options?: { cwd?: string }) => Promise<{
reloadFromDisk: () => Promise<void>;
get: (key: string) => unknown;
}>;
};
/** OMP's credential storage resolver (`discoverAuthStorage` from ./sdk). */
discoverAuthStorage: (agentDir?: string) => Promise<unknown>;
}

export interface OmpSurfaceLoadResult {
surface: OmpSubagentSurface | null;
/** Human-readable failure reason when `surface` is null (for logging). */
reason?: string;
}

let cachedResult: Promise<OmpSurfaceLoadResult> | null = null;

/** Reset the memoized loader (test seam). */
export function clearOmpSurfaceCache(): void {
cachedResult = null;
}

function readPackageName(packageJsonPath: string): string | null {
try {
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
name?: unknown;
};
return typeof manifest.name === "string" ? manifest.name : null;
} catch {
return null;
}
}

/**
* Walk up from `startDir` looking for the OMP package root. Mirrors the
* containment-safe walking in `subagent-runner.ts` (`isOmpHostProcess`).
*/
function findOmpPackageRoot(startDir: string): string | null {
let current = startDir;
// eslint-disable-next-line no-constant-condition
while (true) {
const manifestPath = join(current, "package.json");
if (existsSync(manifestPath) && readPackageName(manifestPath) === OMP_PACKAGE_NAME) {
return current;
}
const parent = dirname(current);
if (parent === current) return null;
current = parent;
}
}

async function importFromFile(filePath: string): Promise<unknown> {
return await import(pathToFileURL(filePath).href);
}

/**
* Resolve the structured-subagent module from the running OMP entry. The
* exports wildcard maps `./task/structured-subagent` → `./src/task/
* structured-subagent.ts`, so importing the source file directly is the same
* module graph edge the host itself uses.
*/
async function loadFromRunningEntry(): Promise<{ surface: OmpSubagentSurface } | { error: string }> {
if (process.env.JITI_VIRTUAL_SCRIPT_PREFIX && process.argv[1]?.startsWith?.(process.env.JITI_VIRTUAL_SCRIPT_PREFIX)) {
return { error: "running entry is a Jiti virtual module" };
}
const entryCandidates = [process.argv[1], process.execPath].filter(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When OMP is started from a source or Nix install identified by PI_PACKAGE_DIR, this loader never tries that package root and falls back from native historian spawning. Include the validated PI_PACKAGE_DIR root in both structured-module and dependency resolution.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/omp-host.ts, line 129:

<comment>When OMP is started from a source or Nix install identified by `PI_PACKAGE_DIR`, this loader never tries that package root and falls back from native historian spawning. Include the validated `PI_PACKAGE_DIR` root in both structured-module and dependency resolution.</comment>

<file context>
@@ -0,0 +1,248 @@
+	if (process.env.JITI_VIRTUAL_SCRIPT_PREFIX && process.argv[1]?.startsWith?.(process.env.JITI_VIRTUAL_SCRIPT_PREFIX)) {
+		return { error: "running entry is a Jiti virtual module" };
+	}
+	const entryCandidates = [process.argv[1], process.execPath].filter(
+		(candidate): candidate is string => typeof candidate === "string" && candidate.length > 0,
+	);
</file context>

(candidate): candidate is string => typeof candidate === "string" && candidate.length > 0,
);
for (const candidate of entryCandidates) {
let startDir: string;
try {
const stat = statSync(candidate);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When OMP is launched through a symlinked omp bin, the loader walks the shim directory instead of the package root and can miss the native surface. Resolve candidate with realpathSync before taking its directory, matching the existing Pi resolver.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/omp-host.ts, line 135:

<comment>When OMP is launched through a symlinked `omp` bin, the loader walks the shim directory instead of the package root and can miss the native surface. Resolve `candidate` with `realpathSync` before taking its directory, matching the existing Pi resolver.</comment>

<file context>
@@ -0,0 +1,248 @@
+	for (const candidate of entryCandidates) {
+		let startDir: string;
+		try {
+			const stat = statSync(candidate);
+			startDir = stat.isFile() ? dirname(resolve(candidate)) : resolve(candidate);
+		} catch {
</file context>

startDir = stat.isFile() ? dirname(resolve(candidate)) : resolve(candidate);
} catch {
continue;
}
const packageRoot = findOmpPackageRoot(startDir);
if (!packageRoot) continue;
const moduleEntry = join(packageRoot, "src", "task", "structured-subagent.ts");
if (!existsSync(moduleEntry)) {
return { error: `${OMP_PACKAGE_NAME} found at ${packageRoot} but src/${STRUCTURED_SUBAGENT_SUBPATH}.ts is missing` };
}
try {
const mod = (await importFromFile(moduleEntry)) as Record<string, unknown>;
return await extractSurface(mod);
} catch (error) {
return {
error: `import of ${moduleEntry} failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
return { error: `no ${OMP_PACKAGE_NAME} package root found from running entry` };
}

async function extractSurface(mod: Record<string, unknown>): Promise<{ surface: OmpSubagentSurface } | { error: string }> {
const run = mod.runStructuredSubagent;
if (typeof run !== "function") {
return { error: "module loaded but runStructuredSubagent is not exported" };
}
// Settings lives at config/settings (re-exported from the package index);
// discoverAuthStorage at ./sdk. The structured-subagent module itself does
// not re-export them, so load them from the same package root the spawn
// surface came from — they must be the same copies the host runs.
let Settings: OmpSubagentSurface["Settings"] | undefined;
let discoverAuthStorage: OmpSubagentSurface["discoverAuthStorage"] | undefined;
const searchRoots: string[] = [];
if (process.argv[1]) {
const fromEntry = findOmpPackageRoot(dirname(resolve(process.argv[1])));
if (fromEntry) searchRoots.push(fromEntry);
}
const fromModule = findOmpPackageRoot(dirname(fileURLToPath(import.meta.url)));
if (fromModule && !searchRoots.includes(fromModule)) searchRoots.push(fromModule);
for (const root of searchRoots) {
try {
const settingsMod = (await importFromFile(join(root, "src", "config", "settings.ts"))) as Record<string, unknown>;
const sdkMod = (await importFromFile(join(root, "src", "sdk.ts"))) as Record<string, unknown>;
const settingsClass = settingsMod.Settings as OmpSubagentSurface["Settings"] | undefined;
const authResolver = sdkMod.discoverAuthStorage as OmpSubagentSurface["discoverAuthStorage"] | undefined;
if (typeof settingsClass?.init === "function" && typeof authResolver === "function") {
Settings = settingsClass;
discoverAuthStorage = authResolver;
break;
}
} catch {
// Try the next root.
}
}
if (!Settings || !discoverAuthStorage) {
return { error: "module loaded but Settings/discoverAuthStorage could not be resolved from the OMP package" };
}
return { surface: { runStructuredSubagent: run as OmpSubagentSurface["runStructuredSubagent"], Settings, discoverAuthStorage } };
}

/**
* Load the OMP structured-subagent surface. Memoized; never throws. Resolves
* to `{ surface: null, reason }` on any failure so callers fall back to the
* subprocess runner.
*/
export function loadOmpSubagentSurface(): Promise<OmpSurfaceLoadResult> {
cachedResult ??= (async (): Promise<OmpSurfaceLoadResult> => {
// 1. Running-entry walk (same OMP copy that owns the live session).
const fromEntry = await loadFromRunningEntry();
if ("surface" in fromEntry) return fromEntry;

// 2. Walk up from THIS module's own install location. Under a real
// OMP install the plugin lives at
// `~/.omp/plugins/node_modules/@cortexkit/pi-magic-context/`, whose
// ancestor `node_modules` also hosts `@oh-my-pi/pi-coding-agent` —
// importing the subpath from there loads the exact copy the host runs.
const here = fileURLToPath(import.meta.url);
const packageRoot = findOmpPackageRoot(dirname(here));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The documented colocated-install fallback never checks the sibling @oh-my-pi/pi-coding-agent package, so this branch always misses when bare ESM resolution is unavailable. Resolve the OMP package from the plugin's sibling node_modules and pass that root to extractSurface.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/omp-host.ts, line 214:

<comment>The documented colocated-install fallback never checks the sibling `@oh-my-pi/pi-coding-agent` package, so this branch always misses when bare ESM resolution is unavailable. Resolve the OMP package from the plugin's sibling `node_modules` and pass that root to `extractSurface`.</comment>

<file context>
@@ -0,0 +1,248 @@
+		// ancestor `node_modules` also hosts `@oh-my-pi/pi-coding-agent` —
+		// importing the subpath from there loads the exact copy the host runs.
+		const here = fileURLToPath(import.meta.url);
+		const packageRoot = findOmpPackageRoot(dirname(here));
+		if (packageRoot) {
+			const moduleEntry = join(packageRoot, "src", "task", "structured-subagent.ts");
</file context>

if (packageRoot) {
const moduleEntry = join(packageRoot, "src", "task", "structured-subagent.ts");
if (existsSync(moduleEntry)) {
try {
const mod = (await importFromFile(moduleEntry)) as Record<string, unknown>;
const extracted = await extractSurface(mod);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When OMP uses a Jiti virtual entry, the bare structured-subagent import cannot resolve Settings or discoverAuthStorage because extractSurface never derives the imported package root. Resolve those companion modules from the bare package's install location.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/pi-plugin/src/omp-host.ts, line 220:

<comment>When OMP uses a Jiti virtual entry, the bare structured-subagent import cannot resolve `Settings` or `discoverAuthStorage` because `extractSurface` never derives the imported package root. Resolve those companion modules from the bare package's install location.</comment>

<file context>
@@ -0,0 +1,248 @@
+			if (existsSync(moduleEntry)) {
+				try {
+					const mod = (await importFromFile(moduleEntry)) as Record<string, unknown>;
+					const extracted = await extractSurface(mod);
+					if ("surface" in extracted) return extracted;
+					return { surface: null, reason: extracted.error };
</file context>

if ("surface" in extracted) return extracted;
return { surface: null, reason: extracted.error };
} catch (error) {
return {
surface: null,
reason: `${fromEntry.error}; sibling-package import failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
}

// 3. Bare ESM subpath (covers non-standard launch shapes).
try {
const mod = (await import(
/* @vite-ignore */ `${OMP_PACKAGE_NAME}/${STRUCTURED_SUBAGENT_SUBPATH}`
)) as Record<string, unknown>;
const extracted = await extractSurface(mod);
if ("surface" in extracted) return extracted;
return { surface: null, reason: extracted.error };
} catch (error) {
return {
surface: null,
reason: `${fromEntry.error}; bare subpath import failed: ${error instanceof Error ? error.message : String(error)}`,
};
}
})();
return cachedResult;
}
Loading