fix: use handle.runtimeName for plugin lookup, atomic session ID reservation

- kill() and send() prefer handle.runtimeName over project defaults
- spawn() uses O_EXCL atomic file creation to prevent concurrent ID collisions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Prateek 2026-02-13 20:41:11 +05:30
parent a225bc17f5
commit 65787f08e3
2 changed files with 54 additions and 17 deletions

View File

@ -19,6 +19,9 @@ import {
mkdirSync,
unlinkSync,
readdirSync,
openSync,
closeSync,
constants,
} from "node:fs";
import { join, dirname } from "node:path";
import type { SessionId, SessionMetadata } from "./types.js";
@ -188,3 +191,19 @@ export function listMetadata(dataDir: string): SessionId[] {
return readdirSync(dir).filter((name) => name !== "archive" && !name.startsWith("."));
}
/**
* Atomically reserve a session ID by creating its metadata file with O_EXCL.
* Returns true if the ID was successfully reserved, false if it already exists.
*/
export function reserveSessionId(dataDir: string, sessionId: SessionId): boolean {
const path = metadataPath(dataDir, sessionId);
mkdirSync(dirname(path), { recursive: true });
try {
const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
closeSync(fd);
return true;
} catch {
return false;
}
}

View File

@ -29,7 +29,13 @@ import type {
PluginRegistry,
RuntimeHandle,
} from "./types.js";
import { readMetadataRaw, writeMetadata, deleteMetadata, listMetadata } from "./metadata.js";
import {
readMetadataRaw,
writeMetadata,
deleteMetadata,
listMetadata,
reserveSessionId,
} from "./metadata.js";
import { createEvent } from "./event-bus.js";
/** Escape regex metacharacters in a string. */
@ -157,10 +163,19 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager {
throw new Error(`Agent plugin '${project.agent ?? config.defaults.agent}' not found`);
}
// Determine session ID
// Determine session ID — atomically reserve to prevent concurrent collisions
const existingSessions = listMetadata(config.dataDir);
const num = getNextSessionNumber(existingSessions, project.sessionPrefix);
const sessionId = `${project.sessionPrefix}-${num}`;
let num = getNextSessionNumber(existingSessions, project.sessionPrefix);
let sessionId: string;
for (let attempts = 0; attempts < 10; attempts++) {
sessionId = `${project.sessionPrefix}-${num}`;
if (reserveSessionId(config.dataDir, sessionId)) break;
num++;
if (attempts === 9) {
throw new Error(`Failed to reserve session ID after 10 attempts (prefix: ${project.sessionPrefix})`);
}
}
sessionId = `${project.sessionPrefix}-${num}`;
// Determine branch name — explicit branch always takes priority
let branch: string;
@ -355,13 +370,14 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager {
const projectId = raw["project"] ?? "";
const project = config.projects[projectId];
// Destroy runtime — use handle's runtimeName to find the correct plugin
// Destroy runtime — prefer handle.runtimeName to find the correct plugin
if (raw["runtimeHandle"]) {
const handle = safeJsonParse<RuntimeHandle>(raw["runtimeHandle"]);
if (handle) {
const runtimePlugin = project
? resolvePlugins(project).runtime
: registry.get<Runtime>("runtime", handle.runtimeName ?? config.defaults.runtime);
const runtimePlugin = registry.get<Runtime>(
"runtime",
handle.runtimeName ?? (project ? project.runtime ?? config.defaults.runtime : config.defaults.runtime),
);
if (runtimePlugin) {
try {
await runtimePlugin.destroy(handle);
@ -469,14 +485,6 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager {
const raw = readMetadataRaw(config.dataDir, sessionId);
if (!raw) throw new Error(`Session ${sessionId} not found`);
const project = config.projects[raw["project"] ?? ""];
if (!project) throw new Error(`Project not found for session ${sessionId}`);
const plugins = resolvePlugins(project);
if (!plugins.runtime) {
throw new Error(`No runtime plugin for session ${sessionId}`);
}
if (!raw["runtimeHandle"]) {
throw new Error(`No runtime handle for session ${sessionId}`);
}
@ -486,7 +494,17 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager {
throw new Error(`Corrupted runtime handle for session ${sessionId}`);
}
await plugins.runtime.sendMessage(handle, message);
// Prefer handle.runtimeName to find the correct plugin
const project = config.projects[raw["project"] ?? ""];
const runtimePlugin = registry.get<Runtime>(
"runtime",
handle.runtimeName ?? (project ? project.runtime ?? config.defaults.runtime : config.defaults.runtime),
);
if (!runtimePlugin) {
throw new Error(`No runtime plugin for session ${sessionId}`);
}
await runtimePlugin.sendMessage(handle, message);
}
return { spawn, list, get, kill, cleanup, send };