From 902e3f56d11488336ecb6f1cf325a3d28e976a7c Mon Sep 17 00:00:00 2001 From: Artem Khoroshilov Date: Fri, 30 Jan 2026 04:59:25 +0200 Subject: [PATCH] fix(agents): initialize ExtensionRunner in embedded mode (#2027) In pi-coding-agent, ExtensionRunner defaults ctx.model to undefined until extensionRunner.initialize() is called. Moltbot uses the SDK in embedded mode (no interactive UI), so compaction hooks that rely on ctx.model (e.g. compaction-safeguard) would fall back to "Summary unavailable..." and history would be truncated without a real summary. --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/run/attempt.ts | 75 +++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a134359f5..a0598df2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Status: beta. - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes +- Agents: initialize ExtensionRunner in embedded mode so compaction hooks produce real summaries instead of "Summary unavailable...". (#2027) - Security: harden SSH tunnel target parsing to prevent option injection/DoS. (#4001) Thanks @YLChen-007. - Security: prevent PATH injection in exec sandbox; harden file serving; pin DNS in URL fetches; verify Twilio webhooks; fix LINE webhook timing-attack edge case; validate Tailscale Serve identity; flag loopback Control UI with auth disabled as critical. (#1616, #1795) - Gateway: prevent crashes on transient network errors, suppress AbortError/unhandled rejections, sanitize error responses, clean session locks on exit, and harden reverse proxy handling for unauthenticated proxied connects. (#2980, #2451, #2483, #1795) diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 46a53bd8f..a5d2a2f6f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -4,7 +4,12 @@ import os from "node:os"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai"; import { streamSimple } from "@mariozechner/pi-ai"; -import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent"; +import { + createAgentSession, + SessionManager, + SettingsManager, + type CompactOptions, +} from "@mariozechner/pi-coding-agent"; import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js"; import { @@ -492,6 +497,74 @@ export async function runEmbeddedAttempt( // Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai. activeSession.agent.streamFn = streamSimple; + // IMPORTANT (embedded mode): initialize the pi extension runner. + // + // In pi-coding-agent, ExtensionRunner defaults `ctx.model` to `undefined` until + // `extensionRunner.initialize(...)` is called. Moltbot uses the SDK in embedded + // mode (no interactive UI), so without this initialization compaction hooks that + // rely on `ctx.model` (e.g. `compaction-safeguard`) will always fall back to + // "Summary unavailable..." and history will be truncated without a summary. + const extensionRunner = activeSession.extensionRunner; + if (extensionRunner) { + extensionRunner.initialize( + { + // Embedded runs don't have a UI; keep UI-dependent actions as safe no-ops. + sendMessage: async () => {}, + sendUserMessage: async () => {}, + appendEntry: () => {}, + setSessionName: () => {}, + getSessionName: () => undefined, + setLabel: () => {}, + getActiveTools: () => + (activeSession.agent.state.tools ?? []) + .map((tool) => { + if (!tool) return undefined; + if (typeof tool === "string") return tool; + if (typeof tool === "object" && "name" in tool) { + const name = (tool as { name?: unknown }).name; + return typeof name === "string" ? name : undefined; + } + return undefined; + }) + .filter((name): name is string => typeof name === "string"), + getAllTools: () => [], + setActiveTools: () => {}, + setModel: async (model) => { + const key = await activeSession.modelRegistry.getApiKey(model); + if (!key) return false; + await activeSession.setModel(model); + return true; + }, + getThinkingLevel: () => activeSession.thinkingLevel, + setThinkingLevel: (level) => { + activeSession.setThinkingLevel(level); + }, + }, + { + // Provide live model + context access for extensions. + getModel: () => activeSession.agent.state.model, + isIdle: () => true, + abort: () => { + void activeSession.abort(); + }, + hasPendingMessages: () => false, + shutdown: () => {}, + getContextUsage: () => activeSession.getContextUsage(), + compact: (options?: CompactOptions) => { + void (async () => { + try { + const result = await activeSession.compact(options?.customInstructions); + options?.onComplete?.(result); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + options?.onError?.(err); + } + })(); + }, + }, + ); + } + applyExtraParamsToAgent( activeSession.agent, params.config,