Merge remote-tracking branch 'origin/dev' into fix/task-id-prompt-surface

# Conflicts:
#	src/agents/atlas/default-prompt-sections.ts
#	src/agents/atlas/gemini-prompt-sections.ts
#	src/agents/atlas/gpt-prompt-sections.ts
#	src/agents/hephaestus/gpt-5-3-codex.ts
This commit is contained in:
Disaster-Terminator
2026-05-17 10:17:24 +08:00
1312 changed files with 98022 additions and 12089 deletions
+15 -5
View File
@@ -3,6 +3,7 @@ import {
clearCompactionAgentConfigCheckpoint,
setCompactionAgentConfigCheckpoint,
} from "../../shared/compaction-agent-config-checkpoint"
import { resolveMessageEventSessionID } from "../../shared/event-session-id"
import { log } from "../../shared/logger"
import { COMPACTION_CONTEXT_PROMPT } from "./compaction-context-prompt"
import { resolveSessionPromptConfig } from "./session-prompt-config-resolver"
@@ -35,7 +36,15 @@ export function createCompactionContextInjector(options?: {
const { recoverCheckpointedAgentConfig, maybeWarnAboutNoTextTail } = createRecoveryLogic(ctx, getTailState)
const restore = async (sessionID: string): Promise<boolean> => {
return recoverCheckpointedAgentConfig(sessionID, "compaction.autocontinue")
}
const capture = async (sessionID: string): Promise<void> => {
if (sessionID) {
clearCompactionAgentConfigCheckpoint(sessionID)
}
if (!ctx || !sessionID) {
return
}
@@ -113,14 +122,15 @@ export function createCompactionContextInjector(options?: {
sessionID?: string
} | undefined
if (!info?.sessionID || info.role !== "assistant" || !info.id) {
const sessionID = resolveMessageEventSessionID(props)
if (!sessionID || info?.role !== "assistant" || !info.id) {
return
}
const tailState = getTailState(info.sessionID)
const tailState = getTailState(sessionID)
if (tailState.currentMessageID && tailState.currentMessageID !== info.id) {
finalizeTrackedAssistantMessage(tailState)
await maybeWarnAboutNoTextTail(info.sessionID)
await maybeWarnAboutNoTextTail(sessionID)
}
if (tailState.currentMessageID !== info.id) {
@@ -131,7 +141,7 @@ export function createCompactionContextInjector(options?: {
}
if (event.type === "message.part.delta") {
const sessionID = props?.sessionID as string | undefined
const sessionID = resolveMessageEventSessionID(props)
const messageID = props?.messageID as string | undefined
const field = props?.field as string | undefined
const delta = props?.delta as string | undefined
@@ -160,5 +170,5 @@ export function createCompactionContextInjector(options?: {
}
}
return { capture, inject, event }
return { capture, restore, inject, event }
}
@@ -19,7 +19,26 @@ afterAll(() => {
})
import { createCompactionContextInjector } from "./index"
import type { BackgroundManager } from "../../features/background-agent"
import { TaskHistory } from "../../features/background-agent/task-history"
import { setCompactionAgentConfigCheckpoint } from "../../shared/compaction-agent-config-checkpoint"
type PromptAsyncInput = {
path: { id: string }
body: {
noReply?: boolean
agent?: string
model?: { providerID: string; modelID: string }
tools?: Record<string, boolean | "allow" | "deny" | "ask">
parts: Array<{
type: "text"
text: string
synthetic?: true
metadata?: { compaction_continue?: true }
}>
}
query?: { directory: string }
}
function createMockContext(
messageResponses: Array<Array<{ info?: Record<string, unknown> }>>,
@@ -42,6 +61,10 @@ function createMockContext(
}
}
function createMockBackgroundManager(): BackgroundManager {
return { taskHistory: new TaskHistory() } as BackgroundManager
}
describe("createCompactionContextInjector", () => {
describe("Agent Verification State preservation", () => {
it("includes Agent Verification State section in compaction prompt", async () => {
@@ -112,7 +135,7 @@ describe("createCompactionContextInjector", () => {
it("injects actual task history when backgroundManager and sessionID provided", async () => {
//#given
const mockManager = { taskHistory: new TaskHistory() } as any
const mockManager = createMockBackgroundManager()
mockManager.taskHistory.record("ses_parent", { id: "t1", sessionID: "ses_child", agent: "explore", description: "Find patterns", status: "completed", category: "quick" })
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
@@ -128,7 +151,7 @@ describe("createCompactionContextInjector", () => {
it("does not inject task history section when no entries exist", async () => {
//#given
const mockManager = { taskHistory: new TaskHistory() } as any
const mockManager = createMockBackgroundManager()
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
//#when
@@ -142,7 +165,7 @@ describe("createCompactionContextInjector", () => {
describe("agent checkpoint recovery", () => {
it("re-injects checkpointed agent config after compaction when latest agent is lost", async () => {
//#given
const promptAsyncMock = mock(async () => ({}))
const promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({}))
const ctx = createMockContext(
[
[
@@ -164,12 +187,22 @@ describe("createCompactionContextInjector", () => {
},
},
],
[
{
info: {
role: "user",
agent: "compaction",
model: { providerID: "anthropic", modelID: "claude-opus-4-1" },
},
},
],
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
},
},
],
@@ -185,27 +218,110 @@ describe("createCompactionContextInjector", () => {
})
//#then
expect(promptAsyncMock).toHaveBeenCalledWith({
path: { id: "ses_checkpoint" },
body: {
noReply: true,
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
parts: [
const recoveryCall = promptAsyncMock.mock.calls[0]?.[0]
expect(recoveryCall?.path).toEqual({ id: "ses_checkpoint" })
expect(recoveryCall?.body.noReply).toBe(true)
expect(recoveryCall?.body.agent).toBe("atlas")
expect(recoveryCall?.body.model).toEqual({ providerID: "openai", modelID: "gpt-5" })
expect(recoveryCall?.body.tools).toEqual({ bash: true })
expect(recoveryCall?.body.parts[0]?.type).toBe("text")
expect(recoveryCall?.body.parts[0]?.text).toContain("restore checkpointed session agent configuration")
expect(recoveryCall?.body.parts[0]?.synthetic).toBe(true)
expect(recoveryCall?.body.parts[0]?.metadata).toEqual({ compaction_continue: true })
expect(recoveryCall?.query).toEqual({ directory: "/tmp/test" })
})
it("re-injects checkpointed agent config during autocontinue before synthetic continue", async () => {
//#given
const promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({}))
const ctx = createMockContext(
[
[
{
type: "text",
text: expect.stringContaining("restore checkpointed session agent configuration"),
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: "allow" },
},
},
],
},
query: { directory: "/tmp/test" },
[
{
info: {
role: "user",
agent: "compaction",
model: { providerID: "anthropic", modelID: "claude-opus-4-1" },
},
},
],
[
{
info: {
role: "user",
agent: "compaction",
model: { providerID: "anthropic", modelID: "claude-opus-4-1" },
},
},
],
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
},
},
],
],
promptAsyncMock,
)
const injector = createCompactionContextInjector({ ctx })
//#when
await injector.capture("ses_autocontinue_checkpoint")
const restored = await injector.restore("ses_autocontinue_checkpoint")
//#then
expect(restored).toBe(true)
const recoveryCall = promptAsyncMock.mock.calls[0]?.[0]
expect(recoveryCall?.path).toEqual({ id: "ses_autocontinue_checkpoint" })
expect(recoveryCall?.body.noReply).toBe(true)
expect(recoveryCall?.body.agent).toBe("atlas")
expect(recoveryCall?.body.model).toEqual({ providerID: "openai", modelID: "gpt-5" })
expect(recoveryCall?.body.tools).toEqual({ bash: true })
expect(recoveryCall?.body.parts[0]?.type).toBe("text")
expect(recoveryCall?.body.parts[0]?.text).toContain("restore checkpointed session agent configuration")
expect(recoveryCall?.body.parts[0]?.synthetic).toBe(true)
expect(recoveryCall?.body.parts[0]?.metadata).toEqual({ compaction_continue: true })
expect(recoveryCall?.query).toEqual({ directory: "/tmp/test" })
})
it("clears stale checkpoint when the next compaction capture has no prompt config", async () => {
//#given
const promptAsyncMock = mock(async () => ({}))
const sessionID = "ses_empty_checkpoint_capture"
setCompactionAgentConfigCheckpoint(sessionID, {
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
})
const ctx = createMockContext([[], [], []], promptAsyncMock)
const injector = createCompactionContextInjector({ ctx })
//#when
await injector.capture(sessionID)
const restored = await injector.restore(sessionID)
//#then
expect(restored).toBe(false)
expect(promptAsyncMock).not.toHaveBeenCalled()
})
it("recovers after five consecutive assistant messages with no text", async () => {
//#given
const promptAsyncMock = mock(async () => ({}))
const promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({}))
const ctx = createMockContext(
[
[
@@ -266,15 +382,10 @@ describe("createCompactionContextInjector", () => {
//#then
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
expect(promptAsyncMock).toHaveBeenCalledWith(
expect.objectContaining({
path: { id: "ses_no_text_tail" },
body: expect.objectContaining({
noReply: true,
agent: "atlas",
}),
}),
)
const recoveryCall = promptAsyncMock.mock.calls[0]?.[0]
expect(recoveryCall?.path).toEqual({ id: "ses_no_text_tail" })
expect(recoveryCall?.body.noReply).toBe(true)
expect(recoveryCall?.body.agent).toBe("atlas")
})
})
})
@@ -15,7 +15,12 @@ type PromptAsyncInput = {
agent?: string
model?: { providerID: string; modelID: string }
tools?: Record<string, boolean>
parts: Array<{ type: "text"; text: string }>
parts: Array<{
type: "text"
text: string
synthetic?: true
metadata?: { compaction_continue?: true }
}>
}
query?: { directory: string }
}
@@ -96,46 +101,31 @@ describe("createCompactionContextInjector recovery", () => {
it("re-injects after compaction when agent and model match but tools are missing", async () => {
//#given
const promptAsyncRecorder = createPromptAsyncRecorder()
const checkpointedPromptConfig = [
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
},
},
]
const incompletePromptConfig = [
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
]
const ctx = createMockContext(
[
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
},
},
],
[
{
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" },
tools: { bash: true },
},
},
],
checkpointedPromptConfig,
incompletePromptConfig,
incompletePromptConfig,
checkpointedPromptConfig,
],
promptAsyncRecorder.promptAsync,
)
@@ -157,6 +147,55 @@ describe("createCompactionContextInjector recovery", () => {
expect(promptAsyncRecorder.calls[0]?.body.tools).toEqual({ bash: true })
})
it("marks the recovery prompt as synthetic compaction continuation", async () => {
//#given
const promptAsyncRecorder = createPromptAsyncRecorder()
const incompletePromptConfig = [
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
]
const recoveredPromptConfig = [
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
tools: { bash: true },
},
},
]
const ctx = createMockContext(
[
recoveredPromptConfig,
incompletePromptConfig,
incompletePromptConfig,
recoveredPromptConfig,
],
promptAsyncRecorder.promptAsync,
)
const injector = createCompactionContextInjector({ ctx })
//#when
await injector.capture("ses_synthetic_recovery")
await injector.event({
event: {
type: "session.compacted",
properties: { sessionID: "ses_synthetic_recovery" },
},
})
//#then
expect(promptAsyncRecorder.calls.length).toBe(1)
const recoveryPart = promptAsyncRecorder.calls[0]?.body.parts[0]
expect(recoveryPart?.synthetic).toBe(true)
expect(recoveryPart?.metadata).toEqual({ compaction_continue: true })
})
it("retries recovery when the recovered prompt config still mismatches expected model or tools", async () => {
//#given
const promptAsyncRecorder = createPromptAsyncRecorder()
@@ -5,7 +5,7 @@ import {
import {
getCompactionAgentConfigCheckpoint,
} from "../../shared/compaction-agent-config-checkpoint"
import { createInternalAgentTextPart } from "../../shared/internal-initiator-marker"
import { createInternalAgentContinuationTextPart } from "../../shared/internal-initiator-marker"
import { log } from "../../shared/logger"
import { setSessionModel } from "../../shared/session-model-state"
import { setSessionTools } from "../../shared/session-tools-store"
@@ -21,6 +21,7 @@ import {
import { AGENT_RECOVERY_PROMPT, NO_TEXT_TAIL_THRESHOLD, RECOVERY_COOLDOWN_MS, RECENT_COMPACTION_WINDOW_MS } from "./constants"
import type { CompactionContextClient } from "./types"
import type { TailMonitorState } from "./tail-monitor"
import { promptAsyncAfterSessionIdle, releasePromptAsyncReservation } from "../shared/prompt-async-gate"
export function createRecoveryLogic(
ctx: CompactionContextClient | undefined,
@@ -28,7 +29,7 @@ export function createRecoveryLogic(
) {
const recoverCheckpointedAgentConfig = async (
sessionID: string,
reason: "session.compacted" | "no-text-tail",
reason: "compaction.autocontinue" | "session.compacted" | "no-text-tail",
): Promise<boolean> => {
if (!ctx) {
return false
@@ -73,7 +74,7 @@ export function createRecoveryLogic(
const model = expectedPromptConfig.model
const tools = expectedPromptConfig.tools
if (reason === "session.compacted") {
if (reason === "compaction.autocontinue" || reason === "session.compacted") {
const latestPromptConfig = await resolveLatestSessionPromptConfig(ctx, sessionID)
if (isPromptConfigRecovered(latestPromptConfig, expectedPromptConfig)) {
return false
@@ -81,17 +82,30 @@ export function createRecoveryLogic(
}
try {
await ctx.client.session.promptAsync({
path: { id: sessionID },
body: {
noReply: true,
agent: launchAgent ?? expectedPromptConfig.agent,
...(model ? { model } : {}),
...(tools ? { tools } : {}),
parts: [createInternalAgentTextPart(AGENT_RECOVERY_PROMPT)],
const promptResult = await promptAsyncAfterSessionIdle({
client: ctx.client,
sessionID,
source: "compaction-context-injector",
input: {
path: { id: sessionID },
body: {
noReply: true,
agent: launchAgent ?? expectedPromptConfig.agent,
...(model ? { model } : {}),
...(tools ? { tools } : {}),
parts: [createInternalAgentContinuationTextPart(AGENT_RECOVERY_PROMPT)],
},
query: { directory: ctx.directory },
},
query: { directory: ctx.directory },
})
if (promptResult.status !== "dispatched") {
log(`[compaction-context-injector] Recovery skipped by promptAsync gate`, {
sessionID,
reason,
status: promptResult.status,
})
return false
}
const recoveredPromptConfig = await resolveLatestSessionPromptConfig(ctx, sessionID)
if (!isPromptConfigRecovered(recoveredPromptConfig, expectedPromptConfig)) {
@@ -103,6 +117,9 @@ export function createRecoveryLogic(
hasTools: !!tools,
recoveredPromptConfig,
})
releasePromptAsyncReservation(sessionID, "compaction-context-injector:incomplete-recovery", {
reservedBy: "compaction-context-injector",
})
return false
}
@@ -1,8 +1,9 @@
import { resolveSessionEventID } from "../../shared/event-session-id"
export function isCompactionAgent(agent: string | undefined): boolean {
return agent?.trim().toLowerCase() === "compaction"
}
export function resolveSessionID(props?: Record<string, unknown>): string | undefined {
return (props?.sessionID ??
(props?.info as { id?: string } | undefined)?.id) as string | undefined
return resolveSessionEventID(props)
}
@@ -1,5 +1,6 @@
export interface CompactionContextInjector {
capture: (sessionID: string) => Promise<void>
restore: (sessionID: string) => Promise<boolean>
inject: (sessionID?: string) => string
event: (input: { event: { type: string; properties?: unknown } }) => Promise<void>
}
@@ -19,6 +20,7 @@ export type CompactionContextClient = {
}
query?: { directory: string }
}) => Promise<unknown>
status?: () => Promise<unknown>
}
}
directory: string