diff --git a/AGENTS.md b/AGENTS.md index cc51b07..48fa49c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -141,7 +141,9 @@ The non-negotiable details: for a compact handoff summary, and return exactly one synthetic `compaction` output item before `response.completed`. Encrypt the summary with AES-256-GCM using a key derived from the stable router token; on later DeepSeek requests, decrypt only DSCodex-prefixed compaction items and - restore them as assistant summary context. A compaction item that cannot be decrypted + restore them as user handoff context. Automatic compaction resumes the same turn without a + new user message; restoring the final summary as `assistant` while tools are enabled makes + DeepSeek demand a missing `reasoning_text` and reject the continuation. A compaction item that cannot be decrypted (GPT-sealed after a provider switch, or a rotated token) must be dropped, never forwarded raw to DeepSeek. Never route compaction through GPT or store the summary as plaintext in the rollout file. diff --git a/src/proxy.mjs b/src/proxy.mjs index bf0ce6b..f82d30b 100644 --- a/src/proxy.mjs +++ b/src/proxy.mjs @@ -178,9 +178,14 @@ function convertInputItem(item, compactionSecret) { if (item.type === "compaction") { const summary = openCompaction(item.encrypted_content, compactionSecret); if (summary) { + // Automatic compaction resumes the same DeepSeek turn without a new user + // message. If the restored summary is the final `assistant` item and tools + // are enabled, DeepSeek treats it as its own prior thinking turn and rejects + // the continuation because no matching `reasoning_text` was replayed. + // Replay the compact summary as user handoff context instead. return { type: "message", - role: "assistant", + role: "user", content: [{ type: "input_text", text: `[Compacted prior context]\n${summary}` }], }; } diff --git a/test/proxy.test.mjs b/test/proxy.test.mjs index 0ca7746..09df433 100644 --- a/test/proxy.test.mjs +++ b/test/proxy.test.mjs @@ -277,21 +277,22 @@ test("adapts Codex remote compaction v2 to a DeepSeek summary and restores it on assert.equal(observed[0].model, "deepseek-flash"); assert.match(observed[0].input.at(-1).content[0].text, /compact handoff summary/i); + // Automatic compaction resumes the same turn. There is no new user message + // after the compaction item, and the continuation still has tools enabled. const replayResponse = await fetch(route(proxyUrl), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model: "deepseek/deepseek-v4-pro", - input: [ - compactItem, - { type: "message", role: "user", content: [{ type: "input_text", text: "Continue" }] }, - ], + tools: [{ type: "function", name: "shell" }], + parallel_tool_calls: false, + input: [compactItem], }), }); assert.equal(replayResponse.status, 200); await replayResponse.text(); assert.equal(observed[1].input.some((item) => item.type === "compaction"), false); - const restored = observed[1].input.find((item) => item.role === "assistant"); + const restored = observed[1].input.find((item) => item.role === "user"); assert.match(restored.content[0].text, /Compacted prior context/); assert.match(restored.content[0].text, /tests and a restart are still pending/); const gptReplay = await fetch(route(proxyUrl), {