Skip to content

Commit 24a070d

Browse files
fix(session-log): walk ancestor PIDs to resolve correct session log (#598)
* fix(session-log): walk ancestor PIDs to find Claude Code session metadata When plannotator is invoked from a slash command's `!` bang, the direct parent process is an intermediate bash shell spawned by the Bash tool — not Claude Code itself. The old resolveSessionLogByPpid() only checked process.ppid, so it always missed the session metadata file and fell back to mtime-based selection, which picks the wrong log when multiple sessions exist for the same project. New resolution ladder (four tiers): 1. Ancestor-PID walk: call `ps -o ppid=` repeatedly from process.ppid up to 8 hops, checking ~/.claude/sessions/<pid>.json at each hop. Deterministic — no guessing, matches exact session every time. 2. Cwd-scan: read every ~/.claude/sessions/*.json, filter by cwd, pick most recent startedAt. Handles cases where ps is unavailable. 3. CWD slug mtime (legacy): existing behavior, fragile with multiple sessions. 4. Ancestor directory walk: handles cd-deeper-into-subdirectory cases. Adds getAncestorPids (injectable getParent for testing), resolveSessionLogByAncestorPids, and resolveSessionLogByCwdScan. Exports SessionMetadata and accepts projectsDirOverride on findSessionLogsForCwd for test isolation. 17 new tests cover: edge cases for getAncestorPids (cycles, maxHops, self-loops), resolveSessionLogByAncestorPids (finds correct session among multiple, skips missing logs, falls back when no metadata matches), and resolveSessionLogByCwdScan (picks newest startedAt, ignores mismatched cwd, handles missing sessions dir). Closes #458 * refactor(session-log): simplify resolver and findSessionLogs for clarity * fix(session-log): add Windows support to ancestor-PID resolver Tier 1 used `ps`, which doesn't exist on Windows. Multiple Claude Code sessions in the same repo remained broken on Windows because tier 2 (cwd scan) can't disambiguate when both sessions share the same cwd. - Replace per-hop `ps` calls with a single process-table snapshot (`ps` on Unix, PowerShell Get-CimInstance on Windows), cached across the walk. One spawn instead of up to eight; faster on both platforms. - Export pure-function parsers (parseProcessTablePs, parseProcessTableCsv) so the Windows path is unit-testable without a Windows runner. - Normalize cwd comparison in resolveSessionLogByCwdScan: Windows is case-insensitive and processes may report drive letters in either case, so fold slashes and lowercase before comparing. Windows viability confirmed empirically: ~/.claude/sessions/<pid>.json exists with the expected schema on Windows 11 + Claude Code 2.1.116. For provenance purposes, this commit was AI assisted. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com>
1 parent 53d2246 commit 24a070d

3 files changed

Lines changed: 681 additions & 34 deletions

File tree

apps/hook/server/index.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,14 @@ import { hostnameOrFallback } from "@plannotator/shared/project";
8181
import { planDenyFeedback } from "@plannotator/shared/feedback-templates";
8282
import { readImprovementHook } from "@plannotator/shared/improvement-hooks";
8383
import { AGENT_CONFIG, type Origin } from "@plannotator/shared/agents";
84-
import { findSessionLogsForCwd, resolveSessionLogByPpid, findSessionLogsByAncestorWalk, getLastRenderedMessage, type RenderedMessage } from "./session-log";
84+
import {
85+
findSessionLogsByAncestorWalk,
86+
findSessionLogsForCwd,
87+
getLastRenderedMessage,
88+
resolveSessionLogByAncestorPids,
89+
resolveSessionLogByCwdScan,
90+
type RenderedMessage,
91+
} from "./session-log";
8592
import { findCodexRolloutByThreadId, getLastCodexMessage } from "./codex-session";
8693
import { findCopilotPlanContent, findCopilotSessionForCwd, getLastCopilotMessage } from "./copilot-session";
8794
import {
@@ -652,12 +659,18 @@ if (args[0] === "sessions") {
652659
// Claude Code path: resolve session log
653660
//
654661
// Strategy (most precise → least precise):
655-
// 1. PPID session metadata: ~/.claude/sessions/<ppid>.json gives us the
656-
// exact sessionId and original cwd. Deterministic, O(1), no scanning.
657-
// 2. CWD slug match: existing behavior — works when the shell CWD hasn't
658-
// changed from the session's project directory.
659-
// 3. Ancestor walk: walk up the directory tree trying parent slugs. Handles
660-
// the common case where the user `cd`'d deeper into a subdirectory.
662+
// 1. Ancestor-PID session metadata: walk up the process tree checking
663+
// ~/.claude/sessions/<pid>.json at each hop. When invoked from a slash
664+
// command's `!` bang, the direct parent is a bash subshell — Claude's
665+
// session file is a few hops up. Deterministic when it matches.
666+
// 2. Cwd-scan of session metadata: read every ~/.claude/sessions/*.json,
667+
// filter by cwd, pick the most recent startedAt. Better than mtime
668+
// guessing because it uses session-level metadata.
669+
// 3. CWD slug match (mtime-based): legacy behavior — picks the most
670+
// recently modified jsonl in the project dir. Fragile when multiple
671+
// sessions exist for the same project.
672+
// 4. Ancestor directory walk: handles the case where the user `cd`'d
673+
// deeper into a subdirectory after session start.
661674

662675
if (process.env.PLANNOTATOR_DEBUG) {
663676
console.error(`[DEBUG] Project root: ${projectRoot}`);
@@ -677,15 +690,19 @@ if (args[0] === "sessions") {
677690
}
678691
}
679692

680-
// 1. Try PPID-based session metadata (most reliable)
681-
const ppidLog = resolveSessionLogByPpid();
682-
tryLogCandidates("PPID session metadata", () => ppidLog ? [ppidLog] : []);
693+
// 1. Walk ancestor PIDs for a matching session metadata file
694+
const ancestorLog = resolveSessionLogByAncestorPids();
695+
tryLogCandidates("Ancestor PID session metadata", () => ancestorLog ? [ancestorLog] : []);
696+
697+
// 2. Scan all session metadata files for one whose cwd matches
698+
const cwdScanLog = resolveSessionLogByCwdScan({ cwd: projectRoot });
699+
tryLogCandidates("Cwd-scan session metadata", () => cwdScanLog ? [cwdScanLog] : []);
683700

684-
// 2. Fall back to CWD slug match
685-
tryLogCandidates("CWD slug match", () => findSessionLogsForCwd(projectRoot));
701+
// 3. Fall back to CWD slug match (mtime-based)
702+
tryLogCandidates("CWD slug match (mtime)", () => findSessionLogsForCwd(projectRoot));
686703

687-
// 3. Fall back to ancestor directory walk
688-
tryLogCandidates("Ancestor walk", () => findSessionLogsByAncestorWalk(projectRoot));
704+
// 4. Fall back to ancestor directory walk
705+
tryLogCandidates("Directory ancestor walk", () => findSessionLogsByAncestorWalk(projectRoot));
689706
}
690707

691708
if (!lastMessage) {

0 commit comments

Comments
 (0)