fix: last-response viewer follows /clear to the new Claude conversation#76
Conversation
Interactive Claude CLI never emits session_id on stdout, so the Session's _claudeSessionId stayed pinned to the pre-/clear jsonl and the last-response viewer kept showing the old conversation. Two complementary update paths: - Session.adoptClaudeSessionId() — public setter mirroring the existing no-op-if-same guard. Called from POST /api/hook-event when Claude Code hooks carry data.session_id (works once hooks are configured). - /api/sessions/:id/last-response now resolves the active id from ~/.claude/history.jsonl before reading the transcript. This is the only source-of-truth that does not require hooks, and we intentionally don't write hooks into arbitrary user repos. History scan filters out sessionIds held by other Codeman sessions in the same cwd, and validates via jsonl mtime to avoid inheriting a dead prior session's id.
|
Nice fix for the /clear session tracking issue! One security concern I noticed: Path traversal via const cs = await fs.stat(join(projectsDir, projDir, `${candidateSid}.jsonl`));
Suggested fix -- Validate that session IDs are UUIDs before use. This can be enforced in // In Session class:
private static readonly SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
adoptClaudeSessionId(newId: string): void {
if (!newId || newId === this._claudeSessionId) return;
if (!Session.SESSION_ID_RE.test(newId)) return;
this._claudeSessionId = newId;
}
// In resolveActiveClaudeSessionIdFromHistory, after extracting candidateSid:
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!candidateSid || !SESSION_ID_RE.test(candidateSid)) return null;This protects both the |
|
Thank you, @TeigenZhang! Reviewed the PR and verified locally — typecheck, lint, and format all pass. The two-pronged design is well thought out:
The known limitation (concurrent Merging now. |
Summary
The last-response viewer (eye icon) keeps showing the pre-/clear transcript after a user runs
/clearin the Claude CLI. This is because the Session's_claudeSessionIdis only refreshed when Claude emits JSON on stdout, which never happens in the interactive PTY mode Codeman spawns.This PR adds two complementary update paths:
Session.adoptClaudeSessionId()— public setter mirroring the existing no-op-if-same guard. Called fromPOST /api/hook-eventwhenever Claude Code hooks carrydata.session_id(works automatically once hooks are configured in the target case)./api/sessions/:id/last-responsenow resolves the active conversation id from~/.claude/history.jsonlbefore reading the transcript. This is the only source-of-truth that does not require hooks, which matters because Codeman intentionally does not write hook config into arbitrary user repos (see the comment atsession-routes.tsPOST /api/sessions).The history scan:
<session.id>.jsonlmtime, so a stale id inherited from a prior dead session is never adopted.Why history.jsonl
Claude Code writes one line per user prompt to
~/.claude/history.jsonlwith{sessionId, project, timestamp}. After/clearthe new conversation uuid shows up there on the first prompt.~/.claude/sessions/<pid>.jsonwould be nicer but older Claude CLI versions don't update it after/clear, so it isn't reliable across versions.Test plan
/cleared:claudeSessionIdadopts the post-/clear uuid on the next eye-icon request, and the returned transcript reflects the new conversation.claudeSessionIdstays equal tosession.id).session_idflipsclaudeSessionIdand a follow-up POST with the original id restores it.tsc --noEmitclean,npm run buildsucceeds.Known limitation
If two concurrent Codeman sessions in the same cwd both run
/clearbefore either has had its id re-resolved, the history scan can't disambiguate them (history.jsonl doesn't record which Codeman session typed). Single-session/clearand single-tab-per-cwd setups are unaffected.