Merge remote-tracking branch 'origin/farm/ceed3a6a/compiled-appserver-extension-exit'

This commit is contained in:
can1357
2026-07-15 15:31:51 +02:00
5 changed files with 96 additions and 36 deletions
+1
View File
@@ -34,6 +34,7 @@
### Fixed
- Fixed compiled appserver startup deadlocking before socket creation when any user extension was present ([#5568](https://github.com/can1357/oh-my-pi/issues/5568)).
- Fixed Bash internal URLs remaining unresolved when used as unquoted arguments inside command substitutions ([#5535](https://github.com/can1357/oh-my-pi/issues/5535)).
- Fixed `--tools` silently dropping hidden tool names (`xdev`, `yield`, ...); hidden built-ins are now addressable per the `hidden` tool contract.
- Fixed the built-in `fd` printing `fd: Broken pipe (os error 32)` when a downstream pipeline reader exited early (e.g. `fd … | head`); it now exits silently with 141 (128+SIGPIPE), matching real fd.
@@ -166,19 +166,22 @@ export async function collectBundledPiEntries(): Promise<BundledPiEntry[]> {
return entries;
}
function renderVirtualModule(entries: readonly BundledPiEntry[]): string {
const imports = entries.map(entry => `import * as ${entry.binding} from ${JSON.stringify(entry.importSpecifier)};`);
/** Render the lazy loader registry; exported so tests can execute the generated module. */
export function __renderLegacyPiVirtualModule(entries: readonly BundledPiEntry[]): string {
const loaders = entries.map(
entry => `const ${entry.binding} = () => import(${JSON.stringify(entry.importSpecifier)});`,
);
const modules = entries.map(entry => `\t${JSON.stringify(entry.key)}: ${entry.binding},`);
return [...imports, "", "export const BUNDLED_PI_MODULES = {", ...modules, "};", ""].join("\n");
return [...loaders, "", "export const BUNDLED_PI_MODULE_LOADERS = {", ...modules, "};", ""].join("\n");
}
/**
* Build plugin that materializes the legacy Pi module graph entirely in
* memory. Bun still needs static import edges at compile time, but no generated
* source or key-list file is written to the repository.
* Build plugin that materializes lazy legacy Pi module loaders entirely in
* memory. Literal dynamic imports retain every compile-time edge without
* evaluating unrelated host modules during extension bootstrap.
*/
export async function createLegacyPiVirtualModulePlugin(): Promise<Bun.BunPlugin> {
const source = renderVirtualModule(await collectBundledPiEntries());
const source = __renderLegacyPiVirtualModule(await collectBundledPiEntries());
return {
name: "omp:legacy-pi-modules",
setup(build) {
@@ -16,43 +16,52 @@ const IS_COMPILED_BINARY = isCompiledBinary();
// embedded entries. Bun.plugin `onResolve` also no longer fires for transitive
// imports inside runtime-loaded extensions.
//
// Compiled builds therefore keep live JS-heap references to the host packages
// and serve requested surfaces through `omp-legacy-pi-bundled:<key>` synthetic
// modules. `scripts/legacy-pi-virtual-module.ts` derives the static import edges
// Compiled builds retain lazy loaders for host packages and serve requested
// surfaces through `omp-legacy-pi-bundled:<key>` synthetic modules.
// `scripts/legacy-pi-virtual-module.ts` derives literal dynamic-import edges
// from current package exports inside a Bun build plugin: no generated source
// or duplicate key list exists on disk. Runtime extension loading stays lazy —
// the virtual module is evaluated only when an extension requests a host
// package — but the compiler still sees every possible edge at build time.
// or duplicate key list exists on disk. Deferring each host module evaluation
// avoids cycles with an extension-loading command that is itself in the
// retained package graph.
const BUNDLED_VIRTUAL_SCHEME = "omp-legacy-pi-bundled:";
const BUNDLED_VIRTUAL_NAMESPACE = "omp-legacy-pi-bundled";
const BUNDLED_MODULES_GLOBAL = "__ompLegacyPiBundledModules";
const TYPEBOX_BUNDLED_MODULE_KEY = "typebox";
type BundledModules = Readonly<Record<string, Readonly<Record<string, unknown>>>>;
type BundledModule = Readonly<Record<string, unknown>>;
type BundledModules = Readonly<Record<string, BundledModule>>;
type BundledModuleLoaders = Readonly<Record<string, () => Promise<BundledModule>>>;
let bundledModulesPromise: Promise<BundledModules> | null = null;
const loadedBundledModules: Record<string, BundledModule> = {};
let bundledModuleLoadersPromise: Promise<BundledModuleLoaders> | null = null;
/**
* Lazy-load the build-supplied host modules and stash them on `globalThis` for
* the synthetic module source emitted by `synthesizeBundledModuleSource`.
* Load the build-supplied module registry without evaluating its host modules.
*
* `globalThis` is the bridge: each `omp-legacy-pi-bundled:<key>` source string
* becomes a separate ES module and cannot close over this file's lexical scope.
* The dynamic import is intentional conditional build code. Dev/test runs
* never execute it; binary builds resolve the literal through the in-memory
* plugin in `scripts/legacy-pi-virtual-module.ts`.
* `globalThis` bridges the synthetic ES modules, which cannot close over this
* file's lexical scope. Dev/test runs never execute the conditional import;
* binary builds resolve it through the in-memory build plugin.
*/
function ensureBundledModulesLoaded(): Promise<BundledModules> {
function ensureBundledModuleLoadersLoaded(): Promise<BundledModuleLoaders> {
if (!IS_COMPILED_BINARY) {
return Promise.reject(new Error("omp:legacy-pi-shim: bundled modules are only available in compiled mode"));
}
if (!bundledModulesPromise) {
bundledModulesPromise = import("omp-legacy-pi-modules").then(module => {
Reflect.set(globalThis, BUNDLED_MODULES_GLOBAL, module.BUNDLED_PI_MODULES);
return module.BUNDLED_PI_MODULES;
if (!bundledModuleLoadersPromise) {
bundledModuleLoadersPromise = import("omp-legacy-pi-modules").then(module => {
Reflect.set(globalThis, BUNDLED_MODULES_GLOBAL, loadedBundledModules);
return module.BUNDLED_PI_MODULE_LOADERS;
});
}
return bundledModulesPromise;
return bundledModuleLoadersPromise;
}
async function loadBundledModule(moduleKey: string): Promise<void> {
const loaders = await ensureBundledModuleLoadersLoaded();
const loader = loaders[moduleKey];
if (!loader) {
throw new Error(`omp:legacy-pi-shim: no bundled module registered for ${moduleKey}`);
}
loadedBundledModules[moduleKey] = await loader();
}
function bundledModuleVirtualSpecifier(moduleKey: string): string {
@@ -95,8 +104,8 @@ function synthesizeBundledModuleSourceFromModules(moduleKey: string, modules: Bu
* `omp-legacy-pi-bundled:<key>` import.
*/
async function synthesizeBundledModuleSource(moduleKey: string): Promise<string> {
const modules = await ensureBundledModulesLoaded();
return synthesizeBundledModuleSourceFromModules(moduleKey, modules);
await loadBundledModule(moduleKey);
return synthesizeBundledModuleSourceFromModules(moduleKey, loadedBundledModules);
}
/** Test seam for the virtual module's named/default export forwarding. */
@@ -369,14 +378,14 @@ export function __buildLegacyPiPackageRootOverrides(
let legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
let legacyPiOverridesReadyPromise: Promise<void> | null = null;
/** Complete compiled-mode overrides once from the lazily evaluated host modules. */
/** Complete compiled-mode overrides from the lazy host-module registry. */
function ensureLegacyPiOverridesReady(): Promise<void> {
if (!IS_COMPILED_BINARY) {
return Promise.resolve();
}
if (!legacyPiOverridesReadyPromise) {
legacyPiOverridesReadyPromise = ensureBundledModulesLoaded().then(modules => {
legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(true, Object.keys(modules));
legacyPiOverridesReadyPromise = ensureBundledModuleLoadersLoaded().then(loaders => {
legacyPiPackageRootOverrides = __buildLegacyPiPackageRootOverrides(true, Object.keys(loaders));
});
}
return legacyPiOverridesReadyPromise;
@@ -1,4 +1,4 @@
declare module "omp-legacy-pi-modules" {
/** Host package namespaces retained by the compiled binary for legacy extensions. */
export const BUNDLED_PI_MODULES: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
/** Lazy host package namespace loaders retained for compiled legacy extensions. */
export const BUNDLED_PI_MODULE_LOADERS: Readonly<Record<string, () => Promise<Readonly<Record<string, unknown>>>>>;
}
@@ -1,6 +1,9 @@
import { describe, expect, it } from "bun:test";
import * as path from "node:path";
import * as url from "node:url";
import { __buildLegacyPiPackageRootOverrides } from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
import { collectBundledPiEntries } from "../../scripts/legacy-pi-virtual-module";
import { TempDir } from "@oh-my-pi/pi-utils";
import { __renderLegacyPiVirtualModule, collectBundledPiEntries } from "../../scripts/legacy-pi-virtual-module";
const bundledModuleKeys = new Set((await collectBundledPiEntries()).map(entry => entry.key));
@@ -14,6 +17,50 @@ const bundledModuleKeys = new Set((await collectBundledPiEntries()).map(entry =>
// the same `omp-legacy-pi-bundled:` virtual namespace as package roots without
// a generated registry or duplicate key list.
describe("legacy pi compat compiled-mode subpath overrides (issue #3442)", () => {
it("does not evaluate unrelated host modules while loading the registry", async () => {
using tempDir = TempDir.createSync("@omp-legacy-pi-loaders-");
const alphaPath = path.join(tempDir.path(), "alpha.ts");
const betaPath = path.join(tempDir.path(), "beta.ts");
const registryPath = path.join(tempDir.path(), "registry.ts");
await Bun.write(alphaPath, 'Reflect.set(globalThis, "__alphaLoads", 1);\nexport const value = "alpha";\n');
await Bun.write(betaPath, 'Reflect.set(globalThis, "__betaLoads", 1);\nexport const value = "beta";\n');
const registry = __renderLegacyPiVirtualModule([
{ key: "alpha", binding: "bundledAlpha", importSpecifier: url.pathToFileURL(alphaPath).href },
{ key: "beta", binding: "bundledBeta", importSpecifier: url.pathToFileURL(betaPath).href },
]);
await Bun.write(
registryPath,
`${registry}
const beforeAlpha = Reflect.get(globalThis, "__alphaLoads") ?? 0;
const beforeBeta = Reflect.get(globalThis, "__betaLoads") ?? 0;
await BUNDLED_PI_MODULE_LOADERS.alpha();
const afterAlpha = Reflect.get(globalThis, "__alphaLoads") ?? 0;
const betaAfterAlpha = Reflect.get(globalThis, "__betaLoads") ?? 0;
await BUNDLED_PI_MODULE_LOADERS.beta();
process.stdout.write(JSON.stringify([
beforeAlpha,
beforeBeta,
afterAlpha,
betaAfterAlpha,
Reflect.get(globalThis, "__alphaLoads") ?? 0,
Reflect.get(globalThis, "__betaLoads") ?? 0,
]));
`,
);
const proc = Bun.spawn([process.execPath, registryPath], {
stdout: "pipe",
stderr: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual([0, 0, 1, 0, 1, 1]);
});
it("serves @oh-my-pi/pi-ai/oauth through the bundled virtual namespace in compiled mode", () => {
const overrides = __buildLegacyPiPackageRootOverrides(true, bundledModuleKeys);
expect(overrides["@oh-my-pi/pi-ai/oauth"]).toBe("omp-legacy-pi-bundled:@oh-my-pi/pi-ai/oauth");