feat(hooks): adapt session-recovery for team-mode session semantics
This commit is contained in:
@@ -123,7 +123,9 @@ export function createSessionRecoveryHook(ctx: PluginInput, options?: SessionRec
|
|||||||
let success = false
|
let success = false
|
||||||
|
|
||||||
if (errorType === "tool_result_missing") {
|
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") {
|
} else if (errorType === "unavailable_tool") {
|
||||||
success = await recoverUnavailableTool(ctx.client, sessionID, failedMsg)
|
success = await recoverUnavailableTool(ctx.client, sessionID, failedMsg)
|
||||||
} else if (errorType === "thinking_block_order") {
|
} 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 {}
|
export {}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
import type { createOpencodeClient } from "@opencode-ai/sdk"
|
||||||
import type { MessageData } from "./types"
|
import type { MessageData, ResumeConfig } from "./types"
|
||||||
import { readParts } from "./storage"
|
import { readParts } from "./storage"
|
||||||
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
import { isSqliteBackend } from "../../shared/opencode-storage-detection"
|
||||||
import { normalizeSDKResponse } from "../../shared"
|
import { normalizeSDKResponse } from "../../shared"
|
||||||
@@ -70,7 +70,8 @@ async function readPartsFromSDKFallback(
|
|||||||
export async function recoverToolResultMissing(
|
export async function recoverToolResultMissing(
|
||||||
client: Client,
|
client: Client,
|
||||||
sessionID: string,
|
sessionID: string,
|
||||||
failedAssistantMsg: MessageData
|
failedAssistantMsg: MessageData,
|
||||||
|
resumeConfig?: ResumeConfig
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
let parts = failedAssistantMsg.parts || []
|
let parts = failedAssistantMsg.parts || []
|
||||||
if (parts.length === 0 && failedAssistantMsg.info?.id) {
|
if (parts.length === 0 && failedAssistantMsg.info?.id) {
|
||||||
@@ -93,9 +94,20 @@ export async function recoverToolResultMissing(
|
|||||||
content: "Operation cancelled by user (ESC pressed)",
|
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 = {
|
const promptInput = {
|
||||||
path: { id: sessionID },
|
path: { id: sessionID },
|
||||||
body: { parts: toolResultParts },
|
body: {
|
||||||
|
parts: toolResultParts,
|
||||||
|
...(launchAgent ? { agent: launchAgent } : {}),
|
||||||
|
...(launchModel ? { model: launchModel } : {}),
|
||||||
|
...(launchVariant ? { variant: launchVariant } : {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user