| 44 | // uniformly across Claude Code (where it's a no-op) and Codex (where it |
| 45 | // actually prevents duplicate fires) — and if Codex ever ships a real |
| 46 | // SessionEnd event, the same code keeps working without changes. |
| 47 | function alreadyFiredForSession(sessionId) { |
| 48 | if (!sessionId) return false; // can't guard without an id; allow through |
| 49 | try { |
| 50 | const markerPath = path.join(PROJECT_ROOT, '.planning', 'telemetry', 'session-end.lock'); |
| 51 | if (!fs.existsSync(markerPath)) return false; |
| 52 | const content = fs.readFileSync(markerPath, 'utf8').trim(); |
| 53 | // Marker holds the most recent session_id that ran session-end. |
| 54 | // If it matches, we've already fired for this session. |
| 55 | return content === sessionId; |
| 56 | } catch { |
| 57 | return false; // on any error, allow the hook to fire |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | function recordFiredForSession(sessionId) { |