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:
+15
-3
@@ -401,12 +401,20 @@ function recoverInterruptedProvisioning() {
|
||||
/**
|
||||
* Longest path-boundary prefix match. `/tmp/wt` must NOT capture
|
||||
* `/tmp/wt-sibling`, and a nested lane must beat its parent.
|
||||
*
|
||||
* `tool`, when given, narrows the candidates to that lane's tool before the
|
||||
* prefix comparison — needed now that one cwd can back both a `claude` lane
|
||||
* and a `codex` lane (see `idx_lanes_cwd_tool`). Omitted, it matches any tool,
|
||||
* which stays correct for callers that already know they hold the lane's own
|
||||
* cwd (there's only ever one match for that exact path per tool, and this
|
||||
* caller shape doesn't care which).
|
||||
*/
|
||||
function resolveLaneByCwd(cwd) {
|
||||
function resolveLaneByCwd(cwd, tool) {
|
||||
if (!cwd || typeof cwd !== "string") return null;
|
||||
const target = cwd.replace(/\/+$/, "");
|
||||
let best = null;
|
||||
for (const lane of listLanes()) {
|
||||
if (tool && lane.tool !== tool) continue;
|
||||
const base = lane.cwd.replace(/\/+$/, "");
|
||||
if (target === base || target.startsWith(`${base}/`)) {
|
||||
if (!best || base.length > best.cwd.length) best = lane;
|
||||
@@ -446,14 +454,18 @@ function absolutePathsIn(toolInput) {
|
||||
*
|
||||
* Only stage inference uses this. `session_id` and `needs_action` stay on the
|
||||
* session's own lane, because those genuinely are session-scoped facts.
|
||||
*
|
||||
* Hooks-only, and hooks are only ever emitted by the Claude Code CLI (Codex
|
||||
* has no equivalent hook mechanism here) — so both lookups are pinned to the
|
||||
* `claude` lane at each path, never the `codex` one that might share it.
|
||||
*/
|
||||
function resolveLaneForWork(sessionCwd, toolInput) {
|
||||
let best = null;
|
||||
for (const candidate of absolutePathsIn(toolInput)) {
|
||||
const lane = resolveLaneByCwd(candidate);
|
||||
const lane = resolveLaneByCwd(candidate, "claude");
|
||||
if (lane && (!best || lane.cwd.length > best.cwd.length)) best = lane;
|
||||
}
|
||||
return best || resolveLaneByCwd(sessionCwd);
|
||||
return best || resolveLaneByCwd(sessionCwd, "claude");
|
||||
}
|
||||
|
||||
function classifyLiveness({ status, stage, ageSec }, deadSec = DEAD_SEC) {
|
||||
|
||||
Reference in New Issue
Block a user