Merge main into model-registry

This commit is contained in:
Mario Zechner
2026-06-22 14:00:18 +02:00
220 changed files with 10488 additions and 4354 deletions
@@ -127,6 +127,30 @@ function getAliases(): Record<string, string> {
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
let extensionCacheCwd: string | undefined;
let extensionCacheGeneration = 0;
const extensionCache = new Map<string, ExtensionFactory>();
interface ExtensionCacheToken {
cwd: string;
generation: number;
}
export function clearExtensionCache(): void {
extensionCache.clear();
extensionCacheCwd = undefined;
extensionCacheGeneration++;
}
function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {
const resolvedCwd = resolvePath(cwd);
if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {
clearExtensionCache();
}
extensionCacheCwd = resolvedCwd;
return { cwd: resolvedCwd, generation: extensionCacheGeneration };
}
/**
* Create a runtime with throwing stubs for action methods.
* Runner.bindCore() replaces these with real implementations.
@@ -338,7 +362,22 @@ function createExtensionAPI(
return api;
}
async function loadExtensionModule(extensionPath: string) {
function isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {
return (
cacheToken !== undefined &&
extensionCacheCwd === cacheToken.cwd &&
extensionCacheGeneration === cacheToken.generation
);
}
async function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) {
if (isCurrentCacheToken(cacheToken)) {
const cachedFactory = extensionCache.get(extensionPath);
if (cachedFactory) {
return cachedFactory;
}
}
const jiti = createJiti(import.meta.url, {
moduleCache: false,
// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)
@@ -349,7 +388,13 @@ async function loadExtensionModule(extensionPath: string) {
const module = await jiti.import(extensionPath, { default: true });
const factory = module as ExtensionFactory;
return typeof factory !== "function" ? undefined : factory;
if (typeof factory !== "function") {
return undefined;
}
if (isCurrentCacheToken(cacheToken)) {
extensionCache.set(extensionPath, factory);
}
return factory;
}
/**
@@ -380,11 +425,12 @@ async function loadExtension(
cwd: string,
eventBus: EventBus,
runtime: ExtensionRuntime,
cacheToken?: ExtensionCacheToken,
): Promise<{ extension: Extension | null; error: string | null }> {
const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });
try {
const factory = await loadExtensionModule(resolvedPath);
const factory = await loadExtensionModule(resolvedPath, cacheToken);
if (!factory) {
return { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };
}
@@ -420,20 +466,28 @@ export async function loadExtensionFromFactory(
/**
* Load extensions from paths.
*/
export async function loadExtensions(
async function loadExtensionsInternal(
paths: string[],
cwd: string,
eventBus?: EventBus,
runtime?: ExtensionRuntime,
useCache = false,
): Promise<LoadExtensionsResult> {
const extensions: Extension[] = [];
const errors: Array<{ path: string; error: string }> = [];
const resolvedCwd = resolvePath(cwd);
const cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined;
const resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd);
const resolvedEventBus = eventBus ?? createEventBus();
const resolvedRuntime = runtime ?? createExtensionRuntime();
for (const extPath of paths) {
const { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, resolvedRuntime);
const { extension, error } = await loadExtension(
extPath,
resolvedCwd,
resolvedEventBus,
resolvedRuntime,
cacheToken,
);
if (error) {
errors.push({ path: extPath, error });
@@ -452,6 +506,24 @@ export async function loadExtensions(
};
}
export async function loadExtensions(
paths: string[],
cwd: string,
eventBus?: EventBus,
runtime?: ExtensionRuntime,
): Promise<LoadExtensionsResult> {
return loadExtensionsInternal(paths, cwd, eventBus, runtime);
}
export async function loadExtensionsCached(
paths: string[],
cwd: string,
eventBus?: EventBus,
runtime?: ExtensionRuntime,
): Promise<LoadExtensionsResult> {
return loadExtensionsInternal(paths, cwd, eventBus, runtime, true);
}
interface PiManifest {
extensions?: string[];
themes?: string[];
@@ -571,6 +571,10 @@ export interface SessionBeforeCompactEvent {
preparation: CompactionPreparation;
branchEntries: SessionEntry[];
customInstructions?: string;
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
reason: "manual" | "threshold" | "overflow";
/** True when the aborted turn is retried after this compaction (overflow recovery) */
willRetry: boolean;
signal: AbortSignal;
}
@@ -579,6 +583,10 @@ export interface SessionCompactEvent {
type: "session_compact";
compactionEntry: CompactionEntry;
fromExtension: boolean;
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
reason: "manual" | "threshold" | "overflow";
/** True when the aborted turn is retried after this compaction (overflow recovery) */
willRetry: boolean;
}
/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */