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
- Zero-Blocking: Never
awaitnetwork or heavy I/O in event handlers. OMP will hang. Push to an array and use an out-of-bandsetIntervalflusher. - Zero-Crashing: If an extension throws, OMP dies. Wrap all
pi.onbindings in atry/catch(use thesafeOnpattern). - Zero-Leakage:
tool_callandbefore_provider_requestpayloads contain raw LLM prompts which include user API keys and JWTs. Apply strict regex redaction before storing or transmitting data. - 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): Useev.id,ev.messageId, orev.generationId. NEVER useagentId(agents make multiple LLM calls). - Turns (
turn_start): Useev.idorev.turnId. - Agents (
agent_start): Useev.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;
});
}