diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 266ed8a63..b252345ec 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -3,7 +3,11 @@ import path from "node:path"; import type { ClawdbotConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; -import { DEFAULT_AGENT_ID, normalizeAgentId } from "../routing/session-key.js"; +import { + DEFAULT_AGENT_ID, + normalizeAgentId, + parseAgentSessionKey, +} from "../routing/session-key.js"; import { resolveUserPath } from "../utils.js"; import { DEFAULT_AGENT_WORKSPACE_DIR } from "./workspace.js"; @@ -49,6 +53,26 @@ export function resolveDefaultAgentId(cfg: ClawdbotConfig): string { return normalizeAgentId(chosen || DEFAULT_AGENT_ID); } +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 }; +} + +export function resolveSessionAgentId(params: { + sessionKey?: string; + config?: ClawdbotConfig; +}): string { + return resolveSessionAgentIds(params).sessionAgentId; +} + function resolveAgentEntry( cfg: ClawdbotConfig, agentId: string, diff --git a/src/agents/claude-cli-runner.ts b/src/agents/claude-cli-runner.ts index 5497ddb97..b56afd360 100644 --- a/src/agents/claude-cli-runner.ts +++ b/src/agents/claude-cli-runner.ts @@ -9,6 +9,7 @@ import { shouldLogVerbose } from "../globals.js"; import { createSubsystemLogger } from "../logging.js"; import { runCommandWithTimeout } from "../process/exec.js"; import { resolveUserPath } from "../utils.js"; +import { resolveSessionAgentIds } from "./agent-scope.js"; import { FailoverError, resolveFailoverStatus } from "./failover-error.js"; import { buildBootstrapContextFiles, @@ -136,6 +137,7 @@ function buildSystemPrompt(params: { defaultThinkLevel?: ThinkLevel; extraSystemPrompt?: string; ownerNumbers?: string[]; + heartbeatPrompt?: string; tools: AgentTool[]; contextFiles?: EmbeddedContextFile[]; modelDisplay: string; @@ -150,9 +152,7 @@ function buildSystemPrompt(params: { extraSystemPrompt: params.extraSystemPrompt, ownerNumbers: params.ownerNumbers, reasoningTagHint: false, - heartbeatPrompt: resolveHeartbeatPrompt( - params.config?.agents?.defaults?.heartbeat?.prompt, - ), + heartbeatPrompt: params.heartbeatPrompt, runtimeInfo: { host: "clawdbot", os: `${os.type()} ${os.release()}`, @@ -374,12 +374,23 @@ export async function runClaudeCliAgent(params: { params.sessionKey ?? params.sessionId, ); const contextFiles = buildBootstrapContextFiles(bootstrapFiles); + const { defaultAgentId, sessionAgentId } = resolveSessionAgentIds({ + sessionKey: params.sessionKey, + config: params.config, + }); + const heartbeatPrompt = + sessionAgentId === defaultAgentId + ? resolveHeartbeatPrompt( + params.config?.agents?.defaults?.heartbeat?.prompt, + ) + : undefined; const systemPrompt = buildSystemPrompt({ workspaceDir, config: params.config, defaultThinkLevel: params.thinkLevel, extraSystemPrompt, ownerNumbers: params.ownerNumbers, + heartbeatPrompt, tools: [], contextFiles, modelDisplay, diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index f44a337c6..6d8a5f213 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -3,11 +3,11 @@ 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 { resolveSessionAgentIds } from "./agent-scope.js"; import { applyGoogleTurnOrderingFix, buildEmbeddedSandboxInfo, createSystemPromptOverride, - resolveSessionAgentIds, splitSdkTools, } from "./pi-embedded-runner.js"; import type { SandboxContext } from "./sandbox.js"; @@ -82,6 +82,22 @@ describe("resolveSessionAgentIds", () => { expect(sessionAgentId).toBe("beta"); }); + it("falls back to the configured default for global sessions", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "global", + config: cfg, + }); + expect(sessionAgentId).toBe("beta"); + }); + + it("keeps the agent id for provider-qualified agent sessions", () => { + const { sessionAgentId } = resolveSessionAgentIds({ + sessionKey: "agent:beta:slack:channel:C1", + config: cfg, + }); + expect(sessionAgentId).toBe("beta"); + }); + it("uses the agent id from agent session keys", () => { const { sessionAgentId } = resolveSessionAgentIds({ sessionKey: "agent:main:main", diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index f2fc0ae35..d3a73a002 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -33,14 +33,10 @@ import { type enqueueCommand, enqueueCommandInLane, } from "../process/command-queue.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"; -import { resolveDefaultAgentId } from "./agent-scope.js"; +import { resolveSessionAgentIds } from "./agent-scope.js"; import { markAuthProfileFailure, markAuthProfileGood, @@ -560,19 +556,6 @@ 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; diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index 9370dfc58..c6dbfac00 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -5,8 +5,8 @@ import { fileURLToPath } from "node:url"; import { resolveAgentConfig, resolveAgentDir, - resolveAgentIdFromSessionKey, resolveAgentWorkspaceDir, + resolveSessionAgentId, } from "../agents/agent-scope.js"; import { resolveModelRefFromString } from "../agents/model-selection.js"; import { @@ -250,7 +250,10 @@ export async function getReplyFromConfig( configOverride?: ClawdbotConfig, ): Promise { const cfg = configOverride ?? loadConfig(); - const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey); + const agentId = resolveSessionAgentId({ + sessionKey: ctx.SessionKey, + config: cfg, + }); const agentCfg = cfg.agents?.defaults; const sessionCfg = cfg.session; const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({ diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index f06948626..3543cf739 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -1,3 +1,4 @@ +import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { abortEmbeddedPiRun } from "../../agents/pi-embedded.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { @@ -7,10 +8,7 @@ import { saveSessionStore, type SessionEntry, } from "../../config/sessions.js"; -import { - parseAgentSessionKey, - resolveAgentIdFromSessionKey, -} from "../../routing/session-key.js"; +import { parseAgentSessionKey } from "../../routing/session-key.js"; import { resolveCommandAuthorization } from "../command-auth.js"; import { normalizeCommandBody, @@ -80,9 +78,10 @@ export async function tryFastAbortFromMessage(params: { if (!auth.isAuthorizedSender) return { handled: false, aborted: false }; const targetKey = resolveAbortTargetKey(ctx); - const agentId = resolveAgentIdFromSessionKey( - targetKey ?? ctx.SessionKey ?? "", - ); + const agentId = resolveSessionAgentId({ + sessionKey: targetKey ?? ctx.SessionKey ?? "", + config: cfg, + }); const raw = stripStructuralPrefixes(ctx.Body ?? ""); const isGroup = ctx.ChatType?.trim().toLowerCase() === "group"; const stripped = isGroup ? stripMentions(raw, ctx, cfg, agentId) : raw; diff --git a/src/auto-reply/reply/commands.ts b/src/auto-reply/reply/commands.ts index fb2f01aeb..3cd6a060f 100644 --- a/src/auto-reply/reply/commands.ts +++ b/src/auto-reply/reply/commands.ts @@ -1,6 +1,7 @@ import { resolveAgentDir, resolveDefaultAgentId, + resolveSessionAgentId, } from "../../agents/agent-scope.js"; import { ensureAuthProfileStore, @@ -26,7 +27,6 @@ import { unsetConfigOverride, } from "../../config/runtime-overrides.js"; import { - resolveAgentIdFromSessionKey, resolveSessionFilePath, type SessionEntry, type SessionScope, @@ -148,7 +148,7 @@ export async function buildStatusReply(params: { return undefined; } const statusAgentId = sessionKey - ? resolveAgentIdFromSessionKey(sessionKey) + ? resolveSessionAgentId({ sessionKey, config: cfg }) : resolveDefaultAgentId(cfg); const statusAgentDir = resolveAgentDir(cfg, statusAgentId); let usageLine: string | null = null; diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index fb5479da6..37f7e6698 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -2,6 +2,7 @@ import { resolveAgentConfig, resolveAgentDir, resolveDefaultAgentId, + resolveSessionAgentId, } from "../../agents/agent-scope.js"; import { isProfileInCooldown, @@ -31,7 +32,6 @@ import { import { resolveSandboxConfigForAgent } from "../../agents/sandbox.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { - resolveAgentIdFromSessionKey, resolveAgentMainSessionKey, type SessionEntry, saveSessionStore, @@ -491,14 +491,18 @@ export async function handleDirectiveOnly(params: { currentReasoningLevel, currentElevatedLevel, } = params; - const activeAgentId = params.sessionKey - ? resolveAgentIdFromSessionKey(params.sessionKey) - : resolveDefaultAgentId(params.cfg); + const activeAgentId = resolveSessionAgentId({ + sessionKey: params.sessionKey, + config: params.cfg, + }); const agentDir = resolveAgentDir(params.cfg, activeAgentId); const runtimeIsSandboxed = (() => { const sessionKey = params.sessionKey?.trim(); if (!sessionKey) return false; - const agentId = resolveAgentIdFromSessionKey(sessionKey); + const agentId = resolveSessionAgentId({ + sessionKey, + config: params.cfg, + }); const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId); if (sandboxCfg.mode === "off") return false; const mainKey = resolveAgentMainSessionKey({ @@ -1013,7 +1017,7 @@ export async function persistInlineDirectives(params: { } = params; let { provider, model } = params; const activeAgentId = sessionKey - ? resolveAgentIdFromSessionKey(sessionKey) + ? resolveSessionAgentId({ sessionKey, config: cfg }) : resolveDefaultAgentId(cfg); const agentDir = resolveAgentDir(cfg, activeAgentId); diff --git a/src/auto-reply/reply/route-reply.ts b/src/auto-reply/reply/route-reply.ts index 0258ef797..1e3eaf8a8 100644 --- a/src/auto-reply/reply/route-reply.ts +++ b/src/auto-reply/reply/route-reply.ts @@ -7,12 +7,12 @@ * across multiple providers. */ +import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { resolveEffectiveMessagesConfig } from "../../agents/identity.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { sendMessageDiscord } from "../../discord/send.js"; import { sendMessageIMessage } from "../../imessage/send.js"; import { sendMessageMSTeams } from "../../msteams/send.js"; -import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js"; import { sendMessageSignal } from "../../signal/send.js"; import { sendMessageSlack } from "../../slack/send.js"; import { sendMessageTelegram } from "../../telegram/send.js"; @@ -67,7 +67,10 @@ export async function routeReply( const responsePrefix = params.sessionKey ? resolveEffectiveMessagesConfig( cfg, - resolveAgentIdFromSessionKey(params.sessionKey), + resolveSessionAgentId({ + sessionKey: params.sessionKey, + config: cfg, + }), ).responsePrefix : cfg.messages?.responsePrefix === "auto" ? undefined diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 94a6b8c60..1f11cef14 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -6,6 +6,7 @@ import { CURRENT_SESSION_VERSION, SessionManager, } from "@mariozechner/pi-coding-agent"; +import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import type { ClawdbotConfig } from "../../config/config.js"; import { buildGroupDisplayName, @@ -13,7 +14,6 @@ import { DEFAULT_RESET_TRIGGERS, type GroupKeyResolution, loadSessionStore, - resolveAgentIdFromSessionKey, resolveGroupSessionKey, resolveSessionFilePath, resolveSessionKey, @@ -92,7 +92,10 @@ export async function initSessionState(params: { const { ctx, cfg, commandAuthorized } = params; const sessionCfg = cfg.session; const mainKey = normalizeMainKey(sessionCfg?.mainKey); - const agentId = resolveAgentIdFromSessionKey(ctx.SessionKey); + const agentId = resolveSessionAgentId({ + sessionKey: ctx.SessionKey, + config: cfg, + }); const resetTriggers = sessionCfg?.resetTriggers?.length ? sessionCfg.resetTriggers : DEFAULT_RESET_TRIGGERS;