Back to Directory
omp-extension-dev

omp-extension-dev

Patterns, hooks, and skeleton for building crash-safe, non-blocking, and concurrent-safe Oh My Pi (OMP) extensions using correctly scoped ID-keyed maps.

OMP Extension Development

When writing or modifying OMP extensions, strictly adhere to the following pattern to prevent crashing the agent, hanging the event loop, or leaking secrets.

The 4 Golden Invariants

  1. Zero-Blocking: Never await network or heavy I/O in event handlers. OMP will hang. Push to an array and use an out-of-band setInterval flusher.
  2. Zero-Crashing: If an extension throws, OMP dies. Wrap all pi.on bindings in a try/catch (use the safeOn pattern).
  3. Zero-Leakage: tool_call and before_provider_request payloads contain raw LLM prompts which include user API keys and JWTs. Apply strict regex redaction before storing or transmitting data.
  4. Zero-State-Corruption (Concurrency Safe): Never use global scalar variables to track state. Concurrent subagents interleave events. You MUST use ID-keyed Maps.
    • ⚠️ CRITICAL KEY MAPPING: Map scope must match event scope.
    • LLM Generations (before_provider_request): Use ev.id, ev.messageId, or ev.generationId. NEVER use agentId (agents make multiple LLM calls).
    • Turns (turn_start): Use ev.id or ev.turnId.
    • Agents (agent_start): Use ev.agentId.

Available Lifecycle Hooks

  • Session/Agent: session_start, session_shutdown, goal_updated, agent_start, agent_end
  • Turn: turn_start, turn_end, message_end
  • LLM: before_provider_request, after_provider_response
  • Tools: tool_execution_start, tool_call, tool_execution_end, tool_result
  • Diagnostics: session_compact, auto_retry_start, ttsr_triggered

Safe Skeleton Template

import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";

export default function safeExtension(pi: ExtensionAPI): void {
  const queue: any[] = [];
  let flushTimer: ReturnType<typeof setInterval>;
  let pendingFlush = Promise.resolve();

  // 🛡️ Track discrete async lifecycles strictly by their specific scoped IDs
  const activeGenerations = new Map<string, { startMs: number; model: string }>();
  const activeTurns = new Map<string, { startMs: number }>();

  function safeOn(event: string, handler: (...args: any[]) => Promise<void>) {
    pi.on(event as any, async (...args: any[]) => {
      try { await handler(...args); } 
      catch (err) { pi.logger.warn(`[Ext] Error in ${event}: ${String(err)}`); }
    });
  }

  function flush() {
    if (queue.length === 0) return;
    const batch = queue.splice(0);
    pendingFlush = pendingFlush.then(async () => {
      try { /* await fetch(...) with AbortSignal.timeout(10000) */ } catch (e) {}
    });
  }

  safeOn("session_start", async () => { flushTimer = setInterval(flush, 2000); });
  
  // ✅ Correctly scoping Generation State
  safeOn("before_provider_request", async (ev) => { 
    const genId = ev.id || ev.generationId || crypto.randomUUID();
    activeGenerations.set(genId, { startMs: Date.now(), model: ev.model }); 
  });

  safeOn("after_provider_response", async (ev) => {
    const genId = ev.id || ev.generationId;
    const state = activeGenerations.get(genId);
    if (!state) return;
    activeGenerations.delete(genId);
    // Queue payload...
  });

  safeOn("session_shutdown", async () => {
    clearInterval(flushTimer);
    flush();
    await pendingFlush;
  });
}