mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix(session): resume the conversation when respawning a dead pane
A CLI that launches with `--session-id <id>` refuses an id that is already in use (claude: `Error: Session ID ... is already in use.`), and every session whose agent has been prompted owns a transcript under that id. The dead-pane respawn in `_setupOrAttachMuxSession()` passed the bare launch line, so recovering such a session relaunched a CLI that died on startup, the pane went dead again at once, and the conversation was stranded behind a tab that looked merely idle. `restartCli()` has pinned a resume id against this since the custom-model work, and its comment states the assumption that made the other path look safe: "Unlike the dead-pane respawn, this one kills a WORKING pane whose conversation already has a transcript". A pane whose agent exited has a transcript too. Both relaunch paths now build options through `_buildRespawnPaneOptionsWithResumePin()`, and so does the create-path fallback after a failed respawn, which otherwise met the same refusal that made it the fallback. Four gates guard the pin, each standing for a way of resuming the WRONG conversation or of making a working relaunch fail. A remote or docker session is never pinned. Unlike `restartCli()`, whose route refuses both, the dead-pane respawn is reached by every session shape. Their pane commands already render a self-healing `--session-id || --resume`, and both flip to resume-first once the resume id differs; the conversation lives on the far side, so a local id resolves to nothing there and the `--session-id` fallback then collides with the transcript the far side does hold. The id comes from the conversation CHAIN rather than `_claudeSessionId`, which also holds history-correlated guesses keyed on the working directory. `_recordClaudeSessionInChain()` refuses those so they cannot "write a foreign conversation into this pane's permanent record", and launching from one is worse than the display bug that rule prevents. The chain tail also outranks the launch seed, which is written once at construction and never moves off a `/clear`. A pin no transcript backs is dropped, because the fallback branch keeps `--session-id <this.id>` and would collide. A synthetic `restored-<fragment>` id from socket discovery is dropped too, and logged: it fails claude's `uuid` token pattern, so the renderer would emit the unpinned command while the caller believed otherwise. Tests cover each gate and the rendered command. Four of them fail against the unfixed source; the remote and docker ones were separately checked against a build with only that guard removed, since they pass on master for the wrong reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9466acfc1a
commit
47e7935274
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @fileoverview Does a Claude conversation transcript exist on this host?
|
||||
*
|
||||
* Claude writes one `<conversation-id>.jsonl` per conversation under
|
||||
* `<config dir>/projects/<mangled cwd>/`. Two launch decisions turn on whether
|
||||
* such a file exists: `--resume <id>` needs one, and `--session-id <id>` is
|
||||
* REFUSED when one exists (`Error: Session ID ... is already in use.`).
|
||||
*
|
||||
* The project directory name is derived from the working directory, and a case
|
||||
* that has been moved or renamed leaves its transcript under the OLD name, so
|
||||
* the search is across every project directory rather than the one that matches
|
||||
* the pane's cwd today.
|
||||
*
|
||||
* ⚠️ Existence is the whole question here, with no size floor. The create route
|
||||
* additionally requires ~4 KB before it will resume, which is a "is this
|
||||
* conversation worth resuming" judgement; for a relaunch the question is the
|
||||
* opposite one — a one-line transcript still makes `--session-id` collide.
|
||||
*
|
||||
* @dependencies none
|
||||
* @consumedby session (relaunch resume pinning)
|
||||
*
|
||||
* @module utils/claude-transcript
|
||||
*/
|
||||
|
||||
import { readdir, stat } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/** `<config dir>/projects`, honouring a session's relocated `CLAUDE_CONFIG_DIR`. */
|
||||
export function claudeProjectsDir(configDir?: string): string {
|
||||
return join(configDir || join(homedir(), '.claude'), 'projects');
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a transcript for `conversationId` exists under any project
|
||||
* directory. Returns false for a missing projects dir or an unreadable one:
|
||||
* the caller's fallback is to skip the resume, which is the safe direction.
|
||||
*/
|
||||
export async function claudeTranscriptExists(conversationId: string, configDir?: string): Promise<boolean> {
|
||||
if (!conversationId) return false;
|
||||
const projectsDir = claudeProjectsDir(configDir);
|
||||
let projectDirs: string[];
|
||||
try {
|
||||
projectDirs = await readdir(projectsDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
for (const projectDir of projectDirs) {
|
||||
try {
|
||||
await stat(join(projectsDir, projectDir, `${conversationId}.jsonl`));
|
||||
return true;
|
||||
} catch {
|
||||
// Not in this project directory; keep looking.
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user