mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:20:43 +00:00
fix: rebuild sandbox skill prompts from sandbox workspace (#77661)
Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
@@ -71,6 +71,8 @@ Docs: https://docs.openclaw.ai
|
||||
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
|
||||
- Gateway/startup: load provider plugins that own explicitly configured image, video, or music generation defaults so generation tools become live after gateway restart instead of remaining catalog-only. Fixes #77244. Thanks @buyuangtampan, @Nikoxx99, and @vincentkoc.
|
||||
- Control UI/chat: suppress `HEARTBEAT_OK` acknowledgement history, streams, deltas, and final events before they enter the transcript view, so repeated heartbeat no-op turns do not stack noisy bubbles. Thanks @BunsDev.
|
||||
- Agents/skills: require exact `<location>` skill paths for both single-skill and multi-skill prompt selection, so agents do not guess or hard-code skill file paths. (#74161) Thanks @lanzhi-lee.
|
||||
- Agents/skills: rebuild sandboxed non-rw run skill prompts from the sandbox workspace copy, so `<available_skills>` no longer points at host-only `~/.openclaw/skills` paths. Fixes #50590. Thanks @kidroca and @sallyom.
|
||||
- Slack/subagents: keep resumed parent `message.send` calls in the originating Slack thread when ambient session thread context is present, and suppress successful silent child completion rows from follow-up findings. Thanks @bek91.
|
||||
- Slack/mentions: record thread participation for successful visible threaded Slack sends, including message-tool and media delivery paths, so unmentioned replies in bot-participated threads can bypass mention gating as documented. Fixes #77648. Thanks @bek91.
|
||||
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
|
||||
@@ -1413,7 +1415,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/plugins: enable the native `require()` fast path on Windows for bundled plugin modules so plugin loading uses `require()` instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev.
|
||||
- macOS app: detect stale Gateway TLS certificate pins, automatically repair trusted Tailscale Serve rotations, and surface paired-but-disconnected Mac companion nodes so partial Gateway connections no longer look healthy. Thanks @guti.
|
||||
- Feishu: recreate WebSocket clients with monitor-owned backoff only after SDK reconnect exhaustion, preserving heartbeat defaults and shutdown cleanup without treating recoverable SDK callback errors as terminal, so persistent connections recover without manual gateway restart. Fixes #52618; duplicate evidence #59753; related #55532, #68766, #72411, and #73739. Thanks @vincentkoc, @schumilin, @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.
|
||||
- Agents/skills: require exact `<location>` skill paths for both single-skill and multi-skill prompt selection, so agents do not guess or hard-code skill file paths. (#74161) Thanks @lanzhi-lee.
|
||||
|
||||
## 2026.4.27
|
||||
|
||||
|
||||
@@ -576,15 +576,17 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
let checkpointSnapshot: CapturedCompactionCheckpointSnapshot | null = null;
|
||||
let checkpointSnapshotRetained = false;
|
||||
try {
|
||||
const skillsSnapshotForRun =
|
||||
sandbox?.enabled && sandbox.workspaceAccess !== "rw" ? undefined : params.skillsSnapshot;
|
||||
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
agentId: effectiveSkillAgentId,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
skillsSnapshot: skillsSnapshotForRun,
|
||||
});
|
||||
restoreSkillEnv = params.skillsSnapshot
|
||||
restoreSkillEnv = skillsSnapshotForRun
|
||||
? applySkillEnvOverridesFromSnapshot({
|
||||
snapshot: params.skillsSnapshot,
|
||||
snapshot: skillsSnapshotForRun,
|
||||
config: params.config,
|
||||
})
|
||||
: applySkillEnvOverrides({
|
||||
@@ -592,7 +594,7 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
config: params.config,
|
||||
});
|
||||
const skillsPrompt = resolveSkillsPromptForRun({
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
skillsSnapshot: skillsSnapshotForRun,
|
||||
entries: shouldLoadSkillEntries ? skillEntries : undefined,
|
||||
config: params.config,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -210,6 +211,59 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rebuilds skill prompt inputs from the sandbox workspace for non-rw sandbox runs", async () => {
|
||||
const sandboxWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sandbox-skills-"));
|
||||
tempPaths.push(sandboxWorkspace);
|
||||
hoisted.resolveSandboxContextMock.mockResolvedValue({
|
||||
enabled: true,
|
||||
workspaceAccess: "ro",
|
||||
workspaceDir: sandboxWorkspace,
|
||||
});
|
||||
|
||||
await createContextEngineAttemptRunner({
|
||||
contextEngine: createContextEngineBootstrapAndAssemble(),
|
||||
sessionKey,
|
||||
tempPaths,
|
||||
attemptOverrides: {
|
||||
skillsSnapshot: {
|
||||
prompt:
|
||||
"<available_skills><skill><location>~/.openclaw/skills/smaug/SKILL.md</location></skill></available_skills>",
|
||||
skills: [{ name: "smaug" }],
|
||||
resolvedSkills: [
|
||||
{
|
||||
name: "smaug",
|
||||
description: "Host copy",
|
||||
disableModelInvocation: false,
|
||||
filePath: "/Users/alice/.openclaw/skills/smaug/SKILL.md",
|
||||
baseDir: "/Users/alice/.openclaw/skills/smaug",
|
||||
source: "openclaw-workspace",
|
||||
sourceInfo: {
|
||||
path: "/Users/alice/.openclaw/skills/smaug/SKILL.md",
|
||||
source: "openclaw-workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: "/Users/alice/.openclaw/skills/smaug",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(hoisted.resolveEmbeddedRunSkillEntriesMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: sandboxWorkspace,
|
||||
skillsSnapshot: undefined,
|
||||
}),
|
||||
);
|
||||
expect(hoisted.resolveSkillsPromptForRunMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: sandboxWorkspace,
|
||||
skillsSnapshot: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps before_prompt_build prependContext out of system prompt on transcriptPrompt runs", async () => {
|
||||
const runBeforePromptBuild = vi.fn(async () => ({ prependContext: "dynamic hook context" }));
|
||||
hoisted.getGlobalHookRunnerMock.mockReturnValue({
|
||||
|
||||
@@ -69,13 +69,13 @@ type AttemptSpawnWorkspaceHoisted = {
|
||||
installContextEngineLoopHookMock: UnknownMock;
|
||||
flushPendingToolResultsAfterIdleMock: AsyncUnknownMock;
|
||||
releaseWsSessionMock: UnknownMock;
|
||||
resolveBootstrapFilesForRunMock: Mock<
|
||||
(...args: unknown[]) => Promise<WorkspaceBootstrapFile[]>
|
||||
>;
|
||||
resolveBootstrapFilesForRunMock: Mock<(...args: unknown[]) => Promise<WorkspaceBootstrapFile[]>>;
|
||||
resolveBootstrapContextForRunMock: Mock<() => Promise<BootstrapContext>>;
|
||||
isWorkspaceBootstrapPendingMock: Mock<(workspaceDir: string) => Promise<boolean>>;
|
||||
resolveContextInjectionModeMock: Mock<() => "always" | "continuation-skip">;
|
||||
hasCompletedBootstrapTurnMock: Mock<() => Promise<boolean>>;
|
||||
resolveEmbeddedRunSkillEntriesMock: UnknownMock;
|
||||
resolveSkillsPromptForRunMock: UnknownMock;
|
||||
supportsModelToolsMock: Mock<(model?: unknown) => boolean>;
|
||||
getGlobalHookRunnerMock: Mock<() => unknown>;
|
||||
initializeGlobalHookRunnerMock: UnknownMock;
|
||||
@@ -155,6 +155,11 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
|
||||
() => "always",
|
||||
);
|
||||
const hasCompletedBootstrapTurnMock = vi.fn<() => Promise<boolean>>(async () => false);
|
||||
const resolveEmbeddedRunSkillEntriesMock = vi.fn(() => ({
|
||||
shouldLoadSkillEntries: false,
|
||||
skillEntries: undefined,
|
||||
}));
|
||||
const resolveSkillsPromptForRunMock = vi.fn(() => "");
|
||||
const supportsModelToolsMock = vi.fn<(model?: unknown) => boolean>(() => true);
|
||||
const getGlobalHookRunnerMock = vi.fn<() => unknown>(() => undefined);
|
||||
const initializeGlobalHookRunnerMock = vi.fn();
|
||||
@@ -202,6 +207,8 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
|
||||
isWorkspaceBootstrapPendingMock,
|
||||
resolveContextInjectionModeMock,
|
||||
hasCompletedBootstrapTurnMock,
|
||||
resolveEmbeddedRunSkillEntriesMock,
|
||||
resolveSkillsPromptForRunMock,
|
||||
supportsModelToolsMock,
|
||||
getGlobalHookRunnerMock,
|
||||
initializeGlobalHookRunnerMock,
|
||||
@@ -306,14 +313,12 @@ vi.mock("../../bootstrap-files.js", async () => {
|
||||
vi.mock("../../skills.js", () => ({
|
||||
applySkillEnvOverrides: () => () => {},
|
||||
applySkillEnvOverridesFromSnapshot: () => () => {},
|
||||
resolveSkillsPromptForRun: () => "",
|
||||
resolveSkillsPromptForRun: (...args: unknown[]) => hoisted.resolveSkillsPromptForRunMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../skills-runtime.js", () => ({
|
||||
resolveEmbeddedRunSkillEntries: () => ({
|
||||
shouldLoadSkillEntries: false,
|
||||
skillEntries: undefined,
|
||||
}),
|
||||
resolveEmbeddedRunSkillEntries: (...args: unknown[]) =>
|
||||
hoisted.resolveEmbeddedRunSkillEntriesMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../context-engine-maintenance.js", () => ({
|
||||
@@ -839,6 +844,11 @@ export function resetEmbeddedAttemptHarness(
|
||||
hoisted.isWorkspaceBootstrapPendingMock.mockReset().mockResolvedValue(false);
|
||||
hoisted.resolveContextInjectionModeMock.mockReset().mockReturnValue("always");
|
||||
hoisted.hasCompletedBootstrapTurnMock.mockReset().mockResolvedValue(false);
|
||||
hoisted.resolveEmbeddedRunSkillEntriesMock.mockReset().mockReturnValue({
|
||||
shouldLoadSkillEntries: false,
|
||||
skillEntries: undefined,
|
||||
});
|
||||
hoisted.resolveSkillsPromptForRunMock.mockReset().mockReturnValue("");
|
||||
hoisted.supportsModelToolsMock.mockReset().mockReturnValue(true);
|
||||
hoisted.getGlobalHookRunnerMock.mockReset().mockReturnValue(undefined);
|
||||
hoisted.runContextEngineMaintenanceMock.mockReset().mockResolvedValue(undefined);
|
||||
|
||||
@@ -713,15 +713,17 @@ export async function runEmbeddedAttempt(
|
||||
| ((outcome: "completed" | "aborted" | "error", err?: unknown) => void)
|
||||
| undefined;
|
||||
try {
|
||||
const skillsSnapshotForRun =
|
||||
sandbox?.enabled && sandbox.workspaceAccess !== "rw" ? undefined : params.skillsSnapshot;
|
||||
const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries({
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
skillsSnapshot: skillsSnapshotForRun,
|
||||
});
|
||||
restoreSkillEnv = params.skillsSnapshot
|
||||
restoreSkillEnv = skillsSnapshotForRun
|
||||
? applySkillEnvOverridesFromSnapshot({
|
||||
snapshot: params.skillsSnapshot,
|
||||
snapshot: skillsSnapshotForRun,
|
||||
config: params.config,
|
||||
})
|
||||
: applySkillEnvOverrides({
|
||||
@@ -730,7 +732,7 @@ export async function runEmbeddedAttempt(
|
||||
});
|
||||
|
||||
const skillsPrompt = resolveSkillsPromptForRun({
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
skillsSnapshot: skillsSnapshotForRun,
|
||||
entries: shouldLoadSkillEntries ? skillEntries : undefined,
|
||||
config: params.config,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
|
||||
Reference in New Issue
Block a user