diff --git a/CHANGELOG.md b/CHANGELOG.md index cd47e8c2401..3c194523be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts index cb8ec574ec0..00792956302 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -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"); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts index 32360e52c68..4851498943d 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts @@ -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", () => ({ diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 9542a404958..05ad3d2ca47 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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,