feat(hooks): adapt session-recovery for team-mode session semantics

This commit is contained in:
YeonGyu-Kim
2026-04-28 10:47:44 +09:00
parent 473062d917
commit d2a28c19c6
3 changed files with 75 additions and 4 deletions
+3 -1
View File
@@ -123,7 +123,9 @@ export function createSessionRecoveryHook(ctx: PluginInput, options?: SessionRec
let success = false
if (errorType === "tool_result_missing") {
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg)
const lastUser = findLastUserMessage(msgs ?? [])
const resumeConfig = extractResumeConfig(lastUser, sessionID)
success = await recoverToolResultMissing(ctx.client, sessionID, failedMsg, resumeConfig)
} else if (errorType === "unavailable_tool") {
success = await recoverUnavailableTool(ctx.client, sessionID, failedMsg)
} else if (errorType === "thinking_block_order") {
@@ -129,6 +129,63 @@ describe("recoverToolResultMissing", () => {
},
})
})
it("pins agent, model, and variant on promptAsync body when resumeConfig provides them", async () => {
// given
storedParts = [{
type: "tool",
id: "prt_stored_pin_call",
callID: "toolu_pin",
tool: "bash",
state: { input: {} },
}]
const { client, promptAsync } = createMockClient()
const resumeConfig = {
sessionID: "ses_pin",
agent: "Hephaestus",
model: { providerID: "openai", modelID: "gpt-5.3-codex", variant: "max" },
}
// when
const result = await recoverToolResultMissing(client, "ses_pin", failedAssistantMsg, resumeConfig)
// then
expect(result).toBe(true)
expect(promptAsync).toHaveBeenCalledTimes(1)
const call = promptAsync.mock.calls[0]?.[0] as {
body: {
agent?: string
model?: { providerID: string; modelID: string }
variant?: string
parts: unknown[]
}
}
expect(call.body.agent).toBe("Hephaestus")
expect(call.body.model).toEqual({ providerID: "openai", modelID: "gpt-5.3-codex" })
expect(call.body.variant).toBe("max")
})
it("leaves body unchanged when no resumeConfig is provided", async () => {
// given
storedParts = [{
type: "tool",
id: "prt_stored_nopin_call",
callID: "toolu_nopin",
tool: "bash",
state: { input: {} },
}]
const { client, promptAsync } = createMockClient()
// when
const result = await recoverToolResultMissing(client, "ses_nopin", failedAssistantMsg)
// then
expect(result).toBe(true)
const call = promptAsync.mock.calls[0]?.[0] as { body: Record<string, unknown> }
expect(call.body).not.toHaveProperty("agent")
expect(call.body).not.toHaveProperty("model")
expect(call.body).not.toHaveProperty("variant")
})
})
export {}
@@ -1,5 +1,5 @@
import type { createOpencodeClient } from "@opencode-ai/sdk"
import type { MessageData } from "./types"
import type { MessageData, ResumeConfig } from "./types"
import { readParts } from "./storage"
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
import { normalizeSDKResponse } from "../../shared"
@@ -70,7 +70,8 @@ async function readPartsFromSDKFallback(
export async function recoverToolResultMissing(
client: Client,
sessionID: string,
failedAssistantMsg: MessageData
failedAssistantMsg: MessageData,
resumeConfig?: ResumeConfig
): Promise<boolean> {
let parts = failedAssistantMsg.parts || []
if (parts.length === 0 && failedAssistantMsg.info?.id) {
@@ -93,9 +94,20 @@ export async function recoverToolResultMissing(
content: "Operation cancelled by user (ESC pressed)",
}))
const launchAgent = resumeConfig?.agent
const launchModel = resumeConfig?.model
? { providerID: resumeConfig.model.providerID, modelID: resumeConfig.model.modelID }
: undefined
const launchVariant = resumeConfig?.model?.variant
const promptInput = {
path: { id: sessionID },
body: { parts: toolResultParts },
body: {
parts: toolResultParts,
...(launchAgent ? { agent: launchAgent } : {}),
...(launchModel ? { model: launchModel } : {}),
...(launchVariant ? { variant: launchVariant } : {}),
},
}
try {