diff --git a/CHANGELOG.md b/CHANGELOG.md index 3394faac64a..9caea2f1335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Docs: https://docs.openclaw.ai ### Changes +- Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle. + ### Fixes - Plugin SDK: keep activated linked plugin runtime facades loadable when bundled plugin fallback is disabled. Thanks @shakkernerd. diff --git a/src/agents/bash-process-references.ts b/src/agents/bash-process-references.ts new file mode 100644 index 00000000000..08d7c109a01 --- /dev/null +++ b/src/agents/bash-process-references.ts @@ -0,0 +1,74 @@ +import { formatDurationCompact } from "../infra/format-time/format-duration.js"; +import { listRunningSessions } from "./bash-process-registry.js"; +import { deriveSessionName } from "./bash-tools.shared.js"; + +const DEFAULT_ACTIVE_PROCESS_LIMIT = 8; +const MAX_COMMAND_LABEL_CHARS = 140; + +export type ActiveProcessSessionReference = { + sessionId: string; + status: "running"; + pid?: number; + startedAt: number; + runtimeMs: number; + cwd?: string; + command: string; + name: string; + tail?: string; + truncated: boolean; +}; + +function truncate(value: string, maxChars: number): string { + if (value.length <= maxChars) { + return value; + } + if (maxChars <= 1) { + return value.slice(0, maxChars); + } + return `${value.slice(0, Math.max(0, maxChars - 3))}...`; +} + +export function listActiveProcessSessionReferences(params: { + scopeKey?: string; + now?: number; + limit?: number; +}): ActiveProcessSessionReference[] { + const scopeKey = params.scopeKey?.trim(); + if (!scopeKey) { + return []; + } + const now = params.now ?? Date.now(); + const limit = + typeof params.limit === "number" && Number.isFinite(params.limit) && params.limit > 0 + ? Math.floor(params.limit) + : DEFAULT_ACTIVE_PROCESS_LIMIT; + return listRunningSessions() + .filter((session) => session.backgrounded) + .filter((session) => session.scopeKey === scopeKey) + .toSorted((left, right) => right.startedAt - left.startedAt) + .slice(0, limit) + .map((session) => ({ + sessionId: session.id, + status: "running" as const, + pid: session.pid ?? session.child?.pid, + startedAt: session.startedAt, + runtimeMs: Math.max(0, now - session.startedAt), + cwd: session.cwd, + command: session.command, + name: truncate( + deriveSessionName(session.command) || session.command, + MAX_COMMAND_LABEL_CHARS, + ), + tail: session.tail, + truncated: session.truncated, + })); +} + +export function formatActiveProcessSessionReference( + session: ActiveProcessSessionReference, +): string { + const runtime = formatDurationCompact(session.runtimeMs) ?? "unknown"; + const pid = typeof session.pid === "number" ? ` pid=${session.pid}` : ""; + const cwd = session.cwd ? ` cwd=${session.cwd}` : ""; + return `${session.sessionId} ${session.status} ${runtime}${pid}${cwd} :: ${session.name}`; +} diff --git a/src/agents/pi-embedded-runner/compact.hooks.harness.ts b/src/agents/pi-embedded-runner/compact.hooks.harness.ts index 663f9e90d65..b7b6473a4aa 100644 --- a/src/agents/pi-embedded-runner/compact.hooks.harness.ts +++ b/src/agents/pi-embedded-runner/compact.hooks.harness.ts @@ -470,6 +470,7 @@ export async function loadCompactHooksHarness(): Promise<{ vi.doMock("./lanes.js", () => ({ resolveSessionLane: vi.fn(() => "test-session-lane"), + resolveEmbeddedSessionLane: vi.fn(() => "test-session-lane"), resolveGlobalLane: vi.fn(() => "test-global-lane"), })); @@ -513,6 +514,17 @@ export async function loadCompactHooksHarness(): Promise<{ vi.doMock("../pi-tools.js", () => ({ createOpenClawCodingTools: createOpenClawCodingToolsMock, + resolveProcessToolScopeKey: ({ + scopeKey, + sessionKey, + sessionId, + agentId, + }: { + scopeKey?: string; + sessionKey?: string; + sessionId?: string; + agentId?: string; + }) => scopeKey ?? sessionKey ?? sessionId ?? (agentId ? `agent:${agentId}` : undefined), })); vi.doMock("./replay-history.js", () => ({ diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts index db91cf8d103..2ed90892bd6 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts @@ -39,6 +39,7 @@ import { resolveRunModelFallbacksOverride, resolveSessionAgentIds, } from "../agent-scope.js"; +import { listActiveProcessSessionReferences } from "../bash-process-references.js"; import { makeBootstrapWarn, resolveBootstrapContextForRun, @@ -82,7 +83,7 @@ import { applyPiCompactionSettingsFromConfig, isSilentOverflowProneModel, } from "../pi-settings.js"; -import { createOpenClawCodingTools } from "../pi-tools.js"; +import { createOpenClawCodingTools, resolveProcessToolScopeKey } from "../pi-tools.js"; import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js"; import { registerProviderStreamForModel } from "../provider-stream.js"; import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js"; @@ -834,6 +835,12 @@ async function compactEmbeddedPiSessionDirectOnce( channel: runtimeChannel, capabilities: runtimeCapabilities, channelActions, + activeProcessSessions: listActiveProcessSessionReferences({ + scopeKey: resolveProcessToolScopeKey({ + sessionKey: sandboxSessionKey, + agentId: sessionAgentId, + }), + }), }; const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated); const reasoningTagHint = isReasoningTagProvider(provider, { diff --git a/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts b/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts index ea22b0646ce..b14359419b7 100644 --- a/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts +++ b/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts @@ -1,11 +1,17 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import { addSession, resetProcessRegistryForTests } from "../bash-process-registry.js"; +import { createProcessSessionFixture } from "../bash-process-registry.test-helpers.js"; import { buildEmbeddedCompactionRuntimeContext, resolveEmbeddedCompactionTarget, } from "./compaction-runtime-context.js"; describe("buildEmbeddedCompactionRuntimeContext", () => { + afterEach(() => { + resetProcessRegistryForTests(); + }); + it("preserves sender and current message routing for compaction", () => { const result = buildEmbeddedCompactionRuntimeContext({ sessionKey: "agent:main:thread:1", @@ -120,6 +126,62 @@ describe("buildEmbeddedCompactionRuntimeContext", () => { expect(result.authProfileId).toBe("ollama:default"); }); + it("preserves scoped active process session references for compaction", () => { + const active = createProcessSessionFixture({ + id: "sess-active", + command: "sleep 600", + backgrounded: true, + pid: 1234, + startedAt: 1_000, + }); + active.scopeKey = "agent:main:thread:1"; + const other = createProcessSessionFixture({ + id: "sess-other", + command: "sleep 600", + backgrounded: true, + }); + other.scopeKey = "agent:other"; + addSession(active); + addSession(other); + + const result = buildEmbeddedCompactionRuntimeContext({ + sessionKey: "agent:main:thread:1", + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + config: {} as OpenClawConfig, + }); + + expect(result.activeProcessSessions).toEqual([ + expect.objectContaining({ + sessionId: "sess-active", + status: "running", + command: "sleep 600", + pid: 1234, + }), + ]); + expect(result.activeProcessSessions).not.toEqual( + expect.arrayContaining([expect.objectContaining({ sessionId: "sess-other" })]), + ); + }); + + it("omits active process session references when no safe scope is available", () => { + const active = createProcessSessionFixture({ + id: "sess-active", + command: "sleep 600", + backgrounded: true, + }); + active.scopeKey = "agent:main:thread:1"; + addSession(active); + + const result = buildEmbeddedCompactionRuntimeContext({ + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + config: {} as OpenClawConfig, + }); + + expect(result.activeProcessSessions).toBeUndefined(); + }); + it("applies runtime defaults when resolving the effective compaction target", () => { expect( resolveEmbeddedCompactionTarget({ diff --git a/src/agents/pi-embedded-runner/compaction-runtime-context.ts b/src/agents/pi-embedded-runner/compaction-runtime-context.ts index 636fb72b932..a490bc9e5d4 100644 --- a/src/agents/pi-embedded-runner/compaction-runtime-context.ts +++ b/src/agents/pi-embedded-runner/compaction-runtime-context.ts @@ -1,6 +1,10 @@ import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { + listActiveProcessSessionReferences, + type ActiveProcessSessionReference, +} from "../bash-process-references.js"; import type { ExecElevatedDefaults } from "../bash-tools.js"; import type { SkillSnapshot } from "../skills.js"; @@ -28,6 +32,7 @@ export type EmbeddedCompactionRuntimeContext = { extraSystemPrompt?: string; sourceReplyDeliveryMode?: SourceReplyDeliveryMode; ownerNumbers?: string[]; + activeProcessSessions?: ActiveProcessSessionReference[]; }; /** @@ -95,6 +100,7 @@ export function buildEmbeddedCompactionRuntimeContext(params: { extraSystemPrompt?: string; sourceReplyDeliveryMode?: SourceReplyDeliveryMode; ownerNumbers?: string[]; + activeProcessSessions?: ActiveProcessSessionReference[]; }): EmbeddedCompactionRuntimeContext { const resolved = resolveEmbeddedCompactionTarget({ config: params.config, @@ -102,6 +108,12 @@ export function buildEmbeddedCompactionRuntimeContext(params: { modelId: params.modelId, authProfileId: params.authProfileId, }); + const processScopeKey = params.sessionKey?.trim(); + const activeProcessSessions = + params.activeProcessSessions ?? + listActiveProcessSessionReferences({ + scopeKey: processScopeKey, + }); return { sessionKey: params.sessionKey ?? undefined, messageChannel: params.messageChannel ?? undefined, @@ -126,5 +138,6 @@ export function buildEmbeddedCompactionRuntimeContext(params: { extraSystemPrompt: params.extraSystemPrompt, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, ownerNumbers: params.ownerNumbers, + ...(activeProcessSessions.length > 0 ? { activeProcessSessions } : {}), }; } diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts b/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts index b26e2e5c643..f8f83170c9c 100644 --- a/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts +++ b/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts @@ -548,6 +548,7 @@ export async function loadRunOverflowCompactionHarness(): Promise<{ vi.doMock("../../process/command-queue.js", () => ({ enqueueCommandInLane: vi.fn((_lane: string, task: () => unknown) => task()), + clearCommandLane: vi.fn(() => 0), })); vi.doMock("../../utils/message-channel.js", () => ({ @@ -569,6 +570,7 @@ export async function loadRunOverflowCompactionHarness(): Promise<{ vi.doMock("./lanes.js", () => ({ resolveSessionLane: vi.fn(() => "session-lane"), + resolveEmbeddedSessionLane: vi.fn(() => "session-lane"), resolveGlobalLane: vi.fn(() => "global-lane"), })); diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 93cf14dbf8a..a2d33f76204 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -36,6 +36,7 @@ import { markAuthProfileGood, markAuthProfileUsed, } from "../auth-profiles.js"; +import { listActiveProcessSessionReferences } from "../bash-process-references.js"; import { resolveSessionKeyForRequest, resolveStoredSessionKeyForSessionId, @@ -81,6 +82,7 @@ import { parseImageSizeError, pickFallbackThinkingLevel, } from "../pi-embedded-helpers.js"; +import { resolveProcessToolScopeKey } from "../pi-tools.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import { runAgentCleanupStep } from "../run-cleanup-timeout.js"; import { buildAgentRuntimeAuthPlan } from "../runtime-plan/auth.js"; @@ -1495,6 +1497,13 @@ export async function runEmbeddedPiAgent( extraSystemPrompt: params.extraSystemPrompt, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, ownerNumbers: params.ownerNumbers, + activeProcessSessions: listActiveProcessSessionReferences({ + scopeKey: resolveProcessToolScopeKey({ + sessionKey: params.sandboxSessionKey?.trim() || params.sessionKey, + sessionId: activeSessionId, + agentId: sessionAgentId, + }), + }), }), ...resolveContextEngineCapabilities({ config: params.config, @@ -1660,6 +1669,13 @@ export async function runEmbeddedPiAgent( extraSystemPrompt: params.extraSystemPrompt, sourceReplyDeliveryMode: params.sourceReplyDeliveryMode, ownerNumbers: params.ownerNumbers, + activeProcessSessions: listActiveProcessSessionReferences({ + scopeKey: resolveProcessToolScopeKey({ + sessionKey: params.sandboxSessionKey?.trim() || params.sessionKey, + sessionId: activeSessionId, + agentId: sessionAgentId, + }), + }), }), ...resolveContextEngineCapabilities({ config: params.config, diff --git a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts index eca6a3d8969..79dea72d609 100644 --- a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts @@ -2,10 +2,20 @@ import { describe, expect, it, vi } from "vitest"; const musicGenerationTaskStatusMocks = vi.hoisted(() => ({ buildActiveMusicGenerationTaskPromptContextForSession: vi.fn(), + buildMusicGenerationTaskStatusDetails: vi.fn(() => ({})), + buildMusicGenerationTaskStatusText: vi.fn(() => "Music generation task status"), + findActiveMusicGenerationTaskForSession: vi.fn(), + MUSIC_GENERATION_TASK_KIND: "music_generation", })); const videoGenerationTaskStatusMocks = vi.hoisted(() => ({ buildActiveVideoGenerationTaskPromptContextForSession: vi.fn(), + buildVideoGenerationTaskStatusDetails: vi.fn(() => ({})), + buildVideoGenerationTaskStatusText: vi.fn(() => "Video generation task status"), + findActiveVideoGenerationTaskForSession: vi.fn(), + getVideoGenerationTaskProviderId: vi.fn(), + isActiveVideoGenerationTask: vi.fn(() => false), + VIDEO_GENERATION_TASK_KIND: "video_generation", })); const hostHookStateMocks = vi.hoisted(() => ({ diff --git a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts index a09bda75db9..6868148a8fa 100644 --- a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts +++ b/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts @@ -14,8 +14,10 @@ import type { } from "../../../plugins/types.js"; import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session-key.js"; import { joinPresentTextSegments } from "../../../shared/text/join-segments.js"; +import { listActiveProcessSessionReferences } from "../../bash-process-references.js"; import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js"; import { buildActiveMusicGenerationTaskPromptContextForSession } from "../../music-generation-task-status.js"; +import { resolveProcessToolScopeKey } from "../../pi-tools.js"; import { prependSystemPromptAdditionAfterCacheBoundary } from "../../system-prompt-cache-boundary.js"; import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js"; import { derivePromptTokens, type NormalizedUsage } from "../../usage.js"; @@ -487,30 +489,35 @@ export function resolveAttemptPrependSystemContext(params: { ]); } +type AfterTurnRuntimeContextAttempt = Pick< + EmbeddedRunAttemptParams, + | "sessionKey" + | "sandboxSessionKey" + | "messageChannel" + | "messageProvider" + | "agentAccountId" + | "currentChannelId" + | "currentThreadTs" + | "currentMessageId" + | "config" + | "skillsSnapshot" + | "senderIsOwner" + | "senderId" + | "provider" + | "modelId" + | "thinkLevel" + | "reasoningLevel" + | "bashElevated" + | "extraSystemPrompt" + | "ownerNumbers" + | "authProfileId" +> & { + sessionId?: EmbeddedRunAttemptParams["sessionId"]; +}; + /** Build runtime context passed into context-engine afterTurn hooks. */ export function buildAfterTurnRuntimeContext(params: { - attempt: Pick< - EmbeddedRunAttemptParams, - | "sessionKey" - | "messageChannel" - | "messageProvider" - | "agentAccountId" - | "currentChannelId" - | "currentThreadTs" - | "currentMessageId" - | "config" - | "skillsSnapshot" - | "senderIsOwner" - | "senderId" - | "provider" - | "modelId" - | "thinkLevel" - | "reasoningLevel" - | "bashElevated" - | "extraSystemPrompt" - | "ownerNumbers" - | "authProfileId" - >; + attempt: AfterTurnRuntimeContextAttempt; workspaceDir: string; agentDir: string; activeAgentId?: string; @@ -542,6 +549,13 @@ export function buildAfterTurnRuntimeContext(params: { bashElevated: params.attempt.bashElevated, extraSystemPrompt: params.attempt.extraSystemPrompt, ownerNumbers: params.attempt.ownerNumbers, + activeProcessSessions: listActiveProcessSessionReferences({ + scopeKey: resolveProcessToolScopeKey({ + sessionKey: params.attempt.sandboxSessionKey?.trim() || params.attempt.sessionKey, + sessionId: params.attempt.sessionId, + agentId: params.activeAgentId, + }), + }), }), ...resolveContextEngineCapabilities({ config: params.attempt.config, diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts index a195beace78..6c757295126 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts @@ -541,6 +541,17 @@ vi.mock("../../cache-trace.js", () => ({ vi.mock("../../pi-tools.js", () => ({ createOpenClawCodingTools: (options?: { workspaceDir?: string; spawnWorkspaceDir?: string }) => hoisted.createOpenClawCodingToolsMock(options), + resolveProcessToolScopeKey: ({ + scopeKey, + sessionKey, + sessionId, + agentId, + }: { + scopeKey?: string; + sessionKey?: string; + sessionId?: string; + agentId?: string; + }) => scopeKey ?? sessionKey ?? sessionId ?? (agentId ? `agent:${agentId}` : undefined), resolveToolLoopDetectionConfig: () => undefined, })); diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 641ba1428a7..07c6a5d70d9 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -5,6 +5,8 @@ vi.mock("../context-engine-capabilities.js", () => ({ resolveContextEngineCapabilities: async () => ({ llm: undefined }), })); import type { OpenClawConfig } from "../../../config/config.js"; +import { addSession, resetProcessRegistryForTests } from "../../bash-process-registry.js"; +import { createProcessSessionFixture } from "../../bash-process-registry.test-helpers.js"; import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../../system-prompt-cache-boundary.js"; import { buildAgentSystemPrompt } from "../../system-prompt.js"; import { resolveBootstrapContextTargets } from "./attempt-bootstrap-routing.js"; @@ -3232,6 +3234,58 @@ describe("prependSystemPromptAddition", () => { }); describe("buildAfterTurnRuntimeContext", () => { + it("preserves sessionId-scoped active process sessions for after-turn context", () => { + resetProcessRegistryForTests(); + try { + const active = createProcessSessionFixture({ + id: "sess-session-id", + command: "sleep 600", + backgrounded: true, + pid: 1234, + }); + active.scopeKey = "session-123"; + addSession(active); + const other = createProcessSessionFixture({ + id: "sess-other", + command: "sleep 600", + backgrounded: true, + }); + other.scopeKey = "agent:main"; + addSession(other); + + const legacy = buildAfterTurnRuntimeContext({ + attempt: { + sessionId: "session-123", + config: {} as OpenClawConfig, + skillsSnapshot: undefined, + senderIsOwner: true, + provider: "openai-codex", + modelId: "gpt-5.4", + thinkLevel: "off", + reasoningLevel: "on", + extraSystemPrompt: "extra", + ownerNumbers: ["+15555550123"], + }, + workspaceDir: "/tmp/workspace", + agentDir: "/tmp/agent", + activeAgentId: "main", + }); + + expect(legacy.activeProcessSessions).toEqual([ + expect.objectContaining({ + sessionId: "sess-session-id", + command: "sleep 600", + pid: 1234, + }), + ]); + expect(legacy.activeProcessSessions).not.toEqual( + expect.arrayContaining([expect.objectContaining({ sessionId: "sess-other" })]), + ); + } finally { + resetProcessRegistryForTests(); + } + }); + it("uses primary model when compaction.model is not set", () => { const legacy = buildAfterTurnRuntimeContext({ attempt: { diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 283a02851d2..fede114ac5e 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -61,6 +61,7 @@ import { normalizeMessageChannel } from "../../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../../utils/provider-utils.js"; import { resolveAgentDir, resolveSessionAgentIds } from "../../agent-scope.js"; import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js"; +import { listActiveProcessSessionReferences } from "../../bash-process-references.js"; import { analyzeBootstrapBudget, buildBootstrapPromptWarning, @@ -120,7 +121,11 @@ import { findClientToolNameConflicts, toClientToolDefinitions, } from "../../pi-tool-definition-adapter.js"; -import { createOpenClawCodingTools, resolveToolLoopDetectionConfig } from "../../pi-tools.js"; +import { + createOpenClawCodingTools, + resolveProcessToolScopeKey, + resolveToolLoopDetectionConfig, +} from "../../pi-tools.js"; import { resolveEffectiveToolPolicy, resolveGroupToolPolicy, @@ -1218,6 +1223,12 @@ export async function runEmbeddedAttempt( agentId: sessionAgentId, }); const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`; + const activeProcessSessions = listActiveProcessSessionReferences({ + scopeKey: resolveProcessToolScopeKey({ + sessionKey: sandboxSessionKey, + agentId: sessionAgentId, + }), + }); const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({ config: params.config, agentId: sessionAgentId, @@ -1234,6 +1245,7 @@ export async function runEmbeddedAttempt( channel: runtimeChannel, capabilities: runtimeCapabilities, channelActions, + activeProcessSessions, }, }); const isDefaultAgent = sessionAgentId === defaultAgentId; diff --git a/src/agents/pi-embedded-runner/system-prompt.test.ts b/src/agents/pi-embedded-runner/system-prompt.test.ts index 07b513db4c8..84285ae5fea 100644 --- a/src/agents/pi-embedded-runner/system-prompt.test.ts +++ b/src/agents/pi-embedded-runner/system-prompt.test.ts @@ -152,4 +152,40 @@ describe("buildEmbeddedSystemPrompt", () => { expect(prompt).not.toContain("## Memory Recall"); }); + + it("includes active background process references in the embedded prompt", () => { + const prompt = buildEmbeddedSystemPrompt({ + workspaceDir: "/tmp/openclaw", + reasoningTagHint: false, + runtimeInfo: { + host: "local", + os: "darwin", + arch: "arm64", + node: process.version, + model: "gpt-5.4", + provider: "openai", + activeProcessSessions: [ + { + sessionId: "sess-active", + status: "running", + startedAt: 0, + runtimeMs: 5_000, + command: "sleep 600", + name: "sleep 600", + cwd: "/tmp/work", + pid: 1234, + truncated: false, + }, + ], + }, + tools: [], + modelAliasLines: [], + userTimezone: "UTC", + }); + + expect(prompt).toContain("Active background exec sessions in this scope:"); + expect(prompt).toContain("sess-active running pid=1234 cwd=/tmp/work :: sleep 600"); + expect(prompt).toContain("process tool with a sessionId"); + expect(prompt).toContain("process list"); + }); }); diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/pi-embedded-runner/system-prompt.ts index 7b5166ed389..d3ebe7dec88 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/pi-embedded-runner/system-prompt.ts @@ -4,6 +4,7 @@ import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options import type { SubagentDelegationMode } from "../../config/types.agent-defaults.js"; import type { MemoryCitationsMode } from "../../config/types.memory.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { ActiveProcessSessionReference } from "../bash-process-references.js"; import type { BootstrapMode } from "../bootstrap-mode.js"; import type { ResolvedTimeFormat } from "../date-time.js"; import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; @@ -59,6 +60,7 @@ export function buildEmbeddedSystemPrompt(params: { channel?: string; /** Supported message actions for the current channel (e.g., react, edit, unsend) */ channelActions?: string[]; + activeProcessSessions?: ActiveProcessSessionReference[]; }; messageToolHints?: string[]; sandboxInfo?: EmbeddedSandboxInfo; diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 8446cde8389..c9259cba669 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -144,6 +144,28 @@ function createLazyProcessTool(defaults?: ProcessToolDefaults): AnyAgentTool { } as AnyAgentTool; } +export function resolveProcessToolScopeKey(params: { + scopeKey?: string; + sessionKey?: string; + sessionId?: string; + agentId?: string; +}): string | undefined { + const explicitScopeKey = params.scopeKey?.trim(); + if (explicitScopeKey) { + return explicitScopeKey; + } + const sessionKey = params.sessionKey?.trim(); + if (sessionKey) { + return sessionKey; + } + const sessionId = params.sessionId?.trim(); + if (sessionId) { + return sessionId; + } + const agentId = params.agentId?.trim(); + return agentId ? `agent:${agentId}` : undefined; +} + function applyModelProviderToolPolicy( tools: AnyAgentTool[], params?: { @@ -442,8 +464,12 @@ export function createOpenClawCodingTools(options?: { ]); // Prefer sessionKey for process isolation scope to prevent cross-session process visibility/killing. // Fallback to agentId if no sessionKey is available (e.g. legacy or global contexts). - const scopeKey = - options?.exec?.scopeKey ?? options?.sessionKey ?? (agentId ? `agent:${agentId}` : undefined); + const scopeKey = resolveProcessToolScopeKey({ + scopeKey: options?.exec?.scopeKey, + sessionKey: options?.sessionKey, + sessionId: options?.sessionId, + agentId, + }); const subagentStore = resolveSubagentCapabilityStore(options?.sessionKey, { cfg: options?.config, }); diff --git a/src/agents/system-prompt-params.ts b/src/agents/system-prompt-params.ts index 4e4138b3b17..336128120a2 100644 --- a/src/agents/system-prompt-params.ts +++ b/src/agents/system-prompt-params.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { findGitRoot } from "../infra/git-root.js"; +import type { ActiveProcessSessionReference } from "./bash-process-references.js"; import { formatUserTime, resolveUserTimeFormat, @@ -23,6 +24,7 @@ type RuntimeInfoInput = { /** Supported message actions for the current channel (e.g., react, edit, unsend) */ channelActions?: string[]; repoRoot?: string; + activeProcessSessions?: ActiveProcessSessionReference[]; }; type SystemPromptRuntimeParams = { diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 741b0339b6c..8483e3d7bf4 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -14,6 +14,7 @@ import { normalizeOptionalLowercaseString, } from "../shared/string-coerce.js"; import { listDeliverableMessageChannels } from "../utils/message-channel.js"; +import type { ActiveProcessSessionReference } from "./bash-process-references.js"; import type { BootstrapMode } from "./bootstrap-mode.js"; import { buildFullBootstrapPromptLines, @@ -673,6 +674,7 @@ export function buildAgentSystemPrompt(params: { channel?: string; capabilities?: string[]; repoRoot?: string; + activeProcessSessions?: ActiveProcessSessionReference[]; }; messageToolHints?: string[]; sandboxInfo?: EmbeddedSandboxInfo; @@ -1262,12 +1264,30 @@ export function buildAgentSystemPrompt(params: { "## Runtime", buildRuntimeLine(runtimeInfo, runtimeChannel, runtimeCapabilities, params.defaultThinkLevel), ...(modelIdentityLine ? [modelIdentityLine] : []), + ...buildActiveProcessSessionReferenceLines(runtimeInfo?.activeProcessSessions), `Reasoning: ${reasoningLevel} (hidden unless on/stream). Toggle /reasoning; /status shows Reasoning when enabled.`, ); return lines.filter(Boolean).join("\n"); } +function buildActiveProcessSessionReferenceLines( + sessions: ActiveProcessSessionReference[] | undefined, +): string[] { + if (!sessions?.length) { + return []; + } + return [ + "Active background exec sessions in this scope:", + ...sessions.map((session) => { + const pid = typeof session.pid === "number" ? ` pid=${session.pid}` : ""; + const cwd = session.cwd ? ` cwd=${sanitizeForPromptLiteral(session.cwd)}` : ""; + return `- ${session.sessionId} ${session.status}${pid}${cwd} :: ${sanitizeForPromptLiteral(session.name)}`; + }), + "Use the process tool with a sessionId to poll, log, write to, or terminate these sessions. If prior context lost a sessionId, run process list.", + ]; +} + export function buildRuntimeLine( runtimeInfo?: { agentId?: string; @@ -1279,6 +1299,7 @@ export function buildRuntimeLine( defaultModel?: string; shell?: string; repoRoot?: string; + activeProcessSessions?: ActiveProcessSessionReference[]; }, runtimeChannel?: string, runtimeCapabilities: string[] = [],