fix(agents): preserve active exec references across compaction (#79307)

Merged via squash.

Prepared head SHA: b8da3158f9
Co-authored-by: TurboTheTurtle <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Andy Ye
2026-05-09 12:57:20 -07:00
committed by GitHub
parent 54d0baa6e6
commit e7c784f7a8
18 changed files with 403 additions and 27 deletions

View File

@@ -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.

View File

@@ -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}`;
}

View File

@@ -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", () => ({

View File

@@ -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, {

View File

@@ -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({

View File

@@ -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 } : {}),
};
}

View File

@@ -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"),
}));

View File

@@ -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,

View File

@@ -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(() => ({

View File

@@ -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,

View File

@@ -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,
}));

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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");
});
});

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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 = {

View File

@@ -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[] = [],