fix(compaction): recover checkpointed agent config after compaction
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -17,6 +17,27 @@ mock.module("../../shared/system-directive", () => ({
|
||||
import { createCompactionContextInjector } from "./index"
|
||||
import { TaskHistory } from "../../features/background-agent/task-history"
|
||||
|
||||
function createMockContext(
|
||||
messageResponses: Array<Array<{ info?: Record<string, unknown> }>>,
|
||||
promptAsyncMock = mock(async () => ({})),
|
||||
) {
|
||||
let callIndex = 0
|
||||
|
||||
return {
|
||||
client: {
|
||||
session: {
|
||||
messages: mock(async () => {
|
||||
const response = messageResponses[Math.min(callIndex, messageResponses.length - 1)] ?? []
|
||||
callIndex += 1
|
||||
return { data: response }
|
||||
}),
|
||||
promptAsync: promptAsyncMock,
|
||||
},
|
||||
},
|
||||
directory: "/tmp/test",
|
||||
}
|
||||
}
|
||||
|
||||
describe("createCompactionContextInjector", () => {
|
||||
describe("Agent Verification State preservation", () => {
|
||||
it("includes Agent Verification State section in compaction prompt", async () => {
|
||||
@@ -24,7 +45,7 @@ describe("createCompactionContextInjector", () => {
|
||||
const injector = createCompactionContextInjector()
|
||||
|
||||
//#when
|
||||
const prompt = injector()
|
||||
const prompt = injector.inject()
|
||||
|
||||
//#then
|
||||
expect(prompt).toContain("Agent Verification State")
|
||||
@@ -37,7 +58,7 @@ describe("createCompactionContextInjector", () => {
|
||||
const injector = createCompactionContextInjector()
|
||||
|
||||
//#when
|
||||
const prompt = injector()
|
||||
const prompt = injector.inject()
|
||||
|
||||
//#then
|
||||
expect(prompt).toContain("Previous Rejections")
|
||||
@@ -50,7 +71,7 @@ describe("createCompactionContextInjector", () => {
|
||||
const injector = createCompactionContextInjector()
|
||||
|
||||
//#when
|
||||
const prompt = injector()
|
||||
const prompt = injector.inject()
|
||||
|
||||
//#then
|
||||
expect(prompt).toContain("Pending Verifications")
|
||||
@@ -63,7 +84,7 @@ describe("createCompactionContextInjector", () => {
|
||||
const injector = createCompactionContextInjector()
|
||||
|
||||
//#when
|
||||
const prompt = injector()
|
||||
const prompt = injector.inject()
|
||||
|
||||
//#then
|
||||
expect(prompt).toContain("Explicit Constraints (Verbatim Only)")
|
||||
@@ -77,7 +98,7 @@ describe("createCompactionContextInjector", () => {
|
||||
const injector = createCompactionContextInjector()
|
||||
|
||||
//#when
|
||||
const prompt = injector()
|
||||
const prompt = injector.inject()
|
||||
|
||||
//#then
|
||||
expect(prompt).toContain("Delegated Agent Sessions")
|
||||
@@ -89,10 +110,10 @@ describe("createCompactionContextInjector", () => {
|
||||
//#given
|
||||
const mockManager = { taskHistory: new TaskHistory() } as any
|
||||
mockManager.taskHistory.record("ses_parent", { id: "t1", sessionID: "ses_child", agent: "explore", description: "Find patterns", status: "completed", category: "quick" })
|
||||
const injector = createCompactionContextInjector(mockManager)
|
||||
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
|
||||
|
||||
//#when
|
||||
const prompt = injector("ses_parent")
|
||||
const prompt = injector.inject("ses_parent")
|
||||
|
||||
//#then
|
||||
expect(prompt).toContain("Active/Recent Delegated Sessions")
|
||||
@@ -104,13 +125,152 @@ describe("createCompactionContextInjector", () => {
|
||||
it("does not inject task history section when no entries exist", async () => {
|
||||
//#given
|
||||
const mockManager = { taskHistory: new TaskHistory() } as any
|
||||
const injector = createCompactionContextInjector(mockManager)
|
||||
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
|
||||
|
||||
//#when
|
||||
const prompt = injector("ses_empty")
|
||||
const prompt = injector.inject("ses_empty")
|
||||
|
||||
//#then
|
||||
expect(prompt).not.toContain("Active/Recent Delegated Sessions")
|
||||
})
|
||||
})
|
||||
|
||||
describe("agent checkpoint recovery", () => {
|
||||
it("re-injects checkpointed agent config after compaction when latest agent is lost", async () => {
|
||||
//#given
|
||||
const promptAsyncMock = mock(async () => ({}))
|
||||
const ctx = createMockContext(
|
||||
[
|
||||
[
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
tools: { bash: "allow" },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
agent: "compaction",
|
||||
model: { providerID: "anthropic", modelID: "claude-opus-4-1" },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
promptAsyncMock,
|
||||
)
|
||||
const injector = createCompactionContextInjector({ ctx })
|
||||
|
||||
//#when
|
||||
await injector.capture("ses_checkpoint")
|
||||
await injector.event({
|
||||
event: { type: "session.compacted", properties: { sessionID: "ses_checkpoint" } },
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(promptAsyncMock).toHaveBeenCalledWith({
|
||||
path: { id: "ses_checkpoint" },
|
||||
body: {
|
||||
noReply: true,
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
tools: { bash: true },
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: expect.stringContaining("restore checkpointed session agent configuration"),
|
||||
},
|
||||
],
|
||||
},
|
||||
query: { directory: "/tmp/test" },
|
||||
})
|
||||
})
|
||||
|
||||
it("recovers after five consecutive assistant messages with no text", async () => {
|
||||
//#given
|
||||
const promptAsyncMock = mock(async () => ({}))
|
||||
const ctx = createMockContext(
|
||||
[
|
||||
[
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
info: {
|
||||
role: "user",
|
||||
agent: "atlas",
|
||||
model: { providerID: "openai", modelID: "gpt-5" },
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
promptAsyncMock,
|
||||
)
|
||||
const injector = createCompactionContextInjector({ ctx })
|
||||
|
||||
await injector.capture("ses_no_text_tail")
|
||||
await injector.event({
|
||||
event: { type: "session.compacted", properties: { sessionID: "ses_no_text_tail" } },
|
||||
})
|
||||
|
||||
//#when
|
||||
for (let index = 1; index <= 5; index++) {
|
||||
await injector.event({
|
||||
event: {
|
||||
type: "message.updated",
|
||||
properties: {
|
||||
info: {
|
||||
id: `msg_${index}`,
|
||||
role: "assistant",
|
||||
sessionID: "ses_no_text_tail",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
await injector.event({
|
||||
event: { type: "session.idle", properties: { sessionID: "ses_no_text_tail" } },
|
||||
})
|
||||
|
||||
//#then
|
||||
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
|
||||
expect(promptAsyncMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: { id: "ses_no_text_tail" },
|
||||
body: expect.objectContaining({
|
||||
noReply: true,
|
||||
agent: "atlas",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user