mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-18 13:26:45 +00: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:
parent
5053635a70
commit
e04dfd26e2
4 changed files with 64 additions and 5 deletions
|
|
@ -71,6 +71,26 @@ describe("projectContextEngineAssemblyForCodex", () => {
|
||||||
expect(ordered.prePromptMessageCount).toBe(1);
|
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", () => {
|
it("frames projected history as reference data and omits tool payloads", () => {
|
||||||
const result = projectContextEngineAssemblyForCodex({
|
const result = projectContextEngineAssemblyForCodex({
|
||||||
assembledMessages: [
|
assembledMessages: [
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,17 @@ const DEFAULT_CODEX_PROJECTION_RESERVE_TOKENS = 20_000;
|
||||||
const MIN_PROMPT_BUDGET_RATIO = 0.5;
|
const MIN_PROMPT_BUDGET_RATIO = 0.5;
|
||||||
const MIN_PROMPT_BUDGET_TOKENS = 8_000;
|
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. */
|
/** Projects assembled OpenClaw context-engine messages into Codex prompt inputs. */
|
||||||
export function projectContextEngineAssemblyForCodex(params: {
|
export function projectContextEngineAssemblyForCodex(params: {
|
||||||
assembledMessages: AgentMessage[];
|
assembledMessages: AgentMessage[];
|
||||||
|
|
@ -50,10 +61,12 @@ export function projectContextEngineAssemblyForCodex(params: {
|
||||||
const prompt = params.prompt.trim();
|
const prompt = params.prompt.trim();
|
||||||
const contextMessages = dropDuplicateTrailingPrompt(params.assembledMessages, prompt);
|
const contextMessages = dropDuplicateTrailingPrompt(params.assembledMessages, prompt);
|
||||||
const maxRenderedContextChars = normalizeRenderedContextMaxChars(params.maxRenderedContextChars);
|
const maxRenderedContextChars = normalizeRenderedContextMaxChars(params.maxRenderedContextChars);
|
||||||
const renderedContext = renderMessagesForCodexContext(contextMessages, {
|
const renderedContext = neutralizeCodexExplicitMentionSigils(
|
||||||
maxTextPartChars: resolveTextPartMaxChars(maxRenderedContextChars),
|
renderMessagesForCodexContext(contextMessages, {
|
||||||
toolPayloadMode: params.toolPayloadMode ?? "elide",
|
maxTextPartChars: resolveTextPartMaxChars(maxRenderedContextChars),
|
||||||
});
|
toolPayloadMode: params.toolPayloadMode ?? "elide",
|
||||||
|
}),
|
||||||
|
);
|
||||||
const boundedContext = renderedContext
|
const boundedContext = renderedContext
|
||||||
? truncateOlderContext(renderedContext, maxRenderedContextChars)
|
? truncateOlderContext(renderedContext, maxRenderedContextChars)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
|
||||||
21
extensions/codex/src/app-server/run-attempt-state.test.ts
Normal file
21
extensions/codex/src/app-server/run-attempt-state.test.ts
Normal file
|
|
@ -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";
|
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||||
import type { EmbeddedRunAttemptResult } from "./attempt-terminal.js";
|
import type { EmbeddedRunAttemptResult } from "./attempt-terminal.js";
|
||||||
import { CodexAppServerRpcError } from "./client.js";
|
import { CodexAppServerRpcError } from "./client.js";
|
||||||
|
import { neutralizeCodexExplicitMentionSigils } from "./context-engine-projection.js";
|
||||||
import { isJsonObject, type CodexServerNotification } from "./protocol.js";
|
import { isJsonObject, type CodexServerNotification } from "./protocol.js";
|
||||||
import type {
|
import type {
|
||||||
CodexAppServerBindingIdentity,
|
CodexAppServerBindingIdentity,
|
||||||
|
|
@ -136,8 +137,12 @@ export function prependCurrentInboundContext(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
context: EmbeddedRunAttemptParams["currentInboundContext"],
|
context: EmbeddedRunAttemptParams["currentInboundContext"],
|
||||||
): string {
|
): 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();
|
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> {
|
export function waitForCodexNotificationDispatchTurn(): Promise<void> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue