mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-27 01:09:32 +00:00
fix(harness): protect reset and prompt bounds
This commit is contained in:
@@ -255,6 +255,30 @@ describe("projectContextEngineAssemblyForCodex", () => {
|
||||
expect(fitted).toContain("[truncated ");
|
||||
});
|
||||
|
||||
it("keeps the current request when a hook appends oversized context", () => {
|
||||
const before = "OpenClaw assembled context for this turn:\n<conversation_context>\n";
|
||||
const context = `recent context ${"c".repeat(200)}`;
|
||||
const request = "\n</conversation_context>\n\nCurrent user request:\nkeep this request";
|
||||
const hookAppend = `\n\nhook context ${"h".repeat(800)}`;
|
||||
const promptText = `${before}${context}${request}${hookAppend}`;
|
||||
const maxChars = 420;
|
||||
|
||||
const fitted = fitCodexProjectedContextForTurnStart({
|
||||
promptText,
|
||||
contextRange: { start: before.length, end: before.length + context.length },
|
||||
requestRange: {
|
||||
start: before.length + context.length,
|
||||
end: before.length + context.length + request.length,
|
||||
},
|
||||
maxChars,
|
||||
});
|
||||
|
||||
expect(fitted.length).toBeLessThanOrEqual(maxChars);
|
||||
expect(fitted).toContain("recent context");
|
||||
expect(fitted).toContain("Current user request:\nkeep this request");
|
||||
expect(fitted).not.toContain("hook context");
|
||||
});
|
||||
|
||||
it("bounds output for a large request under the default Codex turn limit", () => {
|
||||
const maxChars = CODEX_TURN_START_TEXT_INPUT_MAX_CHARS;
|
||||
// A large assembled header prefix already over the cap forces the
|
||||
|
||||
@@ -121,6 +121,7 @@ export function resolveCodexContextEngineProjectionReserveTokens(params: {
|
||||
export function fitCodexProjectedContextForTurnStart(params: {
|
||||
promptText: string;
|
||||
contextRange?: CodexProjectedContextRange;
|
||||
requestRange?: CodexProjectedContextRange;
|
||||
maxChars?: number;
|
||||
}): string {
|
||||
const maxChars =
|
||||
@@ -138,6 +139,24 @@ export function fitCodexProjectedContextForTurnStart(params: {
|
||||
const beforeContext = params.promptText.slice(0, range.start);
|
||||
const context = params.promptText.slice(range.start, range.end);
|
||||
const afterContext = params.promptText.slice(range.end);
|
||||
const requestRange = normalizeProjectedContextRange(
|
||||
params.requestRange,
|
||||
params.promptText.length,
|
||||
);
|
||||
if (
|
||||
requestRange &&
|
||||
requestRange.start >= range.end &&
|
||||
requestRange.end < params.promptText.length
|
||||
) {
|
||||
const request = params.promptText.slice(requestRange.start, requestRange.end);
|
||||
if (request.length >= maxChars) {
|
||||
return truncateOlderContext(request, maxChars);
|
||||
}
|
||||
const contextBudget = maxChars - request.length;
|
||||
const fittedContext = truncateOlderContext(context, contextBudget);
|
||||
const beforeContextBudget = maxChars - fittedContext.length - request.length;
|
||||
return `${truncateOlderContext(beforeContext, beforeContextBudget)}${fittedContext}${request}`;
|
||||
}
|
||||
const contextBudget = maxChars - beforeContext.length - afterContext.length;
|
||||
if (contextBudget > 0) {
|
||||
const fittedContext = truncateOlderContext(context, contextBudget);
|
||||
|
||||
@@ -1032,7 +1032,12 @@ export async function runCodexAppServerAttempt(
|
||||
prompt: string,
|
||||
promptInputRange: { start: number; end: number } | undefined,
|
||||
turnPromptText: string,
|
||||
): CodexProjectedContextRange | undefined => {
|
||||
):
|
||||
| {
|
||||
contextRange: CodexProjectedContextRange;
|
||||
requestRange: CodexProjectedContextRange;
|
||||
}
|
||||
| undefined => {
|
||||
const promptTextInputOffset = promptInputRange
|
||||
? promptInputRange.end - promptText.length
|
||||
: undefined;
|
||||
@@ -1059,10 +1064,17 @@ export async function runCodexAppServerAttempt(
|
||||
return undefined;
|
||||
}
|
||||
const turnPromptOffset = turnPromptText.length - prompt.length + promptTextOffset;
|
||||
return {
|
||||
const contextRange = {
|
||||
start: turnPromptOffset + promptContextRange.start,
|
||||
end: turnPromptOffset + promptContextRange.end,
|
||||
};
|
||||
return {
|
||||
contextRange,
|
||||
requestRange: {
|
||||
start: contextRange.end,
|
||||
end: turnPromptOffset + promptTextOffset + promptText.length,
|
||||
},
|
||||
};
|
||||
};
|
||||
let promptBuild = await buildPromptFromCurrentInputs();
|
||||
const decorateCodexTurnPromptText = (promptBuild: {
|
||||
@@ -1078,13 +1090,15 @@ export async function runCodexAppServerAttempt(
|
||||
params.bootstrapContextRunKind === "cron",
|
||||
},
|
||||
);
|
||||
const projectedRanges = resolveShiftedPromptContextRange(
|
||||
promptBuild.prompt,
|
||||
promptBuild.promptInputRange,
|
||||
turnPromptText,
|
||||
);
|
||||
return fitCodexProjectedContextForTurnStart({
|
||||
promptText: turnPromptText,
|
||||
contextRange: resolveShiftedPromptContextRange(
|
||||
promptBuild.prompt,
|
||||
promptBuild.promptInputRange,
|
||||
turnPromptText,
|
||||
),
|
||||
contextRange: projectedRanges?.contextRange,
|
||||
requestRange: projectedRanges?.requestRange,
|
||||
});
|
||||
};
|
||||
let codexTurnPromptText = decorateCodexTurnPromptText(promptBuild);
|
||||
|
||||
@@ -625,6 +625,69 @@ describe("createCopilotAgentHarness", () => {
|
||||
expect(sessionStore.entries.get("oc-reset-race")?.sdkSessionId).toBe("sdk-sess-replacement");
|
||||
});
|
||||
|
||||
it("does not reuse a reset target while deferred cleanup is pending", async () => {
|
||||
const cleanup = createDeferred<"aborted" | "completed" | "deadline">();
|
||||
const abort = vi.fn();
|
||||
const replacementDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||
const duringResetDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||
const sessionStore = makeSessionStoreMock();
|
||||
let attempt = 0;
|
||||
mocks.runCopilotAttempt.mockImplementation(async (params, deps) => {
|
||||
attempt += 1;
|
||||
if (attempt === 1) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-before-reset",
|
||||
pooledClient: { key: {} as any, client: {} as any },
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
deps.onDeferredCompaction?.({
|
||||
abort,
|
||||
cleanup: cleanup.promise,
|
||||
sdkSessionId: "sdk-sess-before-reset",
|
||||
});
|
||||
} else if (attempt === 2) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-replacement",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: replacementDeleteSession } as any,
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
} else if (attempt === 3 && !params.initialReplayState?.sdkSessionId) {
|
||||
deps.onSessionEstablished?.({
|
||||
sdkSessionId: "sdk-sess-during-reset",
|
||||
pooledClient: {
|
||||
key: {} as any,
|
||||
client: { deleteSession: duringResetDeleteSession } as any,
|
||||
},
|
||||
sessionConfig: TEST_SESSION_CONFIG,
|
||||
});
|
||||
}
|
||||
return ATTEMPT_RESULT;
|
||||
});
|
||||
const harness = createCopilotAgentHarness({
|
||||
pool: makePoolMock(),
|
||||
sessionStore: sessionStore.store,
|
||||
});
|
||||
const params = { ...ATTEMPT_PARAMS, sessionId: "oc-reset-reuse" };
|
||||
|
||||
await harness.runAttempt(params);
|
||||
await harness.runAttempt(params);
|
||||
const reset = harness.reset?.({ sessionId: "oc-reset-reuse" });
|
||||
await vi.waitFor(() => expect(abort).toHaveBeenCalledOnce());
|
||||
await harness.runAttempt(params);
|
||||
cleanup.resolve("aborted");
|
||||
await reset;
|
||||
|
||||
expect(
|
||||
mocks.runCopilotAttempt.mock.calls[2]?.[0]?.initialReplayState?.sdkSessionId,
|
||||
).toBeUndefined();
|
||||
expect(replacementDeleteSession).toHaveBeenCalledWith("sdk-sess-replacement");
|
||||
expect(duringResetDeleteSession).not.toHaveBeenCalled();
|
||||
expect(sessionStore.entries.get("oc-reset-reuse")?.sdkSessionId).toBe("sdk-sess-during-reset");
|
||||
});
|
||||
|
||||
describe("session reuse across turns (dogfood finding #4)", () => {
|
||||
// These tests pin the harness's session-reuse contract: subsequent
|
||||
// `runAttempt` calls within the same OpenClaw session should pass
|
||||
|
||||
@@ -573,12 +573,13 @@ export function createCopilotAgentHarness(
|
||||
const currentCompactKey = computeSessionCompactKey(params);
|
||||
const compactionCleanupPending =
|
||||
openclawSessionId !== undefined && hasPendingDeferredCompactionCleanup(openclawSessionId);
|
||||
const replayBlocked =
|
||||
openclawSessionId !== undefined &&
|
||||
(compactionCleanupPending || resetBlockedStoredSessions.has(openclawSessionId));
|
||||
const tracked =
|
||||
openclawSessionId && !compactionCleanupPending
|
||||
? trackedSessions.get(openclawSessionId)
|
||||
: undefined;
|
||||
openclawSessionId && !replayBlocked ? trackedSessions.get(openclawSessionId) : undefined;
|
||||
const stored = openclawSessionId
|
||||
? compactionCleanupPending || resetBlockedStoredSessions.has(openclawSessionId)
|
||||
? replayBlocked
|
||||
? undefined
|
||||
: lookupStoredBinding(options?.sessionStore, openclawSessionId)
|
||||
: undefined;
|
||||
|
||||
Reference in New Issue
Block a user