mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-15 07:04:01 -06:00
fix(codex): stop quoted context from triggering explicit skill mentions (#123345)
Codex byte-scans every turn text input for $name skill mentions and [@name](plugin://...) links (codex-rs/skills/src/mentions.rs), so historical $skill tokens inside OpenClaw's projected <conversation_context> block and inbound quoted-reply/room-backlog context counted as current explicit invocations and injected skill bodies the user never requested. Neutralize mention sigils with same-length fullwidth lookalikes (the escapeCodexChatText technique) in projected history and inbound context; only the raw current request stays selectable. Fixes #122812
This commit is contained in:
committed by
GitHub
parent
5053635a70
commit
e04dfd26e2
@@ -71,6 +71,26 @@ describe("projectContextEngineAssemblyForCodex", () => {
|
||||
expect(ordered.prePromptMessageCount).toBe(1);
|
||||
});
|
||||
|
||||
it("neutralizes explicit mention sigils in projected history but not the current request", () => {
|
||||
const result = projectContextEngineAssemblyForCodex({
|
||||
assembledMessages: [
|
||||
textMessage("assistant", "The user did not invoke $example-manual."),
|
||||
textMessage("user", "see [$other-skill](skill://other) and [@pkg](plugin://pkg@mp)"),
|
||||
],
|
||||
originalHistoryMessages: [],
|
||||
prompt: "run $current-skill now",
|
||||
});
|
||||
|
||||
const context = result.promptText.slice(0, result.promptContextRange?.end);
|
||||
// Codex byte-scans the whole turn text for `$name`; historical tokens must
|
||||
// not survive in scannable form (codex-rs/skills/src/mentions.rs).
|
||||
expect(context).not.toContain("$example-manual");
|
||||
expect(context).toContain("$example-manual");
|
||||
expect(context).toContain("[$other-skill](skill://other)");
|
||||
expect(context).toContain("[@pkg](plugin://pkg@mp)");
|
||||
expect(result.promptText).toContain("Current user request:\nrun $current-skill now");
|
||||
});
|
||||
|
||||
it("frames projected history as reference data and omits tool payloads", () => {
|
||||
const result = projectContextEngineAssemblyForCodex({
|
||||
assembledMessages: [
|
||||
|
||||
@@ -38,6 +38,17 @@ const DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS = 20_000;
|
||||
const MIN_PROMPT_BUDGET_RATIO = 0.5;
|
||||
const MIN_PROMPT_BUDGET_TOKENS = 8_000;
|
||||
|
||||
// Codex scans every turn text input byte-for-byte for explicit `$name` skill
|
||||
// mentions and `[@name](plugin://…)` links (codex-rs/skills/src/mentions.rs);
|
||||
// quoted history must never count as a current explicit invocation, so swap
|
||||
// the sigils to same-length fullwidth lookalikes (same technique as
|
||||
// escapeCodexChatText). Only the raw current request stays selectable.
|
||||
export function neutralizeCodexExplicitMentionSigils(text: string): string {
|
||||
return text
|
||||
.replace(/\$(?=[A-Za-z0-9_:-])/gu, "$")
|
||||
.replace(/\[@(?=[A-Za-z0-9_:-]+\]\()/gu, "[@");
|
||||
}
|
||||
|
||||
/** Projects assembled OpenClaw context-engine messages into Codex prompt inputs. */
|
||||
export function projectContextEngineAssemblyForCodex(params: {
|
||||
assembledMessages: AgentMessage[];
|
||||
@@ -50,10 +61,12 @@ export function projectContextEngineAssemblyForCodex(params: {
|
||||
const prompt = params.prompt.trim();
|
||||
const contextMessages = dropDuplicateTrailingPrompt(params.assembledMessages, prompt);
|
||||
const maxRenderedContextChars = normalizeRenderedContextMaxChars(params.maxRenderedContextChars);
|
||||
const renderedContext = renderMessagesForCodexContext(contextMessages, {
|
||||
maxTextPartChars: resolveTextPartMaxChars(maxRenderedContextChars),
|
||||
toolPayloadMode: params.toolPayloadMode ?? "elide",
|
||||
});
|
||||
const renderedContext = neutralizeCodexExplicitMentionSigils(
|
||||
renderMessagesForCodexContext(contextMessages, {
|
||||
maxTextPartChars: resolveTextPartMaxChars(maxRenderedContextChars),
|
||||
toolPayloadMode: params.toolPayloadMode ?? "elide",
|
||||
}),
|
||||
);
|
||||
const boundedContext = renderedContext
|
||||
? truncateOlderContext(renderedContext, maxRenderedContextChars)
|
||||
: undefined;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Codex tests cover run-attempt prompt state helpers.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { prependCurrentInboundContext } from "./run-attempt-state.js";
|
||||
|
||||
describe("prependCurrentInboundContext", () => {
|
||||
it("neutralizes explicit mention sigils in inbound context but not the prompt", () => {
|
||||
const joined = prependCurrentInboundContext("run $current-skill now", {
|
||||
text: "Quoted reply: please try $example-manual later",
|
||||
});
|
||||
|
||||
expect(joined).toBe(
|
||||
"Quoted reply: please try $example-manual later\n\nrun $current-skill now",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the prompt unchanged without inbound context", () => {
|
||||
expect(prependCurrentInboundContext("run $current-skill now", undefined)).toBe(
|
||||
"run $current-skill now",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { EmbeddedRunAttemptResult } from "./attempt-terminal.js";
|
||||
import { CodexAppServerRpcError } from "./client.js";
|
||||
import { neutralizeCodexExplicitMentionSigils } from "./context-engine-projection.js";
|
||||
import { isJsonObject, type CodexServerNotification } from "./protocol.js";
|
||||
import type {
|
||||
CodexAppServerBindingIdentity,
|
||||
@@ -136,8 +137,12 @@ export function prependCurrentInboundContext(
|
||||
prompt: string,
|
||||
context: EmbeddedRunAttemptParams["currentInboundContext"],
|
||||
): string {
|
||||
// Inbound context carries quoted replies and room backlog, not the raw
|
||||
// current request; Codex must not resolve explicit mentions from it.
|
||||
const text = context?.text.trim();
|
||||
return text ? [text, prompt].filter(Boolean).join("\n\n") : prompt;
|
||||
return text
|
||||
? [neutralizeCodexExplicitMentionSigils(text), prompt].filter(Boolean).join("\n\n")
|
||||
: prompt;
|
||||
}
|
||||
|
||||
export function waitForCodexNotificationDispatchTurn(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user