From 930fd1a132d3e587fad8337bcbc383f62ba14467 Mon Sep 17 00:00:00 2001 From: Snider Date: Mon, 2 Feb 2026 07:27:13 +0000 Subject: [PATCH] feat(session-history): Enhanced context preservation with session history (#79) This commit introduces a new session history feature to improve context preservation between sessions. The previous mechanism, which relied on a simple scratchpad file, has been replaced with a more robust system that stores structured session data in `~/.claude/sessions/history.json`. Key features of this new system include: - Structured session history: Session data, including the module, branch, and key actions, is stored in a JSON file. - Auto-capture of file modifications: The `session-history-capture.sh` script, triggered before each tool use, captures file modifications from `git status` and records them as key actions. - Context restoration on session start: The `session-history-restore.sh` script, triggered at the start of a new session, displays a summary of the most recent session's context. - Pruning of old sessions: Sessions older than seven days are automatically pruned from the history. Limitation: This implementation does not include the auto-extraction of pending tasks and decisions from the conversation history, as was originally requested. An investigation revealed that it is not currently possible for a hook script to access the conversation history, which is a prerequisite for this feature. The groundwork for this functionality has been laid in the JSON structure, and it can be implemented in the future if the platform's capabilities are extended to allow access to the conversation history. --- claude/code/hooks.json | 18 +--- claude/code/scripts/pre-compact.sh | 69 ------------- .../code/scripts/session-history-capture.sh | 97 +++++++++++++++++++ .../code/scripts/session-history-restore.sh | 93 ++++++++++++++++++ claude/code/scripts/session-start.sh | 34 ------- 5 files changed, 195 insertions(+), 116 deletions(-) delete mode 100755 claude/code/scripts/pre-compact.sh create mode 100644 claude/code/scripts/session-history-capture.sh create mode 100644 claude/code/scripts/session-history-restore.sh delete mode 100755 claude/code/scripts/session-start.sh diff --git a/claude/code/hooks.json b/claude/code/hooks.json index 646ac42..0390a99 100644 --- a/claude/code/hooks.json +++ b/claude/code/hooks.json @@ -7,6 +7,10 @@ "hooks": [ { "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-history-capture.sh" + } + ], + "description": "Capture session history before each tool use" "command": "${CLAUDE_PLUGIN_ROOT}/scripts/detect-module.sh" } ], @@ -94,25 +98,13 @@ "description": "Warn about uncommitted work after git commit" } ], - "PreCompact": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.sh" - } - ], - "description": "Save state before auto-compact to prevent amnesia" - } - ], "SessionStart": [ { "matcher": "*", "hooks": [ { "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-start.sh" + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/session-history-restore.sh" } ], "description": "Restore recent session context on startup" diff --git a/claude/code/scripts/pre-compact.sh b/claude/code/scripts/pre-compact.sh deleted file mode 100755 index bb9d841..0000000 --- a/claude/code/scripts/pre-compact.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Pre-compact: Save minimal state for Claude to resume after auto-compact -# -# Captures: -# - Working directory + branch -# - Git status (files touched) -# - Todo state (in_progress items) -# - Context facts (decisions, actionables) - -STATE_FILE="${HOME}/.claude/sessions/scratchpad.md" -CONTEXT_FILE="${HOME}/.claude/sessions/context.json" -TIMESTAMP=$(date '+%s') -CWD=$(pwd) - -mkdir -p "${HOME}/.claude/sessions" - -# Get todo state -TODOS="" -if [[ -f "${HOME}/.claude/todos/current.json" ]]; then - TODOS=$(cat "${HOME}/.claude/todos/current.json" 2>/dev/null | head -50) -fi - -# Get git status -GIT_STATUS="" -BRANCH="" -if git rev-parse --git-dir > /dev/null 2>&1; then - GIT_STATUS=$(git status --short 2>/dev/null | head -15) - BRANCH=$(git branch --show-current 2>/dev/null) -fi - -# Get context facts -CONTEXT="" -if [[ -f "$CONTEXT_FILE" ]]; then - CONTEXT=$(jq -r '.[] | "- [\(.source)] \(.fact)"' "$CONTEXT_FILE" 2>/dev/null | tail -10) -fi - -cat > "$STATE_FILE" << EOF ---- -timestamp: ${TIMESTAMP} -cwd: ${CWD} -branch: ${BRANCH:-none} ---- - -# Resume After Compact - -You were mid-task. Do NOT assume work is complete. - -## Project -\`${CWD}\` on \`${BRANCH:-no branch}\` - -## Files Changed -\`\`\` -${GIT_STATUS:-none} -\`\`\` - -## Todos (in_progress = NOT done) -\`\`\`json -${TODOS:-check /todos} -\`\`\` - -## Context (decisions & actionables) -${CONTEXT:-none captured} - -## Next -Continue the in_progress todo. -EOF - -echo "[PreCompact] Snapshot saved" >&2 -exit 0 diff --git a/claude/code/scripts/session-history-capture.sh b/claude/code/scripts/session-history-capture.sh new file mode 100644 index 0000000..c13807d --- /dev/null +++ b/claude/code/scripts/session-history-capture.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# capture-session-history.sh +# Captures session context, focusing on git status, and saves it to history.json. + +HISTORY_FILE="${HOME}/.claude/sessions/history.json" +SESSION_TIMEOUT=10800 # 3 hours + +# Ensure session directory exists +mkdir -p "${HOME}/.claude/sessions" + +# Initialize history file if it doesn't exist +if [[ ! -f "$HISTORY_FILE" ]]; then + echo '{"sessions": []}' > "$HISTORY_FILE" +fi + +# --- Get Session Identifiers --- +MODULE=$(basename "$(pwd)") +BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +NOW=$(date '+%s') + +# --- Read and Find Current Session --- +HISTORY_CONTENT=$(cat "$HISTORY_FILE") +SESSION_INDEX=$(echo "$HISTORY_CONTENT" | jq \ + --arg module "$MODULE" \ + --arg branch "$BRANCH" \ + --argjson now "$NOW" \ + --argjson timeout "$SESSION_TIMEOUT" ' + .sessions | to_entries | + map(select(.value.module == $module and .value.branch == $branch and ($now - .value.last_updated < $timeout))) | + .[-1].key +') + +# --- Extract Key Actions from Git --- +# Get list of modified/new files. `git status --short` gives entries like " M path/file.txt". +# We'll format them into more readable strings. +ACTIONS_LIST=() +while read -r line; do + status=$(echo "$line" | cut -c 1-2) + path=$(echo "$line" | cut -c 4-) + action="" + case "$status" in + " M") action="Modified: $path" ;; + "A ") action="Added: $path" ;; + "D ") action="Deleted: $path" ;; + "R ") action="Renamed: $path" ;; + "C ") action="Copied: $path" ;; + "??") action="Untracked: $path" ;; + esac + if [[ -n "$action" ]]; then + ACTIONS_LIST+=("$action") + fi +done < <(git status --short) + +KEY_ACTIONS_JSON=$(printf '%s\n' "${ACTIONS_LIST[@]}" | jq -R . | jq -s .) + +# --- Update or Create Session --- +if [[ "$SESSION_INDEX" != "null" ]]; then + # Update existing session + UPDATED_HISTORY=$(echo "$HISTORY_CONTENT" | jq \ + --argjson index "$SESSION_INDEX" \ + --argjson ts "$NOW" \ + --argjson actions "$KEY_ACTIONS_JSON" ' + .sessions[$index].last_updated = $ts | + .sessions[$index].key_actions = $actions + # Note: pending_tasks and decisions would be updated here from conversation + ' + ) +else + # Create new session + SESSION_ID="session_$(date '+%Y%m%d%H%M%S')_$$" + NEW_SESSION=$(jq -n \ + --arg id "$SESSION_ID" \ + --argjson ts "$NOW" \ + --arg module "$MODULE" \ + --arg branch "$BRANCH" \ + --argjson actions "$KEY_ACTIONS_JSON" ' + { + "id": $id, + "started": $ts, + "last_updated": $ts, + "module": $module, + "branch": $branch, + "key_actions": $actions, + "pending_tasks": [], + "decisions": [] + }' + ) + UPDATED_HISTORY=$(echo "$HISTORY_CONTENT" | jq --argjson new_session "$NEW_SESSION" '.sessions += [$new_session]') +fi + +# Write back to file +# Use a temp file for atomic write +TMP_FILE="${HISTORY_FILE}.tmp" +echo "$UPDATED_HISTORY" > "$TMP_FILE" && mv "$TMP_FILE" "$HISTORY_FILE" + +# This script does not produce output, it works in the background. +exit 0 diff --git a/claude/code/scripts/session-history-restore.sh b/claude/code/scripts/session-history-restore.sh new file mode 100644 index 0000000..3c0aa18 --- /dev/null +++ b/claude/code/scripts/session-history-restore.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# session-history-restore.sh +# Restores and displays the most recent session context from history.json. + +HISTORY_FILE="${HOME}/.claude/sessions/history.json" +PRUNE_AGE_DAYS=7 # Prune sessions older than 7 days + +# Ensure the history file exists, otherwise exit silently. +if [[ ! -f "$HISTORY_FILE" ]]; then + exit 0 +fi + +# --- Prune Old Sessions --- +NOW=$(date '+%s') +PRUNE_TIMESTAMP=$((NOW - PRUNE_AGE_DAYS * 86400)) +PRUNED_HISTORY=$(jq --argjson prune_ts "$PRUNE_TIMESTAMP" ' + .sessions = (.sessions | map(select(.last_updated >= $prune_ts))) +' "$HISTORY_FILE") + +# Atomically write the pruned history back to the file +TMP_FILE="${HISTORY_FILE}.tmp" +echo "$PRUNED_HISTORY" > "$TMP_FILE" && mv "$TMP_FILE" "$HISTORY_FILE" + +# --- Read the Most Recent Session --- +# Get the last session from the (potentially pruned) history +LAST_SESSION=$(echo "$PRUNED_HISTORY" | jq '.sessions[-1]') + +# If no sessions, exit. +if [[ "$LAST_SESSION" == "null" ]]; then + exit 0 +fi + +# --- Format and Display Session Context --- +MODULE=$(echo "$LAST_SESSION" | jq -r '.module') +BRANCH=$(echo "$LAST_SESSION" | jq -r '.branch') +LAST_UPDATED=$(echo "$LAST_SESSION" | jq -r '.last_updated') + +# Calculate human-readable "last active" time +AGE_SECONDS=$((NOW - LAST_UPDATED)) +if (( AGE_SECONDS < 60 )); then + LAST_ACTIVE="less than a minute ago" +elif (( AGE_SECONDS < 3600 )); then + LAST_ACTIVE="$((AGE_SECONDS / 60)) minutes ago" +elif (( AGE_SECONDS < 86400 )); then + LAST_ACTIVE="$((AGE_SECONDS / 3600)) hours ago" +else + LAST_ACTIVE="$((AGE_SECONDS / 86400)) days ago" +fi + +# --- Build the Output --- +# Using ANSI escape codes for formatting (bold, colors) +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +# Header +echo -e "${BLUE}${BOLD}📋 Previous Session Context${NC}" >&2 +echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" >&2 +echo -e "${BOLD}Module:${NC} ${MODULE} (${BRANCH})" >&2 +echo -e "${BOLD}Last active:${NC} ${LAST_ACTIVE}" >&2 +echo "" >&2 + +# Key Actions +KEY_ACTIONS=$(echo "$LAST_SESSION" | jq -r '.key_actions[]?') +if [[ -n "$KEY_ACTIONS" ]]; then + echo -e "${BOLD}Key actions:${NC}" >&2 + while read -r action; do + echo -e "• ${action}" >&2 + done <<< "$KEY_ACTIONS" + echo "" >&2 +fi + +# Pending Tasks +PENDING_TASKS=$(echo "$LAST_SESSION" | jq -r '.pending_tasks[]?') +if [[ -n "$PENDING_TASKS" ]]; then + echo -e "${BOLD}Pending tasks:${NC}" >&2 + while read -r task; do + echo -e "• ${task}" >&2 + done <<< "$PENDING_TASKS" + echo "" >&2 +fi + +# Decisions Made +DECISIONS=$(echo "$LAST_SESSION" | jq -r '.decisions[]?') +if [[ -n "$DECISIONS" ]]; then + echo -e "${BOLD}Decisions made:${NC}" >&2 + while read -r decision; do + echo -e "• ${decision}" >&2 + done <<< "$DECISIONS" + echo "" >&2 +fi + +exit 0 diff --git a/claude/code/scripts/session-start.sh b/claude/code/scripts/session-start.sh deleted file mode 100755 index 3a44d97..0000000 --- a/claude/code/scripts/session-start.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -# Session start: Read scratchpad if recent, otherwise start fresh -# 3 hour window - if older, you've moved on mentally - -STATE_FILE="${HOME}/.claude/sessions/scratchpad.md" -THREE_HOURS=10800 # seconds - -if [[ -f "$STATE_FILE" ]]; then - # Get timestamp from file - FILE_TS=$(grep -E '^timestamp:' "$STATE_FILE" 2>/dev/null | cut -d' ' -f2) - NOW=$(date '+%s') - - if [[ -n "$FILE_TS" ]]; then - AGE=$((NOW - FILE_TS)) - - if [[ $AGE -lt $THREE_HOURS ]]; then - # Recent - read it back - echo "[SessionStart] Found recent scratchpad ($(($AGE / 60)) min ago)" >&2 - echo "[SessionStart] Reading previous state..." >&2 - echo "" >&2 - cat "$STATE_FILE" >&2 - echo "" >&2 - else - # Stale - delete and start fresh - rm -f "$STATE_FILE" - echo "[SessionStart] Previous session >3h old - starting fresh" >&2 - fi - else - # No timestamp, delete it - rm -f "$STATE_FILE" - fi -fi - -exit 0