mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
A single pin that failed its transcript gate returned the options untouched, so `resumeSessionId` fell back to `_resumeSessionId` — undefined for an ordinary session — and the renderer emitted the bare `claude --dangerously-skip-permissions --session-id "<this.id>"`. Every session prompted before its first `/clear` owns a transcript under that id, so the dropped pin handed back exactly the refusal this branch removes, with no `||` branch to catch it. It was also a regression against master on the `restartCli()` path, which pinned `_claudeSessionId ?? this.id` and, since the constructor seeds that field, could never land unpinned. The pin now walks three candidates in priority order — the conversation chain's tail, the launch seed, then the session's own id — and takes the first one a transcript backs. A candidate that misses is passed over rather than ending the walk. Falling off the end pins nothing, which also settles the second half of the problem: the old code skipped the transcript check whenever the pin was the session's own id, so a genuinely new pane rendered the two-branch form after all. That costs a brand-new session claude's "No conversation found" line in its scrollback, and `wrapWithNice()` prefixes only the first branch of the rendered `a || b`, so the branch that actually runs loses its priority for the life of the session. With no transcript anywhere the bare `--session-id` is the correct command, so the comment claiming an unchanged shape is now true. The transcript lookup reads the server process's own `CLAUDE_CONFIG_DIR` when a session declares none. A pane inherits the server environment through tmux, so on an install that exports it the CLI writes its transcripts there and every lookup under `~/.claude` was a false negative — which under the old code meant the colliding command. `claudeCredentialsPath()` and `realClaudeConfigDir()` resolve the same directory the same way. The header sentence calling a skipped resume "the safe direction" described the opposite of what happens at this call site, and says so now. The create-path fallback writes `_resumeSessionId` alongside the create options. That branch leaves `isRestored` false, so `_claudeSessionId` is recomputed from the launch fields and settled on `this.id` while the CLI resumed the chain tail; the response viewer, Read My Mind and the unified-list alias map read that field until the next first-hand hook. Four new tests: a chain tail with no transcript while the session id has one, no transcript anywhere, the create path's alias, and the process-env lookup. All four fail against the previous commit. Two existing tests move with the gate — the guess-refusal test now backs the session's own id, and the custom-model restart test gives its working pane the transcript that makes `--session-id` collide in the first place, alongside a new one pinning the no-transcript case. CLAUDE.md described the pin as a `restartCli()`-only thing sourced from the live conversation id. All three halves of that moved here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76 lines
3.2 KiB
TypeScript
76 lines
3.2 KiB
TypeScript
/**
|
|
* @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.
|
|
*
|
|
* ⚠️ A false answer is not the conservative one. Skipping a resume leaves the
|
|
* relaunch on `--session-id <id>`, which is safe only when no transcript backs
|
|
* that id either, so a lookup that misses the real config dir turns a
|
|
* recoverable pane into the collision this module exists to prevent.
|
|
*
|
|
* @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`
|
|
* (#255) and, failing that, the server process's own.
|
|
*
|
|
* ⚠️ The process env is not optional here. A pane inherits the server's
|
|
* environment through tmux, so on an install that exports `CLAUDE_CONFIG_DIR`
|
|
* the CLI writes its transcripts there and a lookup under `~/.claude` answers
|
|
* "no transcript" for every conversation on the host. `claudeCredentialsPath()`
|
|
* (claude-credentials.ts) and `realClaudeConfigDir()`
|
|
* (custom-model-injection-apply.ts) resolve the same directory the same way.
|
|
*/
|
|
export function claudeProjectsDir(configDir?: string): string {
|
|
const fromEnv = typeof process.env.CLAUDE_CONFIG_DIR === 'string' && process.env.CLAUDE_CONFIG_DIR.trim();
|
|
return join(configDir || fromEnv || 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,
|
|
* which leaves the caller unpinned: safe where nothing else can collide with
|
|
* the bare `--session-id`, and the reason the caller walks its candidates down
|
|
* to the session's own id rather than treating one false answer as final.
|
|
*/
|
|
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;
|
|
}
|