From c91ec2aab7a593e45c614ef487ea776b3e51abb8 Mon Sep 17 00:00:00 2001 From: Adam Holt Date: Sat, 10 Jan 2026 12:52:54 +1300 Subject: [PATCH 1/2] fix: only inject heartbeat prompt for default agent The heartbeat prompt from agents.defaults.heartbeat.prompt was being injected into the system prompt for ALL agents, causing non-default agents to read the default agent's identity files and adopt its persona. Now the heartbeat prompt is only included when the session's agent ID matches the configured default agent. Other agents receive no heartbeat section in their system prompt. Co-Authored-By: Claude Opus 4.5 --- src/agents/pi-embedded-runner.ts | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 5baf03776..b3b604cfc 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -33,9 +33,11 @@ import { type enqueueCommand, enqueueCommandInLane, } from "../process/command-queue.js"; +import { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; import { normalizeMessageProvider } from "../utils/message-provider.js"; import { resolveUserPath } from "../utils.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; +import { resolveDefaultAgentId } from "./agent-scope.js"; import { markAuthProfileFailure, markAuthProfileGood, @@ -882,15 +884,21 @@ export async function compactEmbeddedPiSession(params: { params.config?.agents?.defaults?.userTimezone, ); const userTime = formatUserTime(new Date(), userTimezone); + // Only include heartbeat prompt for the default agent + const sessionAgentId = resolveAgentIdFromSessionKey(params.sessionKey); + const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); + const isDefaultAgent = sessionAgentId === defaultAgentId; const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, defaultThinkLevel: params.thinkLevel, extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, reasoningTagHint, - heartbeatPrompt: resolveHeartbeatPrompt( - params.config?.agents?.defaults?.heartbeat?.prompt, - ), + heartbeatPrompt: isDefaultAgent + ? resolveHeartbeatPrompt( + params.config?.agents?.defaults?.heartbeat?.prompt, + ) + : undefined, skillsPrompt, runtimeInfo, sandboxInfo, @@ -1245,15 +1253,23 @@ export async function runEmbeddedPiAgent(params: { params.config?.agents?.defaults?.userTimezone, ); const userTime = formatUserTime(new Date(), userTimezone); + // Only include heartbeat prompt for the default agent + const sessionAgentId = resolveAgentIdFromSessionKey( + params.sessionKey, + ); + const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); + const isDefaultAgent = sessionAgentId === defaultAgentId; const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, defaultThinkLevel: thinkLevel, extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, reasoningTagHint, - heartbeatPrompt: resolveHeartbeatPrompt( - params.config?.agents?.defaults?.heartbeat?.prompt, - ), + heartbeatPrompt: isDefaultAgent + ? resolveHeartbeatPrompt( + params.config?.agents?.defaults?.heartbeat?.prompt, + ) + : undefined, skillsPrompt, runtimeInfo, sandboxInfo, From 43975a39dc9910407acf713fa050c33a453b7512 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 01:30:45 +0100 Subject: [PATCH 2/2] fix: gate heartbeat prompt to default agent sessions (#630) (thanks @adam91holt) --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner.test.ts | 34 +++++++++++++++++++++++++++ src/agents/pi-embedded-runner.ts | 32 +++++++++++++++++++------ 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba7bd9fd0..5fdb94ec9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Agents: gate heartbeat prompt to default agent sessions (including non-agent session keys). (#630) — thanks @adam91holt - Agent: fast abort on /stop and cancel tool calls between tool boundaries. (#617) - Models/Auth: add OpenCode Zen (multi-model proxy) onboarding. (#623) — thanks @magimetal - WhatsApp: refactor vCard parsing helper and improve empty contact card summaries. (#624) — thanks @steipete diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index fb102092e..f44a337c6 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -2,10 +2,12 @@ import type { AgentMessage, AgentTool } from "@mariozechner/pi-agent-core"; import { SessionManager } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { describe, expect, it, vi } from "vitest"; +import type { ClawdbotConfig } from "../config/config.js"; import { applyGoogleTurnOrderingFix, buildEmbeddedSandboxInfo, createSystemPromptOverride, + resolveSessionAgentIds, splitSdkTools, } from "./pi-embedded-runner.js"; import type { SandboxContext } from "./sandbox.js"; @@ -57,6 +59,38 @@ describe("buildEmbeddedSandboxInfo", () => { }); }); +describe("resolveSessionAgentIds", () => { + const cfg = { + agents: { + list: [{ id: "main" }, { id: "beta", default: true }], + }, + } as ClawdbotConfig; + + it("falls back to the configured default when sessionKey is missing", () => { + const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ + config: cfg, + }); + expect(defaultAgentId).toBe("beta"); + expect(sessionAgentId).toBe("beta"); + }); + + it("falls back to the configured default when sessionKey is non-agent", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "telegram:slash:123", + config: cfg, + }); + expect(sessionAgentId).toBe("beta"); + }); + + it("uses the agent id from agent session keys", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "agent:main:main", + config: cfg, + }); + expect(sessionAgentId).toBe("main"); + }); +}); + function createStubTool(name: string): AgentTool { return { name, diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index b3b604cfc..7a416c60c 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -33,7 +33,10 @@ import { type enqueueCommand, enqueueCommandInLane, } from "../process/command-queue.js"; -import { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; +import { + normalizeAgentId, + parseAgentSessionKey, +} from "../routing/session-key.js"; import { normalizeMessageProvider } from "../utils/message-provider.js"; import { resolveUserPath } from "../utils.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; @@ -557,6 +560,19 @@ export function buildEmbeddedSandboxInfo( }; } +export function resolveSessionAgentIds(params: { + sessionKey?: string; + config?: ClawdbotConfig; +}): { defaultAgentId: string; sessionAgentId: string } { + const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); + const sessionKey = params.sessionKey?.trim(); + const parsed = sessionKey ? parseAgentSessionKey(sessionKey) : null; + const sessionAgentId = parsed?.agentId + ? normalizeAgentId(parsed.agentId) + : defaultAgentId; + return { defaultAgentId, sessionAgentId }; +} + function buildEmbeddedSystemPrompt(params: { workspaceDir: string; defaultThinkLevel?: ThinkLevel; @@ -885,8 +901,10 @@ export async function compactEmbeddedPiSession(params: { ); const userTime = formatUserTime(new Date(), userTimezone); // Only include heartbeat prompt for the default agent - const sessionAgentId = resolveAgentIdFromSessionKey(params.sessionKey); - const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); + const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ + sessionKey: params.sessionKey, + config: params.config, + }); const isDefaultAgent = sessionAgentId === defaultAgentId; const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace, @@ -1254,10 +1272,10 @@ export async function runEmbeddedPiAgent(params: { ); const userTime = formatUserTime(new Date(), userTimezone); // Only include heartbeat prompt for the default agent - const sessionAgentId = resolveAgentIdFromSessionKey( - params.sessionKey, - ); - const defaultAgentId = resolveDefaultAgentId(params.config ?? {}); + const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ + sessionKey: params.sessionKey, + config: params.config, + }); const isDefaultAgent = sessionAgentId === defaultAgentId; const appendPrompt = buildEmbeddedSystemPrompt({ workspaceDir: effectiveWorkspace,