mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 13:30:45 +00:00
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:
@@ -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.
|
||||
|
||||
74
src/agents/bash-process-references.ts
Normal file
74
src/agents/bash-process-references.ts
Normal 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}`;
|
||||
}
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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[] = [],
|
||||
|
||||
Reference in New Issue
Block a user