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:
parent
a225bc17f5
commit
65787f08e3
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue