diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index 5db40b13a27..e2ef701e030 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -492,6 +492,7 @@ function runAgentAttempt(params: { sessionKey: params.sessionKey, agentId: params.sessionAgentId, trigger: "user", + runContext: { sessionTarget: "main" }, messageChannel: params.messageChannel, agentAccountId: params.runContext.accountId, messageTo: params.opts.replyTo ?? params.opts.to, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index d785218f819..1cc68f131b0 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -2549,6 +2549,13 @@ export async function runEmbeddedAttempt( sessionKey: sandboxSessionKey, sessionId: params.sessionId, agentId: sessionAgentId, + runContext: { + sessionTarget: + params.runContext?.sessionTarget ?? + (params.trigger === "cron" ? "isolated" : sandboxSessionKey ? "main" : undefined), + cronJobId: params.runContext?.cronJobId, + maintenanceScope: params.runContext?.maintenanceScope, + }, }); const { diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/pi-embedded-runner/run/params.ts index f59bb8f27b5..2e33399976f 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/pi-embedded-runner/run/params.ts @@ -20,6 +20,12 @@ export type ClientToolDefinition = { }; }; +export type RunEmbeddedPiAgentContext = { + sessionTarget?: "main" | "isolated"; + cronJobId?: string; + maintenanceScope?: string; +}; + export type RunEmbeddedPiAgentParams = { sessionId: string; sessionKey?: string; @@ -119,6 +125,7 @@ export type RunEmbeddedPiAgentParams = { streamParams?: AgentStreamParams; ownerNumbers?: string[]; enforceFinalTag?: boolean; + runContext?: RunEmbeddedPiAgentContext; /** * Allow a single run attempt even when all auth profiles are in cooldown, * but only for inferred transient cooldowns like `rate_limit` or `overloaded`. diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts b/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts index 911b124113a..4299d2711c8 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts +++ b/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts @@ -9,13 +9,22 @@ vi.mock("../infra/agent-events.js", () => ({ function createContext( lastAssistant: unknown, - overrides?: { onAgentEvent?: (event: unknown) => void }, + overrides?: { + onAgentEvent?: (event: unknown) => void; + sessionKey?: string; + runContext?: { + sessionTarget?: "main" | "isolated"; + cronJobId?: string; + maintenanceScope?: string; + }; + }, ): EmbeddedPiSubscribeContext { return { params: { runId: "run-1", config: {}, - sessionKey: "agent:main:main", + sessionKey: overrides?.sessionKey ?? "agent:main:main", + runContext: overrides?.runContext, onAgentEvent: overrides?.onAgentEvent, }, state: { @@ -60,6 +69,8 @@ describe("handleAgentEnd", () => { runId: "run-1", error: "connection refused", rawErrorPreview: "connection refused", + sessionTarget: "main", + sessionKey: "agent:main:main", }); expect(onAgentEvent).toHaveBeenCalledWith({ stream: "lifecycle", @@ -157,4 +168,38 @@ describe("handleAgentEnd", () => { expect(ctx.log.warn).not.toHaveBeenCalled(); expect(ctx.log.debug).toHaveBeenCalledWith("embedded run agent end: runId=run-1 isError=false"); }); + + it("includes explicit provenance metadata in error logs when provided by the runtime", () => { + const ctx = createContext( + { + role: "assistant", + stopReason: "error", + errorMessage: "provider overloaded", + content: [{ type: "text", text: "" }], + }, + { + sessionKey: "agent:main:cron:job-1", + runContext: { + sessionTarget: "isolated", + cronJobId: "job-1", + maintenanceScope: "watchdog", + }, + }, + ); + + handleAgentEnd(ctx); + + expect(vi.mocked(ctx.log.warn).mock.calls[0]?.[1]).toMatchObject({ + sessionTarget: "isolated", + sessionKey: "agent:main:cron:job-1", + cronJobId: "job-1", + maintenanceScope: "watchdog", + runContext: { + sessionTarget: "isolated", + sessionKey: "agent:main:cron:job-1", + cronJobId: "job-1", + maintenanceScope: "watchdog", + }, + }); + }); }); diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts b/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts index 973de1ebefc..591f466e7be 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts +++ b/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts @@ -1,5 +1,6 @@ import { emitAgentEvent } from "../infra/agent-events.js"; import { createInlineCodeState } from "../markdown/code-spans.js"; +import { isCronSessionKey } from "../sessions/session-key-utils.js"; import { buildApiErrorObservationFields, buildTextObservationFields, @@ -33,6 +34,14 @@ export function handleAgentStart(ctx: EmbeddedPiSubscribeContext) { export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) { const lastAssistant = ctx.state.lastAssistant; const isError = isAssistantMessage(lastAssistant) && lastAssistant.stopReason === "error"; + const runContext = ctx.params.runContext ?? {}; + const sessionTarget = + runContext.sessionTarget ?? + (isCronSessionKey(ctx.params.sessionKey) + ? "isolated" + : ctx.params.sessionKey + ? "main" + : undefined); if (isError && lastAssistant) { const friendlyError = formatAssistantErrorText(lastAssistant, { @@ -59,6 +68,16 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) { failoverReason, model: lastAssistant.model, provider: lastAssistant.provider, + sessionTarget, + sessionKey: ctx.params.sessionKey, + cronJobId: runContext.cronJobId, + maintenanceScope: runContext.maintenanceScope, + runContext: { + sessionTarget, + sessionKey: ctx.params.sessionKey, + cronJobId: runContext.cronJobId, + maintenanceScope: runContext.maintenanceScope, + }, ...observedError, consoleMessage: `embedded run agent end: runId=${safeRunId} isError=true model=${safeModel} provider=${safeProvider} error=${safeErrorText}`, }); diff --git a/src/agents/pi-embedded-subscribe.types.ts b/src/agents/pi-embedded-subscribe.types.ts index bbb2d552d73..8cef20440e3 100644 --- a/src/agents/pi-embedded-subscribe.types.ts +++ b/src/agents/pi-embedded-subscribe.types.ts @@ -36,6 +36,11 @@ export type SubscribeEmbeddedPiSessionParams = { sessionId?: string; /** Agent identity for hook context — resolved from session config in attempt.ts. */ agentId?: string; + runContext?: { + sessionTarget?: "main" | "isolated"; + cronJobId?: string; + maintenanceScope?: string; + }; }; export type { BlockReplyChunking } from "./pi-embedded-block-chunker.js"; diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index c25342e4a28..9a84c852b6b 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -327,6 +327,7 @@ export async function runAgentTurnWithFallback(params: { ...embeddedContext, allowGatewaySubagentBinding: true, trigger: params.isHeartbeat ? "heartbeat" : "user", + runContext: { sessionTarget: "main" }, groupId: resolveGroupSessionKey(params.sessionCtx)?.id, groupChannel: params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(), diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index 267326a7e20..408c820a59a 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -496,6 +496,7 @@ export async function runMemoryFlushIfNeeded(params: { ...runBaseParams, allowGatewaySubagentBinding: true, trigger: "memory", + runContext: { maintenanceScope: "memory-flush" }, memoryFlushWritePath, prompt: resolveMemoryFlushPromptForRun({ prompt: memoryFlushSettings.prompt, diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index 2fd21607095..a6e4165a2dd 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -179,6 +179,7 @@ export function createFollowupRunner(params: { sessionKey: queued.run.sessionKey, agentId: queued.run.agentId, trigger: "user", + runContext: { sessionTarget: "main" }, messageChannel: queued.originatingChannel ?? undefined, messageProvider: queued.run.messageProvider, agentAccountId: queued.run.agentAccountId, diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index 1a122f56864..0b4ebe7bf4b 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -659,6 +659,10 @@ export async function runCronIsolatedAgentTurn(params: { bootstrapContextMode: agentPayload?.lightContext ? "lightweight" : undefined, bootstrapContextRunKind: "cron", runId: cronSession.sessionEntry.sessionId, + runContext: { + sessionTarget: "isolated", + cronJobId: params.job.id, + }, requireExplicitMessageTarget: toolPolicy.requireExplicitMessageTarget, disableMessageTool: toolPolicy.disableMessageTool, allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,