Merge pull request #4044 from code-yeongyu/revert/3825-delegated-bootstrap

Revert "Merge pull request #3825 from tw-yshuang/fix/delegated-child-session-early-failure-fallback"
This commit is contained in:
YeonGyu-Kim
2026-05-15 19:18:26 +09:00
committed by GitHub
10 changed files with 46 additions and 585 deletions
+5 -230
View File
@@ -1,10 +1,7 @@
declare const require: (name: string) => any declare const require: (name: string) => any
const { describe, test, expect, beforeEach, afterEach, afterAll, spyOn, mock } = require("bun:test") const { describe, test, expect, beforeEach, afterEach, afterAll, spyOn, mock } = require("bun:test")
afterAll(() => { afterAll(() => { mock.restore() })
mock.restore()
clearAllDelegatedChildSessionBootstrap()
})
import { getSessionPromptParams, clearSessionPromptParams } from "../../shared/session-prompt-params-state" import { getSessionPromptParams, clearSessionPromptParams } from "../../shared/session-prompt-params-state"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
@@ -18,12 +15,6 @@ import { ConcurrencyManager } from "./concurrency"
import { promptAsyncAfterSessionIdle } from "../../shared/prompt-async-gate" import { promptAsyncAfterSessionIdle } from "../../shared/prompt-async-gate"
import { initTaskToastManager, _resetTaskToastManagerForTesting } from "../task-toast-manager/manager" import { initTaskToastManager, _resetTaskToastManagerForTesting } from "../task-toast-manager/manager"
import { _resetForTesting as resetProcessCleanupState } from "./process-cleanup" import { _resetForTesting as resetProcessCleanupState } from "./process-cleanup"
import {
clearAllDelegatedChildSessionBootstrap,
getDelegatedChildSessionBootstrap,
registerDelegatedChildSessionBootstrap,
} from "../../shared/delegated-child-session-bootstrap"
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
const TASK_TTL_MS = 30 * 60 * 1000 const TASK_TTL_MS = 30 * 60 * 1000
@@ -388,37 +379,23 @@ describe("BackgroundManager session.error fallback hydration", () => {
}) })
describe("BackgroundManager prompt rejection fallback routing", () => { describe("BackgroundManager prompt rejection fallback routing", () => {
test("routes delegated child-session launch-time prompt rejections into tryFallbackRetry before history exists", async () => { test("routes launch-time prompt rejections into tryFallbackRetry before marking interrupt", async () => {
//#given //#given
const promptError = { const promptError = {
name: "APIError", name: "APIError",
data: { message: "Forbidden: Selected provider is forbidden" }, data: { message: "Forbidden: Selected provider is forbidden" },
} }
const fallbackChain = [{ model: "claude-haiku-4-5", providers: ["anthropic"] }]
const messages = mock(async () => ({ data: [] }))
const bootstrapSnapshots: Array<ReturnType<typeof getDelegatedChildSessionBootstrap>> = []
const client = { const client = {
session: { session: {
get: async () => ({ data: { directory: tmpdir() } }), get: async () => ({ data: { directory: tmpdir() } }),
create: async () => ({ data: { id: "ses_launch_retry" } }), create: async () => ({ data: { id: "ses_launch_retry" } }),
messages,
promptAsync: async () => { promptAsync: async () => {
bootstrapSnapshots.push(getDelegatedChildSessionBootstrap("ses_launch_retry"))
throw promptError throw promptError
}, },
abort: async () => ({}), abort: async () => ({}),
}, },
} }
const setSessionFallbackChain = mock(() => {}) const manager = new BackgroundManager({ pluginContext: createPluginInput(client) })
const manager = new BackgroundManager({
pluginContext: createPluginInput(client),
modelFallbackControllerAccessor: {
register: () => {},
setSessionFallbackChain,
getSessionFallbackChain: () => undefined,
clearSessionFallbackChain: () => {},
},
})
stubNotifyParentSession(manager) stubNotifyParentSession(manager)
;(cast<{ ;(cast<{
reserveSubagentSpawn: () => Promise<{ reserveSubagentSpawn: () => Promise<{
@@ -450,9 +427,8 @@ describe("BackgroundManager prompt rejection fallback routing", () => {
agent: "sisyphus-junior", agent: "sisyphus-junior",
parentSessionId: "parent-session", parentSessionId: "parent-session",
parentMessageId: "parent-message", parentMessageId: "parent-message",
category: "deep",
model: { providerID: "genai-proxy-openai", modelID: "gpt-5.4-mini" }, model: { providerID: "genai-proxy-openai", modelID: "gpt-5.4-mini" },
fallbackChain, fallbackChain: [{ model: "claude-haiku-4-5", providers: ["anthropic"] }],
}) })
await flushBackgroundNotifications() await flushBackgroundNotifications()
@@ -465,72 +441,6 @@ describe("BackgroundManager prompt rejection fallback routing", () => {
message: "Forbidden: Selected provider is forbidden", message: "Forbidden: Selected provider is forbidden",
}) })
expect(storedTask?.status).toBe("pending") expect(storedTask?.status).toBe("pending")
expect(messages).not.toHaveBeenCalled()
expect(setSessionFallbackChain).toHaveBeenCalledWith("ses_launch_retry", fallbackChain)
expect(bootstrapSnapshots[0]?.retryParts[0]?.text).toContain("say hi")
})
test("clears delegated bootstrap and fallback context when launch-time prompt failure is terminal", async () => {
//#given
const promptError = new Error("Connection timeout")
const fallbackChain = [{ model: "claude-haiku-4-5", providers: ["anthropic"] }]
const clearSessionFallbackChain = mock(() => {})
const client = {
session: {
get: async () => ({ data: { directory: tmpdir() } }),
create: async () => ({ data: { id: "ses_launch_terminal" } }),
promptAsync: async () => {
throw promptError
},
abort: async () => ({}),
},
}
const manager = new BackgroundManager({
pluginContext: { client, directory: tmpdir() } as unknown as PluginInput,
modelFallbackControllerAccessor: {
register: () => {},
setSessionFallbackChain: () => {},
getSessionFallbackChain: () => undefined,
clearSessionFallbackChain,
},
})
stubNotifyParentSession(manager)
;(manager as unknown as {
reserveSubagentSpawn: () => Promise<{
spawnContext: { rootSessionID: string; parentDepth: number; childDepth: number }
descendantCount: number
commit: () => number
rollback: () => void
}>
}).reserveSubagentSpawn = async () => ({
spawnContext: { rootSessionID: "parent-session", parentDepth: 0, childDepth: 1 },
descendantCount: 1,
commit: () => 1,
rollback: () => {},
})
;(manager as unknown as {
tryFallbackRetry: (task: BackgroundTask, errorInfo: { name?: string; message?: string }, source: string) => Promise<boolean>
}).tryFallbackRetry = async () => false
//#when
const launchedTask = await manager.launch({
description: "background terminal retry test",
prompt: "say bye",
agent: "sisyphus-junior",
parentSessionId: "parent-session",
parentMessageId: "parent-message",
category: "deep",
model: { providerID: "genai-proxy-openai", modelID: "gpt-5.4-mini" },
fallbackChain,
})
await flushBackgroundNotifications()
//#then
const storedTask = getTaskMap(manager).get(launchedTask.id)
expect(storedTask?.status).toBe("interrupt")
expect(getDelegatedChildSessionBootstrap("ses_launch_terminal")).toBeUndefined()
expect(clearSessionFallbackChain).toHaveBeenCalledWith("ses_launch_terminal")
expect(SessionCategoryRegistry.has("ses_launch_terminal")).toBe(false)
}) })
test("routes resume-time prompt rejections into tryFallbackRetry before marking interrupt", async () => { test("routes resume-time prompt rejections into tryFallbackRetry before marking interrupt", async () => {
@@ -749,60 +659,6 @@ describe("BackgroundManager retry observability", () => {
expect(retryReadyNotification).toContain("Forbidden: Selected provider is forbidden") expect(retryReadyNotification).toContain("Forbidden: Selected provider is forbidden")
}) })
test("clears delegated bootstrap and fallback context for the failed session when a fallback retry is scheduled", async () => {
//#given
const clearSessionFallbackChain = mock(() => {})
const manager = createBackgroundManagerWithOptions({
modelFallbackControllerAccessor: {
register: () => {},
setSessionFallbackChain: () => {},
getSessionFallbackChain: () => undefined,
clearSessionFallbackChain,
},
})
registerDelegatedChildSessionBootstrap({
sessionID: "ses_retry_cleanup",
promptText: "retry me",
fallbackChain: [{ model: "claude-haiku-4-5", providers: ["anthropic"] }],
category: "deep",
})
const task = createMockTask({
id: "bg_retry_cleanup",
sessionId: "ses_retry_cleanup",
parentSessionId: "parent-session",
status: "running",
attemptCount: 0,
fallbackChain: [{ model: "claude-haiku-4-5", providers: ["anthropic"] }],
model: { providerID: "genai-proxy-openai", modelID: "gpt-5.4-mini" },
concurrencyKey: "genai-proxy-openai/gpt-5.4-mini",
attempts: [
{
attemptId: "att_retry_cleanup",
attemptNumber: 1,
sessionId: "ses_retry_cleanup",
providerId: "genai-proxy-openai",
modelId: "gpt-5.4-mini",
status: "running",
},
],
currentAttemptID: "att_retry_cleanup",
})
//#when
const retried = await (manager as unknown as {
tryFallbackRetry: (task: BackgroundTask, errorInfo: { name?: string; message?: string }, source: string) => Promise<boolean>
}).tryFallbackRetry(task, {
name: "APIError",
message: "Forbidden: Selected provider is forbidden",
}, "promptAsync.launch")
//#then
expect(retried).toBe(true)
expect(getDelegatedChildSessionBootstrap("ses_retry_cleanup")).toBeUndefined()
expect(clearSessionFallbackChain).toHaveBeenCalledWith("ses_retry_cleanup")
expect(SessionCategoryRegistry.has("ses_retry_cleanup")).toBe(false)
})
test("builds retry-ready links from the parent session directory when it differs from the manager directory", async () => { test("builds retry-ready links from the parent session directory when it differs from the manager directory", async () => {
//#given //#given
const queuePendingParentWake = mock(() => {}) const queuePendingParentWake = mock(() => {})
@@ -2022,7 +1878,7 @@ describe("BackgroundManager.tryCompleteTask", () => {
expect(concurrencyManager.getCount(concurrencyKey)).toBe(0) expect(concurrencyManager.getCount(concurrencyKey)).toBe(0)
}) })
test("should abort session on completion", async () => { test("should abort session on completion", async () => {
// #given // #given
const abortedSessionIDs: string[] = [] const abortedSessionIDs: string[] = []
const client = { const client = {
@@ -2059,47 +1915,6 @@ describe("BackgroundManager.tryCompleteTask", () => {
expect(abortedSessionIDs).toEqual(["session-1"]) expect(abortedSessionIDs).toEqual(["session-1"])
}) })
test("should clear delegated bootstrap and fallback context on completion", async () => {
//#given
const clearSessionFallbackChain = mock(() => {})
manager.shutdown()
manager = createBackgroundManagerWithOptions({
modelFallbackControllerAccessor: {
register: () => {},
setSessionFallbackChain: () => {},
getSessionFallbackChain: () => undefined,
clearSessionFallbackChain,
},
})
stubNotifyParentSession(manager)
registerDelegatedChildSessionBootstrap({
sessionID: "session-bootstrap-complete",
promptText: "complete me",
fallbackChain: [{ model: "fallback-1", providers: ["provider-a"] }],
category: "deep",
})
const task: BackgroundTask = {
id: "task-bootstrap-complete",
sessionId: "session-bootstrap-complete",
parentSessionId: "parent-bootstrap-complete",
parentMessageId: "msg-1",
description: "bootstrap completion task",
prompt: "test",
agent: "explore",
status: "running",
startedAt: new Date(),
}
//#when
await tryCompleteTaskForTest(manager, task)
//#then
expect(getDelegatedChildSessionBootstrap("session-bootstrap-complete")).toBeUndefined()
expect(clearSessionFallbackChain).toHaveBeenCalledWith("session-bootstrap-complete")
expect(SessionCategoryRegistry.has("session-bootstrap-complete")).toBe(false)
})
test("should clean pendingByParent even when promptAsync notification fails", async () => { test("should clean pendingByParent even when promptAsync notification fails", async () => {
// given // given
const client = { const client = {
@@ -4022,46 +3837,6 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => {
manager.shutdown() manager.shutdown()
resetToastManager() resetToastManager()
}) })
test("should clear delegated bootstrap and fallback context when cancelling a running task", async () => {
//#given
const clearSessionFallbackChain = mock(() => {})
const manager = createBackgroundManagerWithOptions({
modelFallbackControllerAccessor: {
register: () => {},
setSessionFallbackChain: () => {},
getSessionFallbackChain: () => undefined,
clearSessionFallbackChain,
},
})
registerDelegatedChildSessionBootstrap({
sessionID: "session-cancel-bootstrap",
promptText: "cancel me",
fallbackChain: [{ model: "fallback-1", providers: ["provider-a"] }],
category: "deep",
})
const task = createMockTask({
id: "task-cancel-bootstrap",
sessionId: "session-cancel-bootstrap",
parentSessionId: "parent-cancel-bootstrap",
status: "running",
})
getTaskMap(manager).set(task.id, task)
//#when
const cancelled = await manager.cancelTask(task.id, {
source: "test",
skipNotification: true,
})
//#then
expect(cancelled).toBe(true)
expect(getDelegatedChildSessionBootstrap("session-cancel-bootstrap")).toBeUndefined()
expect(clearSessionFallbackChain).toHaveBeenCalledWith("session-cancel-bootstrap")
expect(SessionCategoryRegistry.has("session-cancel-bootstrap")).toBe(false)
manager.shutdown()
})
}) })
describe("multiple keys process in parallel", () => { describe("multiple keys process in parallel", () => {
+7 -28
View File
@@ -95,11 +95,6 @@ import {
resolveSubagentSpawnContext, resolveSubagentSpawnContext,
type SubagentSpawnContext, type SubagentSpawnContext,
} from "./subagent-spawn-limits" } from "./subagent-spawn-limits"
import {
clearDelegatedChildSessionBootstrap,
registerDelegatedChildSessionBootstrap,
} from "../../shared/delegated-child-session-bootstrap"
import { settleAfterSessionIdle } from "../../hooks/shared/session-idle-settle"
type OpencodeClient = PluginInput["client"] type OpencodeClient = PluginInput["client"]
type ParentWakePromptContext = { type ParentWakePromptContext = {
@@ -323,12 +318,6 @@ export class BackgroundManager {
} }
} }
private cleanupDelegatedSessionContext(sessionID: string): void {
clearDelegatedChildSessionBootstrap(sessionID)
this.modelFallbackControllerAccessor?.clearSessionFallbackChain(sessionID)
SessionCategoryRegistry.remove(sessionID)
}
async assertCanSpawn(parentSessionID: string): Promise<SubagentSpawnContext> { async assertCanSpawn(parentSessionID: string): Promise<SubagentSpawnContext> {
const spawnContext = await resolveSubagentSpawnContext(this.client, parentSessionID, this.directory) const spawnContext = await resolveSubagentSpawnContext(this.client, parentSessionID, this.directory)
const maxDepth = getMaxSubagentDepth(this.config) const maxDepth = getMaxSubagentDepth(this.config)
@@ -807,14 +796,6 @@ export class BackgroundManager {
return return
} }
registerDelegatedChildSessionBootstrap({
sessionID,
promptText: input.prompt,
fallbackChain: input.fallbackChain,
category: input.category,
modelFallbackControllerAccessor: this.modelFallbackControllerAccessor,
})
task.progress = { task.progress = {
toolCalls: 0, toolCalls: 0,
lastUpdate: new Date(), lastUpdate: new Date(),
@@ -982,7 +963,6 @@ The fallback retry session is now created and can be inspected directly.
// Abort the session to prevent infinite polling hang // Abort the session to prevent infinite polling hang
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT) // Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
await this.abortSessionWithLogging(sessionID, "launch error cleanup") await this.abortSessionWithLogging(sessionID, "launch error cleanup")
this.cleanupDelegatedSessionContext(sessionID)
this.markForNotification(existingTask) this.markForNotification(existingTask)
this.enqueueNotificationForParent(existingTask.parentSessionId, () => this.notifyParentSession(existingTask)).catch(err => { this.enqueueNotificationForParent(existingTask.parentSessionId, () => this.notifyParentSession(existingTask)).catch(err => {
@@ -1883,7 +1863,7 @@ The fallback retry session is now created and can be inspected directly.
} }
this.rootDescendantCounts.delete(sessionID) this.rootDescendantCounts.delete(sessionID)
this.cleanupDelegatedSessionContext(sessionID) SessionCategoryRegistry.remove(sessionID)
} }
if (event.type === "session.status") { if (event.type === "session.status") {
@@ -2071,7 +2051,7 @@ The fallback retry session is now created and can be inspected directly.
} }
this.scheduleTaskRemoval(task.id) this.scheduleTaskRemoval(task.id)
if (task.sessionId) { if (task.sessionId) {
this.cleanupDelegatedSessionContext(task.sessionId) SessionCategoryRegistry.remove(task.sessionId)
} }
// Update continuation marker for CLI run mode // Update continuation marker for CLI run mode
@@ -2129,7 +2109,6 @@ The task was re-queued on a fallback model after a retryable failure.
this.clearSessionOutputObserved(previousSessionID) this.clearSessionOutputObserved(previousSessionID)
this.clearSessionTodoObservation(previousSessionID) this.clearSessionTodoObservation(previousSessionID)
subagentSessions.delete(previousSessionID) subagentSessions.delete(previousSessionID)
this.cleanupDelegatedSessionContext(previousSessionID)
} }
return retried return retried
} }
@@ -2293,7 +2272,7 @@ The task was re-queued on a fallback model after a retryable failure.
this.clearTaskHistoryWhenParentTasksGone(task.parentSessionId) this.clearTaskHistoryWhenParentTasksGone(task.parentSessionId)
if (task.sessionId) { if (task.sessionId) {
subagentSessions.delete(task.sessionId) subagentSessions.delete(task.sessionId)
this.cleanupDelegatedSessionContext(task.sessionId) SessionCategoryRegistry.remove(task.sessionId)
} }
log("[background-agent] Removed completed task from memory:", taskId) log("[background-agent] Removed completed task from memory:", taskId)
}, TASK_CLEANUP_DELAY_MS) }, TASK_CLEANUP_DELAY_MS)
@@ -2368,7 +2347,7 @@ The task was re-queued on a fallback model after a retryable failure.
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT) // Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
await this.abortSessionWithLogging(task.sessionId, `task cancellation (${source})`) await this.abortSessionWithLogging(task.sessionId, `task cancellation (${source})`)
this.cleanupDelegatedSessionContext(task.sessionId) SessionCategoryRegistry.remove(task.sessionId)
} }
removeTaskToastTracking(task.id) removeTaskToastTracking(task.id)
@@ -2493,7 +2472,7 @@ The task was re-queued on a fallback model after a retryable failure.
// Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT) // Awaited to prevent dangling promise during subagent teardown (Bun/WebKit SIGABRT)
await this.abortSessionWithLogging(task.sessionId, `task completion (${source})`) await this.abortSessionWithLogging(task.sessionId, `task completion (${source})`)
this.cleanupDelegatedSessionContext(task.sessionId) SessionCategoryRegistry.remove(task.sessionId)
} }
// Update continuation marker for CLI run mode // Update continuation marker for CLI run mode
@@ -2942,7 +2921,7 @@ The task was re-queued on a fallback model after a retryable failure.
removeTaskToastTracking(task.id) removeTaskToastTracking(task.id)
this.scheduleTaskRemoval(task.id) this.scheduleTaskRemoval(task.id)
if (task.sessionId) { if (task.sessionId) {
this.cleanupDelegatedSessionContext(task.sessionId) SessionCategoryRegistry.remove(task.sessionId)
} }
// Update continuation marker for CLI run mode // Update continuation marker for CLI run mode
@@ -3157,7 +3136,7 @@ The task was re-queued on a fallback model after a retryable failure.
for (const sessionID of trackedSessionIDs) { for (const sessionID of trackedSessionIDs) {
subagentSessions.delete(sessionID) subagentSessions.delete(sessionID)
this.cleanupDelegatedSessionContext(sessionID) SessionCategoryRegistry.remove(sessionID)
} }
this.concurrencyManager.clear() this.concurrencyManager.clear()
+1 -1
View File
@@ -130,7 +130,7 @@ export function createAutoRetryHelpers(deps: HookDeps) {
path: { id: sessionID }, path: { id: sessionID },
query: { directory: ctx.directory }, query: { directory: ctx.directory },
}) })
const retryParts = getLastUserRetryParts(messagesResp, sessionID) const retryParts = getLastUserRetryParts(messagesResp)
if (retryParts.length > 0) { if (retryParts.length > 0) {
log(`[${HOOK_NAME}] Auto-retrying with fallback model (${source})`, { log(`[${HOOK_NAME}] Auto-retrying with fallback model (${source})`, {
sessionID, sessionID,
+5 -78
View File
@@ -2,10 +2,6 @@ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test"
import type { RuntimeFallbackConfig, OhMyOpenCodeConfig } from "../../config" import type { RuntimeFallbackConfig, OhMyOpenCodeConfig } from "../../config"
import * as loggerModule from "../../shared/logger" import * as loggerModule from "../../shared/logger"
import { SessionCategoryRegistry } from "../../shared/session-category-registry" import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import {
clearAllDelegatedChildSessionBootstrap,
registerDelegatedChildSessionBootstrap,
} from "../../shared/delegated-child-session-bootstrap"
import { unsafeTestValue } from "../../../test-support/unsafe-test-value" import { unsafeTestValue } from "../../../test-support/unsafe-test-value"
type RuntimeFallbackModule = typeof import("./hook") type RuntimeFallbackModule = typeof import("./hook")
@@ -20,7 +16,6 @@ describe("runtime-fallback", () => {
logCalls = [] logCalls = []
toastCalls = [] toastCalls = []
SessionCategoryRegistry.clear() SessionCategoryRegistry.clear()
clearAllDelegatedChildSessionBootstrap()
const cacheBuster = `${Date.now()}-${Math.random()}` const cacheBuster = `${Date.now()}-${Math.random()}`
@@ -37,7 +32,6 @@ describe("runtime-fallback", () => {
afterEach(() => { afterEach(() => {
SessionCategoryRegistry.clear() SessionCategoryRegistry.clear()
clearAllDelegatedChildSessionBootstrap()
mock.restore() mock.restore()
}) })
@@ -2854,18 +2848,9 @@ describe("runtime-fallback", () => {
expect(abortCalls.some((call) => call.path?.id === sessionID)).toBe(true) expect(abortCalls.some((call) => call.path?.id === sessionID)).toBe(true)
}) })
test("delegated child-session empty-history fallback retries with captured bootstrap prompt", async () => { test("pendingFallbackModel advances chain on subsequent error even when persisted", async () => {
//#given //#given
const promptCalls: Array<Record<string, unknown>> = [] const hook = createRuntimeFallbackHook(createMockPluginInput(), {
const hook = createRuntimeFallbackHook(createMockPluginInput({
session: {
messages: async () => ({ data: [] }),
promptAsync: async (args) => {
promptCalls.push(args as Record<string, unknown>)
return {}
},
},
}), {
config: createMockConfig({ notify_on_fallback: false }), config: createMockConfig({ notify_on_fallback: false }),
pluginConfig: { pluginConfig: {
git_master: { git_master: {
@@ -2880,12 +2865,8 @@ describe("runtime-fallback", () => {
}, },
}, },
}) })
const sessionID = "test-delegated-empty-history-pending-persists" const sessionID = "test-race-pending-persists"
registerDelegatedChildSessionBootstrap({ SessionCategoryRegistry.register(sessionID, "test")
sessionID,
promptText: "delegated retry payload",
category: "test",
})
await hook.event({ await hook.event({
event: { event: {
@@ -2901,13 +2882,8 @@ describe("runtime-fallback", () => {
}, },
}) })
expect(promptCalls).toHaveLength(1)
expect(promptCalls[0]?.body).toMatchObject({
model: { providerID: "provider-a", modelID: "model-a" },
parts: [{ type: "text", text: expect.stringContaining("delegated retry payload") }],
})
const autoRetryLog = logCalls.find((call) => call.msg.includes("No user message found for auto-retry")) const autoRetryLog = logCalls.find((call) => call.msg.includes("No user message found for auto-retry"))
expect(autoRetryLog).toBeUndefined() expect(autoRetryLog).toBeDefined()
//#when - second error fires after retry completed (retryInFlight cleared) //#when - second error fires after retry completed (retryInFlight cleared)
await hook.event({ await hook.event({
@@ -2921,54 +2897,5 @@ describe("runtime-fallback", () => {
const fallbackLogs = logCalls.filter((call) => call.msg.includes("Preparing fallback")) const fallbackLogs = logCalls.filter((call) => call.msg.includes("Preparing fallback"))
expect(fallbackLogs.length).toBeGreaterThanOrEqual(2) expect(fallbackLogs.length).toBeGreaterThanOrEqual(2)
}) })
test("empty-history fallback without delegated bootstrap still does not invent retry payloads", async () => {
//#given
const promptCalls: Array<Record<string, unknown>> = []
const hook = createRuntimeFallbackHook(createMockPluginInput({
session: {
messages: async () => ({ data: [] }),
promptAsync: async (args) => {
promptCalls.push(args as Record<string, unknown>)
return {}
},
},
}), {
config: createMockConfig({ notify_on_fallback: false }),
pluginConfig: {
git_master: {
commit_footer: true,
include_co_authored_by: true,
git_env_prefix: "GIT_MASTER=1",
},
categories: {
test: {
fallback_models: ["provider-a/model-a", "provider-b/model-b"],
},
},
},
})
const sessionID = "test-empty-history-without-bootstrap"
SessionCategoryRegistry.register(sessionID, "test")
//#when
await hook.event({
event: {
type: "session.created",
properties: { info: { id: sessionID, model: "google/gemini-2.5-pro" } },
},
})
await hook.event({
event: {
type: "session.error",
properties: { sessionID, error: { statusCode: 429, message: "Rate limit" } },
},
})
//#then
const autoRetryLog = logCalls.find((call) => call.msg.includes("No user message found for auto-retry"))
expect(autoRetryLog).toBeDefined()
expect(promptCalls).toHaveLength(0)
})
}) })
}) })
@@ -1,9 +1,7 @@
import { extractSessionMessages } from "./session-messages" import { extractSessionMessages } from "./session-messages"
import { getDelegatedChildSessionBootstrap } from "../../shared/delegated-child-session-bootstrap"
export function getLastUserRetryParts( export function getLastUserRetryParts(
messagesResponse: unknown, messagesResponse: unknown,
sessionID?: string,
): Array<{ type: "text"; text: string }> { ): Array<{ type: "text"; text: string }> {
const messages = extractSessionMessages(messagesResponse) const messages = extractSessionMessages(messagesResponse)
const lastUserMessage = messages?.filter((message) => message.info?.role === "user").pop() const lastUserMessage = messages?.filter((message) => message.info?.role === "user").pop()
@@ -11,7 +9,7 @@ export function getLastUserRetryParts(
lastUserMessage?.parts lastUserMessage?.parts
?? (lastUserMessage?.info?.parts as Array<{ type?: string; text?: string }> | undefined) ?? (lastUserMessage?.info?.parts as Array<{ type?: string; text?: string }> | undefined)
const retryParts = (lastUserParts ?? []) return (lastUserParts ?? [])
.filter( .filter(
(part): part is { type: "text"; text: string } => (part): part is { type: "text"; text: string } =>
part.type === "text" part.type === "text"
@@ -19,12 +17,4 @@ export function getLastUserRetryParts(
&& part.text.length > 0, && part.text.length > 0,
) )
.map((part) => ({ type: "text" as const, text: part.text })) .map((part) => ({ type: "text" as const, text: part.text }))
if (retryParts.length > 0) {
return retryParts
}
return sessionID
? (getDelegatedChildSessionBootstrap(sessionID)?.retryParts ?? [])
: []
} }
@@ -1,55 +0,0 @@
import type { FallbackEntry } from "./model-requirements"
import type { ModelFallbackControllerAccessor } from "../hooks/model-fallback"
import { createInternalAgentTextPart } from "./internal-initiator-marker"
import { SessionCategoryRegistry } from "./session-category-registry"
export type DelegatedChildSessionRetryPart = {
type: "text"
text: string
}
export type DelegatedChildSessionBootstrap = {
retryParts: DelegatedChildSessionRetryPart[]
}
const delegatedChildSessionBootstrapMap = new Map<string, DelegatedChildSessionBootstrap>()
export function createDelegatedChildSessionRetryParts(promptText: string): DelegatedChildSessionRetryPart[] {
return [createInternalAgentTextPart(promptText)]
}
export function registerDelegatedChildSessionBootstrap(args: {
sessionID: string
promptText: string
fallbackChain?: FallbackEntry[]
category?: string
modelFallbackControllerAccessor?: ModelFallbackControllerAccessor
}): void {
delegatedChildSessionBootstrapMap.set(args.sessionID, {
retryParts: createDelegatedChildSessionRetryParts(args.promptText),
})
args.modelFallbackControllerAccessor?.setSessionFallbackChain(args.sessionID, args.fallbackChain)
if (args.category) {
SessionCategoryRegistry.register(args.sessionID, args.category)
}
}
export function getDelegatedChildSessionBootstrap(sessionID: string): DelegatedChildSessionBootstrap | undefined {
const bootstrap = delegatedChildSessionBootstrapMap.get(sessionID)
if (!bootstrap) {
return undefined
}
return {
retryParts: bootstrap.retryParts.map((part) => ({ ...part })),
}
}
export function clearDelegatedChildSessionBootstrap(sessionID: string): void {
delegatedChildSessionBootstrapMap.delete(sessionID)
}
export function clearAllDelegatedChildSessionBootstrap(): void {
delegatedChildSessionBootstrapMap.clear()
}
+14 -12
View File
@@ -6,31 +6,26 @@ import { buildTaskPrompt } from "./prompt-builder"
import { publishToolMetadata } from "../../features/tool-metadata-store" import { publishToolMetadata } from "../../features/tool-metadata-store"
import { formatDetailedError } from "./error-formatting" import { formatDetailedError } from "./error-formatting"
import { getSessionTools } from "../../shared/session-tools-store" import { getSessionTools } from "../../shared/session-tools-store"
import { SessionCategoryRegistry } from "../../shared/session-category-registry"
import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission" import { QUESTION_DENIED_SESSION_PERMISSION } from "../../shared/question-denied-session-permission"
import { stripAgentListSortPrefix } from "../../shared/agent-display-names" import { stripAgentListSortPrefix } from "../../shared/agent-display-names"
import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract" import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-metadata-contract"
import { resolveMetadataModel } from "./resolve-metadata-model" import { resolveMetadataModel } from "./resolve-metadata-model"
import { registerDelegatedChildSessionBootstrap } from "../../shared/delegated-child-session-bootstrap"
function registerBackgroundSessionContext(args: { function registerBackgroundSessionContext(args: {
sessionId: string sessionId: string
promptText: string
fallbackChain?: FallbackEntry[] fallbackChain?: FallbackEntry[]
category?: string category?: string
modelFallbackControllerAccessor?: ExecutorContext["modelFallbackControllerAccessor"] modelFallbackControllerAccessor?: ExecutorContext["modelFallbackControllerAccessor"]
}): void { }): void {
registerDelegatedChildSessionBootstrap({ args.modelFallbackControllerAccessor?.setSessionFallbackChain(args.sessionId, args.fallbackChain)
sessionID: args.sessionId, if (args.category) {
promptText: args.promptText, SessionCategoryRegistry.register(args.sessionId, args.category)
fallbackChain: args.fallbackChain, }
category: args.category,
modelFallbackControllerAccessor: args.modelFallbackControllerAccessor,
})
} }
function continueSessionSetup(args: { function continueSessionSetup(args: {
taskID: string taskID: string
promptText: string
manager: ExecutorContext["manager"] manager: ExecutorContext["manager"]
timing: ReturnType<typeof getTimingConfig> timing: ReturnType<typeof getTimingConfig>
fallbackChain?: FallbackEntry[] fallbackChain?: FallbackEntry[]
@@ -60,7 +55,6 @@ function continueSessionSetup(args: {
registerBackgroundSessionContext({ registerBackgroundSessionContext({
sessionId, sessionId,
promptText: args.promptText,
fallbackChain: args.fallbackChain, fallbackChain: args.fallbackChain,
category: args.category, category: args.category,
modelFallbackControllerAccessor: args.modelFallbackControllerAccessor, modelFallbackControllerAccessor: args.modelFallbackControllerAccessor,
@@ -150,7 +144,6 @@ export async function executeBackgroundTask(
onAbort: () => { onAbort: () => {
continueSessionSetup({ continueSessionSetup({
taskID: task.id, taskID: task.id,
promptText: effectivePrompt,
manager, manager,
timing, timing,
fallbackChain, fallbackChain,
@@ -167,6 +160,15 @@ export async function executeBackgroundTask(
return `Task failed to start (status: ${updatedTask.status}).\n\nTask ID: ${task.id}` return `Task failed to start (status: ${updatedTask.status}).\n\nTask ID: ${task.id}`
} }
if (sessionId) {
registerBackgroundSessionContext({
sessionId,
fallbackChain,
category: args.category,
modelFallbackControllerAccessor: executorCtx.modelFallbackControllerAccessor,
})
}
const resolvedModel = resolveMetadataModel(categoryModel, parentContext.model) const resolvedModel = resolveMetadataModel(categoryModel, parentContext.model)
const metadata = { const metadata = {
prompt: args.prompt, prompt: args.prompt,
@@ -58,7 +58,6 @@ export async function sendSyncPrompt(
sessionID: string sessionID: string
agentToUse: string agentToUse: string
args: DelegateTaskArgs args: DelegateTaskArgs
promptText?: string
systemContent: string | undefined systemContent: string | undefined
categoryModel: DelegatedModelConfig | undefined categoryModel: DelegatedModelConfig | undefined
directory: string directory: string
@@ -70,7 +69,7 @@ export async function sendSyncPrompt(
): Promise<string | null> { ): Promise<string | null> {
const allowTask = isPlanFamily(input.agentToUse) const allowTask = isPlanFamily(input.agentToUse)
const tddEnabled = input.sisyphusAgentConfig?.tdd const tddEnabled = input.sisyphusAgentConfig?.tdd
const effectivePrompt = input.promptText ?? buildTaskPrompt(input.args.prompt, input.agentToUse, tddEnabled) const effectivePrompt = buildTaskPrompt(input.args.prompt, input.agentToUse, tddEnabled)
const tools = { const tools = {
task: allowTask, task: allowTask,
call_omo_agent: true, call_omo_agent: true,
+7 -150
View File
@@ -29,8 +29,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
addCalls = [] addCalls = []
clearRequireCache("./sync-task") clearRequireCache("./sync-task")
const { clearAllDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
clearAllDelegatedChildSessionBootstrap()
const { initTaskToastManager, _resetTaskToastManagerForTesting } = require("../../features/task-toast-manager/manager") const { initTaskToastManager, _resetTaskToastManagerForTesting } = require("../../features/task-toast-manager/manager")
_resetTaskToastManagerForTesting() _resetTaskToastManagerForTesting()
@@ -64,8 +62,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
mock.restore() mock.restore()
resetToastManager?.() resetToastManager?.()
resetToastManager = null resetToastManager = null
const { clearAllDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
clearAllDelegatedChildSessionBootstrap()
}) })
test("cleans up toast and subagentSessions when fetchSyncResult returns ok: false", async () => { test("cleans up toast and subagentSessions when fetchSyncResult returns ok: false", async () => {
@@ -389,35 +385,16 @@ describe("executeSyncTask - cleanup on error paths", () => {
} }
const { executeSyncTask } = require("./sync-task") const { executeSyncTask } = require("./sync-task")
const { getDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
const attemptedModels: Array<{ providerID: string; modelID: string; variant?: string } | undefined> = [] const attemptedModels: Array<{ providerID: string; modelID: string; variant?: string } | undefined> = []
const promptSessionIDs: string[] = []
const pollSessionIDs: string[] = []
const fetchSessionIDs: string[] = []
const bootstrapSnapshots: Array<{ retryParts: Array<{ type: "text"; text: string }> } | undefined> = []
let createSyncSessionCalls = 0
const setSessionFallbackChain = mock(() => {})
const clearSessionFallbackChain = mock(() => {})
const deps = { const deps = {
createSyncSession: async () => { createSyncSession: async () => ({ ok: true, sessionID: "ses_test_12345678" }),
createSyncSessionCalls += 1 sendSyncPrompt: async (_client: unknown, input: { categoryModel?: { providerID: string; modelID: string; variant?: string } }) => {
return { ok: true as const, sessionID: "ses_test_12345678" }
},
sendSyncPrompt: async (_client: unknown, input: { sessionID: string; categoryModel?: { providerID: string; modelID: string; variant?: string } }) => {
promptSessionIDs.push(input.sessionID)
bootstrapSnapshots.push(getDelegatedChildSessionBootstrap(input.sessionID))
attemptedModels.push(input.categoryModel) attemptedModels.push(input.categoryModel)
return attemptedModels.length === 1 ? "Initial failure" : null return attemptedModels.length === 1 ? "Initial failure" : null
}, },
pollSyncSession: async (_ctx: unknown, _client: unknown, input: { sessionID: string }) => { pollSyncSession: async () => null,
pollSessionIDs.push(input.sessionID) fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
return null
},
fetchSyncResult: async (_client: unknown, sessionID: string) => {
fetchSessionIDs.push(sessionID)
return { ok: true as const, textContent: "Result" }
},
} }
const mockCtx = { const mockCtx = {
@@ -430,10 +407,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
client: mockClient, client: mockClient,
directory: "/tmp", directory: "/tmp",
onSyncSessionCreated: null, onSyncSessionCreated: null,
modelFallbackControllerAccessor: {
setSessionFallbackChain,
clearSessionFallbackChain,
},
} }
const args = { const args = {
@@ -467,14 +440,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
{ providerID: "anthropic", modelID: "claude-opus-4-7", variant: "max" }, { providerID: "anthropic", modelID: "claude-opus-4-7", variant: "max" },
{ providerID: "opencode-go", modelID: "kimi-k2.6", variant: undefined }, { providerID: "opencode-go", modelID: "kimi-k2.6", variant: undefined },
]) ])
expect(setSessionFallbackChain).toHaveBeenCalledWith("ses_test_12345678", fallbackChain)
expect(bootstrapSnapshots[0]?.retryParts[0]?.text).toContain("test prompt")
expect(createSyncSessionCalls).toBe(1)
expect(promptSessionIDs).toEqual(["ses_test_12345678", "ses_test_12345678"])
expect(pollSessionIDs).toEqual(["ses_test_12345678"])
expect(fetchSessionIDs).toEqual(["ses_test_12345678"])
expect(clearSessionFallbackChain).toHaveBeenCalledWith("ses_test_12345678")
expect(getDelegatedChildSessionBootstrap("ses_test_12345678")).toBeUndefined()
}) })
test("#given fallback chain exhausted #when all retries fail #then returns final error", async () => { test("#given fallback chain exhausted #when all retries fail #then returns final error", async () => {
@@ -545,110 +510,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
]) ])
}) })
test("keeps concurrent delegated first-prompt fallback bootstrap isolated per session", async () => {
const mockClient = {
session: {
create: async () => ({ data: { id: "ignored" } }),
},
}
const { executeSyncTask } = require("./sync-task")
const { getDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
const perSessionAttempts = new Map<string, number>()
const bootstrapSnapshots: Array<{ sessionID: string; text: string | undefined }> = []
const setSessionFallbackChain = mock(() => {})
const clearSessionFallbackChain = mock(() => {})
const deps = {
createSyncSession: async (_client: unknown, input: { description: string }) => {
return {
ok: true as const,
sessionID: input.description === "alpha task" ? "ses_alpha" : "ses_beta",
}
},
sendSyncPrompt: async (_client: unknown, input: { sessionID: string; categoryModel?: { providerID: string; modelID: string; variant?: string } }) => {
const bootstrap = getDelegatedChildSessionBootstrap(input.sessionID)
bootstrapSnapshots.push({
sessionID: input.sessionID,
text: bootstrap?.retryParts[0]?.text,
})
const currentAttempt = (perSessionAttempts.get(input.sessionID) ?? 0) + 1
perSessionAttempts.set(input.sessionID, currentAttempt)
return currentAttempt === 1 ? `Initial failure for ${input.sessionID}` : null
},
pollSyncSession: async () => null,
fetchSyncResult: async (_client: unknown, sessionID: string) => ({ ok: true as const, textContent: `Result from ${sessionID}` }),
}
const mockExecutorCtx = {
client: mockClient,
directory: "/tmp",
onSyncSessionCreated: null,
modelFallbackControllerAccessor: {
setSessionFallbackChain,
clearSessionFallbackChain,
},
}
const alphaArgs = {
prompt: "alpha delegated prompt",
description: "alpha task",
category: "test",
load_skills: [],
run_in_background: false,
command: null,
}
const betaArgs = {
prompt: "beta delegated prompt",
description: "beta task",
category: "test",
load_skills: [],
run_in_background: false,
command: null,
}
const mockCtx = {
sessionID: "parent-session",
callID: "call-123",
metadata: () => {},
}
const [alphaResult, betaResult] = await Promise.all([
executeSyncTask(alphaArgs, mockCtx, mockExecutorCtx, { sessionID: "parent-session" }, "test-agent", {
providerID: "anthropic",
modelID: "claude-opus-4-7",
variant: "max",
}, undefined, undefined, [
{ providers: ["anthropic"], model: "claude-opus-4-7", variant: "max" },
{ providers: ["openai"], model: "gpt-5.4" },
], deps),
executeSyncTask(betaArgs, mockCtx, mockExecutorCtx, { sessionID: "parent-session" }, "test-agent", {
providerID: "genai-proxy-openai",
modelID: "gpt-5.4-mini",
variant: undefined,
}, undefined, undefined, [
{ providers: ["genai-proxy-openai"], model: "gpt-5.4-mini" },
{ providers: ["genai-proxy-aws"], model: "us.anthropic.claude-haiku-4-5-20251001-v1:0" },
], deps),
])
expect(alphaResult).toContain("Result from ses_alpha")
expect(betaResult).toContain("Result from ses_beta")
expect(bootstrapSnapshots.filter((snapshot) => snapshot.sessionID === "ses_alpha").every((snapshot) => snapshot.text?.includes("alpha delegated prompt"))).toBe(true)
expect(bootstrapSnapshots.filter((snapshot) => snapshot.sessionID === "ses_beta").every((snapshot) => snapshot.text?.includes("beta delegated prompt"))).toBe(true)
expect(setSessionFallbackChain).toHaveBeenCalledWith("ses_alpha", [
{ providers: ["anthropic"], model: "claude-opus-4-7", variant: "max" },
{ providers: ["openai"], model: "gpt-5.4" },
])
expect(setSessionFallbackChain).toHaveBeenCalledWith("ses_beta", [
{ providers: ["genai-proxy-openai"], model: "gpt-5.4-mini" },
{ providers: ["genai-proxy-aws"], model: "us.anthropic.claude-haiku-4-5-20251001-v1:0" },
])
expect(clearSessionFallbackChain).toHaveBeenCalledWith("ses_alpha")
expect(clearSessionFallbackChain).toHaveBeenCalledWith("ses_beta")
expect(getDelegatedChildSessionBootstrap("ses_alpha")).toBeUndefined()
expect(getDelegatedChildSessionBootstrap("ses_beta")).toBeUndefined()
})
test("cleans up toast and subagentSessions on successful completion", async () => { test("cleans up toast and subagentSessions on successful completion", async () => {
const mockClient = { const mockClient = {
session: { session: {
@@ -714,7 +575,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
}) })
test("retries sync session on retryable runtime session error using next fallback model", async () => { test("retries sync session on retryable runtime session error using next fallback model", async () => {
const { getDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
const mockClient = { const mockClient = {
session: { session: {
create: async () => ({ data: { id: "ignored" } }), create: async () => ({ data: { id: "ignored" } }),
@@ -793,8 +653,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
]) ])
expect(result).toContain("Result from ses_second") expect(result).toContain("Result from ses_second")
expect(deleteCalls).toContain("ses_first") expect(deleteCalls).toContain("ses_first")
expect(getDelegatedChildSessionBootstrap("ses_first")).toBeUndefined()
expect(getDelegatedChildSessionBootstrap("ses_second")).toBeUndefined()
const finalMetadata = metadataCalls[metadataCalls.length - 1] const finalMetadata = metadataCalls[metadataCalls.length - 1]
expect(finalMetadata.metadata.sessionId).toBe("ses_second") expect(finalMetadata.metadata.sessionId).toBe("ses_second")
@@ -882,7 +740,6 @@ describe("executeSyncTask - cleanup on error paths", () => {
}) })
test("publishes latest retry session metadata when final retry still fails", async () => { test("publishes latest retry session metadata when final retry still fails", async () => {
const { getDelegatedChildSessionBootstrap } = require("../../shared/delegated-child-session-bootstrap")
const mockClient = { const mockClient = {
session: { session: {
create: async () => ({ data: { id: "ignored" } }), create: async () => ({ data: { id: "ignored" } }),
@@ -948,9 +805,7 @@ describe("executeSyncTask - cleanup on error paths", () => {
}, "sisyphus-junior", initialModel, undefined, undefined, fallbackChain, deps) }, "sisyphus-junior", initialModel, undefined, undefined, fallbackChain, deps)
expect(result).toBe("Final retry failed") expect(result).toBe("Final retry failed")
expect(getDelegatedChildSessionBootstrap("ses_first")).toBeUndefined() const finalMetadata = metadataCalls[metadataCalls.length - 1]
expect(getDelegatedChildSessionBootstrap("ses_second")).toBeUndefined()
const finalMetadata = metadataCalls.at(-1)
expect(finalMetadata.metadata.sessionId).toBe("ses_second") expect(finalMetadata.metadata.sessionId).toBe("ses_second")
expect(finalMetadata.metadata.taskId).toBe("ses_second") expect(finalMetadata.metadata.taskId).toBe("ses_second")
expect(finalMetadata.metadata.model).toEqual({ expect(finalMetadata.metadata.model).toEqual({
@@ -1082,3 +937,5 @@ describe("executeSyncTask - cleanup on error paths", () => {
expect(taskMeta.metadata.spawnDepth).toBe(3) // NOT 1 (the fallback value) expect(taskMeta.metadata.spawnDepth).toBe(3) // NOT 1 (the fallback value)
}) })
}) })
export {}
+5 -18
View File
@@ -14,11 +14,6 @@ import { buildTaskMetadataBlock } from "../../features/tool-metadata-store/task-
import { resolveMetadataModel } from "./resolve-metadata-model" import { resolveMetadataModel } from "./resolve-metadata-model"
import { shouldRetryError } from "../../shared/model-error-classifier" import { shouldRetryError } from "../../shared/model-error-classifier"
import type { ModelFallbackState } from "../../hooks/model-fallback/hook" import type { ModelFallbackState } from "../../hooks/model-fallback/hook"
import { buildTaskPrompt } from "./prompt-builder"
import {
clearDelegatedChildSessionBootstrap,
registerDelegatedChildSessionBootstrap,
} from "../../shared/delegated-child-session-bootstrap"
function shouldAttemptPollErrorRecovery(pollError: string): boolean { function shouldAttemptPollErrorRecovery(pollError: string): boolean {
const trimmed = pollError.trim() const trimmed = pollError.trim()
@@ -67,9 +62,6 @@ export async function executeSyncTask(
| undefined | undefined
try { try {
const tddEnabled = executorCtx.sisyphusAgentConfig?.tdd
const delegatedPromptText = buildTaskPrompt(args.prompt, agentToUse, tddEnabled)
if (typeof manager?.reserveSubagentSpawn === "function") { if (typeof manager?.reserveSubagentSpawn === "function") {
spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID) spawnReservation = await manager.reserveSubagentSpawn(parentContext.sessionID)
} }
@@ -114,13 +106,11 @@ export async function executeSyncTask(
subagentSessions.add(newSessionID) subagentSessions.add(newSessionID)
syncSubagentSessions.add(newSessionID) syncSubagentSessions.add(newSessionID)
setSessionAgent(newSessionID, agentToUse) setSessionAgent(newSessionID, agentToUse)
registerDelegatedChildSessionBootstrap({ executorCtx.modelFallbackControllerAccessor?.setSessionFallbackChain(newSessionID, fallbackChain)
sessionID: newSessionID,
promptText: delegatedPromptText, if (args.category) {
fallbackChain, SessionCategoryRegistry.register(newSessionID, args.category)
category: args.category, }
modelFallbackControllerAccessor: executorCtx.modelFallbackControllerAccessor,
})
if (onSyncSessionCreated) { if (onSyncSessionCreated) {
log("[task] Invoking onSyncSessionCreated callback", { sessionID: newSessionID, parentID: parentContext.sessionID }) log("[task] Invoking onSyncSessionCreated callback", { sessionID: newSessionID, parentID: parentContext.sessionID })
@@ -186,7 +176,6 @@ export async function executeSyncTask(
sessionID, sessionID,
agentToUse, agentToUse,
args, args,
promptText: delegatedPromptText,
systemContent, systemContent,
directory: createSessionResult.parentDirectory, directory: createSessionResult.parentDirectory,
toastManager, toastManager,
@@ -209,7 +198,6 @@ export async function executeSyncTask(
const cleanupRetrySession = (currentSessionID: string): void => { const cleanupRetrySession = (currentSessionID: string): void => {
subagentSessions.delete(currentSessionID) subagentSessions.delete(currentSessionID)
syncSubagentSessions.delete(currentSessionID) syncSubagentSessions.delete(currentSessionID)
clearDelegatedChildSessionBootstrap(currentSessionID)
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(currentSessionID) executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(currentSessionID)
SessionCategoryRegistry.remove(currentSessionID) SessionCategoryRegistry.remove(currentSessionID)
} }
@@ -374,7 +362,6 @@ ${buildTaskMetadataBlock({
if (syncSessionID) { if (syncSessionID) {
subagentSessions.delete(syncSessionID) subagentSessions.delete(syncSessionID)
syncSubagentSessions.delete(syncSessionID) syncSubagentSessions.delete(syncSessionID)
clearDelegatedChildSessionBootstrap(syncSessionID)
executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(syncSessionID) executorCtx.modelFallbackControllerAccessor?.clearSessionFallbackChain(syncSessionID)
SessionCategoryRegistry.remove(syncSessionID) SessionCategoryRegistry.remove(syncSessionID)
} }