feat(lanes): let claude and codex lanes coexist at the same cwd
Repo/Adopt mode (POST /api/lanes/ensure) treated lanes.cwd as globally unique, so adopting a path already driven by one CLI silently ignored a different tool choice. Uniqueness moves to the composite (cwd, tool): one directory can now back a claude lane and a codex lane side by side. - server/db.js: drop the column-level UNIQUE on cwd, migrate existing DBs via table rebuild, add idx_lanes_cwd_tool. - resolveLaneByCwd gains an optional tool filter; hook ingestion always pins to "claude" since Codex has no hook mechanism here, so a hook event never touches a codex lane sharing the path. - POST /ensure resolves the existing owner by (cwd, tool) instead of cwd alone, so a different tool creates a sibling lane. - LaneConsolePane's cwd-only lane lookups now also match tool, so a shared cwd doesn't attach a run to the wrong sibling lane. Worktree mode is unaffected (already allocates a unique directory per lane). docs/LANES.md and CLAUDE.md updated to describe the new composite uniqueness.
This commit is contained in:
@@ -169,7 +169,9 @@ const LANE_DONE_HOOKS = new Set(["Stop", "SessionEnd"]);
|
||||
function touchLaneFromHook(hookType, data) {
|
||||
try {
|
||||
if (!data || !data.cwd) return;
|
||||
const lane = lanesLib.resolveLaneByCwd(data.cwd);
|
||||
// Hooks are only ever emitted by the Claude Code CLI, so pin to the
|
||||
// `claude` lane at this cwd — never the `codex` lane that might share it.
|
||||
const lane = lanesLib.resolveLaneByCwd(data.cwd, "claude");
|
||||
if (!lane) return;
|
||||
|
||||
// Infer the lane's stage from this one tool event, best-effort. A real
|
||||
|
||||
@@ -265,7 +265,11 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
}
|
||||
|
||||
const owner = lanesLib.resolveLaneByCwd(cwd);
|
||||
// A cwd can now back one lane per tool (idx_lanes_cwd_tool) — "does a lane
|
||||
// already own this (cwd, tool) pair" is the real question, so a request for
|
||||
// a different tool than an existing lane at this cwd falls through to
|
||||
// creating that tool's own lane instead of adopting the wrong one.
|
||||
const owner = lanesLib.resolveLaneByCwd(cwd, body.tool || "claude");
|
||||
if (owner) {
|
||||
// First-write wins on the title: an ensure-with-title on an existing lane
|
||||
// is how callers like Sagi adopt the lane, so don't clobber a real title
|
||||
@@ -292,7 +296,7 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
// The cwd UNIQUE constraint is the arbiter: someone else won the race, so
|
||||
// re-read and return THEIR lane rather than reporting a conflict.
|
||||
if (err.code === "SQLITE_CONSTRAINT_UNIQUE" || String(err.message).includes("UNIQUE")) {
|
||||
const winner = lanesLib.resolveLaneByCwd(cwd);
|
||||
const winner = lanesLib.resolveLaneByCwd(cwd, body.tool || "claude");
|
||||
if (winner) return res.json({ lane: payload(winner), created: false });
|
||||
}
|
||||
return res.status(500).json({ error: { code: err.code, message: err.message } });
|
||||
|
||||
Reference in New Issue
Block a user