this error throws running /new here and saying yes to analyzing for memory

file:///home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/runner.js:297
            throw new Error(this.staleMessage);─────────────────────────────────────────────────────────────────────────────────────────────────────────────
            ^
$0.000 (sub) 0.0%/272k (auto)                                                                                (openai-codex) gpt-5.4 • low
Error: This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().
    at ExtensionRunner.assertActive (file:///home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/runner.js:297:19)
    at get sessionManager (file:///home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/runner.js:394:24)
    at WebSocket.<anonymous> (/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts:41:23)
    at WebSocket.emit (node:events:519:28)
    at WebSocket.setSocket (/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/node_modules/ws/lib/websocket.js:252:10)
    at ClientRequest.<anonymous> (/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/node_modules/ws/lib/websocket.js:1017:15)
    at ClientRequest.emit (node:events:519:28)
    at Socket.socketOnData (node:_http_client:654:11)
    at Socket.emit (node:events:519:28)
    at addChunk (node:internal/streams/readable:561:12)


**Investigating npm issues**

I need to inspect the extension, possibly in pi-tau-mux. It seems like I should diagnose and potentially patch global npm packages due to an error reported by the user. I might have to analyze the repo, but I’m currently in /home/sebas, which might not be where the repo is. Reading from skill memory could be helpful, though it might not be necessary unless there's a database involved. I should inspect the installed package files and use bash to read them.

bash

cd /home/sebas && npm root -g && node -v && npm -v


/**
 * Tau Client Extension
 *
 * Lightweight client that connects to pi-tau-mux-server.
 * - Registers this pi instance with the mux server
 * - Forwards events (messages, model changes, etc.) to the server
 * - Unregisters on shutdown
 * - Optionally prompts to start the mux server if not running
 */

import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { WebSocket } from "ws";
import * as fs from "node:fs";
import * as path from "node:path";
import { exec } from "node:child_process";

// Configuration
const MUX_PORT = parseInt(process.env.TAU_PORT || "3001");
const MUX_HOST = process.env.TAU_HOST || "localhost";
const MUX_URL = `ws://${MUX_HOST}:${MUX_PORT}/pi`;
const AUTO_CONNECT = process.env.TAU_AUTO_CONNECT !== "0";

let ws: WebSocket | null = null;
let latestCtx: ExtensionContext | null = null;
let reconnectTimer: NodeJS.Timeout | null = null;
let sessionId: string | null = null;
let serverUrls: { local?: string; tailscale?: string; magicDns?: string } | null = null;

// ─────────────────────────────────────────────────────────────
// Connection management
// ─────────────────────────────────────────────────────────────

function connect(pi: ExtensionAPI, ctx: ExtensionContext, promptIfDown = true) {
  if (ws) return; // Already connected

  try {
    ws = new WebSocket(MUX_URL);

    ws.on("open", () => {
      // Register this instance
      sessionId = ctx.sessionManager.getSessionFile()?.split("/").pop()?.replace(".jsonl", "") || Date.now().toString();
      send({ type: "register", sessionId, cwd: ctx.cwd || process.cwd() });

      // Send initial state
      sendState(ctx, pi);

      // Notify user of successful connection
      // Use server's Tailscale URL if available, otherwise fall back to connection URL
      const displayUrl = serverUrls?.tailscale || serverUrls?.magicDns || `http://${MUX_HOST}:${MUX_PORT}`;
      const location = serverUrls?.tailscale ? 'Tailscale' : serverUrls?.magicDns ? 'MagicDNS' : (MUX_HOST === 'localhost' ? 'local' : MUX_HOST);
      ctx.ui.notify(`Connected to Tau mux server at ${location}  •  View UI at ${displayUrl}`, "info");
    });

    ws.on("message", (data) => {
      try {
        const msg = JSON.parse(data.toString());
        handleMessage(msg, ctx, pi);
      } catch {}
    });

    ws.on("close", () => {
      ws = null;

      // Attempt reconnect after delay
      if (!reconnectTimer) {
        reconnectTimer = setTimeout(() => {
          reconnectTimer = null;
          if (latestCtx) connect(pi, latestCtx, false);
        }, 5000);
      }
    });

    ws.on("error", (e) => {
      // Prompt to start server if not running
      if (promptIfDown && e.message.includes("ECONNREFUSED")) {
        promptStartMux(pi, ctx);
      }

      ws = null;
    });
  } catch {
    // Connection failed silently
  }
}

function disconnect() {
  if (ws) {
    if (sessionId) {
      send({ type: "unregister", sessionId });
    }
    ws.close();
    ws = null;
  }

  if (reconnectTimer) {
    clearTimeout(reconnectTimer);
    reconnectTimer = null;
  }
}

function send(msg: any) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(msg));
  }
}

function sendState(ctx: ExtensionContext, pi: ExtensionAPI) {
  if (!sessionId) return;

  const entries = ctx.sessionManager.getEntries();
  const model = ctx.model;
  const thinkingLevel = pi.getThinkingLevel();
  const sessionName = pi.getSessionName();
  const sessionFile = ctx.sessionManager.getSessionFile();

  send({
    type: "state",
    sessionId,
    state: {
      entries,
      model,
      thinkingLevel,
      sessionName,
      sessionFile,
      isStreaming: !ctx.isIdle(),
    }
  });
}

function promptStartMux(pi: ExtensionAPI, ctx: ExtensionContext) {
  ctx.ui.confirm(
    "Tau mux server not running. Start it now?",
    "Yes, start server",
    "No, skip"
  ).then((response) => {
    if (response === "Yes, start server") {
      exec("npx pi-tau-mux-server", (err) => {
        if (err) {
          ctx.ui.notify("Failed to start tau mux server: " + err.message, "error");
        }
      });

      // Wait a moment for server to start, then connect
      setTimeout(() => connect(pi, ctx, false), 2000);
    }
  });
}

function handleMessage(msg: any, ctx: ExtensionContext, pi: ExtensionAPI) {
  // Handle commands from mux server (e.g., from browser)
  switch (msg.type) {
    case "registered":
      // Store server URLs from registration response
      serverUrls = msg.serverUrls || null;
      break;

    case "prompt":
      pi.sendUserMessage(msg.message);
      break;

    case "set_model":
      pi.setModel(msg.model);
      break;

    case "set_thinking_level":
      pi.setThinkingLevel(msg.level);
      break;

    // Add more command handlers as needed
  }
}

// ─────────────────────────────────────────────────────────────
// Event forwarding
// ─────────────────────────────────────────────────────────────

const EVENT_TYPES = [
  "agent_start", "agent_end",
  "turn_start", "turn_end",
  "message_start", "message_update", "message_end",
  "tool_execution_start", "tool_execution_update", "tool_execution_end",
  "auto_compaction_start", "auto_compaction_end",
  "auto_retry_start", "auto_retry_end",
  "model_select",
] as const;

// ─────────────────────────────────────────────────────────────
// Extension entry point
// ─────────────────────────────────────────────────────────────

export default function (pi: ExtensionAPI) {
  // Register commands
  pi.registerCommand("tauconnect", {
    description: "Connect to tau mux server",
    handler: async (_args, ctx) => {
      if (ws) {
        ctx.ui.notify("Already connected to tau mux", "warning");
        return;
      }
      connect(pi, ctx, true);
    },
  });

  pi.registerCommand("taudisconnect", {
    description: "Disconnect from tau mux server",
    handler: async (_args, ctx) => {
      disconnect();
      ctx.ui.notify("Disconnected from tau mux", "info");
    },
  });

  // Auto-connect on session start
  pi.on("session_start", async (_event, ctx) => {
    latestCtx = ctx;

    if (AUTO_CONNECT) {
      ctx.ui.notify(`Connecting to Tau mux server...`, "info");
      connect(pi, ctx, true);
    }
  });

  // Forward events
  for (const eventType of EVENT_TYPES) {
    pi.on(eventType as any, async (event: any, ctx: ExtensionContext) => {
      latestCtx = ctx;
      send({ type: "event", sessionId, event: { type: eventType, ...event } });
    });
  }

  // Update state periodically
  pi.on("turn_end", async (_event, ctx) => {
    sendState(ctx, pi);
  });

  // Cleanup on shutdown
  pi.on("session_shutdown", async () => {
    disconnect();
  });
}


        const addDiagnostic = (message, extensionPath) => {
            this.shortcutDiagnostics.push({ type: "warning", message, path: extensionPath });
            if (!this.hasUI()) {
                console.warn(message);
            }
        };
        for (const ext of this.extensions) {
            for (const [key, shortcut] of ext.shortcuts) {
                const normalizedKey = key.toLowerCase();
                const builtInKeybinding = builtinKeybindings[normalizedKey];
                if (builtInKeybinding?.restrictOverride === true) {
                    addDiagnostic(`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`, shortcut.extensionPath);
                    continue;
                }
                if (builtInKeybinding?.restrictOverride === false) {
                    addDiagnostic(`Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.keybinding} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, shortcut.extensionPath);
                }
                const existingExtensionShortcut = extensionShortcuts.get(normalizedKey);
                if (existingExtensionShortcut) {
                    addDiagnostic(`Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, shortcut.extensionPath);
                }
                extensionShortcuts.set(normalizedKey, shortcut);
            }
        }
        return extensionShortcuts;
    }
    getShortcutDiagnostics() {
        return this.shortcutDiagnostics;
    }
    invalidate(message = "This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().") {
        if (!this.staleMessage) {
            this.staleMessage = message;
            this.runtime.invalidate(message);
        }
    }
    assertActive() {
        if (this.staleMessage) {
            throw new Error(this.staleMessage);
        }
    }
    onError(listener) {
        this.errorListeners.add(listener);
        return () => this.errorListeners.delete(listener);
    }
    emitError(error) {
        for (const listener of this.errorListeners) {
            listener(error);
        }
    }
    hasHandlers(eventType) {
        for (const ext of this.extensions) {
            const handlers = ext.handlers.get(eventType);
            if (handlers && handlers.length > 0) {
                return true;
            }
        }
        return false;
    }
    getMessageRenderer(customType) {
        for (const ext of this.extensions) {
            const renderer = ext.messageRenderers.get(customType);
            if (renderer) {
                return renderer;
            }
        }
        return undefined;
    }
    resolveRegisteredCommands() {
        const commands = [];
        const counts = new Map();
        for (const ext of this.extensions) {
            for (const command of ext.commands.values()) {
                commands.push(command);
                counts.set(command.name, (counts.get(command.name) ?? 0) + 1);
            }
        }
        const seen = new Map();
        const takenInvocationNames = new Set();
        return commands.map((command) => {
            const occurrence = (seen.get(command.name) ?? 0) + 1;
            seen.set(command.name, occurrence);
            let invocationName = (counts.get(command.name) ?? 0) > 1 ? `${command.name}:${occurrence}` : command.name;
            if (takenInvocationNames.has(invocationName)) {
                let suffix = occurrence;
                do {
                    suffix++;
                    invocationName = `${command.name}:${suffix}`;
                } while (takenInvocationNames.has(invocationName));
            }
            takenInvocationNames.add(invocationName);
            return {
                ...command,
                invocationName,
            };
        });
    }
    getRegisteredCommands() {
        this.commandDiagnostics = [];
        return this.resolveRegisteredCommands();
    }
    getCommandDiagnostics() {
        return this.commandDiagnostics;
    }
    getCommand(name) {
        return this.resolveRegisteredCommands().find((command) => command.invocationName === name);
    }
    /**
     * Request a graceful shutdown. Called by extension tools and event handlers.
     * The actual shutdown behavior is provided by the mode via bindExtensions().
     */
    shutdown() {
        this.shutdownHandler();
    }
    /**
     * Create an ExtensionContext for use in event handlers and tool execution.
     * Context values are resolved at call time, so changes via bindCore/bindUI are reflected.
     */
    createContext() {
        const runner = this;
        const getModel = this.getModel;
        return {
            get ui() {
                runner.assertActive();
                return runner.uiContext;
            },
            get hasUI() {
                runner.assertActive();
                return runner.hasUI();
            },
            get cwd() {
                runner.assertActive();
                return runner.cwd;
            },
            get sessionManager() {
                runner.assertActive();
                return runner.sessionManager;
            },
            get modelRegistry() {
                runner.assertActive();
                return runner.modelRegistry;
            },
            get model() {
                runner.assertActive();
                return getModel();
            },
            isIdle: () => {
                runner.assertActive();
                return runner.isIdleFn();
            },
            get signal() {
                runner.assertActive();
                return runner.getSignalFn();
            },
            abort: () => {
                runner.assertActive();
                runner.abortFn();
            },
            hasPendingMessages: () => {
                runner.assertActive();
                return runner.hasPendingMessagesFn();
            },
            shutdown: () => {
                runner.assertActive();
                runner.shutdownHandler();
            },
            getContextUsage: () => {
                runner.assertActive();
                return runner.getContextUsageFn();
            },
            compact: (options) => {
                runner.assertActive();
                runner.compactFn(options);
            },
            getSystemPrompt: () => {
                runner.assertActive();
                return runner.getSystemPromptFn();
            },
        };
    }
    createCommandContext() {

[385 more lines in file. Use offset=440 to continue.]


/home/sebas/.npm-global/lib/node_modules
v22.22.0
10.9.4


**Exploring newSession events**

I need to find potential newSession events in the extension API, perhaps checking if there's a withSession available. I'm thinking of searching for "pi-tau-mux" and possibly looking into a new flow as well. It feels like there's a lot to uncover here, and I want to ensure I'm on the right track as I explore these options. I hope this leads me to the information I need!

grep -R "withSession\|newSession\|switchSession\|reload" -n /home/sebas/.npm-global/lib/node_modules/pi-tau-mux /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/dist | tail -n 200

grep -R "session_replace\|session_start\|session_shutdown\|session_" -n /home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/dist/core /home/sebas/.npm-global/lib/node_modules/pi-tau-mux | tail -n 200


ns.fork;\n\t\t\tthis.navigateTreeHandler = actions.navigateTree;\n\t\t\tthis.switchSessionHandler = actions.switchSession;\n\t\t\tthis.reloadHandler = actions.reload;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.waitForIdleFn = async () => {};\n\t\tthis.newSessionHandler = async () => ({ cancelled: false });\n\t\tthis.forkHandler = async () => ({ cancelled: false });\n\t\tthis.navigateTreeHandler = async () => ({ cancelled: false });\n\t\tthis.switchSessionHandler = async () => ({ cancelled: false });\n\t\tthis.reloadHandler = async () => {};\n\t}\n\n\tsetUIContext(uiContext?: ExtensionUIContext): void {\n\t\tthis.uiContext = uiContext ?? noOpUIContext;\n\t}\n\n\tgetUIContext(): ExtensionUIContext {\n\t\treturn this.uiContext;\n\t}\n\n\thasUI(): boolean {\n\t\treturn this.uiContext !== noOpUIContext;\n\t}\n\n\tgetExtensionPaths(): string[] {\n\t\treturn this.extensions.map((e) => e.path);\n\t}\n\n\t/** Get all registered tools from all extensions (first registration per name wins). */\n\tgetAllRegisteredTools(): RegisteredTool[] {\n\t\tconst toolsByName = new Map<string, RegisteredTool>();\n\t\tfor (const ext of this.extensions) {\n\t\t\tfor (const tool of ext.tools.values()) {\n\t\t\t\tif (!toolsByName.has(tool.definition.name)) {\n\t\t\t\t\ttoolsByName.set(tool.definition.name, tool);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Array.from(toolsByName.values());\n\t}\n\n\t/** Get a tool definition by name. Returns undefined if not found. */\n\tgetToolDefinition(toolName: string): RegisteredTool[\"definition\"] | undefined {\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst tool = ext.tools.get(toolName);\n\t\t\tif (tool) {\n\t\t\t\treturn tool.definition;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tgetFlags(): Map<string, ExtensionFlag> {\n\t\tconst allFlags = new Map<string, ExtensionFlag>();\n\t\tfor (const ext of this.extensions) {\n\t\t\tfor (const [name, flag] of ext.flags) {\n\t\t\t\tif (!allFlags.has(name)) {\n\t\t\t\t\tallFlags.set(name, flag);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn allFlags;\n\t}\n\n\tsetFlagValue(name: string, value: boolean | string): void {\n\t\tthis.runtime.flagValues.set(name, value);\n\t}\n\n\tgetFlagValues(): Map<string, boolean | string> {\n\t\treturn new Map(this.runtime.flagValues);\n\t}\n\n\tgetShortcuts(resolvedKeybindings: KeybindingsConfig): Map<KeyId, ExtensionShortcut> {\n\t\tthis.shortcutDiagnostics = [];\n\t\tconst builtinKeybindings = buildBuiltinKeybindings(resolvedKeybindings);\n\t\tconst extensionShortcuts = new Map<KeyId, ExtensionShortcut>();\n\n\t\tconst addDiagnostic = (message: string, extensionPath: string) => {\n\t\t\tthis.shortcutDiagnostics.push({ type: \"warning\", message, path: extensionPath });\n\t\t\tif (!this.hasUI()) {\n\t\t\t\tconsole.warn(message);\n\t\t\t}\n\t\t};\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tfor (const [key, shortcut] of ext.shortcuts) {\n\t\t\t\tconst normalizedKey = key.toLowerCase() as KeyId;\n\n\t\t\t\tconst builtInKeybinding = builtinKeybindings[normalizedKey];\n\t\t\t\tif (builtInKeybinding?.restrictOverride === true) {\n\t\t\t\t\taddDiagnostic(\n\t\t\t\t\t\t`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`,\n\t\t\t\t\t\tshortcut.extensionPath,\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (builtInKeybinding?.restrictOverride === false) {\n\t\t\t\t\taddDiagnostic(\n\t\t\t\t\t\t`Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.keybinding} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,\n\t\t\t\t\t\tshortcut.extensionPath,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst existingExtensionShortcut = extensionShortcuts.get(normalizedKey);\n\t\t\t\tif (existingExtensionShortcut) {\n\t\t\t\t\taddDiagnostic(\n\t\t\t\t\t\t`Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,\n\t\t\t\t\t\tshortcut.extensionPath,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\textensionShortcuts.set(normalizedKey, shortcut);\n\t\t\t}\n\t\t}\n\t\treturn extensionShortcuts;\n\t}\n\n\tgetShortcutDiagnostics(): ResourceDiagnostic[] {\n\t\treturn this.shortcutDiagnostics;\n\t}\n\n\tinvalidate(\n\t\tmessage = \"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\",\n\t): void {\n\t\tif (!this.staleMessage) {\n\t\t\tthis.staleMessage = message;\n\t\t\tthis.runtime.invalidate(message);\n\t\t}\n\t}\n\n\tprivate assertActive(): void {\n\t\tif (this.staleMessage) {\n\t\t\tthrow new Error(this.staleMessage);\n\t\t}\n\t}\n\n\tonError(listener: ExtensionErrorListener): () => void {\n\t\tthis.errorListeners.add(listener);\n\t\treturn () => this.errorListeners.delete(listener);\n\t}\n\n\temitError(error: ExtensionError): void {\n\t\tfor (const listener of this.errorListeners) {\n\t\t\tlistener(error);\n\t\t}\n\t}\n\n\thasHandlers(eventType: string): boolean {\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(eventType);\n\t\t\tif (handlers && handlers.length > 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\tgetMessageRenderer(customType: string): MessageRenderer | undefined {\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst renderer = ext.messageRenderers.get(customType);\n\t\t\tif (renderer) {\n\t\t\t\treturn renderer;\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate resolveRegisteredCommands(): ResolvedCommand[] {\n\t\tconst commands: RegisteredCommand[] = [];\n\t\tconst counts = new Map<string, number>();\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tfor (const command of ext.commands.values()) {\n\t\t\t\tcommands.push(command);\n\t\t\t\tcounts.set(command.name, (counts.get(command.name) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\n\t\tconst seen = new Map<string, number>();\n\t\tconst takenInvocationNames = new Set<string>();\n\n\t\treturn commands.map((command) => {\n\t\t\tconst occurrence = (seen.get(command.name) ?? 0) + 1;\n\t\t\tseen.set(command.name, occurrence);\n\n\t\t\tlet invocationName = (counts.get(command.name) ?? 0) > 1 ? `${command.name}:${occurrence}` : command.name;\n\n\t\t\tif (takenInvocationNames.has(invocationName)) {\n\t\t\t\tlet suffix = occurrence;\n\t\t\t\tdo {\n\t\t\t\t\tsuffix++;\n\t\t\t\t\tinvocationName = `${command.name}:${suffix}`;\n\t\t\t\t} while (takenInvocationNames.has(invocationName));\n\t\t\t}\n\n\t\t\ttakenInvocationNames.add(invocationName);\n\t\t\treturn {\n\t\t\t\t...command,\n\t\t\t\tinvocationName,\n\t\t\t};\n\t\t});\n\t}\n\n\tgetRegisteredCommands(): ResolvedCommand[] {\n\t\tthis.commandDiagnostics = [];\n\t\treturn this.resolveRegisteredCommands();\n\t}\n\n\tgetCommandDiagnostics(): ResourceDiagnostic[] {\n\t\treturn this.commandDiagnostics;\n\t}\n\n\tgetCommand(name: string): ResolvedCommand | undefined {\n\t\treturn this.resolveRegisteredCommands().find((command) => command.invocationName === name);\n\t}\n\n\t/**\n\t * Request a graceful shutdown. Called by extension tools and event handlers.\n\t * The actual shutdown behavior is provided by the mode via bindExtensions().\n\t */\n\tshutdown(): void {\n\t\tthis.shutdownHandler();\n\t}\n\n\t/**\n\t * Create an ExtensionContext for use in event handlers and tool execution.\n\t * Context values are resolved at call time, so changes via bindCore/bindUI are reflected.\n\t */\n\tcreateContext(): ExtensionContext {\n\t\tconst runner = this;\n\t\tconst getModel = this.getModel;\n\t\treturn {\n\t\t\tget ui() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.uiContext;\n\t\t\t},\n\t\t\tget hasUI() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.hasUI();\n\t\t\t},\n\t\t\tget cwd() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.cwd;\n\t\t\t},\n\t\t\tget sessionManager() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.sessionManager;\n\t\t\t},\n\t\t\tget modelRegistry() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.modelRegistry;\n\t\t\t},\n\t\t\tget model() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn getModel();\n\t\t\t},\n\t\t\tisIdle: () => {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.isIdleFn();\n\t\t\t},\n\t\t\tget signal() {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.getSignalFn();\n\t\t\t},\n\t\t\tabort: () => {\n\t\t\t\trunner.assertActive();\n\t\t\t\trunner.abortFn();\n\t\t\t},\n\t\t\thasPendingMessages: () => {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.hasPendingMessagesFn();\n\t\t\t},\n\t\t\tshutdown: () => {\n\t\t\t\trunner.assertActive();\n\t\t\t\trunner.shutdownHandler();\n\t\t\t},\n\t\t\tgetContextUsage: () => {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.getContextUsageFn();\n\t\t\t},\n\t\t\tcompact: (options) => {\n\t\t\t\trunner.assertActive();\n\t\t\t\trunner.compactFn(options);\n\t\t\t},\n\t\t\tgetSystemPrompt: () => {\n\t\t\t\trunner.assertActive();\n\t\t\t\treturn runner.getSystemPromptFn();\n\t\t\t},\n\t\t};\n\t}\n\n\tcreateCommandContext(): ExtensionCommandContext {\n\t\t// Use property descriptors instead of object spread so the guarded getters from\n\t\t// createContext() stay lazy. A spread would eagerly read them once and freeze the\n\t\t// old values into the returned object, bypassing stale-instance checks.\n\t\tconst context = Object.defineProperties(\n\t\t\t{},\n\t\t\tObject.getOwnPropertyDescriptors(this.createContext()),\n\t\t) as ExtensionCommandContext;\n\t\tcontext.waitForIdle = () => {\n\t\t\tthis.assertActive();\n\t\t\treturn this.waitForIdleFn();\n\t\t};\n\t\tcontext.newSession = (options) => {\n\t\t\tthis.assertActive();\n\t\t\treturn this.newSessionHandler(options);\n\t\t};\n\t\tcontext.fork = (entryId, options) => {\n\t\t\tthis.assertActive();\n\t\t\treturn this.forkHandler(entryId, options);\n\t\t};\n\t\tcontext.navigateTree = (targetId, options) => {\n\t\t\tthis.assertActive();\n\t\t\treturn this.navigateTreeHandler(targetId, options);\n\t\t};\n\t\tcontext.switchSession = (sessionPath, options) => {\n\t\t\tthis.assertActive();\n\t\t\treturn this.switchSessionHandler(sessionPath, options);\n\t\t};\n\t\tcontext.reload = () => {\n\t\t\tthis.assertActive();\n\t\t\treturn this.reloadHandler();\n\t\t};\n\t\treturn context;\n\t}\n\n\tprivate isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {\n\t\treturn (\n\t\t\tevent.type === \"session_before_switch\" ||\n\t\t\tevent.type === \"session_before_fork\" ||\n\t\t\tevent.type === \"session_before_compact\" ||\n\t\t\tevent.type === \"session_before_tree\"\n\t\t);\n\t}\n\n\tasync emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: SessionBeforeEventResult | undefined;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(event.type);\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (this.isSessionBeforeEvent(event) && handlerResult) {\n\t\t\t\t\t\tresult = handlerResult as SessionBeforeEventResult;\n\t\t\t\t\t\tif (result.cancel) {\n\t\t\t\t\t\t\treturn result as RunnerEmitResult<TEvent>;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: event.type,\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result as RunnerEmitResult<TEvent>;\n\t}\n\n\tasync emitMessageEnd(event: MessageEndEvent): Promise<AgentMessage | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessage = event.message;\n\t\tlet modified = false;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"message_end\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst currentEvent: MessageEndEvent = { ...event, message: currentMessage };\n\t\t\t\t\tconst handlerResult = (await handler(currentEvent, ctx)) as MessageEndEventResult | undefined;\n\t\t\t\t\tif (!handlerResult?.message) continue;\n\n\t\t\t\t\tif (handlerResult.message.role !== currentMessage.role) {\n\t\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\t\tevent: \"message_end\",\n\t\t\t\t\t\t\terror: \"message_end handlers must return a message with the same role\",\n\t\t\t\t\t\t});\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrentMessage = handlerResult.message;\n\t\t\t\t\tmodified = true;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"message_end\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn modified ? currentMessage : undefined;\n\t}\n\n\tasync emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tconst currentEvent: ToolResultEvent = { ...event };\n\t\tlet modified = false;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"tool_result\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst handlerResult = (await handler(currentEvent, ctx)) as ToolResultEventResult | undefined;\n\t\t\t\t\tif (!handlerResult) continue;\n\n\t\t\t\t\tif (handlerResult.content !== undefined) {\n\t\t\t\t\t\tcurrentEvent.content = handlerResult.content;\n\t\t\t\t\t\tmodified = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (handlerResult.details !== undefined) {\n\t\t\t\t\t\tcurrentEvent.details = handlerResult.details;\n\t\t\t\t\t\tmodified = true;\n\t\t\t\t\t}\n\t\t\t\t\tif (handlerResult.isError !== undefined) {\n\t\t\t\t\t\tcurrentEvent.isError = handlerResult.isError;\n\t\t\t\t\t\tmodified = true;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"tool_result\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!modified) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn {\n\t\t\tcontent: currentEvent.content,\n\t\t\tdetails: currentEvent.details,\n\t\t\tisError: currentEvent.isError,\n\t\t};\n\t}\n\n\tasync emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\t\tlet result: ToolCallEventResult | undefined;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"tool_call\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\tif (handlerResult) {\n\t\t\t\t\tresult = handlerResult as ToolCallEventResult;\n\t\t\t\t\tif (result.block) {\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tasync emitUserBash(event: UserBashEvent): Promise<UserBashEventResult | undefined> {\n\t\tconst ctx = this.createContext();\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"user_bash\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\t\t\t\t\tif (handlerResult) {\n\t\t\t\t\t\treturn handlerResult as UserBashEventResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"user_bash\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tasync emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentMessages = structuredClone(messages);\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"context\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: ContextEvent = { type: \"context\", messages: currentMessages };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult && (handlerResult as ContextEventResult).messages) {\n\t\t\t\t\t\tcurrentMessages = (handlerResult as ContextEventResult).messages!;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"context\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentMessages;\n\t}\n\n\tasync emitBeforeProviderRequest(payload: unknown): Promise<unknown> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentPayload = payload;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"before_provider_request\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: BeforeProviderRequestEvent = {\n\t\t\t\t\t\ttype: \"before_provider_request\",\n\t\t\t\t\t\tpayload: currentPayload,\n\t\t\t\t\t};\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\t\t\t\t\tif (handlerResult !== undefined) {\n\t\t\t\t\t\tcurrentPayload = handlerResult;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"before_provider_request\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentPayload;\n\t}\n\n\tasync emitBeforeAgentStart(\n\t\tprompt: string,\n\t\timages: ImageContent[] | undefined,\n\t\tsystemPrompt: string,\n\t\tsystemPromptOptions: BuildSystemPromptOptions,\n\t): Promise<BeforeAgentStartCombinedResult | undefined> {\n\t\tlet currentSystemPrompt = systemPrompt;\n\t\tconst ctx = Object.defineProperties(\n\t\t\t{},\n\t\t\tObject.getOwnPropertyDescriptors(this.createContext()),\n\t\t) as ExtensionContext;\n\t\tctx.getSystemPrompt = () => {\n\t\t\tthis.assertActive();\n\t\t\treturn currentSystemPrompt;\n\t\t};\n\t\tconst messages: NonNullable<BeforeAgentStartEventResult[\"message\"]>[] = [];\n\t\tlet systemPromptModified = false;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"before_agent_start\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: BeforeAgentStartEvent = {\n\t\t\t\t\t\ttype: \"before_agent_start\",\n\t\t\t\t\t\tprompt,\n\t\t\t\t\t\timages,\n\t\t\t\t\t\tsystemPrompt: currentSystemPrompt,\n\t\t\t\t\t\tsystemPromptOptions,\n\t\t\t\t\t};\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\n\t\t\t\t\tif (handlerResult) {\n\t\t\t\t\t\tconst result = handlerResult as BeforeAgentStartEventResult;\n\t\t\t\t\t\tif (result.message) {\n\t\t\t\t\t\t\tmessages.push(result.message);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (result.systemPrompt !== undefined) {\n\t\t\t\t\t\t\tcurrentSystemPrompt = result.systemPrompt;\n\t\t\t\t\t\t\tsystemPromptModified = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"before_agent_start\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (messages.length > 0 || systemPromptModified) {\n\t\t\treturn {\n\t\t\t\tmessages: messages.length > 0 ? messages : undefined,\n\t\t\t\tsystemPrompt: systemPromptModified ? currentSystemPrompt : undefined,\n\t\t\t};\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\tasync emitResourcesDiscover(\n\t\tcwd: string,\n\t\treason: ResourcesDiscoverEvent[\"reason\"],\n\t): Promise<{\n\t\tskillPaths: Array<{ path: string; extensionPath: string }>;\n\t\tpromptPaths: Array<{ path: string; extensionPath: string }>;\n\t\tthemePaths: Array<{ path: string; extensionPath: string }>;\n\t}> {\n\t\tconst ctx = this.createContext();\n\t\tconst skillPaths: Array<{ path: string; extensionPath: string }> = [];\n\t\tconst promptPaths: Array<{ path: string; extensionPath: string }> = [];\n\t\tconst themePaths: Array<{ path: string; extensionPath: string }> = [];\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tconst handlers = ext.handlers.get(\"resources_discover\");\n\t\t\tif (!handlers || handlers.length === 0) continue;\n\n\t\t\tfor (const handler of handlers) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: ResourcesDiscoverEvent = { type: \"resources_discover\", cwd, reason };\n\t\t\t\t\tconst handlerResult = await handler(event, ctx);\n\t\t\t\t\tconst result = handlerResult as ResourcesDiscoverResult | undefined;\n\n\t\t\t\t\tif (result?.skillPaths?.length) {\n\t\t\t\t\t\tskillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));\n\t\t\t\t\t}\n\t\t\t\t\tif (result?.promptPaths?.length) {\n\t\t\t\t\t\tpromptPaths.push(...result.promptPaths.map((path) => ({ path, extensionPath: ext.path })));\n\t\t\t\t\t}\n\t\t\t\t\tif (result?.themePaths?.length) {\n\t\t\t\t\t\tthemePaths.push(...result.themePaths.map((path) => ({ path, extensionPath: ext.path })));\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tconst stack = err instanceof Error ? err.stack : undefined;\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"resources_discover\",\n\t\t\t\t\t\terror: message,\n\t\t\t\t\t\tstack,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { skillPaths, promptPaths, themePaths };\n\t}\n\n\t/** Emit input event. Transforms chain, \"handled\" short-circuits. */\n\tasync emitInput(text: string, images: ImageContent[] | undefined, source: InputSource): Promise<InputEventResult> {\n\t\tconst ctx = this.createContext();\n\t\tlet currentText = text;\n\t\tlet currentImages = images;\n\n\t\tfor (const ext of this.extensions) {\n\t\t\tfor (const handler of ext.handlers.get(\"input\") ?? []) {\n\t\t\t\ttry {\n\t\t\t\t\tconst event: InputEvent = { type: \"input\", text: currentText, images: currentImages, source };\n\t\t\t\t\tconst result = (await handler(event, ctx)) as InputEventResult | undefined;\n\t\t\t\t\tif (result?.action === \"handled\") return result;\n\t\t\t\t\tif (result?.action === \"transform\") {\n\t\t\t\t\t\tcurrentText = result.text;\n\t\t\t\t\t\tcurrentImages = result.images ?? currentImages;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.emitError({\n\t\t\t\t\t\textensionPath: ext.path,\n\t\t\t\t\t\tevent: \"input\",\n\t\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t\t\tstack: err instanceof Error ? err.stack : undefined,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn currentText !== text || currentImages !== images\n\t\t\t? { action: \"transform\", text: currentText, images: currentImages }\n\t\t\t: { action: \"continue\" };\n\t}\n}\n"]}
/home/sebas/.npm-global/lib/node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/loader.d.ts.map:1:{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AAwFpB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CA8CzD;AA+ND;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,GACxB,OAAO,CAAC,SAAS,CAAC,CAKpB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAwBrH;AA+GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAyC/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n * Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries.\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"@mariozechner/jiti\";\nimport * as _bundledPiAgentCore from \"@mariozechner/pi-agent-core\";\nimport * as _bundledPiAi from \"@mariozechner/pi-ai\";\nimport * as _bundledPiAiOauth from \"@mariozechner/pi-ai/oauth\";\nimport type { KeyId } from \"@mariozechner/pi-tui\";\nimport * as _bundledPiTui from \"@mariozechner/pi-tui\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.js\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from @mariozechner/pi-coding-agent.\nimport * as _bundledPiCodingAgent from \"../../index.js\";\nimport { createEventBus, type EventBus } from \"../event-bus.js\";\nimport type { ExecOptions } from \"../exec.js\";\nimport { execCommand } from \"../exec.js\";\nimport { createSyntheticSourceInfo } from \"../source-info.js\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.js\";\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"@mariozechner/pi-agent-core\": _bundledPiAgentCore,\n\t\"@mariozechner/pi-tui\": _bundledPiTui,\n\t\"@mariozechner/pi-ai\": _bundledPiAi,\n\t\"@mariozechner/pi-ai/oauth\": _bundledPiAiOauth,\n\t\"@mariozechner/pi-coding-agent\": _bundledPiCodingAgent,\n};\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\tconst typeboxEntry = require.resolve(\"typebox\");\n\tconst typeboxCompileEntry = require.resolve(\"typebox/compile\");\n\tconst typeboxValueEntry = require.resolve(\"typebox/value\");\n\n\tconst packagesRoot = path.resolve(__dirname, \"../../../../\");\n\tconst resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {\n\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\tif (fs.existsSync(workspacePath)) {\n\t\t\treturn workspacePath;\n\t\t}\n\t\treturn fileURLToPath(import.meta.resolve(specifier));\n\t};\n\n\t_aliases = {\n\t\t\"@mariozechner/pi-coding-agent\": packageIndex,\n\t\t\"@mariozechner/pi-agent-core\": resolveWorkspaceOrImport(\"agent/dist/index.js\", \"@mariozechner/pi-agent-core\"),\n\t\t\"@mariozechner/pi-tui\": resolveWorkspaceOrImport(\"tui/dist/index.js\", \"@mariozechner/pi-tui\"),\n\t\t\"@mariozechner/pi-ai\": resolveWorkspaceOrImport(\"ai/dist/index.js\", \"@mariozechner/pi-ai\"),\n\t\t\"@mariozechner/pi-ai/oauth\": resolveWorkspaceOrImport(\"ai/dist/oauth.js\", \"@mariozechner/pi-ai/oauth\"),\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\nconst UNICODE_SPACES = /[\\u00A0\\u2000-\\u200A\\u202F\\u205F\\u3000]/g;\n\nfunction normalizeUnicodeSpaces(str: string): string {\n\treturn str.replace(UNICODE_SPACES, \" \");\n}\n\nfunction expandPath(p: string): string {\n\tconst normalized = normalizeUnicodeSpaces(p);\n\tif (normalized.startsWith(\"~/\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(2));\n\t}\n\tif (normalized.startsWith(\"~\")) {\n\t\treturn path.join(os.homedir(), normalized.slice(1));\n\t}\n\treturn normalized;\n}\n\nfunction resolvePath(extPath: string, cwd: string): string {\n\tconst expanded = expandPath(extPath);\n\tif (path.isAbsolute(expanded)) {\n\t\treturn expanded;\n\t}\n\treturn path.resolve(cwd, expanded);\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.js\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\tevents: eventBus,\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function loadExtensionModule(extensionPath: string) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\tconst module = await jiti.import(extensionPath, { default: true });\n\tconst factory = module as ExtensionFactory;\n\treturn typeof factory !== \"function\" ? undefined : factory;\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd);\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\tawait factory(api);\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface PiManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n}\n\nfunction readPiManifest(packageJsonPath: string): PiManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content);\n\t\tif (pkg.pi && typeof pkg.pi === \"object\") {\n\t\t\treturn pkg.pi as PiManifest;\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"pi.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"pi\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readPiManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with \"pi\" field → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(cwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(agentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, cwd);\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with pi manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, cwd, eventBus);\n}\n"]}


/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/public/app.js:246:    case 'session_name':
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/public/app.js:755:  const data = await rpcCommand({ type: 'get_session_stats' }, 'Loading stats...');
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/public/session-sidebar.js:281:            body: JSON.stringify({ type: 'set_session_name', name: newName }),
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/public/websocket-client.js:133:      case 'session_switch':
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/imessage-bridge.ts:309:  pi.on("session_start", async (_event, ctx) => {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/imessage-bridge.ts:335:  pi.on("session_shutdown", async () => {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts:213:  pi.on("session_start", async (_event, ctx) => {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts:236:  pi.on("session_shutdown", async () => {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:133: * (e.g. tmux pane was killed without session_shutdown firing).
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:208:      type: "session_end",
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:234:    return endChunk.includes('"type":"session_end"');
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:440:  pi.on("session_start", async (_event, ctx) => {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:483:      broadcast({ type: "event", event: { type: "session_name", name: title } });
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:755:        case "get_session_stats": {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:757:            sendTo(ws, error("get_session_stats", "No context available"));
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:770:          sendTo(ws, success("get_session_stats", {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:781:        case "set_session_name": {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:784:            sendTo(ws, error("set_session_name", "Name cannot be empty"));
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:788:          sendTo(ws, success("set_session_name"));
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:1173:      const paneInfo = execSync("tmux list-panes -a -F '#{pane_pid}:#{session_name}' 2>/dev/null", { encoding: "utf8" });
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:1456:        else if (entry.type === "session_info" && entry.name) sessionName = entry.name;
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:1598:                if (entry.type === "session_info" && entry.name) {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:1821:  pi.on("session_start", async (_event, ctx) => {
/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/mirror-server.ts:1835:  pi.on("session_shutdown", async (_event, ctx) => {

[Showing lines 8-32 of 32 (50.0KB limit). Full output: /tmp/pi-bash-ad1b327f76839cff.log]


edit

let ws: WebSocket | null = null;
let latestCtx: ExtensionContext | null = null;
let reconnectTimer: NodeJS.Timeout | null = null;
let sessionId: string | null = null;
let serverUrls: { local?: string; tailscale?: string; magicDns?: string } | null = null;

function getActiveCtx(fallback?: ExtensionContext): ExtensionContext | null {
  return latestCtx || fallback || null;
}

    ws.on("open", () => {
      // Register this instance
      sessionId = ctx.sessionManager.getSessionFile()?.split("/").pop()?.replace(".jsonl", "") || Date.now().toString();
      send({ type: "register", sessionId, cwd: ctx.cwd || process.cwd() });

      // Send initial state
      sendState(ctx, pi);

      // Notify user of successful connection
      // Use server's Tailscale URL if available, otherwise fall back to connection URL
      const displayUrl = serverUrls?.tailscale || serverUrls?.magicDns || `http://${MUX_HOST}:${MUX_PORT}`;
      const location = serverUrls?.tailscale ? 'Tailscale' : serverUrls?.magicDns ? 'MagicDNS' : (MUX_HOST === 'localhost' ? 'local' : MUX_HOST);
      ctx.ui.notify(`Connected to Tau mux server at ${location}  •  View UI at ${displayUrl}`, "info");
    });

    ws.on("message", (data) => {
      try {
        const msg = JSON.parse(data.toString());
        handleMessage(msg, ctx, pi);
      } catch {}
    });

    ws.on("open", () => {
      const activeCtx = getActiveCtx(ctx);
      if (!activeCtx) return;

      try {
        // Register this instance
        sessionId = activeCtx.sessionManager.getSessionFile()?.split("/").pop()?.replace(".jsonl", "") || Date.now().toString();
        send({ type: "register", sessionId, cwd: activeCtx.cwd || process.cwd() });

        // Send initial state
        sendState(activeCtx, pi);

        // Notify user of successful connection
        // Use server's Tailscale URL if available, otherwise fall back to connection URL
        const displayUrl = serverUrls?.tailscale || serverUrls?.magicDns || `http://${MUX_HOST}:${MUX_PORT}`;
        const location = serverUrls?.tailscale ? 'Tailscale' : serverUrls?.magicDns ? 'MagicDNS' : (MUX_HOST === 'localhost' ? 'local' : MUX_HOST);
        activeCtx.ui.notify(`Connected to Tau mux server at ${location}  •  View UI at ${displayUrl}`, "info");
      } catch {
        // Session was replaced before socket opened; ignore and wait for next event/context.
      }
    });

    ws.on("message", (data) => {
      try {
        const msg = JSON.parse(data.toString());
        const activeCtx = getActiveCtx(ctx);
        if (!activeCtx) return;
        handleMessage(msg, activeCtx, pi);
      } catch {}
    });

      if (!reconnectTimer) {
        reconnectTimer = setTimeout(() => {
          reconnectTimer = null;
          if (latestCtx) connect(pi, latestCtx, false);
        }, 5000);
      }

      if (!reconnectTimer) {
        reconnectTimer = setTimeout(() => {
          reconnectTimer = null;
          const activeCtx = getActiveCtx();
          if (activeCtx) connect(pi, activeCtx, false);
        }, 5000);
      }

      if (promptIfDown && e.message.includes("ECONNREFUSED")) {
        promptStartMux(pi, ctx);
      }

      if (promptIfDown && e.message.includes("ECONNREFUSED")) {
        const activeCtx = getActiveCtx(ctx);
        if (activeCtx) promptStartMux(pi, activeCtx);
      }


Successfully replaced 4 block(s) in /home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts.

     ...
  25 let reconnectTimer: NodeJS.Timeout | null = null;
  26 let sessionId: string | null = null;
  27 let serverUrls: { local?: string; tailscale?: string; magicDns?: string } | null = null;
  28
+ 29 function getActiveCtx(fallback?: ExtensionContext): ExtensionContext | null {
+ 30   return latestCtx || fallback || null;
+ 31 }
+ 32
  29 // ─────────────────────────────────────────────────────────────
  30 // Connection management
  31 // ─────────────────────────────────────────────────────────────
  32
     ...
  36   try {
  37     ws = new WebSocket(MUX_URL);
  38
  39     ws.on("open", () => {
- 40       // Register this instance
- 41       sessionId = ctx.sessionManager.getSessionFile()?.split("/").pop()?.replace(".jsonl", "") || Date.now().toString();
- 42       send({ type: "register", sessionId, cwd: ctx.cwd || process.cwd() });
- 43
- 44       // Send initial state
- 45       sendState(ctx, pi);
- 46
- 47       // Notify user of successful connection
- 48       // Use server's Tailscale URL if available, otherwise fall back to connection URL
- 49       const displayUrl = serverUrls?.tailscale || serverUrls?.magicDns || `http://${MUX_HOST}:${MUX_PORT}`;
- 50       const location = serverUrls?.tailscale ? 'Tailscale' : serverUrls?.magicDns ? 'MagicDNS' : (MUX_HOST === 'localhost' ? 'local' : MUX_HOST);
- 51       ctx.ui.notify(`Connected to Tau mux server at ${location}  •  View UI at ${displayUrl}`, "info");
+ 44       const activeCtx = getActiveCtx(ctx);
+ 45       if (!activeCtx) return;
+ 46
+ 47       try {
+ 48         // Register this instance
+ 49         sessionId = activeCtx.sessionManager.getSessionFile()?.split("/").pop()?.replace(".jsonl", "") || Date.now().toString();
+ 50         send({ type: "register", sessionId, cwd: activeCtx.cwd || process.cwd() });
+ 51
+ 52         // Send initial state
+ 53         sendState(activeCtx, pi);
+ 54
+ 55         // Notify user of successful connection
+ 56         // Use server's Tailscale URL if available, otherwise fall back to connection URL
+ 57         const displayUrl = serverUrls?.tailscale || serverUrls?.magicDns || `http://${MUX_HOST}:${MUX_PORT}`;
+ 58         const location = serverUrls?.tailscale ? 'Tailscale' : serverUrls?.magicDns ? 'MagicDNS' : (MUX_HOST === 'localhost' ? 'local' : MUX_HOST);
+ 59         activeCtx.ui.notify(`Connected to Tau mux server at ${location}  •  View UI at ${displayUrl}`, "info");
+ 60       } catch {
+ 61         // Session was replaced before socket opened; ignore and wait for next event/context.
+ 62       }
  52     });
  53
  54     ws.on("message", (data) => {
  55       try {
  56         const msg = JSON.parse(data.toString());
- 57         handleMessage(msg, ctx, pi);
+ 68         const activeCtx = getActiveCtx(ctx);
+ 69         if (!activeCtx) return;
+ 70         handleMessage(msg, activeCtx, pi);
  58       } catch {}
  59     });
  60
  61     ws.on("close", () => {
     ...
  64       // Attempt reconnect after delay
  65       if (!reconnectTimer) {
  66         reconnectTimer = setTimeout(() => {
  67           reconnectTimer = null;
- 68           if (latestCtx) connect(pi, latestCtx, false);
+ 81           const activeCtx = getActiveCtx();
+ 82           if (activeCtx) connect(pi, activeCtx, false);
  69         }, 5000);
  70       }
  71     });
  72
  73     ws.on("error", (e) => {
  74       // Prompt to start server if not running
  75       if (promptIfDown && e.message.includes("ECONNREFUSED")) {
- 76         promptStartMux(pi, ctx);
+ 90         const activeCtx = getActiveCtx(ctx);
+ 91         if (activeCtx) promptStartMux(pi, activeCtx);
  77       }
  78
  79       ws = null;
  80     });
     ...


bash

node -e "const fs=require('fs'); const ts=fs.readFileSync('/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts','utf8'); console.log(ts.includes('function getActiveCtx'));"


true


**Considering memory storage**

It looks like I might need to store some information in memory since this could be a reusable project fact or decision. I’m thinking about using a memory-write skill for that. But first, I should consider how to load the skill effectively. Making sure I implement this correctly could help me retrieve the information later when it's needed, so I'm eager to get it right!


---
name: memory-write
description: Save a durable high-signal memory into agents-database
---

# memory-write

## Use When

- new information is likely useful beyond current chat or task
- saving a decision, preference, project fact, reusable source/artifact, or handoff summary
- user asks to remember something durable

## Do Not Use When

- content is trivial, redundant, ephemeral, or likely useless later
- content is secret unless user explicitly asked to store it

## Required Workflow

1. If useful context may already exist, optionally check first:
   - `curl -fsSL "${AGENTS_DB_API:-http://100.116.176.16:8091}/api/search?q=<query>&scope=global&scope=project&limit=10"`
2. Choose best memory type:
   - `profile`: stable user preference, priority, recurring constraint
   - `decision`: chosen approach or operating rule
   - `project`: durable project fact or invariant
   - `artifact`: reusable source, claim, reference, output
   - `episode`: concise session summary, blocked state, handoff context
3. Build a concise payload with:
   - required: `type`, `scope`, `title`, `content`
   - recommended when known: `subtype`, `project_id`, `repo_id`, `task_id`, `run_id`, `source_ref`, `origin_agent`, `url`, `domain`, `metadata`
4. Save it with the helper:
   - `~/.agents/skills/memory-write/memory-write --type <type> --scope <scope> --title "<title>" --content "<content>"`
5. Confirm the created memory id and what was stored.

## Quick Examples

```bash
~/.agents/skills/memory-write/memory-write \
  --type profile \
  --subtype working_preference \
  --scope global \
  --title "User prefers concise status updates" \
  --content "Sebas prefers short, direct updates with business impact first." \
  --source-ref "conversation:2026-04-17" \
  --origin-agent personal-agent

~/.agents/skills/memory-write/memory-write \
  --type decision \
  --subtype operating_rule \
  --scope global \
  --title "Agents should save durable high-signal memory opportunistically" \
  --content "Treat agents-database as shared durable memory, not only search." \
  --metadata '{"source":"conversation"}'
```

## Rules

- prefer one strong memory over many weak ones
- keep `title` short and searchable
- keep `content` factual and compact
- use `episode` for summaries, not for every chat turn
- include `url` and `domain` for web-derived artifacts when known
- include `project_id`, `repo_id`, `task_id`, and `run_id` when context exists


**Considering episode saving**

I think I should probably save the episode or decision about the local hotfix. It seems important, especially since it might help in the future if I need to reference it. Using a bash helper could be a practical way to do this. I wonder if it’s really durable enough. It feels like having a backup plan could save me a lot of time and hassle later on!

~/.agents/skills/memory-write/memory-write --type decision --subtype bugfix --scope global --title "pi-tau-mux stale ctx fix in tau-client" --content "Local hotfix on ballbox-first: /home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts now avoids captured stale ExtensionContext in websocket callbacks by resolving latestCtx at callback time and swallowing stale-session races during /new session replacement. Reapply after pi-tau-mux reinstall/update if the package overwrites global node_modules." --source-ref "conversation:2026-05-01:/new-stale-ctx" --origin-agent personal-agent


pi-tau-mux stale ctx fix in tau-client

Local hotfix on ballbox-first: /home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts now avoids captured stale ExtensionContext in websocket callbacks by resolving latestCtx at callback time and swallowing stale-session races during /new session replacement. Reapply after pi-tau-mux reinstall/update if the package overwrites global node_modules.

Summary: Local hotfix on ballbox-first: /home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts now avoids captured stale ExtensionContext in websocket callbacks by re…


**Finalizing a concise update**

I need to create a final, concise statement that mentions the changed file and the reason for that change. It's important to keep it clear and straightforward. Maybe I should also advise retrying or suggest a new approach if necessary. I want to ensure the user has everything they need, so I’ll focus on making this update as helpful and informative as possible.

Cause: `pi-tau-mux` captured old extension `ctx` across `/new` session replacement. Then websocket `open/message/error` handlers touched stale `ctx` and crashed.

Fix applied:
- patched `/home/sebas/.npm-global/lib/node_modules/pi-tau-mux/extensions/tau-client.ts`
- websocket callbacks now use `latestCtx` at callback time
- stale-session race on socket open is ignored instead of throwing

What to do:
- retry `/new`
- say yes to memory analysis again

Note:
- this is a local hotfix in global `node_modules`
- if `pi-tau-mux` gets updated/reinstalled, patch may be overwritten

Memory saved:
- `mem_ebbca254ff484623b6d63073ee758d8d`

If it still blows up, I’ll check other `pi-tau-mux` extensions for the same stale-ctx pattern.