Files
gh-arittr-tabula-scripta/hooks/session-end.md
2025-11-29 17:58:13 +08:00

621 lines
18 KiB
Markdown

# Session End Hook
This hook runs automatically when a Claude Code session ends, handling session note updates, threshold-based compaction, and archival.
## Overview
**Purpose:** Finalize session notes, compact if threshold exceeded, and archive completed sessions
**Compaction Threshold:** 500 lines OR 3 days old (whichever comes first)
**Archival Strategy:** Never delete, always archive to preserve history
## Implementation
### 1. Load Current Session Note
1. Detect the project context (same logic as session-start hook)
2. Determine the session note path:
- Get current date in YYYY-MM-DD format
- Infer session topic from work log or user input
- Construct path: `claude/projects/{projectContext}/sessions/{date}-{topic}.md`
3. Attempt to load the session note using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/sessions/${date}-${topic}.md`,
returnFullFile: true
})
```
4. Handle different cases:
**If session note exists:**
- Proceed to finalize the session (next steps)
**If FileNotFoundError:**
- No session note was created this session
- Display: "No session note created this session."
- Exit normally (no-op)
**If MCPUnavailableError:**
- Display: "MCP unavailable. Cannot finalize session."
- See error recovery section below for options
### 2. Final Session Note Update
1. Append session closing entry to the "## Work Log" section:
- Add timestamp with current time
- Add heading: "Session End"
- Add message: "Session completed. Work finalized."
2. Update frontmatter:
- Set updated to current date (YYYY-MM-DD)
- Set claude_last_accessed to current date
3. Write the final update using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "update",
path: sessionPath,
content: updatedContentWithFrontmatter
})
```
Or use edit tool for efficient append:
```javascript
// Append closing entry
mcp__obsidian-vault__edit({
action: "append",
path: sessionPath,
content: `\n### ${timestamp} - Session End\n\nSession completed. Work finalized.`
});
// Update frontmatter
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "updated",
operation: "replace",
content: new Date().toISOString().split('T')[0]
});
```
4. Proceed to check compaction threshold (next step)
### 3. Check Compaction Threshold
**Threshold Logic:**
1. Count lines in the session note by splitting content on newlines
2. Calculate age in days:
- Parse created date from frontmatter
- Compare with current date
- Calculate difference in days
3. Check if either threshold is exceeded:
- Line threshold: >= 500 lines
- Age threshold: >= 3 days
4. If either threshold is exceeded:
- Display reason: "Session note {exceeds 500 lines / exceeds 3 days old}. Triggering compaction..."
- Trigger compaction (next step)
5. If both thresholds are not exceeded:
- Display: "Session note below threshold ({lineCount} lines, {ageInDays} days old). Keeping active."
- Mark session status as "active" in frontmatter
- Update the note using MCP edit tool:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "status",
operation: "replace",
content: "active"
})
```
**Threshold Rules:**
- **500 lines:** Session note becomes too large to navigate efficiently
- **3 days old:** Knowledge should be consolidated into entities for long-term retention
- **Whichever comes first:** More aggressive compaction ensures timely knowledge extraction
### 4. Compact Session Note (I1: Simplified Algorithm)
**Compaction Process:**
1. Display: "Compacting session note into entity notes..."
2. Parse the session note to identify extractable knowledge:
- Use high-level heuristics rather than detailed algorithms
- Look for distinct concepts and decisions
3. For each identified piece of knowledge:
- Update or create the appropriate entity note
- See step 5 for entity update logic
4. Archive the session note (step 6)
5. Display: "Compaction complete. Session archived."
**Knowledge Extraction (Simplified):**
Parse the session note to identify key extractable knowledge:
1. **Extract decisions** from "## Decisions Made" section
- Each decision entry becomes an update to an entity note
- Infer which entity based on context and related entities
2. **Extract gotchas** from "## Work Log" section
- Look for debugging work: "fixed bug", "troubleshot", "resolved issue"
- Each gotcha updates the troubleshooting section of an entity
3. **Extract references** from wikilinks throughout the note
- All wikilinked entities get updated with reference to this session
**Extraction Categories:**
- **Architectural decisions** → Update entity notes with decision and rationale
- **Bug fixes / gotchas** → Update entity troubleshooting section
- **New patterns** → Create topic notes or new entities
- **User preferences** → Update user preference entity
**Note:** Keep extraction logic flexible. Claude should use judgment to identify valuable knowledge rather than following rigid algorithms.
### 5. Update Entity Notes
Use the patch operations and conflict detection patterns from the managing-working-memory skill.
For each extracted piece of knowledge:
1. Determine the target entity note path:
- `claude/projects/{projectContext}/entities/{entityName}.md`
2. Attempt to load the existing entity using MCP vault tool:
```javascript
const loadResult = await mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/entities/${entityName}.md`,
returnFullFile: true
});
```
3. Store the loaded content for conflict detection
4. Apply patch operation based on knowledge type (using efficient edit tool):
- **Decision:** Append to "## Key Decisions" section
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "heading",
target: "Key Decisions",
operation: "append",
content: `\n- ${date}: ${decisionText}`
})
```
- **Gotcha:** Append to "## Gotchas & Troubleshooting" section
- **Reference:** Append to "## Recent Changes" section
5. Before writing, reload the entity and check for conflicts:
- Compare loaded content with current content
- If conflict detected, trigger conflict resolution
- If no conflict, proceed with update
6. Update frontmatter timestamps:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "frontmatter",
target: "updated",
operation: "replace",
content: new Date().toISOString().split('T')[0]
})
```
7. Display: "Updated [[{entityName}]] with knowledge from session."
8. Handle errors:
- If FileNotFoundError: Create new entity from session knowledge
- If other error: Propagate
**Conflict Handling:** Uses same timestamp-based detection as defined in `managing-working-memory.md`
### 6. Archive Session Note
**Archival Process:**
1. Determine archive path:
- Format: `claude/projects/{projectContext}/archive/sessions/{filename}`
- Use same filename as original session note
2. Update session note metadata:
- Set status to "archived" in frontmatter
- Set updated to current date
- Add archived_date field with current date
- Add archived_reason: "Compaction threshold exceeded"
3. Ensure archive directory exists (create if needed)
4. Copy session note to archive location using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "create",
path: `claude/projects/${projectContext}/archive/sessions/${filename}`,
content: sessionContentWithArchivedFrontmatter
})
```
5. Verify archive was successful by attempting to read the archived note
6. If verification successful:
- Display: "Session archived to: {archivePath}"
- Leave original note with archived status (default behavior)
- User can manually delete original if desired
7. If verification fails:
- Throw error: "Archive verification failed. Session not moved."
- Keep original note intact
**Archive Location:** `~/.claude-memory/claude/projects/{project}/archive/sessions/{date}-{topic}.md`
**Preservation Strategy:**
- Never delete notes (always archive)
- Archived notes retain full content and frontmatter
- Status marked as "archived" in frontmatter
- Original note can optionally be removed (default: keep with archived status)
### 7. Create Backlinks
**Link archived session to updated entities:**
For each entity that was updated during compaction:
1. Determine the entity path:
- `claude/projects/{projectContext}/entities/{entityName}.md`
2. Load the entity using MCP vault tool:
```javascript
mcp__obsidian-vault__vault({
action: "read",
path: `claude/projects/${projectContext}/entities/${entityName}.md`,
returnFullFile: true
})
```
3. Create a backlink to the archived session:
- Format: `- [[{session-filename}]] - Archived session (compacted)`
- Extract filename from session path (without .md extension)
4. Append the backlink to the "## References" section of the entity
5. Update the entity frontmatter:
- Set updated to current date
6. Write the updated entity using MCP edit tool:
```javascript
// Append backlink to References section
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "heading",
target: "References",
operation: "append",
content: `\n- [[${sessionFilename}]] - Archived session (compacted)`
});
// Update frontmatter
mcp__obsidian-vault__edit({
action: "patch",
path: entityPath,
targetType: "frontmatter",
target: "updated",
operation: "replace",
content: new Date().toISOString().split('T')[0]
});
```
This creates bidirectional links between archived sessions and the entities they contributed to.
## Error Handling and Recovery (I2)
### MCP Unavailable
When MCP server is unavailable at session end:
1. Detect the MCPUnavailableError
2. Display message listing pending actions:
```
Obsidian MCP unavailable. Cannot finalize session.
Pending actions:
- Session note update
- Compaction check
- Archival (if threshold exceeded)
```
3. **Decision Point - Ask User:**
- **Option A: Exit without finalization**
- Session ends without memory updates
- User must manually run session cleanup later
- Provide manual steps:
1. Check session note line count and age
2. If threshold exceeded, compact into entity notes
3. Archive to archive/sessions/ directory
- **Option B: Wait and retry**
- Pause session end
- Wait for user to fix MCP connection
- Retry finalization once fixed
- **Option C: Export pending work**
- Create local file with session summary
- User can manually update memory later
- Provide file path and instructions
4. Display troubleshooting reference: "See docs/setup-guide.md for MCP troubleshooting."
### Compaction Failure
When compaction fails for any reason:
1. Catch the error and log it
2. **Decision Point - Do NOT archive if compaction failed**
- Leave session note as-is
- Session remains active until compaction succeeds
3. Display error message with manual action steps:
```
Session compaction failed. Session note preserved.
Manual action required:
1. Review session note: {sessionPath}
2. Extract knowledge into entity notes manually
3. Archive session when complete
Error: {error.message}
```
4. Mark session with error status:
- Set frontmatter.status = "compaction_failed"
- Add frontmatter.error = error message
- Update note using MCP edit tool:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "status",
operation: "replace",
content: "compaction_failed"
});
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "error",
operation: "replace",
content: error.message
});
```
5. **Recovery options:**
- User can manually compact and archive later
- On next session, user can trigger manual compaction
- Session will remain in active state with error marker
### Archive Verification Failed
When archive verification fails:
1. Detect that archived note cannot be read
2. Display error: "Archive verification failed. Session not moved."
3. **Decision Point - Abort archival, keep session active:**
- Do NOT remove original session note
- Revert session status to "active"
- Add frontmatter.archive_error = "Verification failed"
4. Update the original session note using MCP edit tool:
```javascript
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "status",
operation: "replace",
content: "active"
});
mcp__obsidian-vault__edit({
action: "patch",
path: sessionPath,
targetType: "frontmatter",
target: "archive_error",
operation: "replace",
content: "Verification failed"
});
```
5. Throw error to halt session end process:
- "Archive failed - session preserved at original location"
6. **Recovery options:**
- User can investigate why archive failed
- Retry archival on next session end
- Manually move to archive directory if needed
## Integration with Managing Working Memory Skill
**Session End Checklist:** Uses TodoWrite checklist from `skills/managing-working-memory.md`:
```markdown
- [ ] Final session note update with closing context
- [ ] Check session note size (line count)
- [ ] Check session note age (created date)
- [ ] If threshold met (500 lines OR 3 days old):
- [ ] Parse session note for extractable knowledge
- [ ] Identify entities to create or update
- [ ] Apply patches to entity notes
- [ ] Create new entities if needed
- [ ] Archive session note to archive/sessions/
- [ ] Verify archive successful
- [ ] If threshold not met:
- [ ] Mark session note status: active
- [ ] Note next checkpoint time
- [ ] Review cross-project recall tracking
- [ ] Check for promotion threshold (3 recalls)
- [ ] Confirm all writes successful
```
**Compaction Triggers:** Aligns with triggers defined in managing-working-memory skill
## Performance Considerations
### Compaction Performance
To avoid session-end delays, optimize compaction performance:
1. **Track timing:**
- Record start time before compaction begins
- Record end time after archival completes
- Calculate total duration in milliseconds
2. **Optimize operations:**
- Parse session (fast - single pass)
- Batch entity updates in parallel using Promise.all
- Archive in single operation (not per-entity)
3. **Display timing:**
- Show "Compaction completed in {duration}ms"
4. **Performance target:** <5 seconds for compaction (even with 10+ entity updates)
### Async Session End
Don't block session exit if MCP operations are slow:
1. Display: "Finalizing session..."
2. Run session-end hook asynchronously:
- Execute all finalization steps
- Allow user to exit if it takes too long
3. Handle completion:
**If successful:**
- Display: "Session finalized successfully."
**If error:**
- Display: "Session end failed: {error.message}"
- Display: "Session state preserved. Manual cleanup may be needed."
- Allow user to exit anyway (don't block)
## Testing
### Manual Testing
1. Create session note with >500 lines
2. Run session-end hook
3. Verify compaction triggered
4. Check entity notes updated
5. Verify session archived at correct path
6. Confirm original session marked as "archived"
### Edge Cases
- **Session note <500 lines and <3 days old:** No compaction, marked active
- **MCP unavailable:** Graceful error, pending actions logged
- **Compaction failure:** Session preserved, error status set
- **No session note:** No-op, clean exit
- **Archive directory missing:** Create directory automatically
### Threshold Testing
**Test Case 1: Line Threshold**
- Session with 600 lines, 1 day old
- Expected: Compact (exceeds 500 lines)
**Test Case 2: Age Threshold**
- Session with 300 lines, 4 days old
- Expected: Compact (exceeds 3 days)
**Test Case 3: Both Thresholds**
- Session with 600 lines, 4 days old
- Expected: Compact (exceeds both thresholds)
**Test Case 4: Neither Threshold**
- Session with 200 lines, 1 day old
- Expected: Mark active, no compaction
## Example Session End Flow
```
$ exit
[Session End Hook Executing...]
Finalizing session for project: tabula-scripta
Session note: 2025-11-18-session-hooks.md
Status: 487 lines, 1 day old (below threshold)
Marking session as active (no compaction needed).
Session finalized successfully.
---
Goodbye!
```
**With Compaction:**
```
$ exit
[Session End Hook Executing...]
Finalizing session for project: tabula-scripta
Session note: 2025-11-15-memory-design.md
Status: 623 lines, 1 day old (exceeds 500 line threshold)
Triggering compaction...
Extracting knowledge:
- 3 architectural decisions → [[Memory System Architecture]]
- 2 debugging insights → [[Conflict Detection]]
- 5 entity references → Updated backlinks
Compaction complete. Session archived to:
archive/sessions/2025-11-15-memory-design.md
Session finalized successfully.
---
Goodbye!
```
## Summary
This session-end hook finalizes working memory at the end of each session:
1. **Loads current session note** and applies final update
2. **Checks compaction threshold** (500 lines OR 3 days old)
3. **Compacts if threshold exceeded** by extracting knowledge into entity notes
4. **Archives session note** to preserve history (never deletes)
5. **Updates entity notes** with decisions, gotchas, and patterns
6. **Creates backlinks** from entities to archived session
7. **Handles errors gracefully** (MCP unavailable, compaction failures, etc.)
Users experience automatic knowledge consolidation without manual effort, while maintaining full session history in archived notes for future reference.