fix(agents): bootstrap runtime plugins before context-engine resolution
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Hooks/session-memory: keep `/new` and `/reset` memory artifacts in the bound agent workspace and align saved reset session keys with that workspace when stale main-agent keys leak into the hook path. (#39875) thanks @rbutera.
|
||||
- Sessions/model switch: clear stale cached `contextTokens` when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii.
|
||||
- ACP/session history: persist transcripts for successful ACP child runs, preserve exact transcript text, record ACP spawned-session lineage, and keep spawn-time transcript-path persistence best-effort so history storage failures do not block execution. (#40137) thanks @mbelinky.
|
||||
<<<<<<< HEAD
|
||||
- Agents/openai-codex: normalize `gpt-5.4` fallback transport back to `openai-codex-responses` on `chatgpt.com/backend-api` when config drifts to the generic OpenAI responses endpoint. (#38736) Thanks @0xsline.
|
||||
- Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for `/json/*` tab operations so local `ws://` / `wss://` profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150.
|
||||
- Browser/CDP: rewrite wildcard `ws://0.0.0.0` and `ws://[::]` debugger URLs from remote `/json/version` responses back to the external CDP host/port, fixing Browserless-style container endpoints. (#17760) Thanks @joeharouni.
|
||||
@@ -44,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Podman/setup: fix `cannot chdir: Permission denied` in `run_as_user` when `setup-podman.sh` is invoked from a directory the target user cannot access, by wrapping user-switch calls in a subshell that cd's to `/tmp` with `/` fallback. (#39435) Thanks @langdon and @jlcbk.
|
||||
- Podman/SELinux: auto-detect SELinux enforcing/permissive mode and add `:Z` relabel to bind mounts in `run-openclaw-podman.sh` and the Quadlet template, fixing `EACCES` on Fedora/RHEL hosts. Supports `OPENCLAW_BIND_MOUNT_OPTIONS` override. (#39449) Thanks @langdon and @githubbzxs.
|
||||
- TUI/theme: detect light terminal backgrounds via `COLORFGBG` and pick a WCAG AA-compliant light palette, with `OPENCLAW_THEME=light|dark` override for terminals without auto-detection. (#38636) Thanks @ademczuk and @vincentkoc.
|
||||
- Agents/context-engine plugins: bootstrap runtime plugins once at embedded-run, compaction, and subagent boundaries so plugin-provided context engines and hooks load from the active workspace before runtime resolution. (#40232)
|
||||
|
||||
## 2026.3.7
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
hookRunner,
|
||||
ensureRuntimePluginsLoaded,
|
||||
resolveModelMock,
|
||||
sessionCompactImpl,
|
||||
triggerInternalHook,
|
||||
@@ -12,6 +13,7 @@ const {
|
||||
runBeforeCompaction: vi.fn(),
|
||||
runAfterCompaction: vi.fn(),
|
||||
},
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
resolveModelMock: vi.fn(() => ({
|
||||
model: { provider: "openai", api: "responses", id: "fake", input: [] },
|
||||
error: null,
|
||||
@@ -32,6 +34,10 @@ vi.mock("../../plugins/hook-runner-global.js", () => ({
|
||||
getGlobalHookRunner: () => hookRunner,
|
||||
}));
|
||||
|
||||
vi.mock("../runtime-plugins.js", () => ({
|
||||
ensureRuntimePluginsLoaded,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/internal-hooks.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../../hooks/internal-hooks.js")>(
|
||||
"../../hooks/internal-hooks.js",
|
||||
@@ -254,6 +260,7 @@ const sessionHook = (action: string) =>
|
||||
|
||||
describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
beforeEach(() => {
|
||||
ensureRuntimePluginsLoaded.mockReset();
|
||||
triggerInternalHook.mockClear();
|
||||
hookRunner.hasHooks.mockReset();
|
||||
hookRunner.runBeforeCompaction.mockReset();
|
||||
@@ -279,6 +286,19 @@ describe("compactEmbeddedPiSessionDirect hooks", () => {
|
||||
unregisterApiProviders(getCustomApiRegistrySourceId("ollama"));
|
||||
});
|
||||
|
||||
it("bootstraps runtime plugins with the resolved workspace", async () => {
|
||||
await compactEmbeddedPiSessionDirect({
|
||||
sessionId: "session-1",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
expect(ensureRuntimePluginsLoaded).toHaveBeenCalledWith({
|
||||
config: undefined,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits internal + plugin compaction hooks with counts", async () => {
|
||||
hookRunner.hasHooks.mockReturnValue(true);
|
||||
let sanitizedCount = 0;
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "../pi-embedded-helpers.js";
|
||||
import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js";
|
||||
import { createOpenClawCodingTools } from "../pi-tools.js";
|
||||
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
|
||||
import { resolveSandboxContext } from "../sandbox.js";
|
||||
import { repairSessionFileIfNeeded } from "../session-file-repair.js";
|
||||
import { guardSessionManager } from "../session-tool-result-guard-wrapper.js";
|
||||
@@ -269,6 +270,10 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
const maxAttempts = params.maxAttempts ?? 1;
|
||||
const runId = params.runId ?? params.sessionId;
|
||||
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: params.config,
|
||||
workspaceDir: resolvedWorkspace,
|
||||
});
|
||||
const prevCwd = process.cwd();
|
||||
|
||||
// Resolve compaction model: prefer config override, then fall back to caller-supplied model
|
||||
@@ -910,6 +915,10 @@ export async function compactEmbeddedPiSession(
|
||||
params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
|
||||
return enqueueCommandInLane(sessionLane, () =>
|
||||
enqueueGlobal(async () => {
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
ensureContextEnginesInitialized();
|
||||
const contextEngine = await resolveContextEngine(params.config);
|
||||
try {
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
pickFallbackThinkingLevel,
|
||||
type FailoverReason,
|
||||
} from "../pi-embedded-helpers.js";
|
||||
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
|
||||
import { derivePromptTokens, normalizeUsage, type UsageLike } from "../usage.js";
|
||||
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
|
||||
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
|
||||
@@ -287,6 +288,10 @@ export async function runEmbeddedPiAgent(
|
||||
`[workspace-fallback] caller=runEmbeddedPiAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`,
|
||||
);
|
||||
}
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: params.config,
|
||||
workspaceDir: resolvedWorkspace,
|
||||
});
|
||||
const prevCwd = process.cwd();
|
||||
|
||||
let provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import "./run.overflow-compaction.mocks.shared.js";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const runtimePluginMocks = vi.hoisted(() => ({
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../runtime-plugins.js", () => ({
|
||||
ensureRuntimePluginsLoaded: runtimePluginMocks.ensureRuntimePluginsLoaded,
|
||||
}));
|
||||
|
||||
import { runEmbeddedPiAgent } from "./run.js";
|
||||
import { runEmbeddedAttempt } from "./run/attempt.js";
|
||||
|
||||
@@ -10,6 +19,32 @@ describe("runEmbeddedPiAgent usage reporting", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("bootstraps runtime plugins with the resolved workspace before running", async () => {
|
||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce({
|
||||
aborted: false,
|
||||
promptError: null,
|
||||
timedOut: false,
|
||||
sessionIdUsed: "test-session",
|
||||
assistantTexts: ["Response 1"],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
await runEmbeddedPiAgent({
|
||||
sessionId: "test-session",
|
||||
sessionKey: "test-key",
|
||||
sessionFile: "/tmp/session.json",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
prompt: "hello",
|
||||
timeoutMs: 30000,
|
||||
runId: "run-plugin-bootstrap",
|
||||
});
|
||||
|
||||
expect(runtimePluginMocks.ensureRuntimePluginsLoaded).toHaveBeenCalledWith({
|
||||
config: undefined,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards sender identity fields into embedded attempts", async () => {
|
||||
mockedRunEmbeddedAttempt.mockResolvedValueOnce({
|
||||
aborted: false,
|
||||
|
||||
18
src/agents/runtime-plugins.ts
Normal file
18
src/agents/runtime-plugins.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
export function ensureRuntimePluginsLoaded(params: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string | null;
|
||||
}): void {
|
||||
const workspaceDir =
|
||||
typeof params.workspaceDir === "string" && params.workspaceDir.trim()
|
||||
? resolveUserPath(params.workspaceDir)
|
||||
: undefined;
|
||||
|
||||
loadOpenClawPlugins({
|
||||
config: params.config,
|
||||
workspaceDir,
|
||||
});
|
||||
}
|
||||
91
src/agents/subagent-registry.context-engine.test.ts
Normal file
91
src/agents/subagent-registry.context-engine.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
ensureRuntimePluginsLoaded: vi.fn(),
|
||||
ensureContextEnginesInitialized: vi.fn(),
|
||||
resolveContextEngine: vi.fn(),
|
||||
onSubagentEnded: vi.fn(async () => {}),
|
||||
onAgentEvent: vi.fn(() => () => {}),
|
||||
persistSubagentRunsToDisk: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../context-engine/init.js", () => ({
|
||||
ensureContextEnginesInitialized: mocks.ensureContextEnginesInitialized,
|
||||
}));
|
||||
|
||||
vi.mock("../context-engine/registry.js", () => ({
|
||||
resolveContextEngine: mocks.resolveContextEngine,
|
||||
}));
|
||||
|
||||
vi.mock("../infra/agent-events.js", () => ({
|
||||
onAgentEvent: mocks.onAgentEvent,
|
||||
}));
|
||||
|
||||
vi.mock("./runtime-plugins.js", () => ({
|
||||
ensureRuntimePluginsLoaded: mocks.ensureRuntimePluginsLoaded,
|
||||
}));
|
||||
|
||||
vi.mock("./subagent-registry-state.js", () => ({
|
||||
getSubagentRunsSnapshotForRead: vi.fn((runs: Map<string, unknown>) => new Map(runs)),
|
||||
persistSubagentRunsToDisk: mocks.persistSubagentRunsToDisk,
|
||||
restoreSubagentRunsFromDisk: vi.fn(() => 0),
|
||||
}));
|
||||
|
||||
vi.mock("./subagent-announce-queue.js", () => ({
|
||||
resetAnnounceQueuesForTests: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./timeout.js", () => ({
|
||||
resolveAgentTimeoutMs: vi.fn(() => 1_000),
|
||||
}));
|
||||
|
||||
import {
|
||||
registerSubagentRun,
|
||||
releaseSubagentRun,
|
||||
resetSubagentRegistryForTests,
|
||||
} from "./subagent-registry.js";
|
||||
|
||||
describe("subagent-registry context-engine bootstrap", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveContextEngine.mockResolvedValue({
|
||||
onSubagentEnded: mocks.onSubagentEnded,
|
||||
});
|
||||
resetSubagentRegistryForTests({ persist: false });
|
||||
});
|
||||
|
||||
it("reloads runtime plugins with the spawned workspace before subagent end hooks", async () => {
|
||||
registerSubagentRun({
|
||||
runId: "run-1",
|
||||
childSessionKey: "agent:main:session:child",
|
||||
requesterSessionKey: "agent:main:session:parent",
|
||||
requesterDisplayKey: "parent",
|
||||
task: "task",
|
||||
cleanup: "keep",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
|
||||
releaseSubagentRun("run-1");
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mocks.ensureRuntimePluginsLoaded).toHaveBeenCalledWith({
|
||||
config: {},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
});
|
||||
expect(mocks.ensureContextEnginesInitialized).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.onSubagentEnded).toHaveBeenCalledWith({
|
||||
childSessionKey: "agent:main:session:child",
|
||||
reason: "released",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { onAgentEvent } from "../infra/agent-events.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
|
||||
import { ensureRuntimePluginsLoaded } from "./runtime-plugins.js";
|
||||
import { resetAnnounceQueuesForTests } from "./subagent-announce-queue.js";
|
||||
import {
|
||||
captureSubagentCompletionReply,
|
||||
@@ -313,10 +314,16 @@ function schedulePendingLifecycleError(params: { runId: string; endedAt: number;
|
||||
async function notifyContextEngineSubagentEnded(params: {
|
||||
childSessionKey: string;
|
||||
reason: SubagentEndReason;
|
||||
workspaceDir?: string;
|
||||
}) {
|
||||
try {
|
||||
const cfg = loadConfig();
|
||||
ensureRuntimePluginsLoaded({
|
||||
config: cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
ensureContextEnginesInitialized();
|
||||
const engine = await resolveContextEngine(loadConfig());
|
||||
const engine = await resolveContextEngine(cfg);
|
||||
if (!engine.onSubagentEnded) {
|
||||
return;
|
||||
}
|
||||
@@ -714,6 +721,7 @@ async function sweepSubagentRuns() {
|
||||
void notifyContextEngineSubagentEnded({
|
||||
childSessionKey: entry.childSessionKey,
|
||||
reason: "swept",
|
||||
workspaceDir: entry.workspaceDir,
|
||||
});
|
||||
subagentRuns.delete(runId);
|
||||
mutated = true;
|
||||
@@ -963,6 +971,7 @@ function completeCleanupBookkeeping(params: {
|
||||
void notifyContextEngineSubagentEnded({
|
||||
childSessionKey: params.entry.childSessionKey,
|
||||
reason: "deleted",
|
||||
workspaceDir: params.entry.workspaceDir,
|
||||
});
|
||||
subagentRuns.delete(params.runId);
|
||||
persistSubagentRuns();
|
||||
@@ -972,6 +981,7 @@ function completeCleanupBookkeeping(params: {
|
||||
void notifyContextEngineSubagentEnded({
|
||||
childSessionKey: params.entry.childSessionKey,
|
||||
reason: "completed",
|
||||
workspaceDir: params.entry.workspaceDir,
|
||||
});
|
||||
params.entry.cleanupCompletedAt = params.completedAt;
|
||||
persistSubagentRuns();
|
||||
@@ -1143,6 +1153,7 @@ export function registerSubagentRun(params: {
|
||||
cleanup: "delete" | "keep";
|
||||
label?: string;
|
||||
model?: string;
|
||||
workspaceDir?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
expectsCompletionMessage?: boolean;
|
||||
spawnMode?: "run" | "session";
|
||||
@@ -1171,6 +1182,7 @@ export function registerSubagentRun(params: {
|
||||
spawnMode,
|
||||
label: params.label,
|
||||
model: params.model,
|
||||
workspaceDir: params.workspaceDir,
|
||||
runTimeoutSeconds,
|
||||
createdAt: now,
|
||||
startedAt: now,
|
||||
@@ -1285,6 +1297,7 @@ export function releaseSubagentRun(runId: string) {
|
||||
void notifyContextEngineSubagentEnded({
|
||||
childSessionKey: entry.childSessionKey,
|
||||
reason: "released",
|
||||
workspaceDir: entry.workspaceDir,
|
||||
});
|
||||
}
|
||||
const didDelete = subagentRuns.delete(runId);
|
||||
|
||||
@@ -13,6 +13,7 @@ export type SubagentRunRecord = {
|
||||
cleanup: "delete" | "keep";
|
||||
label?: string;
|
||||
model?: string;
|
||||
workspaceDir?: string;
|
||||
runTimeoutSeconds?: number;
|
||||
spawnMode?: SpawnSubagentMode;
|
||||
createdAt: number;
|
||||
|
||||
@@ -650,6 +650,7 @@ export async function spawnSubagentDirect(
|
||||
cleanup,
|
||||
label: label || undefined,
|
||||
model: resolvedModel,
|
||||
workspaceDir: spawnedMetadata.workspaceDir,
|
||||
runTimeoutSeconds,
|
||||
expectsCompletionMessage,
|
||||
spawnMode,
|
||||
|
||||
Reference in New Issue
Block a user