fix(run): stop Start/Resume from silently no-oping on an idle lane session
spawnRun adopted any existing `ccam-lane-<id>` tmux session without looking
at it, so a Resume issued while the session sat at a bare shell prompt (left
by `ccam lanes shell`, or by a `claude` that had already exited) dropped the
whole argv: no `--resume` ran, no initial prompt was typed, and the API still
answered 200. The lane's DB `run_id` is not set in that case, so the ERUNLIVE
guard in the start route never saw it either.
Reuse the pane instead of erroring: when the session exists and
`#{pane_current_command}` is a shell, type the argv into that pane and record
the run. A pane running a program (a live `claude`, an editor, a build) is
still adopted untouched, so attaching shows what is running rather than typing
over it. `sendCommand` POSIX single-quotes every argument and uses
`send-keys -l`, the only place in tmux.js that composes a command line.
This commit is contained in:
+40
-21
@@ -9,6 +9,10 @@
|
||||
* out-of-band (crash, manual `tmux kill-session`, host reboot) self-corrects
|
||||
* on the next read instead of leaving a ghost "running" row.
|
||||
*
|
||||
* Start/Resume is create-or-reuse: when the lane's tmux session already
|
||||
* exists but its pane sits at a shell prompt, the argv is typed into that
|
||||
* pane instead of being dropped on the floor by a silent adopt.
|
||||
*
|
||||
* Every session is named `ccam-lane-<laneId>` so a real terminal can attach
|
||||
* to the exact same session (`tmux attach -t ccam-lane-<id>`, or
|
||||
* `ccam lanes shell`) — that's the whole point: the dashboard both creates
|
||||
@@ -30,6 +34,8 @@ try {
|
||||
const RUN_ID_RE = /^ccam-lane-(\d+)$/;
|
||||
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
||||
const ALLOWED_PERMISSION_MODES = new Set(["acceptEdits", "default", "plan", "bypassPermissions"]);
|
||||
// Pane commands that mean "idle shell prompt, safe to type a command into".
|
||||
const SHELL_COMMANDS = new Set(["sh", "bash", "zsh", "fish", "dash", "ksh", "csh", "tcsh"]);
|
||||
|
||||
function runIdForLane(laneId) {
|
||||
return `ccam-lane-${laneId}`;
|
||||
@@ -99,31 +105,44 @@ function spawnRun(args) {
|
||||
|
||||
const id = runIdForLane(laneId);
|
||||
const startedAt = Date.now();
|
||||
const argv = buildArgv({ model, permissionMode, effort, resumeSessionId, initialPrompt });
|
||||
|
||||
const record = () => {
|
||||
if (!dashboardRuns) return;
|
||||
dashboardRuns.recordRun({
|
||||
id,
|
||||
sessionId: resumeSessionId || null,
|
||||
mode: null,
|
||||
cwd,
|
||||
model: model || null,
|
||||
permissionMode: permissionMode || "acceptEdits",
|
||||
effort: effort || null,
|
||||
resumeSessionId: resumeSessionId || null,
|
||||
prompt: initialPrompt || "",
|
||||
status: "running",
|
||||
startedAt,
|
||||
endedAt: null,
|
||||
exitCode: null,
|
||||
laneId,
|
||||
});
|
||||
};
|
||||
|
||||
if (!tmux.hasSession(id)) {
|
||||
const argv = buildArgv({ model, permissionMode, effort, resumeSessionId, initialPrompt });
|
||||
tmux.newSession({ name: id, cwd, argv });
|
||||
if (dashboardRuns) {
|
||||
dashboardRuns.recordRun({
|
||||
id,
|
||||
sessionId: resumeSessionId || null,
|
||||
mode: null,
|
||||
cwd,
|
||||
model: model || null,
|
||||
permissionMode: permissionMode || "acceptEdits",
|
||||
effort: effort || null,
|
||||
resumeSessionId: resumeSessionId || null,
|
||||
prompt: initialPrompt || "",
|
||||
status: "running",
|
||||
startedAt,
|
||||
endedAt: null,
|
||||
exitCode: null,
|
||||
laneId,
|
||||
});
|
||||
}
|
||||
record();
|
||||
} else if (SHELL_COMMANDS.has(tmux.paneCommand(id) || "")) {
|
||||
// The session exists but its pane is sitting at a bare shell prompt — a
|
||||
// `ccam lanes shell`, or a `claude` that already exited. Adopting it
|
||||
// silently here would swallow the whole request: a Resume would spawn no
|
||||
// `--resume` and an initial prompt would never be typed, while the API
|
||||
// still answered 200. Run the argv in the pane the user already sees
|
||||
// instead of erroring or opening a second session.
|
||||
tmux.sendCommand(id, argv);
|
||||
record();
|
||||
}
|
||||
// Already running: adopt silently, same convention as this repo's server
|
||||
// port-adoption logic — no error, no duplicate session.
|
||||
// Pane is running something (a live `claude`, an editor, a build): adopt
|
||||
// silently, same convention as this repo's server port-adoption logic — no
|
||||
// error, no duplicate session. Attaching shows the user what is running.
|
||||
|
||||
return getRun(id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user