diff --git a/script/run-ci-tests.ts b/script/run-ci-tests.ts index 5466885ce..10cf80b21 100644 --- a/script/run-ci-tests.ts +++ b/script/run-ci-tests.ts @@ -29,13 +29,7 @@ async function usesModuleMock(rootDirectory: string, testFile: string): Promise< } function toIsolatedTarget(testFile: string): string { - const pathSegments = testFile.split("/") - - if (pathSegments.length <= 3) { - return testFile - } - - return pathSegments.slice(0, -1).join("/") + return testFile } function isCoveredByTarget(testFile: string, isolatedTarget: string): boolean { diff --git a/src/features/background-agent/manager.test.ts b/src/features/background-agent/manager.test.ts index 9e10e3c57..5a3a430e6 100644 --- a/src/features/background-agent/manager.test.ts +++ b/src/features/background-agent/manager.test.ts @@ -218,6 +218,10 @@ function getRootDescendantCounts(manager: BackgroundManager): Map }).rootDescendantCounts } +function getPreStartDescendantReservations(manager: BackgroundManager): Set { + return (manager as unknown as { preStartDescendantReservations: Set }).preStartDescendantReservations +} + function getQueuesByKey( manager: BackgroundManager ): Map> { @@ -1144,7 +1148,18 @@ describe("BackgroundManager.notifyParentSession - notifications toggle", () => { prompt: promptMock, promptAsync: promptMock, abort: async () => ({}), - messages: async () => ({ data: [] }), + messages: async () => ({ + data: [{ + info: { + agent: "explore", + model: { + providerID: "anthropic", + modelID: "claude-opus-4-6", + variant: "high", + }, + }, + }], + }), }, } const manager = new BackgroundManager( @@ -1178,7 +1193,7 @@ describe("BackgroundManager.notifyParentSession - notifications toggle", () => { }) describe("BackgroundManager.notifyParentSession - variant propagation", () => { - test("should propagate variant in parent notification promptAsync body", async () => { + test("should prefer parent session variant over child task variant in parent notification promptAsync body", async () => { //#given const promptCalls: Array<{ body: Record }> = [] const client = { @@ -1189,16 +1204,27 @@ describe("BackgroundManager.notifyParentSession - variant propagation", () => { return {} }, abort: async () => ({}), - messages: async () => ({ data: [] }), + messages: async () => ({ + data: [{ + info: { + agent: "explore", + model: { + providerID: "anthropic", + modelID: "claude-opus-4-6", + variant: "max", + }, + }, + }], + }), }, } const manager = new BackgroundManager({ client, directory: tmpdir() } as unknown as PluginInput) const task: BackgroundTask = { - id: "task-variant-test", + id: "task-parent-variant-wins", sessionID: "session-child", parentSessionID: "session-parent", parentMessageID: "msg-parent", - description: "task with variant", + description: "task with mismatched variant", prompt: "test", agent: "explore", status: "completed", @@ -1214,7 +1240,7 @@ describe("BackgroundManager.notifyParentSession - variant propagation", () => { //#then expect(promptCalls).toHaveLength(1) - expect(promptCalls[0].body.variant).toBe("high") + expect(promptCalls[0].body.variant).toBe("max") manager.shutdown() }) @@ -1521,6 +1547,7 @@ describe("BackgroundManager.tryCompleteTask", () => { const task = createMockTask({ id: "task-zombie-session", + sessionID: "session-zombie-placeholder", parentSessionID: "parent-zombie", status: "pending", agent: "explore", @@ -1863,10 +1890,10 @@ describe("BackgroundManager.resume model persistence", () => { expect(getSessionPromptParams("session-advanced")).toEqual({ temperature: 0.25, topP: 0.55, + maxOutputTokens: 8192, options: { reasoningEffort: "high", thinking: { type: "disabled" }, - maxTokens: 8192, }, }) }) @@ -2463,6 +2490,46 @@ describe("BackgroundManager - Non-blocking Queue Integration", () => { expect(retryTask.status).toBe("pending") }) + test("should only roll back the failed task reservation once when siblings still exist", async () => { + // given + const concurrencyKey = "test-agent" + const task = createMockTask({ + id: "task-single-reservation-rollback", + sessionID: "session-single-reservation-rollback", + parentSessionID: "session-root", + status: "pending", + agent: "test-agent", + rootSessionID: "session-root", + }) + delete (task as Partial).sessionID + + const input = { + description: task.description, + prompt: task.prompt, + agent: task.agent, + parentSessionID: task.parentSessionID, + parentMessageID: task.parentMessageID, + } + + getTaskMap(manager).set(task.id, task) + getQueuesByKey(manager).set(concurrencyKey, [{ task, input }]) + getRootDescendantCounts(manager).set("session-root", 2) + getPreStartDescendantReservations(manager).add(task.id) + stubNotifyParentSession(manager) + + ;(manager as unknown as { + startTask: (item: { task: BackgroundTask; input: typeof input }) => Promise + }).startTask = async () => { + throw new Error("session create failed") + } + + // when + await processKeyForTest(manager, concurrencyKey) + + // then + expect(getRootDescendantCounts(manager).get("session-root")).toBe(1) + }) + test("should keep the next queued task when the first task is cancelled during session creation", async () => { // given const firstSessionID = "ses-first-cancelled-during-create" diff --git a/src/features/background-agent/manager.ts b/src/features/background-agent/manager.ts index 1a23c3569..bd8ff2477 100644 --- a/src/features/background-agent/manager.ts +++ b/src/features/background-agent/manager.ts @@ -422,10 +422,6 @@ export class BackgroundManager { this.concurrencyManager.release(key) } - if (item.task.rootSessionID) { - this.unregisterRootDescendant(item.task.rootSessionID) - } - removeTaskToastTracking(item.task.id) // Abort the orphaned session if one was created before the error @@ -1783,6 +1779,7 @@ export class BackgroundManager { let agent: string | undefined = task.parentAgent let model: { providerID: string; modelID: string } | undefined let tools: Record | undefined = task.parentTools + let promptContext: ReturnType = null if (this.enableParentSessionNotifications) { try { @@ -1796,7 +1793,7 @@ export class BackgroundManager { tools?: Record } }>) - const promptContext = resolvePromptContextFromSessionMessages( + promptContext = resolvePromptContextFromSessionMessages( messages, task.parentSessionID, ) @@ -1840,7 +1837,7 @@ export class BackgroundManager { const isTaskFailure = task.status === "error" || task.status === "cancelled" || task.status === "interrupt" const shouldReply = allComplete || isTaskFailure - const variant = task.model?.variant + const variant = promptContext?.model?.variant try { await this.client.session.promptAsync({ diff --git a/src/features/background-agent/spawner.test.ts b/src/features/background-agent/spawner.test.ts index d3896e55f..4c5ddeaf2 100644 --- a/src/features/background-agent/spawner.test.ts +++ b/src/features/background-agent/spawner.test.ts @@ -400,10 +400,10 @@ describe("background-agent spawner fallback model promotion", () => { expect(getSessionPromptParams("session-123")).toEqual({ temperature: 0.4, topP: 0.7, + maxOutputTokens: 4096, options: { reasoningEffort: "high", thinking: { type: "disabled" }, - maxTokens: 4096, }, }) }) diff --git a/src/features/tmux-subagent/zombie-pane.test.ts b/src/features/tmux-subagent/zombie-pane.test.ts index 42fcfb760..267c03cb3 100644 --- a/src/features/tmux-subagent/zombie-pane.test.ts +++ b/src/features/tmux-subagent/zombie-pane.test.ts @@ -40,10 +40,22 @@ mock.module("./action-executor", () => ({ mock.module("../../shared/tmux", () => ({ isInsideTmux: mockIsInsideTmux, getCurrentPaneId: mockGetCurrentPaneId, + isServerRunning: mock(async () => true), + resetServerCheck: mock(() => {}), + markServerRunningInProcess: mock(() => {}), + getPaneDimensions: mock(async () => ({ width: 220, height: 44 })), + spawnTmuxPane: mock(async () => ({ success: true, paneId: "%1" })), + closeTmuxPane: mock(async () => ({ success: true })), + replaceTmuxPane: mock(async () => ({ success: true, paneId: "%1" })), + spawnTmuxWindow: mock(async () => ({ success: true, windowId: "@1" })), + spawnTmuxSession: mock(async () => ({ success: true, sessionId: "mock" })), + applyLayout: mock(async () => ({ success: true })), + enforceMainPaneWidth: mock(async () => ({ success: true })), POLL_INTERVAL_BACKGROUND_MS: 10, SESSION_READY_POLL_INTERVAL_MS: 10, SESSION_READY_TIMEOUT_MS: 50, SESSION_MISSING_GRACE_MS: 1_000, + SESSION_TIMEOUT_MS: 600_000, })) afterAll(() => { mock.restore() }) diff --git a/src/shared/session-prompt-params-helpers.ts b/src/shared/session-prompt-params-helpers.ts index 7ce24c826..f50707956 100644 --- a/src/shared/session-prompt-params-helpers.ts +++ b/src/shared/session-prompt-params-helpers.ts @@ -20,12 +20,12 @@ export function applySessionPromptParams( const promptOptions: Record = { ...(model.reasoningEffort ? { reasoningEffort: model.reasoningEffort } : {}), ...(model.thinking ? { thinking: model.thinking } : {}), - ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}), } setSessionPromptParams(sessionID, { ...(model.temperature !== undefined ? { temperature: model.temperature } : {}), ...(model.top_p !== undefined ? { topP: model.top_p } : {}), + ...(model.maxTokens !== undefined ? { maxOutputTokens: model.maxTokens } : {}), ...(Object.keys(promptOptions).length > 0 ? { options: promptOptions } : {}), }) } diff --git a/src/shared/session-prompt-params-state.test.ts b/src/shared/session-prompt-params-state.test.ts index b97a80565..d52670be6 100644 --- a/src/shared/session-prompt-params-state.test.ts +++ b/src/shared/session-prompt-params-state.test.ts @@ -18,9 +18,9 @@ describe("session-prompt-params-state", () => { const params = { temperature: 0.4, topP: 0.7, + maxOutputTokens: 4096, options: { reasoningEffort: "high", - maxTokens: 4096, }, } diff --git a/src/tools/call-omo-agent/sync-executor.test.ts b/src/tools/call-omo-agent/sync-executor.test.ts index baa59fb78..404e3fee0 100644 --- a/src/tools/call-omo-agent/sync-executor.test.ts +++ b/src/tools/call-omo-agent/sync-executor.test.ts @@ -190,10 +190,10 @@ describe("executeSync", () => { expect(promptInput?.body.temperature).toBe(0.12) expect(promptInput?.body.topP).toBe(0.34) expect(promptInput?.body.options).toEqual({ - maxTokens: 5678, reasoningEffort: "medium", thinking: { type: "disabled" }, }) + expect(promptInput?.body.maxOutputTokens).toBe(5678) }) test("records metadata with description and created session id", async () => { diff --git a/src/tools/call-omo-agent/sync-executor.ts b/src/tools/call-omo-agent/sync-executor.ts index 096a80216..f0f65d7e1 100644 --- a/src/tools/call-omo-agent/sync-executor.ts +++ b/src/tools/call-omo-agent/sync-executor.ts @@ -43,12 +43,12 @@ function buildPromptGenerationParams(model: DelegatedModelConfig | undefined): R const promptOptions: Record = { ...(model.reasoningEffort ? { reasoningEffort: model.reasoningEffort } : {}), ...(model.thinking ? { thinking: model.thinking } : {}), - ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}), } return { ...(model.temperature !== undefined ? { temperature: model.temperature } : {}), ...(model.top_p !== undefined ? { topP: model.top_p } : {}), + ...(model.maxTokens !== undefined ? { maxOutputTokens: model.maxTokens } : {}), ...(Object.keys(promptOptions).length > 0 ? { options: promptOptions } : {}), } } diff --git a/src/tools/delegate-task/sync-prompt-sender.test.ts b/src/tools/delegate-task/sync-prompt-sender.test.ts index 32970e72a..f86e87997 100644 --- a/src/tools/delegate-task/sync-prompt-sender.test.ts +++ b/src/tools/delegate-task/sync-prompt-sender.test.ts @@ -277,15 +277,15 @@ bunDescribe("sendSyncPrompt", () => { bunExpect(promptArgs.body.options).toEqual({ reasoningEffort: "high", thinking: { type: "disabled" }, - maxTokens: 4096, }) + bunExpect(promptArgs.body.maxOutputTokens).toBe(4096) bunExpect(getSessionPromptParams("test-session")).toEqual({ temperature: 0.4, topP: 0.7, + maxOutputTokens: 4096, options: { reasoningEffort: "high", thinking: { type: "disabled" }, - maxTokens: 4096, }, }) }) diff --git a/src/tools/delegate-task/sync-prompt-sender.ts b/src/tools/delegate-task/sync-prompt-sender.ts index 882258d98..bd38830e5 100644 --- a/src/tools/delegate-task/sync-prompt-sender.ts +++ b/src/tools/delegate-task/sync-prompt-sender.ts @@ -30,12 +30,12 @@ function buildPromptGenerationParams(model: DelegatedModelConfig | undefined): R const promptOptions: Record = { ...(model.reasoningEffort ? { reasoningEffort: model.reasoningEffort } : {}), ...(model.thinking ? { thinking: model.thinking } : {}), - ...(model.maxTokens !== undefined ? { maxTokens: model.maxTokens } : {}), } return { ...(model.temperature !== undefined ? { temperature: model.temperature } : {}), ...(model.top_p !== undefined ? { topP: model.top_p } : {}), + ...(model.maxTokens !== undefined ? { maxOutputTokens: model.maxTokens } : {}), ...(Object.keys(promptOptions).length > 0 ? { options: promptOptions } : {}), } }