27 KiB
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-inputEvent, 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:
// 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-inputEvent. Das nächstbeste istagent_end, welches signalisiert dass der Agent-Loop beendet ist. Kombiniert mitctx.isIdle()kann man ableiten, ob Pi auf User-Input wartet. - ❌ Kein
thinkingvs.idleState-Enum. Muss selbst aus Events abgeleitet werden.
Ableitung des Pi-States für S-07:
// 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):
// 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:
// Quelle: dist/core/extensions/types.d.ts, Zeile 772
pi.getCommands(): SlashCommandInfo[];
Return-Type:
// Quelle: dist/core/slash-commands.d.ts, Zeile 1-9
export interface SlashCommandInfo {
name: string;
description?: string;
source: "extension" | "prompt" | "skill";
sourceInfo: SourceInfo;
}
Beispiel:
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/<id>/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:
// Quelle: dist/core/extensions/types.d.ts, Zeile 87
ctx.sessionManager: ReadonlySessionManager;
Relevante Methoden (dist/core/session-manager.d.ts):
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:
// 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):
// 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:
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→ triggertsession_before_fork+session_tree/new→ triggertsession_before_switch/compact→ triggertsession_compact- Checkout via
/tree→ triggertsession_tree
Events für Tree-Tracking:
// 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):
// 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:
// Quelle: types.d.ts, Zeile 758-763
pi.sendUserMessage(
content: string | (TextContent | ImageContent)[],
options?: { deliverAs?: "steer" | "followUp" }
): void;
Was NICHT existiert:
// ❌ 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 setsexpandPromptTemplates: false, bypassing slash command processing.session.prompt()(which handles slash commands) is not exposed inExtensionAPI.
Workaround-Optionen:
Option A: Hack via (pi as any)
// 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
- ❌
runtimekönnteundefinedsein je nach Timing - ❌ Nicht dokumentiert, keine Garantie
Option B: Re-Implement Commands lokal
// 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
// Gewünschte API:
pi.prompt(text: string): Promise<void>;
// oder
pi.executeCommand(commandName: string, args: string): Promise<void>;
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:
// 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):
// 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
// Quelle: types.d.ts, Zeile 90
ctx.model: Model<any> | 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
// Quelle: types.d.ts, Zeile 85
ctx.cwd: string;
Use-Case: Session-Metadaten (S-09) könnte CWD pro Session exposen.
Abort-Signal
// 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
// 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:
pi.prompt(text: string, options?: {
expandPromptTemplates?: boolean
}): Promise<void>;
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 explizitespi.prompt()signalisiert Intent. - Alternative:
pi.executeCommand(name, args)wäre noch expliziter, aber weniger flexibel.
Implementierung (Skizze):
// 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:
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):
// 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:
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):
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):
// 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):
// 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)
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:
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.jsonder ExtensionpeerDependenciesfestlegen:"@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_inputEvent (siehe 5.2) - Gruppe T dann sauber implementieren
Nächste Schritte:
- Spec v3 schreiben mit finalen Feature-Entscheidungen
- Phase 0 (Stream-PoC) starten — unabhängig von diesem Audit
- Phase 1 (Sidecar mit S-07, S-08, S-13) parallel entwickeln
- iOS-App-Entwicklung kann starten sobald Phase 1 läuft
Audit abgeschlossen.