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:
2026-08-27 13:06:08 +07:00
parent f8fba0a51d
commit ccf2fdb8eb
9 changed files with 530 additions and 17 deletions
+3 -1
View File
@@ -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