fix(task): guard deferred launch work

This commit is contained in:
can1357
2026-07-31 19:10:14 +02:00
parent fbedf80b01
commit 305ad95a20
3 changed files with 143 additions and 7 deletions
+11 -7
View File
@@ -2685,6 +2685,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
const effectiveThinkingLevel =
effortLevel ?? (explicitThinkingLevel ? resolvedThinkingLevel : (thinkingLevel ?? resolvedThinkingLevel));
resolvedAt = performance.now();
const effectiveCwd = worktree ?? cwd;
const sessionManagerPromise = sessionFile
? SessionManager.open(sessionFile, undefined, undefined, {
initialCwd: effectiveCwd,
suppressBreadcrumb: true,
})
: Promise.resolve(SessionManager.inMemory(effectiveCwd));
// Setup below can fail before this promise's consumption boundary.
// Observe rejection immediately while preserving it for the later await.
sessionManagerPromise.catch(() => {});
// Per-agent prewalk: the agent definition's `prewalk` frontmatter or the
// `task.agentPrewalk` settings override hands the subagent off to a
// fast/cheap target at its first edit/write — the same mechanism as the
@@ -2697,6 +2707,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
agentPrewalk: resolveAgentPrewalkDefault(agent, settings.get("task.prewalk")),
});
if (prewalkPattern) {
await awaitAbortable(modelRegistry.awaitBackgroundRefresh());
const resolvedPrewalk = resolveModelOverride([prewalkPattern], modelRegistry, settings);
const target = resolvedPrewalk.model;
if (!target || !modelRegistry.hasConfiguredAuth(target)) {
@@ -2718,13 +2729,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
}
}
const effectiveCwd = worktree ?? cwd;
const sessionManagerPromise = sessionFile
? SessionManager.open(sessionFile, undefined, undefined, {
initialCwd: effectiveCwd,
suppressBreadcrumb: true,
})
: Promise.resolve(SessionManager.inMemory(effectiveCwd));
const restrictToolNames = options.restrictToolNames === true;
const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
const mcpManager = enableMCP ? options.mcpManager : undefined;
@@ -0,0 +1,103 @@
import { afterEach, expect, it, vi } from "bun:test";
import { AuthStorage } from "@oh-my-pi/pi-ai";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import type { CreateAgentSessionResult } from "@oh-my-pi/pi-coding-agent/sdk";
import * as sdkModule from "@oh-my-pi/pi-coding-agent/sdk";
import type { AgentSession, AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { runSubprocess } from "@oh-my-pi/pi-coding-agent/task/executor";
import { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
import { TempDir } from "@oh-my-pi/pi-utils";
const authStorages: AuthStorage[] = [];
const tempDirs: TempDir[] = [];
afterEach(async () => {
vi.restoreAllMocks();
for (const authStorage of authStorages.splice(0)) await authStorage.close();
for (const tempDir of tempDirs.splice(0)) tempDir[Symbol.dispose]();
});
it("overlaps registry refresh with session-file opening and session setup", async () => {
const tempDir = TempDir.createSync("@pi-task-launch-");
tempDirs.push(tempDir);
const authStorage = await AuthStorage.create(tempDir.join("auth.db"));
authStorages.push(authStorage);
const refreshGate = Promise.withResolvers<void>();
vi.spyOn(ModelRegistry.prototype, "refresh").mockImplementation(() => refreshGate.promise);
const sessionManager = SessionManager.inMemory(tempDir.path());
const openGate = Promise.withResolvers<SessionManager>();
const openStarted = Promise.withResolvers<void>();
const openSpy = vi.spyOn(SessionManager, "open").mockImplementation(() => {
openStarted.resolve();
return openGate.promise;
});
const sessionCreationStarted = Promise.withResolvers<void>();
let sessionCreated = false;
const listeners: Array<(event: AgentSessionEvent) => void> = [];
const session = {
state: { messages: [] },
agent: { state: { systemPrompt: ["test"] } },
model: undefined,
extensionRunner: undefined,
sessionManager: { appendSessionInit: () => {} },
getActiveToolNames: () => ["yield"],
getEnabledToolNames: () => ["yield"],
setActiveToolsByName: async () => {},
subscribe: (listener: (event: AgentSessionEvent) => void) => {
listeners.push(listener);
return () => {};
},
prompt: async () => {
for (const listener of listeners) {
listener({
type: "tool_execution_end",
toolCallId: "yield",
toolName: "yield",
result: { content: [], details: { status: "success", data: { ok: true } } },
isError: false,
} as AgentSessionEvent);
}
},
waitForIdle: async () => {},
getLastAssistantMessage: () => undefined,
abort: async () => {},
dispose: async () => {},
} as unknown as AgentSession;
vi.spyOn(sdkModule, "createAgentSession").mockImplementation(async () => {
sessionCreationStarted.resolve();
sessionCreated = true;
return {
session,
extensionsResult: {},
setToolUIContext: () => {},
eventBus: new EventBus(),
} as CreateAgentSessionResult;
});
const run = runSubprocess({
cwd: tempDir.path(),
artifactsDir: tempDir.path(),
agent: { name: "task", description: "test", systemPrompt: "test", source: "bundled" },
task: "test",
index: 0,
id: "task-launch-overlap",
authStorage,
enableLsp: false,
enableIrc: false,
});
await openStarted.promise;
expect(openSpy).toHaveBeenCalledTimes(1);
expect(sessionCreated).toBe(false);
openGate.resolve(sessionManager);
await sessionCreationStarted.promise;
expect(sessionCreated).toBe(true);
refreshGate.resolve();
expect((await run).exitCode).toBe(0);
});
@@ -96,6 +96,7 @@ function createModelRegistry(models: Model[]): ModelRegistry {
return {
authStorage: {},
refresh: async () => {},
awaitBackgroundRefresh: async () => {},
getAvailable: () => models,
getApiKey: async () => "test-key",
hasConfiguredAuth: () => true,
@@ -149,6 +150,34 @@ describe("runSubprocess per-agent prewalk", () => {
expect(forwarded?.prewalk?.target.provider).toBe(target.provider);
});
it("waits for background discovery before resolving a configured prewalk target", async () => {
const models = [primary];
const registry = createModelRegistry(models);
const refreshGate = Promise.withResolvers<void>();
vi.spyOn(registry, "awaitBackgroundRefresh").mockImplementation(async () => {
await refreshGate.promise;
models.push(target);
});
const spy = vi
.spyOn(sdkModule, "createAgentSession")
.mockResolvedValue(createSessionResult(yieldEmittingSession()));
const run = runSubprocess({
...baseOptions("subagent-prewalk-discovery", Settings.isolated()),
modelRegistry: registry,
agent: {
...baseAgent,
model: [`${primary.provider}/${primary.id}`],
prewalk: `${target.provider}/${target.id}`,
},
});
expect(spy).not.toHaveBeenCalled();
refreshGate.resolve();
expect((await run).exitCode).toBe(0);
expect(spy.mock.calls[0]?.[0]?.prewalk?.target.id).toBe(target.id);
});
it("reports the prewalk target as the active model after handoff", async () => {
const progressModels: string[] = [];
vi.spyOn(sdkModule, "createAgentSession").mockResolvedValue(