# pi ExtensionAPI Audit (für iOS-App-Spec v2) **Datum:** 2026-05-15 **pi Version:** 0.74.0 **Auditor:** Subagent (Claude Sonnet 4.6) --- ## TL;DR - ✅ **S-07 State Side-Channel** — machbar mit Einschränkungen (kein explizites `awaiting-input` Event, muss abgeleitet werden) - ✅ **S-08 Slash-Command-Registry** — direkt verfügbar via `pi.getCommands()` - ⚠️ **S-13 Tree-State Side-Channel** — Tree-Read funktioniert, aber keine Subscribe-Mechanik für Live-Updates (Polling oder Event-basiert möglich) - ❌ **Gruppe T Tree-Navigation** — Slash-Command-Injection funktioniert NICHT (Commands werden als Text an LLM gesendet, nicht dispatched). Hack via `(pi as any)` nötig oder Upstream-Change. --- ## 1. Methodik **Untersuchte Quellen:** - pi-Installation: `/usr/local/lib/node_modules/@earendil-works/pi-coding-agent/` (v0.74.0) - TypeScript-Definitionen: `dist/core/extensions/types.d.ts` (1173 Zeilen) - Session-Manager: `dist/core/session-manager.d.ts` (308 Zeilen) - Slash-Commands: `dist/core/slash-commands.d.ts` (15 Zeilen) - Offizielle Doku: `docs/extensions.md` (97KB) - Bestehende Extension: `/Users/jay/.pi/agent/git/git.vpsj.de/jay/pi-remote-control/extensions/remote-control/` **Verifikationsmethode:** Statische Analyse der TypeScript-Definitionen + Cross-Check gegen bestehenden Code der `pi-remote-control` Extension. --- ## 2. Capability-Matrix | Capability | Status | Spec-Feature | Workaround | Aufwand | |---|---|---|---|---| | **Lifecycle Events** | ✅ Vorhanden | S-07 | `agent_end` statt `awaiting-input` | Trivial | | **Tool Execution Events** | ✅ Vorhanden | S-07 | — | Trivial | | **Slash-Command-Registry** | ✅ Vorhanden | S-08 | — | Trivial | | **Tree-Read** | ✅ Vorhanden | S-13 | — | Trivial | | **Tree-Subscribe (Live-Updates)** | ⚠️ Event-basiert | S-13 | Events tracken statt Subscribe | Einfach | | **Slash-Command-Dispatch** | ❌ Nicht exposed | Gruppe T | `(pi as any)` Hack | Fragil | | **Tool-Call-Details** | ✅ Vorhanden | S-07 | — | Trivial | **Legende:** - ✅ = Out-of-the-box verfügbar - ⚠️ = Machbar mit Einschränkungen - ❌ = Nicht ohne Hack oder Upstream-Change --- ## 3. Capabilities im Detail ### 3.1 Lifecycle Events **Was existiert:** ```typescript // Quelle: dist/core/extensions/types.d.ts, Zeile 458-477 pi.on("agent_start", async (event, ctx) => { ... }); pi.on("agent_end", async (event, ctx) => { ... }); pi.on("tool_execution_start", async (event, ctx) => { ... }); pi.on("tool_execution_end", async (event, ctx) => { ... }); pi.on("message_start", async (event, ctx) => { ... }); pi.on("message_update", async (event, ctx) => { ... }); pi.on("message_end", async (event, ctx) => { ... }); ``` **Event-Payloads (relevante Felder):** | Event | Payload | Code-Beleg | |---|---|---| | `agent_start` | `{ type: "agent_start" }` | types.d.ts:223 | | `agent_end` | `{ type: "agent_end", messages: AgentMessage[] }` | types.d.ts:228 | | `tool_execution_start` | `{ toolCallId, toolName, args }` | types.d.ts:261 | | `tool_execution_end` | `{ toolCallId, toolName, result, isError }` | types.d.ts:275 | | `message_update` | `{ message, assistantMessageEvent }` | types.d.ts:250 | **Lücken:** - ❌ **Kein explizites `awaiting-input` Event.** Das nächstbeste ist `agent_end`, welches signalisiert dass der Agent-Loop beendet ist. Kombiniert mit `ctx.isIdle()` kann man ableiten, ob Pi auf User-Input wartet. - ❌ **Kein `thinking` vs. `idle` State-Enum.** Muss selbst aus Events abgeleitet werden. **Ableitung des Pi-States für S-07:** ```typescript // Pseudo-Code für die Sidecar-Logik let state: "thinking" | "tool" | "idle" | "awaiting-input" = "idle"; pi.on("agent_start", () => { state = "thinking"; broadcast({ type: "state", value: "thinking" }); }); pi.on("tool_execution_start", (event) => { state = "tool"; broadcast({ type: "state", value: "tool", tool: event.toolName }); }); pi.on("tool_execution_end", () => { state = "thinking"; // zurück zu thinking nach Tool }); pi.on("agent_end", (event, ctx) => { if (ctx.isIdle()) { state = "awaiting-input"; broadcast({ type: "state", value: "awaiting-input" }); } else { state = "thinking"; // Follow-up-Messages pending } }); ``` **Code-Beleg (bestehende Extension nutzt diese Events):** ```typescript // pi-remote-control/extensions/remote-control/index.ts, Zeile 89-109 pi.on("agent_start", async (_event, ctx) => { server?.broadcast({ type: "agent_start" }); updateStatus(ctx); }); pi.on("agent_end", async (_event, ctx) => { server?.broadcast({ type: "agent_end" }); updateStatus(ctx); }); pi.on("tool_execution_start", async (event) => { server?.broadcast({ type: "tool_start", toolCallId: event.toolCallId, toolName: event.toolName, args: event.args, }); }); ``` **Fazit:** S-07 State-Side-Channel ist **machbar**. Die Granularität ist ausreichend für die Spec-Anforderungen (thinking, tool, idle, awaiting-input). Kein Upstream-Change nötig. --- ### 3.2 Slash-Command-Registry **Was existiert:** ```typescript // Quelle: dist/core/extensions/types.d.ts, Zeile 772 pi.getCommands(): SlashCommandInfo[]; ``` **Return-Type:** ```typescript // Quelle: dist/core/slash-commands.d.ts, Zeile 1-9 export interface SlashCommandInfo { name: string; description?: string; source: "extension" | "prompt" | "skill"; sourceInfo: SourceInfo; } ``` **Beispiel:** ```typescript pi.on("session_start", async (event, ctx) => { const commands = pi.getCommands(); // [ // { name: "new", description: "Start a new session", source: "extension", ... }, // { name: "fork", description: "Fork from an entry", source: "extension", ... }, // { name: "tree", description: "Show conversation tree", source: "extension", ... }, // { name: "my-custom", description: "...", source: "skill", ... } // ] // Für S-08 Endpoint /sessions//commands: const payload = commands.map(c => ({ name: c.name, description: c.description, source: c.source })); // Send via WebSocket or REST }); ``` **Code-Beleg:** Die bestehende Extension nutzt `pi.registerCommand()` aber `pi.getCommands()` ist bisher nicht verwendet. Die Funktion ist aber in `types.d.ts:772` klar definiert. **Fazit:** S-08 Slash-Command-Registry ist **direkt machbar**, out-of-the-box. Kein Hack, kein Upstream-Change nötig. --- ### 3.3 Conversation Tree **Was existiert:** ```typescript // Quelle: dist/core/extensions/types.d.ts, Zeile 87 ctx.sessionManager: ReadonlySessionManager; ``` **Relevante Methoden (dist/core/session-manager.d.ts):** ```typescript interface ReadonlySessionManager { getBranch(fromId?: string): SessionEntry[]; getTree(): SessionTreeNode[]; getLeafId(): string | null; getEntry(id: string): SessionEntry | undefined; getChildren(parentId: string): SessionEntry[]; getSessionName(): string | undefined; // ... } ``` **Tree-Structure:** ```typescript // Quelle: session-manager.d.ts, Zeile 74-81 export interface SessionTreeNode { entry: SessionEntry; children: SessionTreeNode[]; /** Resolved label for this entry, if any */ label?: string; /** Timestamp of the latest label change for this entry, if any */ labelTimestamp?: string; } ``` **SessionEntry (relevant für Tree-State):** ```typescript // session-manager.d.ts, Zeile 16-27 export interface SessionEntryBase { type: string; id: string; parentId: string | null; timestamp: string; } export interface SessionMessageEntry extends SessionEntryBase { type: "message"; message: AgentMessage; } ``` **Beispiel für S-13 Tree-State-Endpoint:** ```typescript pi.on("session_start", async (event, ctx) => { const tree = ctx.sessionManager.getTree(); const currentLeafId = ctx.sessionManager.getLeafId(); // Tree als JSON für iOS-App serialisieren: function serializeTree(node: SessionTreeNode) { const entry = node.entry; let summary = ""; let msgCount = 0; if (entry.type === "message") { const msg = (entry as SessionMessageEntry).message; if (msg.role === "user") { const text = typeof msg.content === "string" ? msg.content : msg.content.filter(c => c.type === "text").map(c => c.text).join(""); summary = text.slice(0, 50); } } // Count messages in subtree const countMessages = (n: SessionTreeNode): number => { let count = n.entry.type === "message" ? 1 : 0; for (const child of n.children) count += countMessages(child); return count; }; msgCount = countMessages(node); return { id: entry.id, parent: entry.parentId, summary, messageCount: msgCount, createdAt: entry.timestamp, children: node.children.map(serializeTree) }; } const treeData = { type: "tree", nodes: tree.map(serializeTree), current: currentLeafId }; // Broadcast oder per REST-Endpoint abrufbar machen broadcast(treeData); }); ``` **Live-Updates (Subscribe-Problem):** Es gibt **kein** `on("tree_update")` Event. Tree-Mutationen passieren nur via: - `/fork` → triggert `session_before_fork` + `session_tree` - `/new` → triggert `session_before_switch` - `/compact` → triggert `session_compact` - Checkout via `/tree` → triggert `session_tree` **Events für Tree-Tracking:** ```typescript // Quelle: types.d.ts, Zeile 177-189 pi.on("session_tree", async (event, ctx) => { // event = { // newLeafId: string | null, // oldLeafId: string | null, // summaryEntry?: BranchSummaryEntry, // fromExtension?: boolean // } // Tree hat sich geändert → re-serialize und broadcasten const updatedTree = serializeTree(ctx.sessionManager.getTree()); broadcast({ type: "tree", ...updatedTree }); }); pi.on("session_compact", async (event, ctx) => { // Compaction hat Tree verändert → Update broadcasten const updatedTree = serializeTree(ctx.sessionManager.getTree()); broadcast({ type: "tree", ...updatedTree }); }); ``` **Code-Beleg (bestehende Extension nutzt getBranch):** ```typescript // pi-remote-control/extensions/remote-control/messages.ts, Zeile 66 const branch = ctx.sessionManager.getBranch(); ``` **Fazit:** S-13 Tree-State ist **machbar mit Event-basiertem Push**. Kein echtes Subscribe-Pattern, aber ausreichend für die Spec. Tree-Read ist trivial, Live-Updates erfordern Event-Handling (`session_tree`, `session_compact`). Kein Upstream-Change nötig. --- ### 3.4 Programmatic Prompt-Dispatch **Was existiert:** ```typescript // Quelle: types.d.ts, Zeile 758-763 pi.sendUserMessage( content: string | (TextContent | ImageContent)[], options?: { deliverAs?: "steer" | "followUp" } ): void; ``` **Was NICHT existiert:** ```typescript // ❌ NICHT in der ExtensionAPI exposed: pi.prompt(text: string): void; // würde Slash-Commands dispatchen ``` **Problem (aus früherer Recherche bestätigt):** `pi.sendUserMessage()` ruft intern `session.prompt()` mit `expandPromptTemplates: false` auf, was Slash-Command-Processing überspringt. Slash-Commands wie `/fork`, `/new`, `/compact` werden als **literaler Text an den LLM gesendet**, nicht als Befehle ausgeführt. **Code-Beleg (aus vorheriger Session):** > Wir hatten in der vorherigen Session festgestellt: `pi.sendUserMessage()` explicitly sets `expandPromptTemplates: false`, bypassing slash command processing. `session.prompt()` (which handles slash commands) is not exposed in `ExtensionAPI`. **Workaround-Optionen:** #### Option A: Hack via `(pi as any)` ```typescript // FRAGIL: greift auf interne APIs zu, kann bei Updates brechen pi.on("some_event", async (event, ctx) => { // @ts-ignore const runtime = (pi as any).runtime; if (runtime && runtime.session) { await runtime.session.prompt("/fork abc123"); } }); ``` **Risiken:** - ❌ Keine TypeScript-Typen - ❌ Bricht bei internen Refactorings - ❌ `runtime` könnte `undefined` sein je nach Timing - ❌ Nicht dokumentiert, keine Garantie #### Option B: Re-Implement Commands lokal ```typescript // iOS-App sendet Command-Request // Sidecar implementiert /fork, /new, /compact direkt via ExtensionCommandContext pi.registerCommand("ios-fork", { description: "Fork from iOS app", handler: async (args, ctx) => { // args = entryId await ctx.fork(args, { position: "at" }); } }); // iOS schickt dann via WebSocket: // { type: "command", name: "ios-fork", args: "abc123" } // Sidecar ruft pi-internen Command-Handler auf ``` **Problem:** Geht nur in interactive mode, nicht in RPC/print mode. Außerdem muss die Extension selbst die Commands implementieren, was dupliziert Code. #### Option C: Upstream-Feature-Request ```typescript // Gewünschte API: pi.prompt(text: string): Promise; // oder pi.executeCommand(commandName: string, args: string): Promise; ``` **Fazit:** Gruppe T Tree-Navigation via **Slash-Command-Injection ist NICHT machbar** ohne Hack. Option A (`(pi as any)`) funktioniert vermutlich, ist aber fragil. Option B (Re-Implement) ist sauberer, aber aufwändiger. Option C (Upstream) ist langfristig richtig, aber blockiert die iOS-App. **Empfehlung:** Für MVP: **Option B** (Re-Implement Commands in Extension). Für v2: **Upstream-Feature-Request für `pi.prompt()`**. --- ### 3.5 Tool-Call-Daten **Was existiert:** ```typescript // Quelle: types.d.ts, Zeile 261-278 pi.on("tool_execution_start", async (event, ctx) => { // event = { // type: "tool_execution_start", // toolCallId: string, // toolName: string, // args: any // } }); pi.on("tool_execution_end", async (event, ctx) => { // event = { // type: "tool_execution_end", // toolCallId: string, // toolName: string, // result: any, // NICHT truncated! // isError: boolean // } }); ``` **Code-Beleg (bestehende Extension nutzt das bereits):** ```typescript // pi-remote-control/extensions/remote-control/index.ts, Zeile 103-127 pi.on("tool_execution_start", async (event) => { server?.broadcast({ type: "tool_start", toolCallId: event.toolCallId, toolName: event.toolName, args: event.args, }); }); pi.on("tool_execution_end", async (event) => { type TextContent = { type: string; text: string }; type ToolResult = { content?: TextContent[] } | string; const result = event.result as ToolResult; const content = typeof result === "object" ? result.content : undefined; const resultText = Array.isArray(content) ? content .filter((c) => c.type === "text") .map((c) => c.text) .join("") : typeof result === "string" ? result : ""; server?.broadcast({ type: "tool_end", toolCallId: event.toolCallId, result: resultText, isError: event.isError, }); }); ``` **Fazit:** Tool-Call-Daten sind **vollständig verfügbar** für S-07. Name, Args, Result (ungekürzt), Error-Status — alles da. Trivial implementierbar. --- ### 3.6 Sonstiges (Bonus) **Weitere nützliche Capabilities, die wir ggf. übersehen haben:** #### Model-Info ```typescript // Quelle: types.d.ts, Zeile 90 ctx.model: Model | undefined; // Beispiel: pi.on("model_select", async (event, ctx) => { broadcast({ type: "model", model: { id: event.model.id, name: event.model.name, provider: event.model.provider } }); }); ``` **Use-Case für iOS-App:** Session-Switcher (iOS-D-01) könnte pro Session anzeigen, welches Modell verwendet wurde. #### CWD-Tracking ```typescript // Quelle: types.d.ts, Zeile 85 ctx.cwd: string; ``` **Use-Case:** Session-Metadaten (S-09) könnte CWD pro Session exposen. #### Abort-Signal ```typescript // Quelle: types.d.ts, Zeile 95 ctx.abort(): void; ctx.signal: AbortSignal | undefined; ``` **Use-Case:** iOS-App könnte einen "Stop"-Button haben, der `abort()` triggert via WebSocket-Control-Frame. #### Context-Usage ```typescript // Quelle: types.d.ts, Zeile 102 ctx.getContextUsage(): ContextUsage | undefined; interface ContextUsage { tokens: number | null; contextWindow: number; percent: number | null; } ``` **Use-Case:** Status-Bar (iOS-C-01) könnte "Context: 45%" anzeigen. --- ## 4. Empfehlung pro Spec-Feature | Spec-Feature | Status | Empfehlung | Begründung | |---|---|---|---| | **S-07 State Side-Channel** | ✅ Machbar | Implementieren | Events ausreichend granular, `isIdle()` + `agent_end` = awaiting-input | | **S-08 Slash-Command-Registry** | ✅ Machbar | Implementieren | `pi.getCommands()` out-of-the-box | | **S-13 Tree-State Side-Channel** | ⚠️ Machbar | Implementieren mit Events | Tree-Read trivial, Live-Updates via `session_tree` + `session_compact` | | **Gruppe T Tree-Navigation** | ❌ Hack nötig | **Nicht empfohlen für MVP** | Slash-Command-Injection geht nicht. Option B (Re-Implement) aufwändig, Option A (Hack) fragil. Upstream-Request für v2. | --- ## 5. Vorgeschlagene Upstream-Änderungen ### 5.1 `pi.prompt()` für Slash-Command-Dispatch **Problem:** Extensions können User-Messages senden (`pi.sendUserMessage()`), aber keine Slash-Commands programmatisch dispatchen. Das limitiert iOS-App-Features wie Tree-Navigation (Gruppe T), weil `/fork`, `/new`, `/compact` nur als Text an den LLM gehen, nicht ausgeführt werden. **Vorschlag:** ```typescript pi.prompt(text: string, options?: { expandPromptTemplates?: boolean }): Promise; ``` **Begründung:** - Extensions die als "Remote-Control" fungieren (RPC, iOS-App, Web-UI) brauchen die Möglichkeit, Slash-Commands im Namen des Users auszuführen. - Bestehende `pi.sendUserMessage()` umgeht absichtlich Template-Expansion (vermutlich für Safety). Ein explizites `pi.prompt()` signalisiert Intent. - Alternative: `pi.executeCommand(name, args)` wäre noch expliziter, aber weniger flexibel. **Implementierung (Skizze):** ```typescript // In extensions/wrapper.ts api.prompt = async (text: string, options = {}) => { runtime.assertActive(); const { expandPromptTemplates = true } = options; // Ruft die interne session.prompt() auf, die Slash-Commands dispatched await runtime.session.prompt(text, { expandPromptTemplates }); }; ``` **Impact:** Niedrig. Nur eine neue Methode in der `ExtensionAPI`, delegiert an existierende `session.prompt()`. --- ### 5.2 `on("awaiting_input")` Event **Problem:** Um iOS-C-02 (Push-Notification "Pi ist fertig") sauber zu implementieren, brauchen wir ein explizites Event statt Ableitung via `agent_end` + `ctx.isIdle()`. **Vorschlag:** ```typescript pi.on("awaiting_input", async (event, ctx) => { // event = { type: "awaiting_input" } }); ``` **Begründung:** - Explizit ist besser als implizit. `agent_end` + `isIdle()` funktioniert, ist aber umständlich und fehleranfällig (z.B. wenn Follow-Up-Messages pending sind). - Semantisch klarer: "Pi wartet auf User-Input" ist ein State, kein abgeleitetes Konstrukt. **Implementierung (Skizze):** ```typescript // In core/agent-session.ts, nach dem agent loop: if (this.isIdle() && !this.hasPendingMessages()) { await this.eventBus.emit("awaiting_input", {}, this.buildContext()); } ``` **Impact:** Sehr niedrig. Ein neues Event, keine Breaking-Changes. --- ### 5.3 Tree-Subscribe-Pattern (Optional) **Problem:** `session_tree` + `session_compact` Events decken die meisten Tree-Mutations ab, aber nicht alle (z.B. neue Messages werden per `message_end` gemeldet, nicht als Tree-Mutation). **Vorschlag:** ```typescript pi.on("tree_change", async (event, ctx) => { // event = { // type: "tree_change", // operation: "append" | "branch" | "compact" | "fork", // entryId: string, // parentId: string | null // } }); ``` **Begründung:** - Vereinheitlicht alle Tree-Mutations in einem Event. - Extensions müssen nicht mehrere Events tracken (`message_end`, `session_tree`, `session_compact`). **Implementierung:** Moderater Aufwand, weil es alle Append-Operationen tracken muss. **Impact:** Niedrig, optional. Die bestehenden Events reichen aus, aber das wäre cleaner. **Status:** **NICE-TO-HAVE**, nicht kritisch für die iOS-App. --- ## 6. Zusammenfassung: Implementierungs-Roadmap für Sidecar ### Phase 1: S-07 State Side-Channel (MUST) **Code-Änderungen (pi-remote-control/extensions/remote-control/server.ts):** ```typescript interface StateMessage { type: "state"; value: "thinking" | "tool" | "idle" | "awaiting-input"; tool?: string; ts: number; } let currentState: StateMessage["value"] = "idle"; pi.on("agent_start", async () => { currentState = "thinking"; server?.broadcast({ type: "state", value: "thinking", ts: Date.now() }); }); pi.on("tool_execution_start", async (event) => { currentState = "tool"; server?.broadcast({ type: "state", value: "tool", tool: event.toolName, ts: Date.now() }); }); pi.on("tool_execution_end", async () => { currentState = "thinking"; server?.broadcast({ type: "state", value: "thinking", ts: Date.now() }); }); pi.on("agent_end", async (event, ctx) => { if (ctx.isIdle()) { currentState = "awaiting-input"; server?.broadcast({ type: "state", value: "awaiting-input", ts: Date.now() }); } }); ``` **Aufwand:** 1-2 Stunden --- ### Phase 2: S-08 Slash-Command-Registry (SHOULD) **Code-Änderungen (neuer Endpoint):** ```typescript // In server.ts, HTTP-Handler erweitern: if (pathname === "/commands") { const commands = pi.getCommands(); const payload = commands.map(c => ({ name: c.name, description: c.description, source: c.source })); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(payload)); return; } ``` **Aufwand:** 30 Minuten --- ### Phase 3: S-13 Tree-State Side-Channel (SHOULD) **Code-Änderungen (neuer Endpoint + Events):** ```typescript // Helper für Tree-Serialisierung function serializeTree(ctx: ExtensionContext) { const tree = ctx.sessionManager.getTree(); const leafId = ctx.sessionManager.getLeafId(); function mapNode(node: SessionTreeNode) { let summary = ""; let msgCount = 0; // Entry-Type-spezifische Logik if (node.entry.type === "message") { const msg = (node.entry as any).message; if (msg.role === "user") { const text = typeof msg.content === "string" ? msg.content : msg.content.filter(c => c.type === "text").map(c => c.text).join(""); summary = text.slice(0, 50); } } // Count messages recursive const count = (n: SessionTreeNode): number => { let c = n.entry.type === "message" ? 1 : 0; for (const child of n.children) c += count(child); return c; }; msgCount = count(node); return { id: node.entry.id, parent: node.entry.parentId, summary, messageCount: msgCount, createdAt: node.entry.timestamp, children: node.children.map(mapNode) }; } return { type: "tree", nodes: tree.map(mapNode), current: leafId }; } // REST-Endpoint if (pathname === "/tree") { const treeData = serializeTree(ctx); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(treeData)); return; } // Live-Updates via Events pi.on("session_tree", async (event, ctx) => { const treeData = serializeTree(ctx); server?.broadcast(treeData); }); pi.on("session_compact", async (event, ctx) => { const treeData = serializeTree(ctx); server?.broadcast(treeData); }); pi.on("message_end", async (event, ctx) => { // Neue Message = Tree-Mutation const treeData = serializeTree(ctx); server?.broadcast(treeData); }); ``` **Aufwand:** 2-3 Stunden --- ### Phase 4: Gruppe T — Tree-Navigation (CONDITIONAL) **Ohne Upstream-Change: Option B (Re-Implement)** ```typescript pi.registerCommand("ios-fork", { description: "Fork from iOS app (internal)", handler: async (args, ctx) => { const entryId = args.trim(); await ctx.fork(entryId, { position: "at" }); } }); pi.registerCommand("ios-new", { description: "New session from iOS app (internal)", handler: async (args, ctx) => { await ctx.newSession(); } }); pi.registerCommand("ios-compact", { description: "Compact from iOS app (internal)", handler: async (args, ctx) => { ctx.compact(); } }); // WebSocket-Handler für iOS-Command-Requests ws.on("message", (data) => { const msg = JSON.parse(data.toString()); if (msg.type === "execute_command") { // iOS schickt: { type: "execute_command", name: "ios-fork", args: "abc123" } const command = registeredCommands.get(msg.name); if (command) { command.handler(msg.args, ctx); } } }); ``` **Aufwand:** 3-4 Stunden (inkl. Testing) **Alternative mit Upstream-Change:** Sobald `pi.prompt()` verfügbar: ```typescript ws.on("message", (data) => { const msg = JSON.parse(data.toString()); if (msg.type === "slash_command") { // iOS schickt: { type: "slash_command", text: "/fork abc123" } await pi.prompt(msg.text); } }); ``` **Aufwand:** 30 Minuten --- ## 7. Risiken & Offene Punkte ### R-1: Timing-Race-Condition bei State-Ableitung **Problem:** `agent_end` + `ctx.isIdle()` könnte in Randfällen falsch sein (z.B. wenn zwischen Event-Emit und `isIdle()`-Check ein Follow-Up-Message queued wurde). **Mitigation:** Upstream-Event `awaiting_input` (siehe 5.2) wäre sauberer. Für MVP akzeptabel, für Production ggf. nachbessern. --- ### R-2: Tree-Serialisierung bei großen Sessions **Problem:** Bei sehr langen Sessions (> 1000 Entries) kann `getTree()` + Serialisierung langsam werden. **Mitigation:** - Lazy-Loading: iOS-App ruft nur Subtrees ab (via `ctx.sessionManager.getChildren(parentId)`). - Caching: Sidecar cached serialisiertes Tree und invalidiert nur bei Tree-Events. --- ### R-3: Slash-Command-Hack (`(pi as any)`) bricht bei Updates **Problem:** Wenn wir Option A (Hack) für Gruppe T nutzen, kann das bei pi-Updates brechen. **Mitigation:** - Tests einbauen die warnen wenn der Hack bricht. - Upstream-Feature-Request parallel verfolgen. - Für MVP Option B (Re-Implement) bevorzugen. --- ### R-4: ExtensionAPI-Version-Lock **Problem:** Wir auditieren gegen pi v0.74.0. Künftige Versionen könnten API-Changes haben. **Mitigation:** - In `package.json` der Extension `peerDependencies` festlegen: `"@earendil-works/pi-coding-agent": "^0.74.0"`. - Bei pi-Updates: Re-Audit durchführen. --- ## 8. Fazit & Next Steps **Für MVP (Phase 0):** - ✅ S-07, S-08, S-13 sind **implementierbar ohne Upstream-Changes** - ⚠️ Gruppe T (Tree-Navigation) **nicht empfohlen für MVP** (Hack fragil, Re-Implement aufwändig) - → **Empfehlung:** MVP ohne Gruppe T. Tree-View als read-only (S-13), Navigation bleibt über SSH-Terminal. **Für v2 (nach Upstream-Discussion):** - Feature-Request für `pi.prompt()` stellen (siehe 5.1) - Optional: `awaiting_input` Event (siehe 5.2) - Gruppe T dann sauber implementieren **Nächste Schritte:** 1. Spec v3 schreiben mit finalen Feature-Entscheidungen 2. Phase 0 (Stream-PoC) starten — unabhängig von diesem Audit 3. Phase 1 (Sidecar mit S-07, S-08, S-13) parallel entwickeln 4. iOS-App-Entwicklung kann starten sobald Phase 1 läuft --- **Audit abgeschlossen.**