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:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user