fix(sdk): cleared async singleton after startup failures

If createAgentSession failed after installing a newly created AsyncJobManager but before AgentSession took ownership, the process-global singleton stayed installed. The new singleton guard then caused the next top-level session to skip constructing a scoped manager, disabling async bash/task support.

The startup-error cleanup now clears the singleton only when it still points at the newly created manager, disposes that manager, and then continues the existing registry/kernel cleanup. The regression test forces a startup failure after singleton installation and verifies the next top-level session can create and use its own async manager.
This commit is contained in:
roboomp
2026-06-05 09:34:10 +00:00
parent ac304eacdc
commit be9e5218ed
3 changed files with 43 additions and 1 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### Fixed
- Fixed `AsyncJobManager.instance()` being cleared while the owning top-level session was still live, which broke the `task` async path with "Async execution is enabled but no async job manager is available" until process restart. Any in-process secondary top-level `createAgentSession()` call (e.g. the Agent Control Center's create flow in `agent-dashboard.ts`) constructed a fresh `AsyncJobManager`, overwrote the singleton, and then cleared it on its own dispose. Secondary sessions now leave the live singleton untouched, and their dispose-time cleanup is scoped so it can no longer cancel the primary session's running bash/task jobs. `bash` / `task` / `job` tools and session job snapshots now resolve the manager through session-scoped async manager wiring rather than `AsyncJobManager.instance()`, so a secondary in-process top-level session cannot accidentally register background work on the owning session's manager or report the owning session's jobs; subagents still inherit the parent's manager via their scoped async manager ([#1923](https://github.com/can1357/oh-my-pi/issues/1923)).
- Fixed `AsyncJobManager.instance()` being cleared while the owning top-level session was still live, which broke the `task` async path with "Async execution is enabled but no async job manager is available" until process restart. Any in-process secondary top-level `createAgentSession()` call (e.g. the Agent Control Center's create flow in `agent-dashboard.ts`) constructed a fresh `AsyncJobManager`, overwrote the singleton, and then cleared it on its own dispose. Secondary sessions now leave the live singleton untouched, and their dispose-time cleanup is scoped so it can no longer cancel the primary session's running bash/task jobs. `bash` / `task` / `job` tools and session job snapshots now resolve the manager through session-scoped async manager wiring rather than `AsyncJobManager.instance()`, so a secondary in-process top-level session cannot accidentally register background work on the owning session's manager or report the owning session's jobs; subagents still inherit the parent's manager via their scoped async manager. Startup failures after a top-level session installs its manager now clear and dispose that manager before the next session decides whether it can create its own ([#1923](https://github.com/can1357/oh-my-pi/issues/1923)).
## [15.9.1] - 2026-06-04
+6
View File
@@ -2276,6 +2276,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
await session.dispose();
} else {
if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
if (asyncJobManager) {
if (AsyncJobManager.instance() === asyncJobManager) {
AsyncJobManager.setInstance(undefined);
}
await asyncJobManager.dispose({ timeoutMs: 3_000 });
}
await disposeKernelSessionsByOwner(evalKernelOwnerId);
}
} catch (cleanupError) {
@@ -133,4 +133,40 @@ describe("AsyncJobManager singleton across concurrent top-level sessions", () =>
await primary.dispose();
}
});
it("clears a manager installed before a top-level session startup failure takes ownership", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `pi-sdk-async-startup-failure-${Snowflake.next()}-`));
tempDirs.push(tempDir);
const cwd = path.join(tempDir, `project-${Snowflake.next()}`);
const agentDir = path.join(tempDir, "agent");
fs.mkdirSync(cwd, { recursive: true });
await expect(
createAgentSession({
cwd,
agentDir,
settings: Settings.isolated({ "bash.autoBackground.enabled": true }),
disableExtensionDiscovery: true,
skills: [],
contextFiles: [],
promptTemplates: [],
slashCommands: [],
enableMCP: false,
enableLsp: false,
systemPrompt: () => {
throw new Error("forced startup failure");
},
}),
).rejects.toThrow("forced startup failure");
expect(AsyncJobManager.instance()).toBeUndefined();
const replacement = await spawnTopLevelSession();
try {
expect(AsyncJobManager.instance()).toBeDefined();
expect(replacement.getAsyncJobSnapshot()).not.toBeNull();
} finally {
await replacement.dispose();
}
});
});