Initial commit
This commit is contained in:
26
hooks/hooks.json
Normal file
26
hooks/hooks.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
{
|
||||
"matcher": ".*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
620
hooks/session-end.md
Normal file
620
hooks/session-end.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# 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.
|
||||
27
hooks/session-end.sh
Executable file
27
hooks/session-end.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# SessionEnd hook for tabula-scripta plugin
|
||||
# Injects working memory finalization instructions into Claude's context
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Determine plugin root directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Read session-end hook documentation
|
||||
session_end_instructions=$(cat "${PLUGIN_ROOT}/hooks/session-end.md" 2>&1 || echo "Error reading session-end hook documentation")
|
||||
|
||||
# Escape for JSON
|
||||
instructions_escaped=$(echo "$session_end_instructions" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
|
||||
|
||||
# Output context injection as JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionEnd",
|
||||
"additionalContext": "<IMPORTANT>\n**Tabula Scripta: Session Finalization**\n\nThis hook automatically finalizes working memory at session end.\n\n**Instructions:**\n\n${instructions_escaped}\n\n**Action Required:**\n1. Follow the session-end implementation steps above\n2. Check compaction threshold (500 lines OR 3 days old)\n3. If threshold met: extract knowledge, update entities, archive session\n4. Use the correct MCP tool names (mcp__obsidian-vault__vault, mcp__obsidian-vault__edit, etc.)\n5. Handle errors gracefully\n\n**Note:** If MCP is unavailable, notify user and provide manual cleanup steps.\n</IMPORTANT>"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
509
hooks/session-start.md
Normal file
509
hooks/session-start.md
Normal file
@@ -0,0 +1,509 @@
|
||||
# MANDATORY SESSION START PROTOCOL
|
||||
|
||||
**YOU MUST execute these steps BEFORE responding to the user's first message.**
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Without loading memory proactively, you will:
|
||||
- Lose context from previous sessions
|
||||
- Repeat solved problems
|
||||
- Miss important decisions and open questions
|
||||
- Waste user's time re-establishing context
|
||||
|
||||
## The Iron Law
|
||||
|
||||
```
|
||||
LOAD MEMORY FIRST, RESPOND TO USER SECOND
|
||||
```
|
||||
|
||||
No exceptions. Execute the protocol below, THEN respond to whatever the user asked.
|
||||
|
||||
## Required Protocol (Execute in Order)
|
||||
|
||||
**Performance Target:** <2 seconds total
|
||||
**User Experience:** 1-3 sentence summary, then respond to their message
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Detect Project Context
|
||||
|
||||
1. Attempt to detect the current project from the git repository:
|
||||
- Run `git rev-parse --show-toplevel` to find the git repository root
|
||||
- Extract the project name from the repository directory name
|
||||
- Return the project name
|
||||
|
||||
2. If not in a git repository (command fails):
|
||||
- Fall back to using the current working directory name
|
||||
- If that also fails, use 'default' as the project name
|
||||
|
||||
3. Store the detected project context for all subsequent operations
|
||||
|
||||
**Fallback:** If not in git repo, use current directory name. If that fails, use "default".
|
||||
|
||||
### 2. Load Project Index
|
||||
|
||||
1. Construct the project index path: `claude/projects/{projectContext}/_index.md`
|
||||
|
||||
2. Attempt to load the project index using MCP vault tool:
|
||||
```javascript
|
||||
mcp__obsidian-vault__vault({
|
||||
action: "read",
|
||||
path: `claude/projects/${projectContext}/_index.md`,
|
||||
returnFullFile: true
|
||||
})
|
||||
```
|
||||
|
||||
3. If successful:
|
||||
- Extract wikilinks from the index content
|
||||
- These represent the most important entities for this project
|
||||
|
||||
4. Handle errors:
|
||||
|
||||
**If FileNotFoundError:**
|
||||
- This is the first time working on this project
|
||||
- Display message: "Starting new project: {projectContext}"
|
||||
- Offer to create project index for future sessions
|
||||
|
||||
**If MCPUnavailableError:**
|
||||
- Graceful degradation - continue without memory
|
||||
- Display message: "Obsidian MCP unavailable. See docs/setup-guide.md"
|
||||
- Continue session without memory loading (see error recovery in section below)
|
||||
|
||||
### 3. Query Last 3 Session Notes
|
||||
|
||||
**Using Dataview Query (if available):**
|
||||
|
||||
1. Construct a Dataview query to find recent sessions:
|
||||
- Source: `claude/projects/{projectContext}/sessions`
|
||||
- Filter: status = "active" OR status = "archived"
|
||||
- Sort: created DESC (most recent first)
|
||||
- Limit: 3
|
||||
|
||||
2. Invoke MCP bases tool to query sessions:
|
||||
```javascript
|
||||
mcp__obsidian-vault__bases({
|
||||
action: "query",
|
||||
filters: [
|
||||
{ property: "status", operator: "in", value: ["active", "archived"] }
|
||||
],
|
||||
sort: { property: "created", order: "desc" },
|
||||
pagination: { page: 1, pageSize: 3 }
|
||||
})
|
||||
```
|
||||
|
||||
Note: If Dataview base not configured, falls back to search (see fallback below)
|
||||
|
||||
3. For each returned session path, load the full content in parallel:
|
||||
```javascript
|
||||
mcp__obsidian-vault__vault({
|
||||
action: "read",
|
||||
path: sessionPath,
|
||||
returnFullFile: true
|
||||
})
|
||||
```
|
||||
|
||||
4. Handle errors:
|
||||
|
||||
**If Dataview base not available:**
|
||||
- Fallback to list and filter approach:
|
||||
```javascript
|
||||
// List all session files
|
||||
const listResult = await mcp__obsidian-vault__vault({
|
||||
action: "list",
|
||||
directory: `claude/projects/${projectContext}/sessions`
|
||||
});
|
||||
|
||||
// Load frontmatter for each in parallel
|
||||
const sessions = await Promise.all(
|
||||
listResult.result.map(async path => {
|
||||
const r = await mcp__obsidian-vault__vault({
|
||||
action: "read",
|
||||
path: path
|
||||
});
|
||||
return { path, frontmatter: r.result.frontmatter };
|
||||
})
|
||||
);
|
||||
|
||||
// Filter and sort
|
||||
const recent = sessions
|
||||
.filter(s => s.frontmatter.status === 'active' || s.frontmatter.status === 'archived')
|
||||
.sort((a, b) => b.frontmatter.created.localeCompare(a.frontmatter.created))
|
||||
.slice(0, 3);
|
||||
```
|
||||
|
||||
**Fallback Strategy:** If Dataview base unavailable, use list + filter approach with frontmatter sorting.
|
||||
|
||||
### 4. Load Linked Entities
|
||||
|
||||
1. From the project index, extract the linked entities (wikilinks)
|
||||
|
||||
2. Limit to top 5 most important entities to keep load time under 2 seconds
|
||||
|
||||
3. For each entity (in parallel):
|
||||
- First attempt: Try to load from project entities path:
|
||||
```javascript
|
||||
mcp__obsidian-vault__vault({
|
||||
action: "read",
|
||||
path: `claude/projects/${projectContext}/entities/${entityName}.md`,
|
||||
returnFullFile: true
|
||||
})
|
||||
```
|
||||
- If not found (error.message includes "not found"): Try global entities path:
|
||||
```javascript
|
||||
mcp__obsidian-vault__vault({
|
||||
action: "read",
|
||||
path: `claude/global/entities/${entityName}.md`,
|
||||
returnFullFile: true
|
||||
})
|
||||
```
|
||||
|
||||
4. Collect all successfully loaded entities
|
||||
|
||||
**Optimization:** Only load top 5 most important entities (limit based on recency or importance)
|
||||
|
||||
### 5. Generate Summary
|
||||
|
||||
**Summary Generation:**
|
||||
|
||||
1. Initialize an empty highlights array
|
||||
|
||||
2. Extract key information from the most recent session:
|
||||
- Parse the "## Decisions Made" section to find recent decisions
|
||||
- Parse the "## Open Questions" section to find blockers or uncertainties
|
||||
- If decisions exist, add the most recent one to highlights
|
||||
- If open questions exist, add the first one with "Open:" prefix
|
||||
|
||||
3. Limit to 1-3 highlights total to create a concise summary
|
||||
|
||||
4. Join highlights into a single summary string (1-3 sentences)
|
||||
|
||||
**Summary Content Focus:**
|
||||
- Recent decisions and rationale
|
||||
- Open questions or blockers
|
||||
- Key patterns or architectural choices
|
||||
- Next steps from previous session
|
||||
|
||||
**Length Limit:** 1-3 sentences maximum (not a memory dump)
|
||||
|
||||
### 6. Present Summary to User
|
||||
|
||||
**Output Format:**
|
||||
|
||||
```
|
||||
Working Memory Loaded for Project: {project-name}
|
||||
|
||||
Summary:
|
||||
{1-3 sentence summary of recent context}
|
||||
|
||||
Last session: {date} - {topic}
|
||||
Active entities: {count} loaded
|
||||
Recent sessions: {count} reviewed
|
||||
|
||||
Need more context? Ask me to recall specific entities or sessions.
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
Working Memory Loaded for Project: tabula-scripta
|
||||
|
||||
Summary:
|
||||
Last session completed plugin foundation and core memory skill. Currently implementing session hooks with proactive recall. Open question: how to handle MCP connection failures gracefully.
|
||||
|
||||
Last session: 2025-11-17 - Memory System Design
|
||||
Active entities: 5 loaded
|
||||
Recent sessions: 3 reviewed
|
||||
|
||||
Need more context? Ask me to recall specific entities or sessions.
|
||||
```
|
||||
|
||||
### 7. Offer Additional Context
|
||||
|
||||
**User can request more:**
|
||||
|
||||
```
|
||||
User: "Tell me about the memory system architecture"
|
||||
|
||||
Claude: Loads [[Memory System Architecture]] entity and presents details
|
||||
```
|
||||
|
||||
**User can request session details:**
|
||||
|
||||
```
|
||||
User: "What did we work on last session?"
|
||||
|
||||
Claude: Loads full content of last session note and summarizes
|
||||
```
|
||||
|
||||
## Red Flags - STOP and Execute Protocol
|
||||
|
||||
If you catch yourself thinking ANY of these thoughts, STOP. Execute the memory loading protocol FIRST:
|
||||
|
||||
- "User seems urgent, I'll respond immediately"
|
||||
- "Their question is simple, I don't need memory"
|
||||
- "I'll load memory if they specifically ask for it"
|
||||
- "Let me respond quickly, memory later"
|
||||
- "They just asked 'what do u remember' - that's different"
|
||||
- "I can see the git history, that's good enough"
|
||||
- "This is just a greeting, skip the protocol"
|
||||
|
||||
**ALL of these mean:** Stop. Execute the MANDATORY SESSION START PROTOCOL. THEN respond.
|
||||
|
||||
## Common Rationalizations (Don't Do These)
|
||||
|
||||
| Excuse | Reality |
|
||||
|--------|---------|
|
||||
| "User seems urgent, respond immediately" | Protocol takes <2 seconds. User benefits from context. |
|
||||
| "Question is simple, don't need memory" | You don't know what's simple without loading context. |
|
||||
| "I'll load if they ask for it" | Proactive loading IS the feature. Don't make user ask. |
|
||||
| "Git history is good enough" | Git shows files, not decisions/questions/context. Load memory. |
|
||||
| "This is just a greeting" | Every session starts with greeting. Still load memory. |
|
||||
| "They said 'what do u remember'" | That's still a first message. Execute protocol FIRST. |
|
||||
|
||||
## Error Handling and Recovery
|
||||
|
||||
### MCP Unavailable (I2: Error Recovery Paths)
|
||||
|
||||
When MCP server is unavailable at session start:
|
||||
|
||||
1. Detect the MCPUnavailableError during any memory operation
|
||||
|
||||
2. Display user-friendly message:
|
||||
```
|
||||
Obsidian MCP server unavailable. Working memory features disabled.
|
||||
|
||||
To restore memory:
|
||||
1. Ensure Obsidian is running
|
||||
2. Verify obsidian-mcp-plugin installed
|
||||
3. Check Claude Code config includes MCP server
|
||||
|
||||
See docs/setup-guide.md for setup instructions.
|
||||
```
|
||||
|
||||
3. **Decision Point - Ask User:**
|
||||
- **Option A: Continue without memory**
|
||||
- Session proceeds normally but without context loading
|
||||
- Memory writes will also be disabled for this session
|
||||
- User can still work on tasks, just no memory features
|
||||
- **Option B: Wait and retry**
|
||||
- Pause session startup
|
||||
- Wait for user to fix MCP connection
|
||||
- Retry memory loading once fixed
|
||||
- **Option C: Exit and fix setup**
|
||||
- Exit the session
|
||||
- User fixes MCP setup
|
||||
- Restart session with working memory
|
||||
|
||||
4. If user chooses to continue without memory:
|
||||
- Set session flag: `memory_disabled = true`
|
||||
- Skip all memory operations for this session
|
||||
- Display reminder that memory is disabled
|
||||
|
||||
### Project Index Not Found
|
||||
|
||||
If the project index doesn't exist (FileNotFoundError):
|
||||
|
||||
1. Display message:
|
||||
```
|
||||
Starting fresh project: {projectContext}
|
||||
|
||||
I'll create a project index to track your work.
|
||||
Would you like me to create it now? (yes/no)
|
||||
```
|
||||
|
||||
2. If user confirms:
|
||||
- Create a new project index note
|
||||
- Initialize with basic structure
|
||||
|
||||
3. If user declines:
|
||||
- Continue session without index
|
||||
- Index will be created on first memory write
|
||||
|
||||
### No Previous Sessions
|
||||
|
||||
If no session notes are found for this project:
|
||||
|
||||
1. Display welcome message:
|
||||
```
|
||||
Welcome to project: {projectContext}
|
||||
|
||||
This is your first session. I'll start building working memory as we work.
|
||||
|
||||
Ready to begin!
|
||||
```
|
||||
|
||||
2. Return and start the session normally
|
||||
- Memory features are active but no context to load yet
|
||||
|
||||
## Performance Optimization and Measurement
|
||||
|
||||
### Parallel Loading
|
||||
|
||||
1. Load project index, sessions, and entities in parallel using Promise.all:
|
||||
- Load project index
|
||||
- Query recent sessions (limit 3)
|
||||
- Load linked entities (limit 5)
|
||||
|
||||
2. Wait for all operations to complete before generating summary
|
||||
|
||||
**Target:** <2 seconds total time for all operations
|
||||
|
||||
### I3: Performance Measurement and Warning
|
||||
|
||||
1. **Track timing:**
|
||||
- Record start time before beginning memory loading
|
||||
- Record end time after all operations complete
|
||||
- Calculate total duration in milliseconds
|
||||
|
||||
2. **Performance check:**
|
||||
- If total duration < 2000ms: Success, no message needed
|
||||
- If total duration >= 2000ms but < 5000ms: Display warning
|
||||
```
|
||||
Working memory loaded in {duration}ms (target: <2s)
|
||||
|
||||
Consider optimizing:
|
||||
- Reduce number of linked entities in project index
|
||||
- Archive old session notes
|
||||
- Check Obsidian vault performance
|
||||
```
|
||||
- If total duration >= 5000ms: Display strong warning
|
||||
```
|
||||
Working memory loading is slow ({duration}ms)
|
||||
|
||||
This may impact session startup time. Recommendations:
|
||||
1. Archive sessions older than 30 days
|
||||
2. Limit project index to 5 most important entities
|
||||
3. Check Obsidian performance (large vault, plugins)
|
||||
4. Consider reducing session note size threshold
|
||||
|
||||
Continue with memory features? (yes/no)
|
||||
```
|
||||
|
||||
3. **User decision on slow performance:**
|
||||
- If user says no: Disable memory for this session
|
||||
- If user says yes: Continue with warning noted
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Instead of loading full session content upfront, load summaries first:
|
||||
|
||||
1. For each recent session, extract only:
|
||||
- Path
|
||||
- Title from frontmatter
|
||||
- Created date from frontmatter
|
||||
|
||||
2. Don't load full content until user requests it
|
||||
|
||||
3. This speeds up initial load for sessions with large notes
|
||||
|
||||
### Caching
|
||||
|
||||
To avoid repeated MCP calls during the session:
|
||||
|
||||
1. Create a session cache (Map or similar structure)
|
||||
|
||||
2. When loading a note:
|
||||
- Check if already in cache
|
||||
- If in cache: Return cached version
|
||||
- If not in cache: Load via MCP vault tool and store in cache
|
||||
```javascript
|
||||
mcp__obsidian-vault__vault({
|
||||
action: "read",
|
||||
path: notePath,
|
||||
returnFullFile: true
|
||||
})
|
||||
```
|
||||
|
||||
3. Cache persists for session duration only (cleared on exit)
|
||||
|
||||
## Integration with Managing Working Memory Skill
|
||||
|
||||
**Relationship:** This hook triggers the recall flow defined in `skills/managing-working-memory.md`
|
||||
|
||||
**Update claude_last_accessed:** When loading notes, update frontmatter:
|
||||
|
||||
1. For each loaded note:
|
||||
- Set frontmatter.claude_last_accessed to current date (YYYY-MM-DD)
|
||||
- Update the note using MCP edit tool (efficient patch):
|
||||
```javascript
|
||||
mcp__obsidian-vault__edit({
|
||||
action: "patch",
|
||||
path: notePath,
|
||||
targetType: "frontmatter",
|
||||
target: "claude_last_accessed",
|
||||
operation: "replace",
|
||||
content: new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
||||
})
|
||||
```
|
||||
|
||||
**Cross-Project Tracking:** If loading entity from different project, log cross-project recall:
|
||||
|
||||
1. Check if entity is from a different project:
|
||||
- Compare entity.frontmatter.project with currentProject
|
||||
- Exclude global entities (project = 'global')
|
||||
|
||||
2. If cross-project recall detected:
|
||||
- Append to cross_project_recalls array:
|
||||
- project: Current project
|
||||
- date: Current date
|
||||
- context: "Session start recall"
|
||||
|
||||
3. Check promotion threshold:
|
||||
- If cross_project_recalls length >= 3, trigger promotion prompt
|
||||
- Use promotion flow defined in managing-working-memory skill
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. Start Claude Code session in git repo
|
||||
2. Verify project detection works (correct project name)
|
||||
3. Check summary generated (1-3 sentences)
|
||||
4. Verify <2 second load time
|
||||
5. Request additional context (entities, sessions)
|
||||
6. Test in non-git directory (fallback to directory name)
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- **No previous sessions:** Welcome message
|
||||
- **MCP unavailable:** Graceful degradation message
|
||||
- **Large vault (>100 notes):** Performance still <2s
|
||||
- **Project index missing:** Offer to create
|
||||
- **Dataview not installed:** Fallback to search_notes
|
||||
|
||||
## Example Session Start Flow
|
||||
|
||||
```
|
||||
$ claude-code
|
||||
|
||||
[Session Start Hook Executing...]
|
||||
|
||||
Working Memory Loaded for Project: tabula-scripta
|
||||
|
||||
Summary:
|
||||
Completed Phase 2 (Core Memory Skill) implementing write triggers and conflict detection. Now implementing Phase 3 session hooks. Performance target is <2 seconds for session start recall.
|
||||
|
||||
Last session: 2025-11-17 - Implementing Core Skill
|
||||
Active entities: 3 loaded ([[Memory System]], [[MCP Integration]], [[Conflict Detection]])
|
||||
Recent sessions: 2 reviewed
|
||||
|
||||
Need more context? Ask me to recall specific entities or sessions.
|
||||
|
||||
---
|
||||
|
||||
How can I help you today?
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
This session-start hook provides proactive recall of working memory at the beginning of each session:
|
||||
|
||||
1. **Detects project** from git repo or directory name
|
||||
2. **Loads project index** with linked entities
|
||||
3. **Queries last 3 sessions** via Dataview
|
||||
4. **Generates 1-3 sentence summary** (not overwhelming)
|
||||
5. **Presents context** with option to request more
|
||||
6. **Performs in <2 seconds** via parallel loading
|
||||
7. **Handles errors gracefully** (MCP unavailable, no sessions, etc.)
|
||||
|
||||
Users experience automatic context restoration without manual searching, while maintaining control to request additional details as needed.
|
||||
27
hooks/session-start.sh
Executable file
27
hooks/session-start.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# SessionStart hook for tabula-scripta plugin
|
||||
# Injects working memory recall instructions into Claude's context
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Determine plugin root directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
|
||||
PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# Read session-start hook documentation
|
||||
session_start_instructions=$(cat "${PLUGIN_ROOT}/hooks/session-start.md" 2>&1 || echo "Error reading session-start hook documentation")
|
||||
|
||||
# Escape for JSON
|
||||
instructions_escaped=$(echo "$session_start_instructions" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
|
||||
|
||||
# Output context injection as JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": "<IMPORTANT>\n**Tabula Scripta: Working Memory System**\n\nThis hook automatically loads relevant context from previous sessions.\n\n**Instructions:**\n\n${instructions_escaped}\n\n**Action Required:**\n1. Follow the session-start implementation steps above\n2. Use the correct MCP tool names (mcp__obsidian-vault__vault, mcp__obsidian-vault__edit, etc.)\n3. Present a concise 1-3 sentence summary to the user\n4. Complete within 2 seconds performance target\n\n**Note:** If MCP is unavailable, gracefully degrade and continue without memory.\n</IMPORTANT>"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user