fix: keep reply context out of image scanning (#76659)

This commit is contained in:
Peter Steinberger
2026-05-03 14:37:15 +01:00
parent abed4231aa
commit 54c0f982d5
4 changed files with 22 additions and 5 deletions

View File

@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- Gateway/update: run `doctor --non-interactive --fix` after Control UI global package updates before reporting success, so legacy config is migrated before the gateway restart. Thanks @stevenchouai.
- Gateway/cron: stop a lazy cron startup that loses a hot-reload race, preventing the old cron service from starting after reload has already replaced cron state.
- CLI/plugins: warn when npm plugin installs remain shadowed by a failing config-selected source and surface the repair path in `plugins doctor`. Thanks @LindalyX-Lee.
- Agents/Telegram: preserve explicit reply and quote context in embedded model prompts without letting quoted text drive prompt-local image loading. Fixes #76419. (#76659) Thanks @cheechnd.
- Active Memory: apply `setupGraceTimeoutMs` to the embedded recall runner as well as the outer prompt-build watchdog, so very-cold first recalls keep the configured setup grace end-to-end. (#74480) Thanks @volcano303.
- CLI/config: keep JSON dry-run patches validating touched channel configuration against bundled channel schemas even when the patch only contains SecretRef objects.
- Plugins/tools: keep disabled bundled tool plugins out of explicit runtime allowlist ownership and fall back from loaded-but-empty channel registries to tool-bearing plugin registries, so Active Memory can use bundled `memory-core` search/get tools even when `memory-lancedb` is disabled. Fixes #76603. Thanks @jwong-art.

View File

@@ -128,6 +128,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
resetEmbeddedAttemptHarness();
clearMemoryPluginState();
hoisted.runContextEngineMaintenanceMock.mockReset().mockResolvedValue(undefined);
hoisted.detectAndLoadPromptImagesMock.mockClear();
});
afterEach(async () => {
@@ -225,7 +226,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
currentTurnContext: {
reply: {
senderLabel: "Mike",
body: "WT daily plan Sat May 2",
body: "WT daily plan - Sat May 2\nSee ./quoted-secret.png and [media attached: media://inbound/quoted.png]",
},
},
},
@@ -241,10 +242,16 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
expect(seenPrompt).toContain("what does this mean?");
expect(seenPrompt).toContain("Replied message (untrusted, for context):");
expect(seenPrompt).toContain('"sender_label": "Mike"');
expect(seenPrompt).toContain('"body": "WT daily plan Sat May 2"');
expect(seenPrompt).toContain("WT daily plan - Sat May 2");
expect(seenPrompt).toContain("./quoted-secret.png");
expect(seenPrompt).toContain("media://inbound/quoted.png");
expect(seenPrompt).not.toContain("OPENCLAW_INTERNAL_CONTEXT");
expect(seenPrompt).not.toContain("secret runtime context");
expect(result.finalPromptText).toBe(seenPrompt);
expect(hoisted.detectAndLoadPromptImagesMock).toHaveBeenCalledTimes(1);
expect(hoisted.detectAndLoadPromptImagesMock.mock.calls[0]?.[0]).toMatchObject({
prompt: "what does this mean?",
});
const trajectoryEvents = (
await fs.readFile(path.join(tempPaths[0] ?? "", "session.trajectory.jsonl"), "utf8")
)
@@ -253,7 +260,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => {
.map((line) => JSON.parse(line) as TrajectoryEvent);
const promptSubmitted = trajectoryEvents.find((event) => event.type === "prompt.submitted");
expect(promptSubmitted?.data?.prompt).toBe(seenPrompt);
expect(promptSubmitted?.data?.prompt).toContain("WT daily plan Sat May 2");
expect(promptSubmitted?.data?.prompt).toContain("WT daily plan - Sat May 2");
expect(promptSubmitted?.data?.prompt).not.toContain("secret runtime context");
});

View File

@@ -77,6 +77,7 @@ type AttemptSpawnWorkspaceHoisted = {
getGlobalHookRunnerMock: Mock<() => unknown>;
initializeGlobalHookRunnerMock: UnknownMock;
runContextEngineMaintenanceMock: AsyncContextEngineMaintenanceMock;
detectAndLoadPromptImagesMock: AsyncUnknownMock;
getHistoryLimitFromSessionKeyMock: Mock<
(sessionKey: string | undefined, config: unknown) => number | undefined
>;
@@ -148,6 +149,12 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
const getGlobalHookRunnerMock = vi.fn<() => unknown>(() => undefined);
const initializeGlobalHookRunnerMock = vi.fn();
const runContextEngineMaintenanceMock = vi.fn(async (_params?: unknown) => undefined);
const detectAndLoadPromptImagesMock = vi.fn(async () => ({
images: [],
detectedRefs: [],
loadedCount: 0,
skippedCount: 0,
}));
const getHistoryLimitFromSessionKeyMock = vi.fn<
(sessionKey: string | undefined, config: unknown) => number | undefined
>(() => undefined);
@@ -187,6 +194,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
getGlobalHookRunnerMock,
initializeGlobalHookRunnerMock,
runContextEngineMaintenanceMock,
detectAndLoadPromptImagesMock,
getHistoryLimitFromSessionKeyMock,
limitHistoryTurnsMock,
preemptiveCompactionCalls,
@@ -386,7 +394,8 @@ vi.mock("../runs.js", () => ({
}));
vi.mock("./images.js", () => ({
detectAndLoadPromptImages: async () => ({ images: [] }),
detectAndLoadPromptImages: (...args: unknown[]) =>
(hoisted.detectAndLoadPromptImagesMock as (...args: unknown[]) => unknown)(...args),
}));
vi.mock("../../system-prompt-params.js", () => ({

View File

@@ -2811,7 +2811,7 @@ export async function runEmbeddedAttempt(
// Detect and load images referenced in the visible prompt for vision-capable models.
// Images are prompt-local only (pi-like behavior).
const imageResult = await detectAndLoadPromptImages({
prompt: promptForModel,
prompt: promptSubmission.prompt,
workspaceDir: effectiveWorkspace,
model: params.model,
existingImages: params.images,